☰
告别Keil:用VSCode搭建APM32F1编译烧录调试全流程
2026/10/7 20:05:02 网站建设 项目流程

APM32F103这块板子,我原本是用Keil在磕。但Keil的代码补全实在熬人,尤其是连续改了几十个结构体字段之后,那个红色波浪线和无响应能直接把人搞到心态崩溃。加上家里这台电脑装了WSL之后,我愈发想把整个嵌入式编译流程从IDE的"黑盒"里掏出来,看清楚每一步到底发生了什么。于是就有了这一篇——用VScode搭一套APM32F1的完整开发环境,走通编译、烧录、调试全流程。这篇文章适合想脱离Keil但还没找到替代方案的嵌入式开发者,也适合已经熟悉VScode但第一次碰国产Cortex-M3 MCU的朋友。如果你对"为什么别人能用VScode写单片机"感到好奇,这篇就是给你准备的完整作业。

1. 为什么要把APM32F1从Keil搬到VScode

1.1 Keil用得好好的,折腾什么?

先别急着骂我瞎折腾。Keil其实并不差,至少对老工程师来说,装个Pack、建个工程、点一下Download就能跑,这套肌肉记忆已经刻进DNA了。但我在APM32F1项目上遇到的痛点很具体:一方面是代码提示太弱,写结构体成员时经常要翻datasheet确认拼写;另一方面,工程文件是分散在.uvprojx里面的,和Git的diff体验几乎没法看。我用的是APM32F103RBT6,这颗芯片是128KB Flash和20KB RAM,外设和STM32F1的高度兼容,但真要切换开发环境,坑并不在芯片本身,而在于工具链的适配。

再一个现实问题:Keil在Windows上调试APM32时,虽然可以用CMSIS-DAP或J-Link,但它的调试器视图、变量观察窗口都比较封闭,想输出自定义日志格式还得靠串口助手。VScode这边不一样,任务面板、输出流、JSON配置全是明文,规则清晰,出了问题能顺着配置一路查下去,而不是面对一堆图形界面的灰色按钮。

1.2 VScode为嵌入式开发准备了什么

VScode本质上是编辑器,能把编译和调试交给外部工具链。这个"外部化"的想法,正好和嵌入式工程经常使用的命令行工具链合拍。我们用arm-none-eabi-gcc编译C代码,用CMake或Makefile描述工程拓扑,用openocd或J-Link实现烧录和调试,VScode只负责把这些工具挂在快捷键上、把输出面板收拢在一起、把调试器的断点状态可视化。

对APM32F1来说,Cortex-M3内核意味着GCC工具链完全支持,配合arm-none-eabi-objcopy生成hex和bin文件,配合arm-none-eabi-gdb进行源码级调试,整套流程在VScode界面上跑起来以后,体验比Keil里的黑盒编译要通透得多。而且插件生态里还有C/C++ IntelliSense,能对嵌入式寄存器定义做实时跳转,这种效率红利是实打实的。

1.3 不适用场景提醒

也要说句公道话,并不是所有情况下都建议迁移。如果整个团队已经围绕Keil建立了完整的脚本体系和培训流程,那换工具链的成本可能高于收益;或者你手头的调试器是官方独有协议,像某些专用烧录器只提供IDE插件,那VScode暂时也替代不了。我的建议是:个人项目、学习项目、极客向的验证项目,放心折腾;产线在跑、所有人都在用Keil、你能不动就不动的项目,别为了玩VScode而玩VScode。

2. 环境搭建:从零装出可用的APM32F1工具箱

2.1 工具链选型:arm-none-eabi-gcc与make

在VScode里编译APM32F1,核心工具链是arm-none-eabi-gcc。这套GNU工具链在ARM官网或者各镜像站都能下到,安装时记得把arm-none-eabi-gcc的bin目录加入系统PATH。装完以后在终端里执行arm-none-eabi-gcc --version,能输出版本信息就说明OK。

接下来是构建工具。我建议直接用make,配合Makefile管理工程。Windows下可以用MSYS2或者pacman安装make,如果不想折腾环境,也可以在WSL里跑所有编译命令,VScode连接WSL远程开发。这里有一个关键点:工具链版本尽量保持一致,我早期在macOS上用较老的arm-none-eabi-gcc编译APM32F1,结果链接时出现了一些奇怪的符号地址错位,换成和维护版本后一切正常。嵌入式开发里,编译器版本往往决定了启动文件的ABI细节,换来换去非常容易踩到莫名其妙的坑。

2.2 VScode插件配置:C/C++、Cortex-Debug、LinkerScript

VScode插件安装谁都会,但关键是怎么配置。我实际使用的插件列表是:

  • C/C++(ms-vscode.cpptools):提供代码补全、跳转和编译诊断;
  • Cortex-Debug(marus25.cortex-debug):专门为Cortex-M内核设计的调试插件,配合OpenOCD和J-Link都能用;
  • LinkerScript语法提示(可选):打开.ld链接脚本时高亮,能减少语法错误;
  • Task Explorer(可选):快速查看Makefile里的target。

装完插件以后,还得建立c_cpp_properties.json,这里最容易被卡住。我一开始没配置includePath,导致寄存器定义和标准外设库的头文件全部爆红,跳转也失效。配置如下(以你的工程实际路径为准):

{ "configurations": [ { "name": "APM32F1", "includePath": [ "${workspaceFolder}/Core/Inc", "${workspaceFolder}/Drivers/CMSIS/Include", "${workspaceFolder}/Drivers/APM32F1xx_StdPeriph_Driver/Inc", "${workspaceFolder}/Startup" ], "defines": [ "USE_STDPERIPH_DRIVER", "APM32F10X_HD" ], "compilerPath": "/usr/bin/arm-none-eabi-gcc", "cStandard": "c11", "intelliSenseMode": "linux-gcc-arm" } ], "version": 4 }

defines里的USE_STDPERIPH_DRIVER是标准外设库的开关,APM32F10X_HD对应高密度型号。注意APM32F103RBT6属于HD(高密度)还是MD(中密度),需要查你自己芯片的Flash大小:256KB以上为HD,128KB及以下为MD或LD。我用的RBT6是128KB,按理说属于MD,但实际原厂SDK里仲裁时需要看清启动文件里用的是HD还是MD,不同的宏会导致不同的外设映射和中断向量。这是VScode配置里最容易忽略而又致命的一步。

2.3 获取APM32F1的支持包和启动文件

APM32F1作为极海半导体的Cortex-M3产品,官网提供的SDK包一般包含CMSIS目录、标准外设库、启动文件和一个模板工程。拿到SDK以后,把以下内容拷到工程里:

  • Drivers/CMSIS/Device/APM32F1xx/Include:内含寄存器地址定义;
  • Drivers/CMSIS/Device/APM32F1xx/Source/startup_apm32f10x_xxx.s:根据容量选择启动文件;
  • Drivers/APM32F1xx_StdPeriph_Driver:标准外设库源码,包括GPI/O、RCC、USART等。

如果你实在找不到原厂SDK,退而求其次用STM32F1的启动文件也能跑,但中断向量和时钟初始化可能不完全匹配国产芯片的私有外设。我的建议是恪守一个原则:能用原厂启动文件就不要用替代品,否则烧进去以后跑飞了,你第一个怀疑工具链,其实只是启动文件里的SystemInit没配好。

3. 工程配置:项目结构、链接脚本与Makefile的兼容日记

3.1 工程目录规划与CMSIS文件整理

一个干净的目录结构能省去后面大量配置时间。我的APM32F103工程最终是这样组织的:

apm32f103_demo/ │ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ └── system_apm32f1xx.h │ └── Src/ │ ├── main.c │ └── system_apm32f1xx.c │ ├── Drivers/ │ ├── CMSIS/ │ │ ├── Include/ │ │ └── Device/APM32F1xx/ │ │ ├── Include/ │ │ └── Source/ │ └── APM32F1xx_StdPeriph_Driver/ │ ├── Inc/ │ └── Src/ │ ├── Startup/ │ └── startup_apm32f10x_hd.s │ ├── obj/ ├── apm32f103.ld ├── Makefile └── README.md

这里有个细节:system_apm32f1xx.c里包含了SystemInit函数的实现,它会被启动文件调用。如果你从STM32工程拷过来,很可能缺失这个文件的APM32特定初始化逻辑,导致时钟频率不对。CMSIS目录的Include里放的是基础寄存器定义,Device目录里放的是设备特有定义,这两个位置都不要放错。

3.2 链接脚本(.ld)里必须改的RAM和FLASH地址

链接脚本是决定程序能不能稳定运行的基石。我用的是APM32F103RBT6,Flash 128KB,RAM 20KB。链接脚本中,最关键的两行是这样的:

MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K }

FLASH的基地址固定为0x08000000,这是Cortex-M3内置Flash统一映射地址。RAM基地址为0x20000000,长度要和你手上的芯片具体规格一致。如果你写错了LENGTH,链接器虽然不会报错,但运行时变量堆栈分配会覆盖到不存在的物理地址,造成极其隐蔽的数据错乱。我早期用256K的Flash脚本烧写RBT6,程序启动正常,但只要运行到频繁写变量的逻辑就随机复位,排查了一天才发现是链接脚本的Flash长度超了实际容量。

堆栈和堆的设置同样重要:

_heap_size = 0x1000; _stack_size = 0x400;

堆为4KB,栈为1KB,对RBT6的20KB RAM来说是够用的。如果开了较大缓冲区或者printf浮点支持,栈还要适当加大。嵌入式里面的经典错误是栈溢出导致系统跑飞,却没有任何预兆。

3.3 Makefile的关键编译参数

Makefile是整套自动构建的核心。一个针对APM32F1的最小可用Makefile关键部分大概长这样:

TARGET = apm32f103_demo # 芯片架构定义 CPU = -mcpu=cortex-m3 -mthumb # 标准外设库开关 DEFS = -D USE_STDPERIPH_DRIVER -D APM32F10X_HD # 源文件收集 C_SOURCES = \ Core/Src/main.c \ Drivers/APM32F1xx_StdPeriph_Driver/Src/apm32f1xx_gpio.c \ Drivers/APM32F1xx_StdPeriph_Driver/Src/apm32f1xx_rcc.c \ Startup/startup_apm32f10x_hd.s # 编译参数 CFLAGS = $(CPU) $(DEFS) -I Core/Inc -I Drivers/... -O2 -Wall -fno-common # 链接参数 LDFLAGS = -T apm32f103.ld $(CPU) --specs=nano.specs --specs=nosys.specs all: $(TARGET).bin $(TARGET).hex $(TARGET).elf: $(C_SOURCES) arm-none-eabi-gcc $(CFLAGS) $(LDFLAGS) -o $@ $^ -lc -lm $(TARGET).bin: $(TARGET).elf arm-none-eabi-objcopy -O binary $< $@ $(TARGET).hex: $(TARGET).elf arm-none-eabi-objcopy -O ihex $< $@ clean: rm -f $(TARGET).elf $(TARGET).bin $(TARGET).hex

这里要重点解释-mcpu=cortex-m3 -mthumb。APM32F1的内核是Cortex-M3,不支持浮点单元,也不需要-mfloat-abi=hard。如果你误加了-mfloat-abi=hard,代码编译出来的浮点运算指令全部非法,芯片一执行就会进HardFault。另一个容易被忽略的是--specs=nano.specs,它引入了优化过的嵌入式C库,占用的Flash空间大幅缩水,代价是某些标准库函数的处理不那么完整,但对单片机场景完全够用。nosys.specs则是提供一套空的系统调用实现,避免链接时因为缺少sbrk之类的符号而失败。

4. 编译、烧录与调试:打通代码到芯片的最后一公里

4.1 一键编译并生成hex/bin

在VScode里配置好tasks.json以后,就可以按Ctrl+Shift+B直接构建。我的tasks.json核心配置是:

{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "make", "args": ["clean", "all"], "group": { "kind": "build", "isDefault": true }, "problemMatcher": [ "$gcc" ] } ] }

problemMatcher设为$gcc,编译器的报错信息就能直接显示在VScode的"问题"窗口里,点击即可跳转到对应代码。这就是VScode开发嵌入式很爽的一个点:不用切到终端窗口去抓error。编好以后,obj/目录下会出现.elf、.bin、.hex三种文件,arm-none-eabi-size工具还能输出占用Flash和RAM的具体情况。我平时编译完会执行一次arm-none-eabi-size -A target.elf,确认段分配是否符合预期。

4.2 OpenOCD与J-Link烧录配置

烧录APM32F1有两种常见路线,取决于手头调试器。

第一种是OpenOCD配合DAP-Link或ST-Link。OpenOCD支持STLINK和CMSIS-DAP协议,而APM32F1和STM32F1的内核一致,所以target配置可以直接使用stm32f1x.cfg。运行命令:

openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program apm32f103_demo.hex verify reset exit"

这里用到的是OpenOCD的命令编程模式:加载配置、烧录hex、校验、复位退出。如果烧录时出现target not in debug state一类错误,先检查调试器连接是否稳固,再确认芯片已经进入SWD模式——部分开发板需要手动拉高BOOT0才能让芯片处于可编程状态。

第二种是J-Link的官方命令行工具。J-Link配合SWD接口烧录更省心,命令如下:

JLinkExe -device STM32F103RB -if SWD -speed 4000 -autoconnect 1

进入J-Link终端后执行:

loadfile apm32f103_demo.hex r g

J-Link的device参数直接写STM32F103RB也能用,因为Cortex-M3的SWD协议栈是内核自带的,调试器只需识别IDCODE即可。当然,你想写APM32F103RB但J-Link软件未必认识,这种情况下用STM32F103RB是最稳妥的兼容写法。

4.3 VScode调试器的launch.json配置与断点体验

调试插件Cortex-Debug连接OpenOCD或J-Link的GDB Server,然后在VScode里体验图形化断点、变量监视和寄存器查看。我惯用的launch.json是这样的:

{ "version": "0.2.0", "configurations": [ { "name": "Cortex Debug OpenOCD", "cwd": "${workspaceFolder}", "executable": "./obj/apm32f103_demo.elf", "request": "launch", "type": "cortex-debug", "servertype": "openocd", "configFiles": [ "interface/stlink.cfg", "target/stm32f1x.cfg" ], "gdbPath": "/usr/bin/arm-none-eabi-gdb", "device": "STM32F103RB", "svdFile": "${workspaceFolder}/APM32F103.svd" } ] }

svdFile填写的是SVD描述文件,它可以让调试器直接映射芯片内部寄存器的名字和位域,调试外设时事半功倍。APM32F103的SVD文件在SDK包里一般能找到,找不到的话用STM32F103的SVD也基本能跑,但寄存器命名可能有些出入。实际调试时,我习惯在main()入口设一个断点,然后Reset后全速跑到断点,接着在RCC_APB2PeriphClockCmd这类外设初始化函数里单步走一遍,观察寄存器值的实时变化,确认外设时钟是否真正打开——这个能力在Keil里也不是没有,但VScode里看浮点数和结构体变量明显更直观。

5. 从工程实操中总结的避坑经验与优化思路

5.1 最容易踩的坑:启动文件与芯片型号不匹配

我踩过最痛的坑就是启动文件用错。APM32F103和STM32F103虽然兼容,但极海原厂的启动文件里,中断向量表在尾部会额外带上一些芯片特定处理。比如某些批次的高密度芯片有多余的外设中断,向量表布局稍微不同,你拿STM32F103的启动文件硬解,程序普遍能启动,但一旦触发了没有对应向量处理的中断,就会跳到一个空指针位置,整个系统变成植物人状态。

所以务必对照你的芯片容量选择startup_apm32f10x_ld.s、startup_apm32f10x_md.s还是startup_apm32f10x_hd.s。这个字母还决定了代码里#define APM32F10X_HD之类的宏,宏定义直接关联外设库中哪些外设被编译进工程,搞错了编译能过,运行必定出问题。出事后最有效的手段是,先读芯片的IDCODE和Flash大小寄存器,确认实际容量,再回MCU选型表核对型号编码——别信包装,信寄存器。

5.2 VScode代码提示失效的排查思路

VScode开发C语言项目,最大的甜头是IntelliSense,但有时候它莫名失效,满屏波浪线。我排查过几类成因:第一,includePath没写全或者相对路径不对,尤其是CMSIS的Device路径和标准外设库路径;第二,defines漏了USE_STDPERIPH_DRIVER或APM32F10X_HD,导致条件编译的一部分代码被跳过,IntelliSense看不到那些声明;第三,.vscode/settings.json里如果设置了C_Cpp.default.configurationProvider,可能会和c_cpp_properties.json产生冲突,需要清掉provider让插件直接读取json。

另外一个小技巧是,改完c_cpp_properties.json以后,按Ctrl+Shift+P执行C/C++: Reset IntelliSense Database,强制刷新索引。VScode的IntelliSense引擎偶尔会因为工程文件变更而缓存旧数据,尤其是新增了源文件或头文件之后,不刷新就会一直报错。有一回我新建了一个system_apm32f1xx.c文件,IntelliSense里函数跳转全部失效,重置数据库以后立刻恢复正常。

5.3 多平台工程管理与效率优化思路

VScode开发APM32F1的优势之一,是可以很方便地在Windows、Linux、macOS之间保持同一个工程。我的做法是:所有路径均用相对路径,Makefile里用变量拼接路径,避免Windows下反斜杠和Linux下斜杠的纠纷。在Windows上如果用WSL做编译,VScode的"Remote-WSL"插件可以让你直接在Linux子系统里编辑代码,性能表现和原生Linux一致。

效率优化方面,还有两个很实用的思路。第一,把构建、烧录、调试三个动作分别绑定到自定义快捷键,比如Ctrl+Shift+B构建,F5调试,这样手不离键盘。第二,在Makefile里增设flash和debug目标,分别调用OpenOCD烧录和启动GDB Server,把重复性的命令行操作全部收拢到Task里。写代码时配合Git做版本管理,每次改动都可以清晰地看到编译脚本、链接脚本的差异,这种全是文本的工程结构,确实把嵌入式开发拉回了现代软件工程的舒适区。

最后再分享一个小习惯:每次换芯片型号的时候,我会第一时间去核对三处——启动文件的容量匹配、链接脚本里Flash和RAM的容量、编译宏定义。这三者不一致,所有后续努力都会白费。APM32F1在VScode下吃透了以后,你再来用任何基于Cortex-M的国产MCU都会觉得无比顺手,毕竟背后的GCC、OpenOCD、GDB这些开源工具链都是相通的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询