一个便于维护的 STM32 工程,需要让目录职责、构建关系和日常操作保持一致:打开工程根目录即可工作,平台代码与应用代码各有归属,构建、下载和验证有固定入口。
本文介绍一种已经用于实际项目的组织方式,并将它提炼成通用模板。它适用于采用 STM32CubeMX、CMake 和 VSCode,生成单个固件镜像的工程。读者无需取得某个特定课程或仓库,也可以直接把文末指令交给 Agent 新建工程。
1. 这套组织方式解决什么问题
模板同时约定三个方面:
- 目录布局:工程描述、平台代码、应用模块、工具和测试分别放在哪里。
- 构建归属:根目录创建唯一固件可执行目标,各功能模块创建独立库目标,通过显式依赖完成装配。Project Outline 应能展示这些模块边界。
- 操作入口:用户从根目录运行 VSCode 任务,脚本处理具体命令。
工程根目录可以就是仓库根目录,也可以位于一个更大的仓库内部。无论放在哪里,VSCode 打开的目录都应直接包含 .ioc、CMakeLists.txt、Core、Drivers 和 App,不需要再进入内部目录寻找实际工程。
本文提炼的是已在项目中使用的结构与操作经验。具体芯片的外设配置和板级行为仍须验证。双核、多镜像或引导程序与应用组合等工程有额外需求,不在本文的单固件模板范围内。
2. 可复用的目录模板
下面是一份可直接采用的目录布局。芯片型号、工程名称和业务模块按项目需要填写。
| |
App 下列出了测量、控制、显示等常见业务分类,新工程按需要使用;每个具体模块采用 Inc、Src 和自己的 CMakeLists.txt。不要为了凑齐模板创建一批没有用途的空模块。
工程还可能包含 .mxproject、.clangd 等文件,是否使用取决于 CubeMX 和代码索引工具的需要。已有 build、缓存、临时文件、旧固件和本机路径配置不属于可复制模板。
3. 最关键的设计:一个固件入口,多个模块库
单工程入口与模块化构建必须同时保留。 只有目录分层还不够:如果把 App、Core、Drivers 的全部源码直接加入一个 Executable,Project Outline 就失去了按功能查看模块的价值。
根 CMakeLists.txt 先创建工程和固件目标,再加入平台与应用目录;具体功能源码由对应库目标编译,根目标通过链接完成装配。组织骨架如下:
| |
这段代码用于说明组织关系,不是完整的可编译工程;还需要目标芯片的工具链、源码清单、链接脚本和 HEX 生成规则。
平台层可以使用两类目标来组织依赖:
stm32cubemx接口库:传播平台包含路径和宏定义。STM32_Drivers对象库:编译 HAL 和系统源码。
平台构建清单通过 target_sources() 把 main.c、外设初始化、IRQ 和启动文件等加入根固件目标。App 的具体模块通常是静态库,由 app 接口库汇总依赖。
因此,工程里可以有很多 CMakeLists.txt,但固件仍然只有一个可执行目标入口。主机测试可以有自己的 project() 和多个测试可执行文件,不过它是独立配置的测试工程,不应被 add_subdirectory() 接入 ARM 固件构建树。
目录中的模块,必须对应构建中的模块
以包含 ADC 采集、PWM、通信和开环斩波控制的应用为例,Project Outline 应能区分下列目标。名称和数量按实际功能调整,下面是目标清单示意,不规定扩展的具体树形排版:
| |
不需要显示器的工程可以不包含显示库;通信适配库应采用目标芯片的实现。不要为了匹配示意图机械加入某一芯片专用模块。
app 和 stm32cubemx 这类 Interface library 用于汇总依赖、包含路径和编译定义,本身不编译源码,可能不会作为独立编译目标出现在 Outline 中。它们不能替代具体功能的 Static library 或 Object library。
一个功能模块的构建清单可以这样组织:
| |
应用层通过 add_subdirectory() 纳入模块,再汇总依赖:
| |
实际项目继续加入采集、控制、协议和硬件适配等库。模块之间使用 target_link_libraries() 声明真实依赖,按头文件接口需要选择 PUBLIC 或 PRIVATE;芯片宏、CPU/FPU 参数和平台包含路径必须作用到需要它们的库,不能只配置根 Executable。
编译归属也应清楚:
| 代码 | 编译归属 |
|---|---|
| main、启动、必要的初始化和 IRQ 接入 | 根固件目标,或明确的平台接入库 |
| HAL、系统底层实现 | STM32_Drivers 对象库等平台目标 |
| ADC 采集、转换、PWM 输出、运行时、控制算法 | 各自的功能静态库 |
| 协议与芯片相关串口传输 | 协议库与传输适配库 |
| 显示驱动与应用显示逻辑 | 对应显示模块库 |
每份源文件应有明确的编译所有者。不要同时在模块库和根目标重复编译,也不要创建空库装饰 Outline,却仍把全部实现堆在 Executable 下。
已经复制了成熟模块时,应优先复用它们的 CMakeLists.txt。 先检查依赖是否适用于目标芯片,再做必要调整。模块化迁移不应改变控制时序、算法、协议或错误处理语义。对完整开环斩波应用,UART 通信、ADC 采集和 PWM 输出是集成链的一部分,单独完成通信验证不能代表整个应用交付。
已有的固件身份生成与网页打包流程也属于依赖关系:例如 firmware_identity 应先于使用生成头文件的模块执行,firmware_candidate 应在固件链接完成后使用对应产物打包。拆分库目标时要保留这些顺序,避免增量构建使用过期身份或网页。
工程入口重复时,要检查目标的目录归属
在一次工程入口重复问题的排查中,检查到 STM32Cube IDE 扩展的以下行为:Project Setup 页面先加入根工程名称,再加入从 CMake File API 识别到的子项目。原生 CMake 子项目识别逻辑会检查非根目录中的目标,并过滤 STATIC_LIBRARY、OBJECT_LIBRARY、INTERFACE_LIBRARY 等库目标。因此,功能库在 Project Outline 中形成模块边界,与 Project Setup 保持单入口并不冲突。
如果根目录声明工程,而同名可执行目标在 firmware 子目录创建,就可能得到“根工程 + 子目录可执行目标”两个同名选项。即使删除子目录中的 project(),目标的目录归属仍然没有改变。
上述行为来自对 STM32Cube IDE Core 1.4.0 和 Build CMake 1.46.0 实现的检查。不能把它泛化成所有版本扩展都采用的规则,也不能仅凭 .ioc 与 CMake 同时存在,就断定它们是重复入口来源。
采用本模板时,应明确要求:根目录创建唯一固件可执行目标,各功能模块以库目标参与构建。
4. Core、Drivers 与 App 各自负责什么
| 位置 | 主要职责 | 更换芯片时的处理 |
|---|---|---|
.ioc | 时钟、引脚、外设和中断配置 | 按目标板重新核对 |
Core | 启动后的系统初始化、IRQ、平台接入 | 替换芯片相关实现 |
Drivers | CMSIS、HAL/LL 厂商代码 | 使用目标芯片系列的匹配版本 |
App | 控制、测量、协议、显示与应用侧硬件适配 | 优先复用,逐项检查平台耦合 |
cmake | 交叉编译参数、平台源码和依赖组织 | 更新型号、源码、链接与包含路径 |
tools | 人工操作和自动验证入口 | 保持操作习惯,调整目标参数 |
目录名表达职责,但不能代替耦合检查。App/Hardware 可以包含应用侧的硬件适配实现,不能因为代码位于 App 就假定完全不依赖 HAL。模板复用应保留成熟业务代码,通过明确的适配边界处理芯片差异。
同样,把文件放入 Core 不会自动获得 CubeMX 再生成保护。应注明哪些文件由工具生成、哪些由项目维护,以及再生成时如何保留用户代码。
5. 用户操作方式也是模板的一部分
日常操作入口定义在 .vscode/tasks.json。任务调用 tools 中的 PowerShell 脚本,脚本再执行配置、构建、下载和网页服务。
| |
任务可以按功能或开发阶段划分,也可以合并为一个动作菜单。关键是操作始终从根目录开始。Debug、Release 和不同功能变体属于构建配置,不应因此拆出多个固件工程入口。
一种已经使用的配置组合是:通过 cube-cmake 接入 STM32 工具,启用 Presets,并关闭打开目录时自动配置。采用这些设置前,应结合本机安装的扩展和工具核对。尤其不要把原电脑上的编译器绝对路径、扩展版本路径原样复制给新工程。
构建产物统一放在 build/<配置>/。配套网页放在 tools/web_workbenches/,测试放在 tests/host/,这样操作脚本和测试可以通过工程内相对路径找到它们。
6. 可直接复制给 Agent 的新建工程指令
把目标目录、工程名、芯片和功能范围补齐即可使用。如果已有可复用的代码,再提供其路径;没有参考仓库也可以按这份结构新建。
| |
7. 怎样判断 Agent 按模板完成了工程
交付时,至少检查以下三层证据:
| 层次 | 应检查的内容 | 能证明什么 |
|---|---|---|
| 目录与构建模型 | 根文件布局、唯一固件目标、功能库类型、源码归属、显式依赖 | 工程具有实际模块边界 |
| 工具执行 | CMake 配置、真实编译链接、ELF/HEX、适用测试 | 工程在当前工具链下可构建 |
| 用户操作 | Project Setup 单入口、Project Outline 模块库、根任务菜单 | 入口简明,模块可见 |
配置成功不能替代编译链接成功;编译成功不能替代 GUI 验证;它们都不能替代新板卡的硬件验证。
这份模板的核心约定是:工程根目录拥有唯一固件目标,功能模块通过独立库目标装配并在 Outline 中可见,用户通过根目录任务完成日常操作。 把这三点写进 Agent 指令,目录结构才会与实际构建和使用方式一致。