ARM MCU 通用开发环境:CMake + Ninja + GCC + OpenOCD + VS Code
面向 Windows + VS Code 的可复用嵌入式裸机开发方案
适用范围:以 Arm Cortex-M MCU 为主,尤其适合 STM32 系列,也可以扩展到其他 OpenOCD 支持的芯片。
1. 为什么要搭建这套开发环境
在 MCU 开发中,我们经常使用 Keil、IAR 等集成开发环境。
这些工具确实方便,但随着项目规模增加、团队协作、自动化构建、跨平台开发等需求出现,一个更加通用的开发链会更有价值。
本文希望建立这样一套开发环境:
VS Code
+
CMake
+
Ninja
+
GNU Arm Embedded Toolchain
+
OpenOCD
+
GDB它的目标不是针对某一个具体 STM32 工程,而是搭建一套可以复用到不同 Cortex-M MCU 项目中的开发方式。
这套环境重点解决以下问题:
- 如何安装和组织开发工具。
- CMake、Ninja、GCC、OpenOCD 之间是什么关系。
- 如何生成 ELF、BIN、HEX。
- 如何实现一键编译。
- 如何实现一键烧录。
- 如何通过 VS Code F5 自动启动调试。
- 换 MCU 时,哪些部分需要修改。
- 如何减少对某一个 IDE 工程格式的依赖。
它可以用于:
- 裸机程序
- STM32 HAL / LL
- CMSIS
- FreeRTOS
- Bootloader
- 驱动验证
- 算法板级移植
- 各类 Cortex-M 工程
2. 整体开发链架构
整个构建和烧录过程可以理解成:
源代码 / HAL / CMSIS / RTOS / 驱动
↓
CMakeLists.txt
↓
CMake
↓
Ninja
↓
arm-none-eabi-gcc / g++ / as / ld
↓
ELF / BIN / HEX / MAP
↓
OpenOCD
↓
CMSIS-DAP / ST-Link / J-Link
↓
SWD / JTAG
↓
MCU在 VS Code 中,还可以增加一层自动化:
VS Code
│
├── CMake Tools
│ └── 配置 / 编译
│
├── tasks.json
│ └── Build / Flash / Erase
│
└── Cortex-Debug
└── F5 启动 OpenOCD + GDB最终就可以做到:
写代码
↓
编译
↓
烧录
↓
调试基本都在 VS Code 中完成。
J-Link 也可以通过 OpenOCD 使用,但实际开发中,SEGGER 自带的 GDB Server 通常更加直接。本文主要以 CMSIS-DAP、ST-Link 和 OpenOCD 作为示例。
3. 这套方案适用于哪些 MCU
这套方案能不能使用,关键不在于“是不是 STM32”,而在于以下三个条件。
3.1 有对应的 GCC 交叉编译器
例如 Cortex-M 裸机开发通常使用:
arm-none-eabi-gcc而不是:
arm-linux-gnueabihf-gcc后者主要用于运行 Linux 的 ARM 平台。
3.2 有正确的启动文件和链接脚本
通常需要:
startup_xxx.s
CMSIS Device Header
HAL / LL / SDK
xxx_FLASH.ld3.3 OpenOCD 支持目标芯片和调试接口
常见平台适用情况:
| 芯片/平台 | 适用性 | 说明 |
|---|---|---|
| STM32F0/F1/F2/F3/F4/F7 | 非常适合 | Cortex-M0/M3/M4/M7,OpenOCD 支持成熟 |
| STM32G0/G4/L0/L4/L5/U5 | 非常适合 | 根据系列选择对应 target cfg |
| STM32H7 | 非常适合 | Cortex-M7/M4,需要关注 Cache、MPU、双核型号 |
| STM32WB/WL | 可用 | 无线/双核型号需要关注核选择和厂商工具 |
| NXP LPC / i.MX RT | 可用 | 需要对应 OpenOCD target 和 linker script |
| Nordic nRF52/nRF53 | 可用 | OpenOCD、pyOCD、J-Link 均比较常见 |
| RP2040 | 可扩展 | 工具链配置不同,但整体思路相同 |
| CH32 / GD32 | 视型号而定 | 重点看 GCC 架构和调试支持 |
因此,真正的通用层其实是:
CMake
+
Ninja
+
GCC
+
GDB
+
OpenOCD
+
VS Code换芯片时,通常修改的是:
CPU 编译参数
FPU 参数
float ABI
startup_xxx.s
CMSIS / HAL / SDK
链接脚本
OpenOCD target cfg4. Windows 环境需要哪些工具
推荐准备:
| 工具 | 作用 | 建议 |
|---|---|---|
| Arm GNU Toolchain | C/C++/汇编编译、链接、GDB | 必须 |
| CMake | 工程配置和构建生成 | 必须 |
| Ninja | 高速构建执行器 | 推荐 |
| OpenOCD | 烧录、擦除、寄存器访问、GDB Server | 必须 |
| VS Code | 编辑、任务、调试前端 | 推荐 |
| CMake Tools | VS Code CMake 集成 | 推荐 |
| C/C++ | IntelliSense、代码导航 | 推荐 |
| Cortex-Debug | MCU 图形化调试 | 强烈推荐 |
| Git | 版本管理 | 强烈推荐 |
5. 安装 GNU Arm Embedded Toolchain
对于 Cortex-M 裸机 MCU,推荐使用:
arm-none-eabi安装完成以后,在 PowerShell 中检查:
arm-none-eabi-gcc --version
arm-none-eabi-g++ --version
arm-none-eabi-objcopy --version
arm-none-eabi-size --version
arm-none-eabi-gdb --version常见工具及作用:
| 工具 | 作用 |
|---|---|
| arm-none-eabi-gcc | C 编译器 / 链接驱动 |
| arm-none-eabi-g++ | C++ 编译器 |
| arm-none-eabi-as | 汇编器 |
| arm-none-eabi-objcopy | ELF 转 BIN / HEX |
| arm-none-eabi-objdump | 反汇编、查看 Section |
| arm-none-eabi-size | 查看 Flash / RAM 占用 |
| arm-none-eabi-nm | 查看符号 |
| arm-none-eabi-gdb | 远程调试 |
典型 PATH 可能类似:
C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\<version>\bin6. 安装 CMake 和 Ninja
安装完成以后验证:
cmake --version
ninja --version查看实际路径:
where cmake
where ninja推荐使用 out-of-source build:
cmake -S . -B build -G Ninja
cmake --build build这样所有中间文件都会进入:
build/而不会污染源码目录。
如果构建缓存出现问题,也可以直接删除:
build/然后重新配置。
7. 安装 OpenOCD
Windows 下可以使用:
MSYS2 UCRT64提供的 OpenOCD,也可以使用其他可靠的 Windows OpenOCD 发行版。
安装完成后:
openocd --version
where openocdOpenOCD 一条典型命令由三部分组成:
openocd \
-s "<scripts目录>" \
-f interface/<调试器>.cfg \
-f target/<芯片>.cfg常见调试器配置:
interface/cmsis-dap.cfg
interface/stlink.cfg
interface/jlink.cfg常见 STM32 Target:
target/stm32f1x.cfg
target/stm32f4x.cfg
target/stm32f7x.cfg
target/stm32g0x.cfg
target/stm32h7x.cfg具体文件名应该以:
OpenOCD/scripts/target/目录实际存在的文件为准。
8. Windows PATH 配置
建议加入 PATH:
...\Arm GNU Toolchain...\bin
C:\Program Files\CMake\bin
...\Ninja
...\OpenOCD\bin统一检查:
where arm-none-eabi-gcc
where cmake
where ninja
where openocd修改 PATH 后,建议:
关闭 VS Code
关闭 PowerShell
重新打开否则旧进程可能仍然使用原来的环境变量。
9. 通用 CMake 工程结构
一个比较清晰的 Cortex-M 工程可以这样组织:
project/
├── CMakeLists.txt
├── cmake/
│ └── arm-none-eabi.cmake
│
├── Core/
│ ├── Inc/
│ └── Src/
│
├── Drivers/
│
├── startup_xxx.s
├── xxx_FLASH.ld
│
└── build/其中:
build/属于自动生成目录,一般不提交到 Git。
一个 MCU 工程至少要解决三个问题:
- CMake 使用的是
arm-none-eabi-gcc。 - 编译器知道目标 CPU / FPU。
- 链接器知道 Flash / RAM 布局。
10. 通用 Toolchain 文件
例如:
cmake/arm-none-eabi.cmake内容:
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)配置工程:
cmake -S . -B build -G Ninja ^
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake如果使用 CubeMX 直接生成了 CMake 工程,并且已经包含 Toolchain 配置,则不应该再重复叠加另一套 Toolchain 文件。
11. Cortex-M CPU 参数
不同内核的编译参数不同。
| 内核 | 示例参数 |
|---|---|
| Cortex-M0/M0+ | -mcpu=cortex-m0 -mthumb |
| Cortex-M3 | -mcpu=cortex-m3 -mthumb |
| Cortex-M4 | -mcpu=cortex-m4 -mthumb |
| Cortex-M4F | -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard |
| Cortex-M7 | -mcpu=cortex-m7 -mthumb + 对应 FPU 参数 |
这里尤其需要注意:
不能仅仅看到芯片是 Cortex-M7,就直接复制另一颗 M7 的 FPU 配置。
必须根据:
MCU 数据手册
官方 SDK
CubeMX 工程
官方编译参数确认:
-mcpu
-mfpu
-mfloat-abi否则可能:
链接失败
HardFault
浮点 ABI 不一致12. CMakeLists.txt 的核心结构
一个简化示例:
cmake_minimum_required(VERSION 3.22)
project(MyMCUProject C ASM)
add_executable(${PROJECT_NAME})
target_sources(${PROJECT_NAME} PRIVATE
Core/Src/main.c
Core/Src/stm32xx_it.c
startup_xxx.s
)
target_include_directories(${PROJECT_NAME} PRIVATE
Core/Inc
Drivers/CMSIS/Include
)
target_compile_definitions(${PROJECT_NAME} PRIVATE
USE_HAL_DRIVER
STM32xxxx
)
target_compile_options(${PROJECT_NAME} PRIVATE
-mcpu=cortex-m4
-mthumb
-ffunction-sections
-fdata-sections
)
target_link_options(${PROJECT_NAME} PRIVATE
-T${CMAKE_SOURCE_DIR}/xxx_FLASH.ld
-Wl,--gc-sections
-Wl,-Map=${PROJECT_NAME}.map
)真实 STM32 工程中还会有:
HAL
LL
CMSIS
FreeRTOS
BSP
Middleware等大量源码。
但是 CMake 的核心思想并不会变化。
13. 自动生成 ELF / BIN / HEX
推荐在 CMake 中直接加入 POST_BUILD:
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_SIZE}
$<TARGET_FILE:${PROJECT_NAME}>
COMMAND ${CMAKE_OBJCOPY}
-O ihex
$<TARGET_FILE:${PROJECT_NAME}>
${PROJECT_NAME}.hex
COMMAND ${CMAKE_OBJCOPY}
-O binary
$<TARGET_FILE:${PROJECT_NAME}>
${PROJECT_NAME}.bin
)这样:
cmake --build build之后自动得到:
MyMCUProject.elf
MyMCUProject.hex
MyMCUProject.bin同时打印:
text
data
bss等占用信息。
14. OpenOCD 手动烧录
开发阶段优先推荐烧:
ELF因为 ELF 自带:
Section
地址
符号
调试信息例如:
openocd ^
-s "<OpenOCD scripts>" ^
-f interface/cmsis-dap.cfg ^
-f target/stm32h7x.cfg ^
-c "program build/MyProject.elf verify reset exit"如果使用 ST-Link:
interface/stlink.cfg如果更换 MCU:
target/stm32h7x.cfg也要换成对应配置。
15. 用 CMake 实现一键 Flash
可以在 CMakeLists.txt 中增加:
set(OPENOCD openocd)
set(
OPENOCD_SCRIPTS
"E:/tools/openocd/share/openocd/scripts"
)
set(
OPENOCD_INTERFACE
"interface/cmsis-dap.cfg"
)
set(
OPENOCD_TARGET
"target/stm32h7x.cfg"
)
add_custom_target(flash
COMMAND ${OPENOCD}
-s ${OPENOCD_SCRIPTS}
-f ${OPENOCD_INTERFACE}
-f ${OPENOCD_TARGET}
-c "program $<TARGET_FILE:${PROJECT_NAME}> verify reset exit"
DEPENDS ${PROJECT_NAME}
USES_TERMINAL
)之后只需要:
cmake --build build --target flash就可以完成:
检查是否需要编译
↓
生成最新 ELF
↓
启动 OpenOCD
↓
写入 Flash
↓
Verify
↓
Reset
↓
退出 OpenOCD16. 增加 erase 和 reset
也可以继续增加:
add_custom_target(erase
COMMAND ${OPENOCD}
-s ${OPENOCD_SCRIPTS}
-f ${OPENOCD_INTERFACE}
-f ${OPENOCD_TARGET}
-c "init; reset halt; flash erase_address 0x08000000 0x100000; reset; shutdown"
USES_TERMINAL
)Reset:
add_custom_target(reset
COMMAND ${OPENOCD}
-s ${OPENOCD_SCRIPTS}
-f ${OPENOCD_INTERFACE}
-f ${OPENOCD_TARGET}
-c "init; reset run; shutdown"
USES_TERMINAL
)注意:
0x08000000
0x100000只是具体 Flash 地址和容量示例。
必须根据目标 MCU 的真实 Flash 布局修改。
17. VS Code 一键编译 / 烧录
可以在:
.vscode/tasks.json定义:
{
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"type": "shell",
"command": "cmake --build build",
"problemMatcher": "$gcc",
"group": "build"
},
{
"label": "Flash",
"type": "shell",
"command": "cmake --build build --target flash",
"dependsOn": "Build",
"problemMatcher": []
}
]
}以后可以:
Terminal
→ Run Task
→ Build或者:
Terminal
→ Run Task
→ Flash也可以绑定快捷键。
18. 自动调试的底层原理
VS Code F5 并不是 VS Code 自己直接连接 MCU。
实际上是:
VS Code
Cortex-Debug
↓
arm-none-eabi-gdb
↓
TCP 3333
↓
OpenOCD GDB Server
↓
SWD / JTAG
↓
MCU典型过程:
按 F5
↓
Cortex-Debug 启动 OpenOCD
↓
OpenOCD 连接调试器
↓
连接 MCU
↓
GDB 加载 ELF
↓
下载程序
↓
Reset
↓
Halt
↓
运行到 main
↓
开始断点调试19. Cortex-Debug 配置
安装:
Cortex-Debug然后创建:
.vscode/launch.json例如:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug MCU",
"cwd": "${workspaceFolder}",
"executable":
"${workspaceFolder}/build/MyProject.elf",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"serverpath": "openocd",
"configFiles": [
"interface/cmsis-dap.cfg",
"target/stm32h7x.cfg"
],
"searchDir": [
"E:/tools/openocd/share/openocd/scripts"
],
"device": "STM32H743VGT6",
"interface": "swd",
"runToEntryPoint": "main",
"preLaunchTask": "Build"
}
]
}换芯片时重点调整:
ELF 文件
configFiles
device
searchDir
interface20. Debug 时是否还需要手动烧录
通常:
不需要如果使用:
"request": "launch"Cortex-Debug 可以:
启动 OpenOCD
↓
连接 MCU
↓
下载 ELF
↓
Reset
↓
运行到 main所以推荐区分:
| 场景 | 操作 |
|---|---|
| 快速运行最新固件 | Flash |
| 断点 / 单步 / Watch | F5 Debug |
| 不想重新烧 Flash | Attach |
21. Attach 调试
如果程序已经烧录并且正在运行,不希望再次写 Flash,可以使用:
{
"name": "Attach MCU",
"type": "cortex-debug",
"request": "attach",
"servertype": "openocd",
"executable":
"${workspaceFolder}/build/MyProject.elf",
"configFiles": [
"interface/cmsis-dap.cfg",
"target/stm32h7x.cfg"
]
}这里 ELF 仍然非常重要,因为 GDB 需要它提供:
符号
函数名
变量
源码映射22. 调试时最常用的能力
通过 Cortex-Debug + GDB 可以使用:
源码断点
单步进入
单步跳过
Continue
Locals
Watch
Call Stack
寄存器查看
Memory View
反汇编
SVD 外设寄存器如果出现 HardFault,还可以重点分析:
MSP
PSP
PC
LR
CFSR
HFSR23. 如何让项目真正可迁移
不要把平台相关参数散落在几十个地方。
推荐集中:
set(MCU_CPU cortex-m7)
set(MCU_FPU fpv5-d16)
set(MCU_FLOAT_ABI hard)
set(
OPENOCD_INTERFACE
interface/cmsis-dap.cfg
)
set(
OPENOCD_TARGET
target/stm32h7x.cfg
)
set(
LINKER_SCRIPT
${CMAKE_SOURCE_DIR}/STM32H743XG_FLASH.ld
)进一步可以拆成:
cmake/
├── arm-none-eabi.cmake
├── stm32h7.cmake
├── stm32f4.cmake
└── openocd.cmake换芯片时就只需要修改少量配置。
24. CMake Presets
可以增加:
CMakePresets.json例如:
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir":
"${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE":
"Debug",
"CMAKE_TOOLCHAIN_FILE":
"${sourceDir}/cmake/arm-none-eabi.cmake"
}
}
]
}之后:
cmake --preset debug
cmake --build --preset debug团队协作时,这比每个人手动输入一长串 CMake 参数更加可靠。
25. Debug 与 Release
典型配置:
| 模式 | 编译参数 | 用途 |
|---|---|---|
| Debug | -Og -g3 | 调试、单步、变量观察 |
| Release | -O2/-Os/-O3 | 正式固件、性能/体积优化 |
需要注意:
Release 不只是“去掉调试信息”。
优化以后:
变量可能被优化掉
函数可能被 inline
代码执行顺序可能变化
断点位置可能变化所以调试体验也会完全不同。
26. FreeRTOS 是否影响这套开发链
不会。
FreeRTOS 本质上仍然只是:
源文件
头文件
FreeRTOSConfig.h构建过程仍然是:
FreeRTOS Sources
↓
CMake
↓
Ninja
↓
GCC
↓
ELF
↓
OpenOCD
↓
GDB区别只是调试 FreeRTOS 时,可以进一步使用:
任务查看
线程感知
RTOS Awareness这些能力取决于具体调试插件和工具。
27. 常见故障排查
| 问题 | 优先检查 |
|---|---|
| CMake 找不到 | PATH、重新打开终端 |
| Ninja 找不到 | PATH、Generator |
| GCC 变成本机 gcc/cl | Toolchain 文件、build 缓存 |
| OpenOCD 找不到 cfg | -s scripts 路径 |
| 找不到调试器 | USB、驱动、interface cfg |
| SWD 失败 | SWDIO、SWCLK、GND、NRST、供电、速度 |
| Flash verify 失败 | Flash 保护、target cfg、供电 |
| F5 不停在 main | -g、runToEntryPoint、优化等级 |
| 断点跳动 | Release 优化、源码与 ELF 是否匹配 |
| 换芯片运行异常 | startup/linker/CPU/FPU/宏/target cfg |
28. 推荐的日常开发流程
第一次拿到工程:
git clone ...
cd project然后:
cmake -S . -B build -G Ninja ^
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake编译:
cmake --build build以后日常开发:
修改代码
↓
Ctrl + Shift + B
↓
Build
↓
Run Task: Flash或者:
修改代码
↓
F5
↓
自动编译
↓
自动下载
↓
自动调试如果:
工程结构变化
工具链变化
CMake 缓存异常建议:
删除 build/
重新 configure
重新 build29. 推荐的一套可复用模板
每一个新的 MCU 工程都可以准备:
project/
├── CMakeLists.txt
├── CMakePresets.json
│
├── cmake/
│ ├── arm-none-eabi.cmake
│ └── openocd.cmake
│
├── .vscode/
│ ├── tasks.json
│ └── launch.json
│
├── startup_xxx.s
├── xxx_FLASH.ld
│
└── ...之后换 MCU 时:
修改 CPU/FPU
修改 startup
修改 linker
修改 OpenOCD target
修改 SDK就可以快速恢复:
Configure
↓
Build
↓
Flash
↓
F5 Debug30. 技术边界
这套方案虽然比较通用,但也存在边界。
例如:
OpenOCD 支持问题
新芯片可能需要:
更新 OpenOCD
使用厂商版本 OpenOCD
使用厂商下载工具TrustZone / Secure Boot
一些芯片带有:
TrustZone
Secure Boot
双核
安全 Flash这类问题不是换一个:
target/*.cfg就可以全部解决的。
外部 Flash
如果代码运行于:
QSPI
OSPI
外部 NOR Flash可能还需要:
Flash Loader
下载算法
特定 OpenOCD DriverFlash 地址不能照抄
Flash / RAM 地址必须来自:
Reference Manual
Datasheet
Linker Script
官方 SDK不能直接复制其他型号。
BIN 和 ELF 不一样
如果烧:
ELF
HEX文件本身通常携带地址信息。
如果烧:
BIN则需要明确指定起始地址。
开发烧录与生产烧录不同
量产阶段通常还会涉及:
序列号
Option Bytes
安全配置
校验
生产日志
测试结果
固件版本不能简单把开发阶段的 Flash 命令直接当成量产方案。
31. 最终记忆框架
最后可以把整套开发链记成:
写代码
↓
VS Code
管理工程
↓
CMake
执行构建
↓
Ninja
编译链接
↓
arm-none-eabi-gcc
生成 BIN / HEX
↓
objcopy
查看 Flash / RAM
↓
size
连接芯片
↓
OpenOCD
底层调试
↓
GDB
图形调试
↓
Cortex-Debug
一键烧录
↓
CMake custom target
+
VS Code Task
一键调试
↓
VS Code F5
+
launch.json掌握这套链路以后,换一颗 MCU 时,需要重新学习的重点就不再是:
“这个 IDE 的按钮在哪里?”而是:
CPU 参数是什么?
startup 文件是什么?
Flash / RAM 怎么布局?
Linker Script 怎么写?
SDK 如何组织?
OpenOCD target 是什么?这才是:
CMake + GCC + OpenOCD方案真正的通用价值。
总结
这套环境的核心不是为了“抛弃所有 IDE”。
它真正解决的是:
构建过程透明
工程结构可控
工具链可替换
烧录可以自动化
调试可以自动化
工程更容易迁移对于 Cortex-M MCU 开发,可以逐步形成:
VS Code
+
CMake
+
Ninja
+
GNU Arm Toolchain
+
OpenOCD
+
GDB
+
Cortex-Debug这样无论后面开发:
STM32
FreeRTOS
Bootloader
驱动
控制系统
其他 Cortex-M MCU都可以复用同一套开发思路。
本文由
rainhorn
创作,采用
知识共享署名4.0
国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。