AI 时代如何开发 MCU:自动编程、构建、烧录与调试全流程
in 嵌入式开发 with 0 comment

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 项目中的开发方式。

这套环境重点解决以下问题:

它可以用于:


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.ld

3.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 cfg

4. Windows 环境需要哪些工具

推荐准备:

工具作用建议
Arm GNU ToolchainC/C++/汇编编译、链接、GDB必须
CMake工程配置和构建生成必须
Ninja高速构建执行器推荐
OpenOCD烧录、擦除、寄存器访问、GDB Server必须
VS Code编辑、任务、调试前端推荐
CMake ToolsVS Code CMake 集成推荐
C/C++IntelliSense、代码导航推荐
Cortex-DebugMCU 图形化调试强烈推荐
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-gccC 编译器 / 链接驱动
arm-none-eabi-g++C++ 编译器
arm-none-eabi-as汇编器
arm-none-eabi-objcopyELF 转 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>\bin

6. 安装 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 openocd

OpenOCD 一条典型命令由三部分组成:

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 工程至少要解决三个问题:

  1. CMake 使用的是 arm-none-eabi-gcc。
  2. 编译器知道目标 CPU / FPU。
  3. 链接器知道 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
        ↓
退出 OpenOCD

16. 增加 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
interface

20. Debug 时是否还需要手动烧录

通常:

不需要

如果使用:

"request": "launch"

Cortex-Debug 可以:

启动 OpenOCD
↓
连接 MCU
↓
下载 ELF
↓
Reset
↓
运行到 main

所以推荐区分:

场景操作
快速运行最新固件Flash
断点 / 单步 / WatchF5 Debug
不想重新烧 FlashAttach

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
HFSR

23. 如何让项目真正可迁移

不要把平台相关参数散落在几十个地方。

推荐集中:

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/clToolchain 文件、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
重新 build

29. 推荐的一套可复用模板

每一个新的 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 Debug

30. 技术边界

这套方案虽然比较通用,但也存在边界。

例如:

OpenOCD 支持问题

新芯片可能需要:

更新 OpenOCD
使用厂商版本 OpenOCD
使用厂商下载工具

TrustZone / Secure Boot

一些芯片带有:

TrustZone
Secure Boot
双核
安全 Flash

这类问题不是换一个:

target/*.cfg

就可以全部解决的。


外部 Flash

如果代码运行于:

QSPI
OSPI
外部 NOR Flash

可能还需要:

Flash Loader
下载算法
特定 OpenOCD Driver

Flash 地址不能照抄

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

都可以复用同一套开发思路。

Responses