ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

把APM32F1开发环境迁到VS Code:GCC、OpenOCD与Makefile实战

把APM32F1开发环境迁到VS Code:GCC、OpenOCD与Makefile实战 1. 把APM32F1搬到VS Code值不值得折腾APM32F1这颗芯片极海那边给的标准外设库和例程默认都是按Keil的使用习惯来的。但说句实话Keil那套操作逻辑放到今天确实有点跟不上节奏了。代码补全基本靠猜文件跳转偶尔还跳错界面谈不上美观最大的麻烦还是一台机器一个授权换电脑就得重新折腾一遍授权。我在用APM32F103做一个小批量项目的时候就萌生了把整个工程迁到VS Code上的念头干脆把这套开发流程彻底换掉。VS Code这边的生态其实已经很成熟了。C/C扩展提供代码补全、语法高亮、引用查找再加上cortex-debug和OpenOCD的组合断点调试做得可以比Keil还顺手。APM32F1本身是Cortex-M3内核指令集和STM32F1系列完全一致外设寄存器映射和用法也基本对齐了STM32F1所以标准外设库、启动文件、链接脚本这些核心内容都可以从极海官方SDK里直接拿过来。剩下要做的事情就是搭编译器、配构建脚本、连调试器这三件事。这篇文章适合想脱离Keil、用VS Code做APM32F1开发的工程师看。已经熟悉芯片外设但对VS Code环境还比较陌生的同学可以照着步骤直接把工程跑起来。我也会把自己踩过的坑一并整理出来比如头文件路径串了导致编译不过、OpenOCD不认APM32的芯片名之类的给后面的人省点时间。2. 环境搭建一套趁手的工具链2.1 需要哪些工具VS Code本身只是一个编辑器按下编译键的时候真正干活的是背后那几样工具缺一不可工具作用Windows下常见安装方式VS Code代码编辑、插件管理官网下载安装包C/C扩展代码提示、跳转、调试扩展商店安装ms-vscode.cpptoolsarm-none-eabi-gccARM交叉编译工具链下载压缩包解压GNU Make驱动编译流程MinGW或单独安装makeOpenOCD烧录和调试桥接下载Windows版本这五个组合起来就是一套完整的编译烧录闭环。注意这里用的是arm-none-eabi-gcc不是PC上用的x86 gcc两者完全是两码事。Cortex-M3支持Thumb-2指令集arm-none-eabi-gcc会根据编译参数生成对应的机器码这一步Keil其实也做了只是没有把细节暴露给你。2.2 ARM GCC工具链安装细节在Windows上装arm-none-eabi-gcc最省事的做法是去Arm官方开发者网站下载最新的Windows版本压缩包。解压之后把bin目录所在的完整路径加进系统环境变量Path里。这一步很多人觉得简单但有两个坑比较常见。第一个坑是路径里有空格。如果你把工具链解压到了C:\Program Files\Arm\...这种带空格的路径Makefile里直接写arm-none-eabi-gcc有时候会出幺蛾子尤其在脚本里拼接完整路径的时候。我的习惯是放到C:\arm-gcc\这种没有空格的根目录下省去后面排错的工夫。第二个坑是环境变量设置完以后终端尤其VS Code内嵌终端不会立刻刷新。需要把VS Code完全关掉重开或者在新开的终端里跑一遍arm-none-eabi-gcc --version确认版本号能正常打印。有时候终端没刷新编译时提示找不到命令很多人以为是自己装错了其实只是环境变量没有重新加载而已。版本选择上现在官网主推12.x或者13.x的版本对Cortex-M3的支持都很完善没必要非得追新。我用的是12.3版本稳定编译出来的固件体积和Keil默认参数相比差别不大。2.3 Make工具和OpenOCD的安装Make在Windows上稍微麻烦一点。可以装MinGW-w64把它的bin目录加入Path里面有mingw32-make.exe。但这个程序名字不叫make有些脚本里直接调make会找不到。解决办法是把mingw32-make.exe复制一份改成make.exe或者直接在Makefile的调用处写mingw32-make。另一个选择是安装Scoop或者Chocolatey然后通过命令行直接装make和openocd包。Scoop装make特别方便一条scoop install make搞定而且路径统一省心。我后来在Windows上用的就是Scoop这套方案。OpenOCD是烧录调试的关键。Windows版可以去OpenOCD官网的release页面下也可以用Scoop装。装完在终端跑openocd --version验证。如果懒得到处找安装包直接在VS Code的终端里跑这两条命令就能把环境补齐scoop install make scoop install openocd一定要确认make和openocd的bin目录都加进了Path然后重新打开一个终端窗口测试。这一步搞定之后工具链部分就算完整了。3. 工程结构与核心配置文件3.1 从SDK里copy出工程骨架从极海官网下载到APM32F1的SDK包解压后里面会有一堆基于Keil的例程工程。第一步就是把这些例程里的核心代码提取出来放到一个干净的目录结构里。Keil的uvprojx工程文件只是一个壳真正的代码和库文件都在那个工程目录下我们只需要拿核心代码不需要那个工程文件本身。我习惯的目录结构是这样apm32f103_vscode_project/ ├── Core/ │ ├── startup_apm32f103.s │ ├── system_apm32f103.c │ └── system_apm32f103.h ├── Libraries/ │ ├── APM32F10x_StdPeriphDriver/ │ │ ├── inc/ │ │ └── src/ │ └── CMSIS/ │ ├── core_cm3.h │ └── system_apm32f103.h ├── User/ │ ├── main.c │ ├── apm32f10x_it.c │ └── apm32f10x_it.h ├── .vscode/ │ ├── c_cpp_properties.json │ ├── tasks.json │ └── launch.json ├── Makefile └── apm32f103c8t6.ldCore目录放启动文件、系统时钟文件Libraries放外设驱动库和CMSIS头文件User放用户代码。这样做的意思是将来要加中间层比如自己写的传感器驱动或者通信协议栈再拆一个Middlewares文件夹出来工程边界清清楚楚查找文件方便也不容易改乱。3.2 启动文件和容量选择启动文件startup_apm32f103.s里面定义的是中断向量表还包含复位后执行的启动代码初始化栈指针、调用SystemInit、然后跳转到main。芯片上电第一步执行的就是它没有这个文件C语言运行环境根本起不来。SDK里面提供的启动文件一般按芯片Flash容量分三种小容量16-32KB Flash、中容量64-128KB Flash、大容量256KB以上后缀分别是ld、md、hd。比如APM32F103C8T6是64KB Flash对应startup_apm32f103_md.s。选错启动文件会导致中断向量表错位表现出来就是程序能跑但一进中断就死机或者偶尔复位之后行为异常。这个只能靠启动流程排查才看得到务必核对清楚。3.3 链接脚本(.ld)的改动点链接脚本决定了代码段、数据段、BSS段在Flash和RAM里的具体位置。APM32F1的SDK例程里面有的型号会带.ld文件如果找不到直接拿STM32F1对应型号的链接脚本改也行因为内存布局基本一致。以APM32F103C8T6为例Flash是64KB起始地址0x08000000RAM是20KB起始地址0x20000000。链接脚本核心只需关注两处MEMORY声明以及ENTRY入口地址ENTRY(Reset_Handler) MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }如果你的板子是APM32F103RCT6256KB Flash把LENGTH改成256K就行。这里有个容易忽略的点如果Flash容量定义比实际芯片大链接的时候不一定报错但生成的固件在刷写时可能超过实际容量OpenOCD会报写越界错误。反过来如果定义比实际小代码稍微多写一点就链接失败提示region FLASH overflowed by xxx bytes一眼就能看明白。3.4 Makefile核心规则解析Makefile是整个构建系统里最核心的脚本文件。我不太喜欢用IDE自动生成的构建系统因为出了问题看不懂它在干什么。手写Makefile虽然看起来土但每一步做什么都明明白白。下面是一个精简但完整的Makefile版本# 工具链定义 CC arm-none-eabi-gcc AS arm-none-eabi-gcc LD arm-none-eabi-gcc OBJCOPY arm-none-eabi-objcopy SIZE arm-none-eabi-size # 芯片参数 MCU -mcpucortex-m3 -mthumb DEFS -DAPM32F10X_MD -DUSE_STDPERIPH_DRIVER # 目录 TARGET apm32f103_demo BUILD_DIR build CORE_DIR Core LIB_DIR Libraries/APM32F10x_StdPeriphDriver USER_DIR User # 源文件收集 C_SOURCES \ $(wildcard $(CORE_DIR)/*.c) \ $(wildcard $(LIB_DIR)/src/*.c) \ $(wildcard $(USER_DIR)/*.c) ASM_SOURCES \ $(wildcard $(CORE_DIR)/*.s) # 头文件路径 C_INCLUDES \ -I$(CORE_DIR) \ -I$(LIB_DIR)/inc \ -I$(LIB_DIR) \ -ILibraries/CMSIS \ -I$(USER_DIR) # 目标文件映射 OBJS $(addprefix $(BUILD_DIR)/, $(C_SOURCES:.c.o) $(ASM_SOURCES:.s.o)) # 编译参数 CFLAGS $(MCU) $(DEFS) $(C_INCLUDES) -Wall -O0 -g3 LDFLAGS $(MCU) -T$(TARGET).ld --specsnano.specs --specsnosys.specs all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).bin $(BUILD_DIR)/%.o: %.c | $(BUILD_DIR) $(CC) $(CFLAGS) -c $ -o $ $(BUILD_DIR)/%.o: %.s | $(BUILD_DIR) $(AS) $(CFLAGS) -x assembler-with-cpp -c $ -o $ $(BUILD_DIR)/$(TARGET).elf: $(OBJS) $(LD) $(LDFLAGS) $^ -o $ $(SIZE) $ $(BUILD_DIR)/$(TARGET).bin: $(BUILD_DIR)/$(TARGET).elf $(OBJCOPY) -O binary $ $ clean: rm -rf $(BUILD_DIR) .PHONY: all clean几个关键参数要展开说一下-mcpucortex-m3 -mthumb指定CPU内核是Cortex-M3并且使用Thumb指令集。这两个参数缺一个都会出严重问题尤其-mthumb不写链接阶段可能报错说target CPU does not support ARM mode。-DAPM32F10X_MD这个宏很重要它告诉外设驱动库当前编译的是中容量型号从而决定Flash分页大小、外设地址范围等配置。宏写错会导致外设寄存器地址判断异常表现就是程序编译没问题但某些外设运行时不工作。--specsnano.specs使用精简版C库能显著减小固件体积。但注意nano库对printf浮点格式化支持有限如果代码里用到了%f需要另做处理。-O0 -g3调试阶段的优化等级和调试信息级别。我用的是-O0变量实时查看不会出现optimized out量产固件再切回-O2。Makefile里还用了addprefix函数做源文件路径到目标文件路径的映射这样不管把.c文件放在哪个目录编译时都能在build目录下生成对应的.o文件不会污染源码目录。| $(BUILD_DIR)这种写法是Makefile的order-only依赖意思是build目录只要不存在就创建但如果目录已经存在不会因为目录时间戳变化而触发重新编译。4. VS Code侧配置让编辑器懂你的工程4.1 c_cpp_properties.json代码提示的关键这是VS Code里C/C扩展读取的配置文件作用是告诉编辑器“我用的编译器是谁、头文件在哪、有哪些宏定义”。很多新手在这个文件上翻车其实它跟编译没有直接关系只影响代码智能提示、跳转、错误标注。我的配置长这样{ configurations: [ { name: APM32F1, includePath: [ ${workspaceFolder}/Core, ${workspaceFolder}/Libraries/APM32F10x_StdPeriphDriver/inc, ${workspaceFolder}/Libraries/CMSIS, ${workspaceFolder}/User ], defines: [ APM32F10X_MD, USE_STDPERIPH_DRIVER ], compilerPath: C:/arm-gcc/bin/arm-none-eabi-gcc.exe, cStandard: c11, intelliSenseMode: gcc-arm } ], version: 4 }两个容易踩坑的地方includePath必须和Makefile里-I指定的路径保持一致否则编辑器会找不到头文件但实际编译能过。很多人被这个问题搞得怀疑人生以为工程出问题了其实只是两边路径没对齐。defines里的宏也要和Makefile里的-D保持一致否则编辑器解析出来的外设驱动库代码路径可能是错的跳转会跳进错误的#if/#else分支。比如你明明定义的是大容量芯片但编辑器按默认情况解析看到的外设寄存器地址范围可能和实际编译用的不一致。配置完这一步代码里输入APM32开头的内容应该就有补全了。如果还是没有提示检查一下VS Code右下角是否正在解析C/C工程大工程第一次全量索引可能要几分钟耐心等一下就出来了。4.2 tasks.json一键编译tasks.json负责把Makefile跑起来。VS Code里按CtrlShiftB就会执行这里定义的构建任务。我的配置是这样的{ version: 2.0.0, tasks: [ { label: build-apm32f1, type: shell, command: make, args: [-j8], options: { cwd: ${workspaceFolder} }, group: { kind: build, isDefault: true }, problemMatcher: [ $gcc ] } ] }-j8表示并行编译8个编译任务多核CPU下编译速度能明显提升。problemMatcher设置为$gcc这样编译报错会直接在源码对应的行里标出波浪线点一下就能跳到具体位置不用在终端里翻输出。如果不想依赖Make也可以用C/C扩展自带的编译模式但那样每个文件都要单独配命令维护成本太高。给MCU工程做构建Makefile或者CMake才是真正的生产力工具。4.3 launch.json断点调试调试APM32F1我推荐cortex-debug插件加OpenOCD的组合。在VS Code扩展商店搜cortex-debug安装即可。launch.json配置如下{ version: 0.2.0, configurations: [ { name: APM32F1 OpenOCD, type: cortex-debug, request: launch, servertype: openocd, cwd: ${workspaceFolder}, executable: ${workspaceFolder}/build/apm32f103_demo.elf, device: apm32f103, configFiles: [ interface/stlink.cfg, target/stm32f1x.cfg ], svdFile: ${workspaceFolder}/APM32F103xx.svd } ] }重点解释两个地方。device字段填apm32f103但至少在我用的OpenOCD版本里没有直接提供APM32F1专属的target配置文件所以实际加载的是target/stm32f1x.cfg。这不是偷懒。是因为APM32F1的Flash和调试接口与STM32F1保持一致OpenOCD在探测芯片ID时能读到对应的IDCODE配置能正常跑起来。如果填一个不存在的设备名OpenOCD会直接报错找不到配置文件。svdFile指定外设寄存器描述文件有了它调试的时候外设寄存器窗口里就能看到GPIOA、USART1这些寄存器位段的实际值和含义。这个.svd文件在极海SDK里不一定带但STM32F1系列的SVD文件也能用毕竟寄存器布局一致。找不到就留空不影响基本调试。5. 编译、烧录、调试全流程5.1 先跑一个LED闪烁验证环境在写任何复杂业务逻辑之前建议先让一个LED亮起来。这一步能验证整条工具链是通的。main.c可以写得非常简单#include apm32f10x.h void delay(void) { volatile uint32_t i; for (i 0; i 500000; i); } int main(void) { GPIO_Config_T gpioConfig; RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_GPIOB); gpioConfig.mode GPIO_MODE_OUT_PP; gpioConfig.pin GPIO_PIN_0; GPIO_Config(GPIOB, gpioConfig); while (1) { GPIO_SetBits(GPIOB, GPIO_PIN_0); delay(); GPIO_ResetBits(GPIOB, GPIO_PIN_0); delay(); } }这段代码做的事情很简单使能GPIOB时钟把PB0配置成推挽输出然后循环翻转电平。如果LED接到PB0编译烧录之后应该看到灯在闪。如果这一步通了说明编译器、链接脚本、启动文件、烧录工具全链路都没问题。5.2 编译流程与产物检查在VS Code里按CtrlShiftB运行构建任务终端里会滚动输出编译日志。正常情况下能看到每个.c文件被编译成.o文件的命令最后链接生成.elf并且打印出各个段的大小。看到类似text数据bss dec hex那几列数字就说明编译通过了。一个点亮LED的工程text段在几千字节量级。如果text超过了芯片Flash容量链接脚本会直接报错这是一个硬性拦截。编译产物里有两个比较重要的文件.elf用于调试带着完整的符号表和调试信息.bin用于烧录纯二进制文件。两者不能混用烧录时优先用.bin调试时才用.elf。5.3 烧录方式选择与实操APM32F1支持的烧录方式和STM32F1几乎一样串口ISP、SWD调试器都可以。在VS Code这套环境里我用得最多的是OpenOCD加ST-Link或者DAP-Link。烧录命令是这样openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \ -c program build/apm32f103_demo.bin 0x08000000 verify reset exit这条命令做了三件事把bin文件烧写到0x08000000地址做一次读回校验然后复位并退出。verify参数强烈建议保留烧写过程中数据不对的情况能提前暴露出来。如果你手头只有串口ISPWindows下可以用极海官方提供的ISP工具。但那个工具对波特率和串口芯片型号比较敏感容易连接不稳定推荐优先用SWD调试器省心得多。5.4 调试配置与实战在launch.json配置好以后按F5就能进入调试会话。我实际调试中最常用的几个操作打断点鼠标在代码行左侧点一下出现红点即可单步执行F10单步不进入函数F11单步进入函数看变量调试侧边栏的变量窗口实时显示局部变量值看外设寄存器如果配了svd文件外设寄存器窗口能直接显示GPIOA、USART1的寄存器状态实际项目里我遇到过一个典型场景USART1发送数据偶尔乱码。直接用调试器看USART1的BRR寄存器值发现初始化顺序导致波特率没生效断点停在初始化代码后面寄存器窗口里SR标志位一直为0说明串口外设的时钟没使能。这种问题如果在Keil里得靠变量窗口手动翻寄存器地址在VS Code里借助SVD文件直接能看到外设寄存器名排查效率高了一个量级。6. 常见问题与避坑实录把我在实际折腾中遇到的典型问题整理成一张表照着排查能省很多时间现象原因排查方法终端提示arm-none-eabi-gcc不是内部或外部命令环境变量没生效或者路径不对关掉重开终端跑arm-none-eabi-gcc --version验证编译器报错无法打开头文件core_cm3.hinclude路径漏了CMSIS目录对比Makefile的-I和c_cpp_properties.json的includePath链接报错region FLASH overflowed代码体积超过Flash或链接脚本容量写小查看size输出核对芯片实际容量OpenOCD报错无法识别设备调试器驱动没装好或接线错误检查ST-Link驱动单独跑openocd探测探针make: 未找到命令Windows环境里没有GNU Make或名字不是make安装make或者用mingw32-make代替调试器连不上目标板目标板没有供电或复位脚被拉低用万用表量VCC和NRST电平程序能跑但一进中断就死机启动文件选错容量或APM32F10X_MD宏写错核对启动文件后缀和宏定义是否匹配烧录后无反应且读回校验失败烧录地址写错或Flash写保护开启检查OpenOCD烧写地址是否为0x08000000这几个问题里头文件路径串了和启动文件容量选错是最隐蔽的。头文件路径问题还好排查报错信息里会直接列出找不到的文件名启动文件容量选错几乎没有提示属于运行期故障只能从启动流程上反复排查。还有一个小坑值得单独说cortex-debug调试时如果连不上先确认OpenOCD有没有被其他进程占用端口。之前遇到过VS Code的调试终端占用了3333端口我又手动起了一个OpenOCD实例结果两个进程打架调试会话一直卡在连接阶段。把这个手动实例杀掉重新按F5就正常了。另外关于烧录时的接线也要提一句。ST-Link的SWD接口是四根线SWCLK、SWDIO、GND和3.3V。有一次我把SWCLK和SWDIO接反了OpenOCD提示找不到目标芯片查了半天才反应过来是线序问题。所以接线之后先用万用表量一遍通断别急着开软件。7. 一些使用体会和扩展方向搭完这套环境我最大的感受是VS Code“编辑器加命令行工具各司其职”的模式很契合嵌入式开发。编辑器负责代码阅读和交互编译和烧录交给专业的命令行工具配置层通过JSON文件对接没有被某个IDE的黑盒工程绑死。遇到问题就从编译日志一行行倒推排错思路比在Keil里清晰很多。扩展方向上这套工程可以直接加一个调试用的RTT日志输出或者引入CMake接管构建流程再配合Git做版本管理。甚至可以把编译、烧录、测试串成一条流水线一个命令全部搞定。这些都是用VS Code做APM32F1开发留下的自然延伸空间。最后分享一个小技巧把Makefile里的目标分成编译、烧录、调试三个独立入口调试的时候在VS Code里开多个终端分别执行比每次手动改launch.json快得多。这也是我从Keil切到VS Code之后最直观的效率提升。APM32F1这套环境搭起来之后后面换芯片工程也只需要改改链接脚本和启动文件整个流程完全可以复用。
返回列表