ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VS Code + CMake + OpenOCD:通用MCU开发环境搭建全指南

VS Code + CMake + OpenOCD:通用MCU开发环境搭建全指南 近几年做MCU开发我用过Keil、IAR、STM32CubeIDE最后主力环境换成了 VS Code CMake Make GNU工具链 OpenOCD 这一套。第一次有人跟我推荐这套组合时我也觉得折腾但真正跑通之后收益远超预期跨平台、可脚本化、不受厂商IDE绑架最重要的是可以在一套编辑器里同时处理固件、上位机乃至测试脚本。这篇文章我会从零开始把通用MCU开发环境怎么搭、每个环节为什么这么做、实际调试烧录时有哪些坑一次性讲清楚。无论你是刚接触STM32、国民技术这类ARM Cortex-M单片机还是被厂商IDE的工程格式折磨已久的老手都可以按步骤复现RISC-V平台的差异我会在最后说明。1. 四个工具分别干什么为什么组合在一起1.1 先说结论这是一套可替换的流水线MCU开发流程其实就三件事写代码、编出固件、把固件烧进芯片里再调试。厂商IDE是把这三件事打包在一起看起来很省事但你一旦需要自定义构建脚本、接持续集成或者换一个品牌MCU工程迁移成本就很高。这套方案把流程拆开。VS Code 只做界面和任务调度让你在同一个窗口里完成编辑、编译、烧录、打断点。CMake 负责描述工程有哪些源文件、用什么编译选项、链接脚本放在哪里它不直接生成固件。Make 是执行者CMake 生成 Makefile 之后Make 根据依赖关系调用编译器实现增量编译。GNU 工具链是真正干“编和链”的活的人arm-none-eabi-gcc 把 C/汇编变成机器码ld 完成地址分配。OpenOCD 是调试和烧录的“翻译官”通过 ST-Link、J-Link、DAPLink 这类调试器把电脑发出的 GDB 命令或烧录指令翻译成芯片能听懂的 SWD/JTAG 时序。从底层到上层整条链路是这样转起来的VS Code 收到“编译”指令调用 CMake 生成 MakefileMake 再去调 GCC产出 ELF/BIN/HEX 固件烧录时 OpenOCD 连接调试器把固件写入芯片内部 Flash调试时 OpenOCD 作为 GDB Serverarm-none-eabi-gdb 作为客户端实现源码级断点、变量查看和单步。每一环都可以独立替换这就是“通用”二字的底气。1.2 为什么有了CMake还要Make两者不是二选一很多刚入门的同学会把 CMake 和 Make 搞混问出“既然用了CMake为什么还要Make”这类问题。其实这是“设计图”和“施工队”的关系。CMake 是设计图Make 是施工队。CMake 本身不直接编译它根据 CMakeLists.txt 生成一份 Makefile如果指定 Ninja则生成 Ninja 文件之后由 Make 逐条执行编译和链接命令。直接手写 Makefile 当然也能做嵌入式工程但跨平台、库依赖、编译选项管理会很快变得混乱。CMake 的好处是生成式管理你写一次 CMakeLists就能在 Windows、Linux、macOS 上生成对应的 Makefile还能一键切换到 Ninja 并行构建。在嵌入式场景里还有个更大的好处通过 toolchain 文件同一份 CMake 逻辑可以切换 ARM GCC、RISC-V GCC甚至本机 x86 GCC方便做单元测试。所以不要纠结“CMake能不能代替Keil”——CMake 完全不替代 IDE 的调试界面它替代的是 Keil 里那个 uvprojx 工程文件。调试和烧录那一半由 OpenOCD 和 VS Code 补齐。理解了这一点你看下面的配置就不会觉得是多余动作。1.3 这套组合和厂商IDE到底怎么选如果你只维护一个项目、一个芯片、一个人开发厂商IDE确实够用。但团队一扩大问题就来了Keil 工程在Linux上没法构建CubeIDE 的工程文件跨版本会变IAR 的编译器只有Windows版本。你总不能让每个工程师都装一模一样的IDE和破解版许可证。用这套开源组合工程描述变成了纯文本的 CMakeLists.txt 和 toolchain 文件。代码提交到Git之后任何人拉下来都能用同一条命令构建CI 服务器也能跑同样的命令。即使团队里有同事坚持用 Keil你也可以保留一份 CMakeLists 作为“唯一真源”Keil工程作为手动维护的发布附件。这套组合不是让你今天就必须抛弃原有IDE而是给你一个不受厂商绑定的备选方案。2. 环境安装与版本验证Windows 和 Ubuntu 双路线2.1 Windows 下安装 VS Code、CMake、GNU 工具链和 OpenOCDWindows 下装这套环境是绝大多数人第一步遇到坑的地方。先装 VS Code安装包用 User Installer 即可不需要管理员权限。装完以后打开扩展面板至少要装这三个插件C/Cms-vscode.cpptools负责语法解析和 IntelliSenseCMake Tools 负责识别 CMakeLists 和编译任务Cortex-Debug 负责对接 OpenOCD 做调试。其中 C/C 插件比较大如果网络不好会卡耐心等它装完。然后装 CMake。去官网下载 Windows x86_64 的 msi 安装包安装时记得勾选“Add CMake to system PATH”。装完以后重新打开终端输入cmake --version确认。很多人遇到“无法将‘cmake’项识别为 cmdlet”就是这个PATH没配对或者终端没有重启。GNU ARM 工具链建议直接下载 Arm 官方提供的 arm-none-eabi 工具链Windows 版本有 exe 安装包和 zip 免安装包。我习惯用 zip 版解压到C:\tools\arm-gnu-toolchain然后把bin目录加进 PATH。验证方式是在新开的终端里输入arm-none-eabi-gcc --version能看到版本号就说明OK。OpenOCD 在 Windows 上推荐用 xPack 发布的版本它的包内部已经带了 libusb 和 WinUSB 驱动。解压后同样把bin目录加进 PATH验证openocd --version。还有一个隐形步骤容易被忽略如果你的调试器是 ST-Link 或 J-LinkWindows 需要安装对应的USB驱动否则 OpenOCD 会提示找不到设备。最后是 Make。Windows 没有原生 make我建议装一个 MSYS2在 MSYS2 shell 里执行pacman -S mingw-w64-x86_64-make装完以后mingw32-make.exe就在 PATH 里了。你也可以用 MinGW-w64 自带的 mingw32-make道理一样。注意不要用 Visual Studio 的 nmake它和 CMake 生成的 Unix Makefile 不兼容。2.2 Ubuntu 下一条命令装完基础环境但是版本有讲究Linux 下的安装比 Windows 省心很多但版本坑需要注意。以 Ubuntu 22.04 为例打开终端执行sudo apt update sudo apt install -y build-essential cmake gcc-arm-none-eabi openocd gdb-multiarchbuild-essential里带了 make 和 gccgcc-arm-none-eabi是ARM交叉编译器openocd是调试烧录服务gdb-multiarch是多架构 GDB可以代替 arm-none-eabi-gdb在调试时少一个工具链安装问题。装完以后验证一下版本cmake --version make --version arm-none-eabi-gcc --version openocd --version gdb-multiarch --versionUbuntu 源里的 OpenOCD 版本通常比较旧如果你用的是新出的 MCUOpenOCD 的 target 文件里可能根本没有这款芯片。这时需要自己编译新版 OpenOCD步骤不复杂git clone https://github.com/openocd-org/openocd.git cd openocd ./bootstrap ./configure --enable-stlink --enable-jlink --enable-cmsis-dap make -j$(nproc) sudo make install自己编译时--enable那一串编译选项决定了支持哪些调试器建议把常见的都打开。编译过程需要 autoconf、automake、libtool、libusb-dev 等依赖缺什么就 apt 装什么。2.3 版本怎么选直接给一张参考表很多老手会告诉你“最新版最好”但在嵌入式工具链上我建议保守一点。GCC 版本太新编译旧工程可能会出现新的警告甚至错误OpenOCD 版本太旧新芯片的烧录算法和支持文件又缺失。我常用的组合参考如下工具最低建议版本备注VS Code1.80扩展市场更新即可CMake3.16太低不支持 toolchain 文件的某些写法GNU ARM 工具链10.310.3 比较稳12.x 开始默认启用新警告Make4.3用 MSYS2 或 MinGW 版本均可OpenOCD0.11新 MCU 建议 0.12 或自己编译最新版选完版本后建议把工具链的 bin 目录写进用户环境变量而不是再额外复制一份到项目目录里。环境变量是全局的项目路径不会因为换电脑而失效。3. 构建工程CMakeLists 的设计与生成3.1 推荐一个干净的工程目录结构很多厂商生成的工程把所有文件堆在一起看起来非常难受。我不建议在源码目录里直接跑 CMake而是在项目根目录放一份 CMakeLists.txt把构建产物全部扔到 build 目录里。推荐的目录布局是这样的project/ ├── CMakeLists.txt ├── cmake/ │ └── toolchain-arm-none-eabi.cmake ├── src/ │ ├── main.c │ ├── startup_stm32f407xx.s │ └── system_stm32f4xx.c ├── include/ │ └── main.h ├── linker/ │ └── stm32f407vgtx_flash.ld ├── openocd/ │ └── board/stm32f4discovery.cfg ├── .vscode/ │ ├── tasks.json │ ├── launch.json │ └── c_cpp_properties.json └── build/cmake目录放交叉编译工具链文件linker目录放链接脚本openocd目录放调试器配置。这样设计的好处是当你换芯片或者换调试器时只需要替换对应的配置文件和链接脚本CMakeLists 的主体不用动。3.2 工具链文件怎么写为什么必须写CMake 默认会调用本机编译器也就是 gcc/clang这显然不能用来编 ARM 固件。所以需要一份 toolchain 文件告诉 CMake目标平台不是本机请使用交叉编译器。下面是一个针对 ARM Cortex-M4 的示例set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR cortex-m4) 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_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)其中CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY这一行很关键。CMake 在配置阶段会尝试编译一个小的可执行程序来判断编译器是否可用但嵌入式环境没有操作系统链接可执行文件容易失败所以让 CMake 编译一个静态库来测试能避免很多莫名其妙的配置报错。3.3 CMakeLists 核心片段从源码到 ELF/BIN/HEX写 CMakeLists 时我不喜欢用file(GLOB_RECURSE)去自动收集源文件因为这种写法新增文件后 CMake 不会立刻感知很容易出现“明明加了文件编译却没带上”的问题。如果你图省事一定要用 GLOB就加CONFIGURE_DEPENDS后缀。更稳的做法是手动列出源文件虽然麻烦一点但可控性强。一个典型的嵌入式 CMakeLists 可以这样写cmake_minimum_required(VERSION 3.16) project(mcu_demo C ASM) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/linker/stm32f407vgtx_flash.ld) set(SOURCES src/main.c src/startup_stm32f407xx.s src/system_stm32f4xx.c ) add_compile_options(-mcpucortex-m4 -mthumb -O2 -Wall -ffunction-sections -fdata-sections) add_executable(${PROJECT_NAME}.elf ${SOURCES}) target_link_options(${PROJECT_NAME}.elf PRIVATE -T ${LINKER_SCRIPT} -Wl,--gc-sections ) add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O ihex ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin COMMAND ${CMAKE_SIZE} ${PROJECT_NAME}.elf )-ffunction-sections -fdata-sections配合链接阶段的-Wl,--gc-sections可以把没有用到的函数和数据从最终固件里剔除这对MCU这种 Flash 有限的场景特别重要。CMAKE_OBJCOPY和CMAKE_SIZE是 CMake 在 toolchain 文件里自动推导的变量不用手动指定但前提是工具链文件里设置的编译器名称能被正确识别。3.4 用 Make 驱动构建一条命令从配置到编译有了上面的 CMakeLists在项目根目录执行cmake -S . -B build -G Unix Makefiles \ -DCMAKE_TOOLCHAIN_FILEcmake/toolchain-arm-none-eabi.cmake \ -DCMAKE_BUILD_TYPEDebug-S . -B build的意思是源码目录是当前目录构建目录是build。-G Unix Makefiles指定生成 Makefile在 Windows 的 MSYS2/MinGW 环境下可以用-G MinGW Makefiles或者把 mingw32-make 复制为 make.exe 继续用 Unix Makefiles。-DCMAKE_TOOLCHAIN_FILE指向我们上面写的工具链文件。接下来执行cmake --build build -- -j$(nproc)cmake --build会调用生成器对应的构建命令这里就是 make--后面的参数原样传给 make-j$(nproc)表示用满CPU核心并行编译。编译结束后build目录下会同时出现.elf、.hex、.bin三个文件这就是最终固件。如果你更习惯直接敲 make也可以先cd build然后make -j。这两种方式本质一样只是前者封装了一层更不容易打错路径。4. VS Code 配置编译、烧录、调试一条龙4.1 用 tasks.json 实现一键编译不用再敲命令行命令行跑通以后就可以把构建过程收进 VS Code。在项目根目录创建.vscode/tasks.json下面是一个可以直接抄的版本{ version: 2.0.0, tasks: [ { label: cmake-configure, type: shell, command: cmake, args: [ -S, ${workspaceFolder}, -B, ${workspaceFolder}/build, -G, Unix Makefiles, -DCMAKE_TOOLCHAIN_FILE${workspaceFolder}/cmake/toolchain-arm-none-eabi.cmake, -DCMAKE_BUILD_TYPEDebug ], group: build, problemMatcher: [] }, { label: build, type: shell, command: cmake, args: [ --build, ${workspaceFolder}/build, --, -j4 ], group: { kind: build, isDefault: true }, dependsOn: [cmake-configure] } ] }配置里dependsOn的作用是第一次按CtrlShiftB时先执行 cmake-configure再执行 build以后如果 CMakeLists 没变cmake --build 会自动跳过重复配置。这里我故意写-j4而不是-j因为在 Windows 上$(nproc)不存在VS Code 的 shell 任务里写死 4 线程更稳妥。4.2 用 launch.json 对接 OpenOCD 和 GDB编译只是第一步能按 F5 进调试才是这套环境最爽的地方。要用的插件是 Cortex-Debug安装后新建.vscode/launch.json{ version: 0.2.0, configurations: [ { name: OpenOCD Debug, cwd: ${workspaceRoot}, type: cortex-debug, request: launch, servertype: openocd, interface: swd, configFiles: [ interface/stlink.cfg, target/stm32f4x.cfg ], searchDir: [ C:/OpenOCD/scripts ], executable: ${workspaceFolder}/build/mcu_demo.elf, svdFile: ${workspaceFolder}/STM32F407.svd, gdbPath: arm-none-eabi-gdb, runToEntryPoint: main } ] }servertype必须是openocd告诉 Cortex-Debug 启动一个 OpenOCD 服务。configFiles里的两个文件对应 OpenOCD 的接口配置和目标芯片配置interface/stlink.cfg表示调试器是 ST-Linktarget/stm32f4x.cfg表示目标芯片是 STM32F4 系列。如果你用的是 J-Link就把interface/stlink.cfg换成interface/jlink.cfg如果用的是 DAPLink就换成interface/cmsis-dap.cfg。searchDir是 OpenOCD 脚本所在目录Windows 上必须写绝对路径。gdbPath指定 arm-none-eabi-gdb在 Ubuntu 上也可以改成gdb-multiarch。svdFile是芯片的外设寄存器描述文件填了之后调试窗口里可以直接看寄存器和外设的值没有也能调试但体验差很多。runToEntryPoint: main表示启动后在 main 函数处暂停比默认停在启动汇编里友好得多。4.3 c_cpp_properties.json 和头文件红色波浪线很多人在 VS Code 里打开工程发现#include下面一片红色波浪线这不是代码错误而是 C/C 插件不知道去哪里找头文件也不知道定义了哪些宏。解决方法是配置.vscode/c_cpp_properties.json{ configurations: [ { name: MCU, includePath: [ ${workspaceFolder}/include, ${workspaceFolder}/src ], defines: [ STM32F407xx ], compilerPath: arm-none-eabi-gcc, cStandard: c11 } ], version: 4 }includePath要包含所有头文件目录defines里填的是编译时才会定义的宏比如 HAL 库经常需要STM32F407xx这样的宏来区分芯片型号。如果编译用的是 CMake更推荐在 CMakeLists 里设置set(CMAKE_EXPORT_COMPILE_COMMANDS ON)然后在c_cpp_properties.json里补一行compileCommands: ${workspaceFolder}/build/compile_commands.json让插件直接从编译数据库中读取所有路径和宏定义一劳永逸。5. OpenOCD 实操烧录、调试与常见报错5.1 命令行手动烧录理解 OpenOCD 到底做了什么调试器配置好以后不一定每次都要进 VS Code。很多情况下你只想快速烧个固件那直接在命令行敲openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \ -c program build/mcu_demo.elf verify reset exit这条命令加载了接口配置和目标配置然后执行program命令。OpenOCD 会通过 ST-Link 以 SWD 协议访问 MCU 的调试接口找到内部 Flash 控制器完成擦除、写入和校验。最后verify做读回校验reset复位芯片exit让 OpenOCD 退出。这里也回答一个很多新人疑惑的问题MCU 内部的 Flash 到底用什么接口访问并不是你把飞线接到 Flash 引脚上而是通过 SWD/JTAG 调试接口由芯片内部的 Flash 控制器去擦写 Flash 区域。OpenOCD 只是把烧录指令翻译给调试器最终操作 Flash 的是芯片自己。这也是为什么在 target 配置文件里必须写对芯片型号因为不同芯片的 Flash 控制器寄存器地址和操作时序差别很大。5.2 “openocd server is not running!” 这类报错怎么排查使用 VS Code 调试时最常见的报错之一就是“cant perform jtag flash, because openocd server is not running!”。看到这个提示先别慌它说明 Cortex-Debug 想操作 OpenOCD但 OpenOCD 进程没有成功跑起来。常见原因有四类。第一OpenOCD 可执行文件没在 PATH 里或者 launch.json 里没有指定绝对路径。解决方法是先打开一个终端敲openocd --version确认能不能找到。第二configFiles里的脚本路径不对OpenOCD 启动后立刻报错退出。解决方法是手动执行openocd -f interface/stlink.cfg -f target/stm32f4x.cfg如果输出停在Info : Listening on port 3333说明 OpenOCD 已经跑起来了如果报Error: open failed就是路径或脚本文件名写错了。第三调试器和目标板接线有问题。OpenOCD 启动时如果找不到目标芯片会出现Error: init mode failed或者超时的提示。检查 SWDIO/SWCLK/GND 三条线是否接对目标板是否上电。第四3333 端口被占用。如果之前有一个异常退出的 OpenOCD 进程没杀干净新进程起不来。在 Windows 上可以开任务管理器找 openocd 进程Linux 上用pkill openocd。5.3 外部 Flash、JTAG 和芯片保护常见问题速查表除了 OpenOCD server 报错我还整理过一张高频问题速查表基本都是团队同事踩过的坑症状可能原因解决方法CMake 提示无法识别命令PATH 没配置或终端未重启把 cmake 安装目录加 PATH重开终端VS Code 任务报 “unable to find suitable visual studio toolc...”CMake 默认生成器选了 VS配置里加-G Unix Makefiles或-G Ninja头文件红色波浪线includePath 或 defines 没配配置 c_cpp_properties.json 或 compile_commandsOpenOCD 找不到设备USB驱动缺失/Linux权限不够Windows 重装调试器驱动Linux 加 udev 规则program 报 verify 失败芯片Flash读保护开启或接线不稳定检查 SWD 线确认芯片没有锁死GDB 连不上 OpenOCD端口 3333 被占用杀掉占用进程确认 OpenOCD 在运行烧录外部 QSPI Flash 失败OpenOCD 缺少外部 Flash loader使用厂商工具或额外加载 loader 脚本Linux 下 OpenOCD 权限问题很常见建议把 OpenOCD 自带的 udev 规则文件复制到系统目录sudo cp /usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d/ sudo udevadm control --reload-rules重新插拔调试器以后普通用户就能直接访问 USB 设备不用每次 sudo。6. 踩坑心得与后续扩展6.1 先用命令行跑通再碰 VS Code 配置这大概是我最想强调的一点。很多人一上来就照着网上的 launch.json 一顿复制结果按 F5 报错然后又不知道错在哪。我的建议是先完全抛开 VS Code在终端里把cmake --build跑到能生成 bin再把openocd -c program ...跑到能烧录最后才去配置 tasks.json 和 launch.json。这样一旦配置出问题你能确定是 VS Code 这层的问题还是工具链或硬件的问题。另外VS Code 的 C/C 插件第一次加载一个大工程时会花较长时间建立索引期间红色波浪线可能一直在跳动。不要急着改配置等索引完成再说。如果加了 compile_commands.json 还不行可以试试C/C: Reset IntelliSense Database命令。6.2 Make 不是只能编固件还能帮你做自动化和 CICMake Make 这套组合最大的优势在于可以脚本化。你可以在仓库里放一个build.sh#!/bin/bash set -e cmake -S . -B build -G Unix Makefiles -DCMAKE_BUILD_TYPERelease cmake --build build -j$(nproc) openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c program build/mcu_demo.elf verify reset exit这样不管是本地手动构建、开发机上自动构建还是 CI 流水线都执行同一份脚本。GitHub Actions 或者 GitLab CI 里只需要装上相同版本的 gcc-arm-none-eabi、cmake、make、openocd其余的交给脚本完成。这个能力是厂商 IDE 很难给你的。6.3 这套方案不是银弹遇到特殊芯片要灵活切换不要以为所有 MCU 都能用同一份 OpenOCD 配置搞定。有些新出的国产芯片、专用 SoCOpenOCD 的目标文件还没有正式支持。这时候几种替代方案我实测下来比较靠谱第一用芯片厂商提供的 OpenOCD 分支很多厂家会维护自己的 fork第二用厂商自己的命令行烧录工具比如 STM32CubeProgrammer CLI然后依然保留 VS Code 做编辑和构建只把烧录命令换成厂商工具第三自己照着 OpenOCD 文档编写 target 配置文件这个门槛高一点但只要搞懂寄存器就可行。那“通用”二字是不是白说了也不算。通用强调的是流程和思路通用而不是一个配置文件走天下。你掌握了 CMake 组织工程和 OpenOCD 调试模型换芯片只是替换工具链后缀和配置文件远比换一个 IDE 来得轻。6.4 最后分享一个我一直在用的小技巧在 Windows 上我习惯把工具链都装到同一个根目录比如C:\tools\然后给 VS Code 的终端设置一个环境变量文件。这样多台电脑换着用只要同步一个环境变量清单新电脑十分钟就能恢复到熟悉环境。在 Ubuntu 上我则习惯在~/.bashrc里写一组 aliasalias mcu-buildcmake --build build -j$(nproc) alias mcu-flashopenocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c program build/mcu_demo.elf verify reset exit这样编译和烧录都只要敲两个短命令手指肌肉记忆形成以后效率真的比打开 IDE、点编译、点下载快很多。我个人体会是环境折腾这件事前期越愿意花时间理解每层工具的关系后面开发就越顺手。如果你现在还在用双击编译的方式做 MCU 开发不妨找个周末按这套方案搭一次哪怕只是点个灯你对编译、链接、调试接口的认知会完全不一样。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进