2026-09-10

一种可复用的 STM32 工程组织方式:目录、构建与 Agent 新建模板

将经过项目实践的 STM32 代码组织方式提炼为通用模板,说明目录分层、模块化库目标、Project Outline 展示和 VSCode 工作流,以及可直接交给 Agent 的新建工程指令。

一个便于维护的 STM32 工程,需要让目录职责、构建关系和日常操作保持一致:打开工程根目录即可工作,平台代码与应用代码各有归属,构建、下载和验证有固定入口。

本文介绍一种已经用于实际项目的组织方式,并将它提炼成通用模板。它适用于采用 STM32CubeMX、CMake 和 VSCode,生成单个固件镜像的工程。读者无需取得某个特定课程或仓库,也可以直接把文末指令交给 Agent 新建工程。

1. 这套组织方式解决什么问题

模板同时约定三个方面:

工程根目录可以就是仓库根目录,也可以位于一个更大的仓库内部。无论放在哪里,VSCode 打开的目录都应直接包含 .iocCMakeLists.txtCoreDriversApp,不需要再进入内部目录寻找实际工程。

本文提炼的是已在项目中使用的结构与操作经验。具体芯片的外设配置和板级行为仍须验证。双核、多镜像或引导程序与应用组合等工程有额外需求,不在本文的单固件模板范围内。

2. 可复用的目录模板

下面是一份可直接采用的目录布局。芯片型号、工程名称和业务模块按项目需要填写。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<project>/
├── <project>.ioc                 # CubeMX 硬件配置
├── CMakeLists.txt                # 根工程、唯一固件可执行目标
├── CMakePresets.json             # 构建配置与输出目录
├── startup_<mcu>.s               # 启动文件
├── <mcu>_FLASH.ld                # Flash/RAM 布局
├── README.md                     # 从打开工程到操作任务的说明
├── .vscode/
│   ├── settings.json             # CMake、STM32、代码索引设置
│   └── tasks.json                # 用户操作菜单
├── .settings/                    # STM32 扩展设备与工具链元数据
├── Core/
│   ├── Inc/                      # 平台接口、HAL 配置、外设头文件
│   └── Src/                      # main、初始化、IRQ、MSP、系统代码
├── Drivers/
│   ├── CMSIS/
│   └── STM32<family>xx_HAL_Driver/
├── App/
│   ├── CMakeLists.txt             # 汇总应用模块
│   ├── Config/
│   ├── Measurement/
│   ├── Control/
│   ├── Hardware/
│   └── Display/
│       └── <Module>/
│           ├── CMakeLists.txt
│           ├── Inc/
│           └── Src/
├── cmake/
│   ├── gcc-arm-none-eabi.cmake    # 工具链与目标架构参数
│   └── stm32cubemx/
│       └── CMakeLists.txt         # Core、HAL、宏、包含路径的构建清单
├── tools/
│   ├── <workflow>.ps1            # 构建、下载、工作台等操作实现
│   ├── verify_*.ps1              # 可重复执行的检查
│   └── web_workbenches/          # 配套网页(需要时保留)
├── tests/
│   └── host/                     # 独立配置的主机测试工程
├── design/                       # 设计资料
├── verification/                 # 验证记录
└── build/                        # 构建输出,排除在源码版本控制之外

App 下列出了测量、控制、显示等常见业务分类,新工程按需要使用;每个具体模块采用 IncSrc 和自己的 CMakeLists.txt。不要为了凑齐模板创建一批没有用途的空模块。

工程还可能包含 .mxproject.clangd 等文件,是否使用取决于 CubeMX 和代码索引工具的需要。已有 build、缓存、临时文件、旧固件和本机路径配置不属于可复制模板。

3. 最关键的设计:一个固件入口,多个模块库

单工程入口与模块化构建必须同时保留。 只有目录分层还不够:如果把 App、Core、Drivers 的全部源码直接加入一个 Executable,Project Outline 就失去了按功能查看模块的价值。

CMakeLists.txt 先创建工程和固件目标,再加入平台与应用目录;具体功能源码由对应库目标编译,根目标通过链接完成装配。组织骨架如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
cmake_minimum_required(VERSION 3.22)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

project(my_stm32_project LANGUAGES C ASM)

# 根目录拥有固件可执行目标。
add_executable(${CMAKE_PROJECT_NAME})

# 子目录补充平台源码和应用库。
add_subdirectory(cmake/stm32cubemx)
add_subdirectory(App)

target_link_libraries(${CMAKE_PROJECT_NAME}
    stm32cubemx
    app
)

这段代码用于说明组织关系,不是完整的可编译工程;还需要目标芯片的工具链、源码清单、链接脚本和 HEX 生成规则。

平台层可以使用两类目标来组织依赖:

平台构建清单通过 target_sources()main.c、外设初始化、IRQ 和启动文件等加入根固件目标。App 的具体模块通常是静态库,由 app 接口库汇总依赖。

因此,工程里可以有很多 CMakeLists.txt,但固件仍然只有一个可执行目标入口。主机测试可以有自己的 project() 和多个测试可执行文件,不过它是独立配置的测试工程,不应被 add_subdirectory() 接入 ARM 固件构建树。

目录中的模块,必须对应构建中的模块

以包含 ADC 采集、PWM、通信和开环斩波控制的应用为例,Project Outline 应能区分下列目标。名称和数量按实际功能调整,下面是目标清单示意,不规定扩展的具体树形排版:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
my_stm32_project          Executable
STM32_Drivers            Object library
adc_acquisition          Static library
analog_sample_conversion Static library
chopper_pwm_output       Static library
chopper_runtime          Static library
open_loop_chopper        Static library
open_loop_power_map      Static library
workbench_protocol       Static library
uart_transport           Static library
ssd1306                  Static library
stm32_hal_i2c_adapter    Static library
oled_pwm_mirror          Static library

不需要显示器的工程可以不包含显示库;通信适配库应采用目标芯片的实现。不要为了匹配示意图机械加入某一芯片专用模块。

appstm32cubemx 这类 Interface library 用于汇总依赖、包含路径和编译定义,本身不编译源码,可能不会作为独立编译目标出现在 Outline 中。它们不能替代具体功能的 Static library 或 Object library。

一个功能模块的构建清单可以这样组织:

1
2
3
4
5
6
7
8
# App/Measurement/AnalogSampleConversion/CMakeLists.txt
add_library(analog_sample_conversion STATIC
    Src/analog_sample_conversion.c
)

target_include_directories(analog_sample_conversion PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/Inc
)

应用层通过 add_subdirectory() 纳入模块,再汇总依赖:

1
2
3
4
5
# App/CMakeLists.txt:只展示一个模块的最小示例
add_subdirectory(Measurement/AnalogSampleConversion)

add_library(app INTERFACE)
target_link_libraries(app INTERFACE analog_sample_conversion)

实际项目继续加入采集、控制、协议和硬件适配等库。模块之间使用 target_link_libraries() 声明真实依赖,按头文件接口需要选择 PUBLICPRIVATE;芯片宏、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_LIBRARYOBJECT_LIBRARYINTERFACE_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、平台接入替换芯片相关实现
DriversCMSIS、HAL/LL 厂商代码使用目标芯片系列的匹配版本
App控制、测量、协议、显示与应用侧硬件适配优先复用,逐项检查平台耦合
cmake交叉编译参数、平台源码和依赖组织更新型号、源码、链接与包含路径
tools人工操作和自动验证入口保持操作习惯,调整目标参数

目录名表达职责,但不能代替耦合检查。App/Hardware 可以包含应用侧的硬件适配实现,不能因为代码位于 App 就假定完全不依赖 HAL。模板复用应保留成熟业务代码,通过明确的适配边界处理芯片差异。

同样,把文件放入 Core 不会自动获得 CubeMX 再生成保护。应注明哪些文件由工具生成、哪些由项目维护,以及再生成时如何保留用户代码。

5. 用户操作方式也是模板的一部分

日常操作入口定义在 .vscode/tasks.json。任务调用 tools 中的 PowerShell 脚本,脚本再执行配置、构建、下载和网页服务。

1
2
3
4
5
打开工程根目录
    → Terminal / Run Task
    → 选择任务和动作
    → tools 中的脚本执行
    → build 中生成对应配置的产物

任务可以按功能或开发阶段划分,也可以合并为一个动作菜单。关键是操作始终从根目录开始。Debug、Release 和不同功能变体属于构建配置,不应因此拆出多个固件工程入口。

一种已经使用的配置组合是:通过 cube-cmake 接入 STM32 工具,启用 Presets,并关闭打开目录时自动配置。采用这些设置前,应结合本机安装的扩展和工具核对。尤其不要把原电脑上的编译器绝对路径、扩展版本路径原样复制给新工程。

构建产物统一放在 build/<配置>/。配套网页放在 tools/web_workbenches/,测试放在 tests/host/,这样操作脚本和测试可以通过工程内相对路径找到它们。

6. 可直接复制给 Agent 的新建工程指令

把目标目录、工程名、芯片和功能范围补齐即可使用。如果已有可复用的代码,再提供其路径;没有参考仓库也可以按这份结构新建。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
请按以下目录与构建约定,新建一个 STM32 单固件工程。

新工程目录:<目标绝对路径>
工程名称:<工程名>
目标板和芯片:<开发板、MCU 完整型号>
本次功能范围:<例如 UART 回显,或复用指定的控制与测量模块>
可复用代码路径(可选):<已有模块的绝对路径;没有则填无>

请先检查目标目录、已安装工具和提供的可复用代码,然后实施。
以下约定同时约束目录布局、CMake 目标归属和 VSCode 操作流程。

结构要求:
1. 新工程目录就是 VSCode 打开的工程根目录。
2. 根目录直接包含 .ioc、CMakeLists.txt、CMakePresets.json、
   启动文件、链接脚本、README、.vscode 和所需的 STM32 元数据。
3. Core/Inc、Core/Src、Drivers、App、cmake、tools、tests
   直接位于工程根目录;配套网页放在 tools/web_workbenches。
4. 不增加 firmware 包装层,不要求用户打开内部子目录。

构建要求:
5. 固件的 project() 和 add_executable() 在根 CMakeLists.txt 中。
6. 由 cmake/stm32cubemx 提供平台源码和依赖,HAL/系统代码形成
   STM32_Drivers 等对象库。App 的实际功能模块形成独立静态库,
   根可执行目标链接这些库;不要把全部 App/Core/Drivers 源码
   直接塞进 Executable,也不要在子目录创建第二个固件入口。
   优先复用已有模块 CMakeLists.txt,按目标芯片调整真实依赖。
   不创建空库装饰 Outline,不重复编译源码,不机械引入旧芯片模块。
   CPU/FPU 参数、平台宏和头文件路径要正确作用到相关库目标。
7. 主机测试单独配置,不接入 ARM 固件构建树。
8. 使用根目录 Presets,输出到 build/<配置>;生成 ELF 和 HEX。

复用和操作要求:
9. 保留适用的业务模块,优先调整平台接口,不改变控制与协议语义。
   若目标是完整控制应用,通信、ADC、PWM 的迁移均服务于完整应用,
   不把整体目标缩减为通信验证。
   若已有 firmware_identity/firmware_candidate 等身份和网页打包流程,
   保留其功能与依赖顺序,把生成头文件依赖挂到实际使用它的模块。
10. 使用目标芯片对应的启动文件、链接脚本、HAL/CMSIS、时钟和引脚。
    明确生成文件与项目维护文件的边界。
11. 用户从根目录 VSCode 任务菜单操作。不要让用户通过多 CMake
    项目选择来完成日常构建、下载或打开配套网页。
12. 不复制原工程的 build、缓存、旧产物、机器绝对路径或凭据。
13. 如果目标目录已有未提交改动,保留它们,不覆盖或混入本次提交。

交付与验证:
14. 给出最终目录树、实际 CMake 目标清单及依赖关系,说明各模块职责。
15. 执行适用的配置、固件构建和主机检查,报告真实结果与产物位置。
16. 检查 CMake File API:唯一固件可执行目标归属根目录;
    功能模块确实是 STATIC_LIBRARY,平台目标采用 OBJECT_LIBRARY 等
    合适类型,源码归属正确。确认生成身份及打包依赖未被破坏。
17. 重新打开 VSCode,分别检查:
    Project Setup 只有一个预期工程入口;
    Project Outline 可见实际功能静态库与平台对象库,能够展开模块源码。
    若无法操作 GUI,明确记录该项未执行,不用静态检查代替。
18. README 写清楚打开哪个目录、运行哪个任务、输出在哪里。
19. 编译成功与上板验证分开报告;下载、复位和硬件操作按已授权范围执行。

请按上述模板完成,不要另行设计目录层级。
若芯片工具确实要求偏离模板,请指出具体原因和最小差异。

7. 怎样判断 Agent 按模板完成了工程

交付时,至少检查以下三层证据:

层次应检查的内容能证明什么
目录与构建模型根文件布局、唯一固件目标、功能库类型、源码归属、显式依赖工程具有实际模块边界
工具执行CMake 配置、真实编译链接、ELF/HEX、适用测试工程在当前工具链下可构建
用户操作Project Setup 单入口、Project Outline 模块库、根任务菜单入口简明,模块可见

配置成功不能替代编译链接成功;编译成功不能替代 GUI 验证;它们都不能替代新板卡的硬件验证。

这份模板的核心约定是:工程根目录拥有唯一固件目标,功能模块通过独立库目标装配并在 Outline 中可见,用户通过根目录任务完成日常操作。 把这三点写进 Agent 指令,目录结构才会与实际构建和使用方式一致。