STM32CubeMX CMake工程添加源文件:从CMakeLists.txt到构建配置
2026/8/29 23:39:07 网站建设 项目流程

LAT1574,这个编号如果你搜过,背后基本都跟着同一个问题:在STM32CubeMX生成的CMake工程里,怎么把自己写的源文件加进去。我问过不少人,十个里有八个第一次都卡在这里。CubeMX生成的CMake工程和Keil、IAR的习惯完全不同,Keil里右键Add Files to Group完事,CMake工程里你的源文件必须出现在构建脚本里,编译器才会理你。这篇就把这件事彻底讲透:生成的CMakeLists.txt怎么读、自己的.c和.S文件怎么加、头文件路径怎么补、哪些坑最容易踩。适合正在用STM32CubeMX + CMake做VSCode、CLion或命令行构建的开发者,也适合刚入门嵌入式、第一次接触CMake的朋友。

1. 为什么在STM32Cube的CMake工程里添加源文件会踩坑

1.1 手动添加源文件为什么是刚需,而不是IDE拖拽就能解决

先泼一盆冷水:CMake不是IDE文件树。你右键工程点“在文件资源管理器中显示”,或者把源文件拖进VSCode的文件夹视图,只是让你在编辑器里看到了这个文件,并没有通知CMake去编译它。CMake构建的编译单元完全由CMakeLists.txt里的变量决定,你在VSCode里看到一个文件,和编译时有没有带上它,完全是两码事。

很多第一次用CubeMX生成CMake工程的人,在工程里创建了一个app.c,从资源管理器看它就在那,编译也“成功”了,但链接时疯狂报undefined reference to xxx,就是这个原因。CubeMX生成的CMake工程,尤其是v6.x之后的版本,最核心的构建逻辑就是一句话:

add_executable(${PROJECT_NAME}.elf ${SOURCES} ...)

只有出现在SOURCES里的源文件,才会参与编译和链接。这件事我见过太多人踩坑,有的同事翻半天CMakeLists.txt问我为什么自己的模块编译不进去,我让他直接搜add_executable,看完就明白了。

1.2 什么场景下最容易触发这个需求

动手加源文件之前,先判断自己属于哪种情况,不同情况的处理路径不太一样:

  • 从旧工程移植代码:比如原来用标准外设库或老版HAL库写的驱动,要搬到新CubeMX工程里,这些文件不会自动进CMake列表。
  • 新增外设驱动:芯片的外设被CubeMX初始化之后,你还要自己写应用层驱动,比如点亮屏幕、驱动编码器、读写外部Flash,这些文件默认不会出现在任何地方。
  • 使用第三方中间件:FreeRTOS、LVGL、FatFS、USB协议栈这类开源库,CubeMX可能给你生成了部分配置,但很多源文件需要自己手动挂到工程里。
  • 自己封装模块:比如把LED、按键、状态机这些业务逻辑放在项目目录下,这是最常见的场景,也是必须会的基本功。
  • 多个板卡共用代码:做产品系列时,主控相同但外设不同,源文件按板卡分目录,CMakeLists里用条件判断决定编译哪些。

无论哪种情况,核心思路都一样:要么让源文件出现在CubeMX生成的SOURCES变量里,要么在构建系统里单独创建一个目标并把它加进去。理解这个逻辑,后面所有操作都不难。

2. 读懂CubeMX生成的CMakeLists.txt:源文件是怎么被收集的

2.1 主CMakeLists.txt里的核心变量与结构

先交代一下CubeMX生成的CMakeLists.txt长什么样。不同CubeMX版本生成的内容有差异,但整体结构基本一致,文件开头通常是:

cmake_minimum_required(VERSION 3.22) project(MyProject C ASM) set(CMAKE_C_STANDARD 11) # 核心变量:把所有要编译的源文件集中在这里 file(GLOB_RECURSE SOURCES "Core/Src/*.c" "Core/Src/*.s" "Drivers/STM32F4xx_HAL_Driver/Src/*.c" "Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/*.c" ) # 头文件搜索路径 include_directories( "Core/Inc" "Drivers/STM32F4xx_HAL_Driver/Inc" "Drivers/CMSIS/Device/ST/STM32F4xx/Include" "Drivers/CMSIS/Include" ) # 编译宏定义和编译选项 add_compile_definitions(USE_HAL_DRIVER STM32F407xx) add_compile_options(-mcpu=cortex-m4 -mthumb -O2 -Wall) # 最终生成elf目标 add_executable(${PROJECT_NAME}.elf ${SOURCES}) target_link_libraries(${PROJECT_NAME}.elf ...)

注意,这里我用STM32F4的例子展示,你自己的芯片型号对应路径会不一样。理解这个结构之后,加源文件的本质就非常清楚了:找到SOURCES怎么收集的,把新的路径加进去;找到include_directories,把新的头文件目录加进去。两件事做完,构建系统就认识你的文件了。

2.2 静态列表和GLOB动态扫描:两种源文件收集方式

这是最容易被网上五花八门的教程带偏的地方。CubeMX不同时期生成的CMakeLists,源文件收集方式完全不一样。

静态列表方式是老版本CubeMX的默认风格,SOURCES是一长串set(SOURCES ...),每个文件一行,写得密密麻麻:

set(SOURCES Core/Src/main.c Core/Src/stm32f4xx_hal_msp.c Core/Src/stm32f4xx_it.c Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c ... )

这种方式的优点是明确,缺点是每次新增文件都要手动改列表,漏一个编译就报错,管理成本高。

GLOB动态扫描方式是较新版本CubeMX的默认风格,用file(GLOB_RECURSE ...)按通配符递归收集:

file(GLOB_RECURSE SOURCES "Core/Src/*.c" "Core/Src/*.s" )

这种方式的优点是新增文件不用频繁改CMakeLists,只要放在被扫描的目录里,重新configure一次就能自动进来。缺点是它只在CMake configure时扫描一次,加了新文件之后如果忘记重新配置,文件不会自动出现。

哪种更好?说实话,CubeMX官方的做法是有历史包袱的,它要兼容各种场景。但对于我们日常开发,我强烈建议自己维护一个独立目录,用GLOB扫描自己的目录,不要直接改CubeMX生成的Core/Src扫描路径,这样CubeMX重新生成工程后你的代码路径还能留在里面。具体怎么弄,第三部分会详细说。

2.3 一个典型CMakeLists.txt完整解读

再来读一个稍微完整的例子,因为实际项目里除了源文件列表,还有几个隐藏的坑。

cmake_minimum_required(VERSION 3.22) project(MyProject C ASM) # 设置C语言标准,嵌入式常用的GNU11 set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) # 声明汇编语言支持,如果没有这句,.S/.s文件不会被识别 enable_language(ASM) # 收集所有需要编译的源文件 file(GLOB_RECURSE SOURCES "Core/Src/*.c" "Core/Src/*.s" "Drivers/STM32F4xx_HAL_Driver/Src/*.c" "Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/*.c" "Middlewares/Third_Party/FreeRTOS/Source/*.c" "Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/*.c" "Middlewares/Third_Party/FreeRTOS/Source/portable/MemMang/heap_4.c" ) # 头文件搜索路径 include_directories( "Core/Inc" "Drivers/STM32F4xx_HAL_Driver/Inc" "Drivers/CMSIS/Device/ST/STM32F4xx/Include" "Drivers/CMSIS/Include" "Middlewares/Third_Party/FreeRTOS/Source/include" "Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F" ) # 编译宏定义,HAL库和芯片系列必须在这里声明 add_compile_definitions(USE_HAL_DRIVER STM32F407xx) # 编译选项,链接脚本、浮点、优化的配置 add_compile_options(-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -O2 -Wall) # 生成elf add_executable(${PROJECT_NAME}.elf ${SOURCES}) # 链接库、链接选项 target_link_libraries(${PROJECT_NAME}.elf -lm) target_link_options(${PROJECT_NAME}.elf PRIVATE -T ${CMAKE_CURRENT_SOURCE_DIR}/STM32F407VGTx_FLASH.ld)

读到这里你应该明白几个关键点:

  • SOURCES变量是所有编译单元的总集合。
  • include_directories告诉编译器去哪里找头文件。
  • add_compile_definitions里的USE_HAL_DRIVERSTM32F407xx是所有源文件共享的,你新增的源文件也会自动带上。
  • target_link_options里的链接脚本和链接选项,只影响最终链接,不影响你添加源文件。

知道这些之后,往工程里加文件就简单了。

3. 把源文件加入工程的标准操作流程(附可用代码)

这一部分我按自己的实际习惯给出一套完整流程,你可以直接照抄。

3.1 第一步:规划用户代码目录

我的建议是,不要把自己的代码塞进Core/SrcDrivers/。原因很简单,CubeMX每次重新生成工程时,Core/Src里由它管理的文件会被覆盖。你自己新建的app.c虽然不会被删除,但和CubeMX生成的文件放一起,时间长了完全分不清哪部分是手写的、哪部分是生成的,维护成本直线上升。

我在工程根目录下推荐建这样一个结构:

MyProject/ ├── CMakeLists.txt ├── Core/ │ ├── Inc/ │ └── Src/ ├── Drivers/ │ └── STM32F4xx_HAL_Driver/ ├── User/ │ ├── inc/ │ │ ├── app.h │ │ ├── led.h │ │ └── key.h │ └── src/ │ ├── app.c │ ├── led.c │ └── key.c ├── Middlewares/ └── STM32F407VGTx_FLASH.ld

User目录下的文件全部是业务代码,不依赖CubeMX重新生成,也不参与CubeMX的工程管理,我们只需要在CMakeLists.txt里把这个目录加进去,以后CubeMX怎么重新生成都不影响。

3.2 第二步:把源文件写进构建列表

接下来打开CMakeLists.txt,在源文件收集部分做修改。如果你用的是新的GLOB风格,最简单的方式是把User目录追加进去:

file(GLOB_RECURSE SOURCES "Core/Src/*.c" "Core/Src/*.s" "Drivers/STM32F4xx_HAL_Driver/Src/*.c" "Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/*.c" "User/src/*.c" # 新增:扫描自己的源文件目录 )

如果你习惯用静态列表,就是set(SOURCES ...)那个版本,直接在列表末尾加一行:

set(SOURCES ... Core/Src/main.c ... User/src/app.c User/src/led.c User/src/key.c )

相比直接改CubeMX生成的这块内容,我其实更推荐另一种更安全的做法:在主CMakeLists.txt末尾添加一个独立的用户CMake片段。CubeMX重新生成后,主文件虽然可能被覆盖,但你可以把include(CMakeLists_user.cmake)这一行想办法加回去,或者干脆在CubeMX生成后重新执行一次。这个方案在后面5.3节会细讲,这里先把核心代码给你:

# CMakeLists_user.cmake,放在工程根目录下 file(GLOB_RECURSE USER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/User/src/*.c" ) list(APPEND SOURCES ${USER_SOURCES}) include_directories( "${CMAKE_CURRENT_SOURCE_DIR}/User/inc" )

然后在主CMakeLists.txt的add_executable(${PROJECT_NAME}.elf ${SOURCES})之前加入:

include(CMakeLists_user.cmake)

这种方式的好处是,你的用户代码永远在一个独立文件里管理,CubeMX生成的CMakeLists被覆盖后,你只需要重新加上一行include,或者用脚本自动处理,比在几个大列表里一点点找路径要省心得多。

3.3 第三步:补上头文件搜索路径

源文件加进去了,头文件搜索路径不加,编译器马上报fatal error: app.h: No such file or directory。这一步通常在include_directories区域补上你的头文件目录:

include_directories( "Core/Inc" "Drivers/STM32F4xx_HAL_Driver/Inc" "Drivers/CMSIS/Include" "User/inc" # 新增 )

如果你用的是target_include_directories,就加在对应target下面:

target_include_directories(${PROJECT_NAME}.elf PRIVATE "User/inc" )

这里有一个容易忽略的细节:如果你的源文件之间互相引用,比如led.c#include "app.h",而app.hUser/inc下,那你只需要保证User/inc在搜索路径里就行,不需要每个目录都加。头文件搜索是一个全局行为,编译每个源文件时都会去这些路径里找。

3.4 第四步:重新配置并验证编译

这一步非常容易被忽略,尤其是用了file(GLOB_RECURSE)的工程。记住,GLOB是在CMake configure的时候扫描文件的,你在User/src下新建了led.c,然后直接cmake --build build,CMake根本不知道这个新文件存在,它不会出现在编译调用里。

所以添加源文件之后,一定要重新跑一次CMake配置。命令行下是:

cmake -S . -B build

如果你是Windows下用VSCode + CMake Tools插件,在命令面板里运行CMake: Configure即可。配置完成后,再编译:

cmake --build build

如果编译输出里能看到User/src/led.c这个文件被编译,且链接阶段没有undefined reference错误,说明你的源文件已经被成功挂进工程了。

3.5 重要提醒:CubeMX重新生成后会覆盖什么

这个坑我必须单独拿出来强调。CubeMX生成的CMake工程,和Keil/IAR工程不一样,它不会像MDK那样用/* USER CODE BEGIN */区块来保护你手动添加的代码。CubeMX重新生成时,它会直接重写CMakeLists.txt,你手动加进去的路径、include目录、编译选项,全部会被覆盖。

我见过一个项目,工程师在add_executable之前加了一堆自己的源文件路径,第二天CubeMX右键重新生成,所有配置全部归位,编译直接报几十个未定义引用。

应对策略就是前面说的:把你的代码放到独立User目录,并用独立CMakeLists_user.cmake管理。CubeMX重新生成后,主CMakeLists被覆盖,但你只需要恢复那一行include(CMakeLists_user.cmake),就可以让所有用户代码回来。甚至更自动化一点,写一个小脚本,每次生成后自动帮你在主CMakeLists里插入这一行,整个流程就很稳了。

4. 常见添加场景该怎么处理:单文件、整目录、汇编与第三方库

实际操作中,加一个文件是最基础的,但更多时候我们是在加整个模块、加汇编启动文件、加第三方库。每种场景处理起来有细微差别,我分别说一下。

4.1 添加单个源文件:最省事的改法

如果你只加一个文件,比如把别人发来的soft_i2c.c放进自己的目录,最省事的办法是直接在SOURCES的GLOB扫描里加一行,单独指定这个文件:

file(GLOB_RECURSE SOURCES "Core/Src/*.c" "Core/Src/*.s" "User/src/*.c" "User/src/soft_i2c.c" # 如果不想递归扫描,可以直接指定 )

但注意,如果User/src/*.c已经扫描了soft_i2c.c,再加一行会重复编译吗?不会,CMake会去重。但为了可读性,不建议写两遍。单个文件的场景,我一般会把文件放到User/src下,然后靠目录扫描自动带上,根本不用改CMakeLists。只有在文件放在项目之外、比如../common/soft_i2c.c这种跨目录位置时,才需要单独加绝对路径。

4.2 添加整个模块目录:用GLOB_RECURSE管理

一个模块通常包含多个源文件和头文件,比如你写了motor模块,里面可能有motor.cmotor_pid.cmotor_encoder.c,把它们放在User/src/motor/User/inc/motor/下,然后修改GLOB扫描:

file(GLOB_RECURSE SOURCES "Core/Src/*.c" "Core/Src/*.s" "User/src/*.c" # 递归扫描,会自动包含子目录 ) include_directories( "User/inc" "User/inc/motor" # 模块自己的头文件路径 )

GLOB_RECURSE会递归扫描子目录,所以motor.cmotor_pid.c都会被自动加进来。以后往motor目录里新增一个motor_debug.c,只要重新configure,CMake就会自动带上,不需要再改任何列表。这是我在日常开发中最喜欢的方式。

但这里有个反模式要提一下:不要无脑用file(GLOB_RECURSE SOURCES ".*")把整个工程都递归进去。因为build目录和.git目录也在工程根目录下,递归会把一堆编译生成物、第三方资源也扫进来,轻则编译变慢,重则导致奇怪的构建错误。给GLOB指定具体的代码目录,是每个人应该养成的习惯。

4.3 添加汇编文件:.S和.s的区别千万别搞错

汇编文件在嵌入式工程里很常见,启动文件startup_stm32f4xx.s就是其中之一,CubeMX默认已经加好了。如果你想自己新增汇编优化代码,比如某个耗时函数的ARM汇编版本,有两点必须注意。

第一,文件后缀的大小写有讲究:

  • .s文件,CMake会把它交给汇编器直接汇编,不经过C预处理器。
  • .S文件,CMake会先让C预处理器处理一遍,再交给汇编器。

这个区别很隐蔽。如果你写的是带#include#define的汇编文件,必须用.S后缀。如果你用.s,预处理器不会运行,#define不被替换,汇编直接报错。

第二,需要在CMakeLists里确认汇编语言已经开启。CubeMX生成的工程默认会声明enable_language(ASM),一般不用操心,但如果你新开一个最小CMake工程,很容易忘记:

enable_language(ASM) set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -mcpu=cortex-m4 -mthumb")

汇编源文件的添加方式和其他源文件一样,放进GLOB扫描路径或者静态列表都行。添加后如果报Error: unknown pseudo-op,先检查是不是汇编器工具链选错了,CMake找到的可能是系统自带的as,而不是arm-none-eabi-as。确认你的工具链在PATH里并且名字正确,一般能解决。

4.4 添加第三方库:以FreeRTOS为例

第三方库比单文件复杂在两点:源文件多、头文件依赖深。以FreeRTOS为例,CubeMX生成的工程本身已经带了FreeRTOS,但如果你手动集成一份新版本,或者把一个原本没有FreeRTOS的工程加上FreeRTOS,需要添加的源文件包括:

FreeRTOS/Source/tasks.c FreeRTOS/Source/queue.c FreeRTOS/Source/list.c FreeRTOS/Source/timers.c FreeRTOS/Source/event_groups.c FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c FreeRTOS/Source/portable/MemMang/heap_4.c

头文件目录:

FreeRTOS/Source/include FreeRTOS/Source/portable/GCC/ARM_CM4F

还有一个非常容易漏的东西:FreeRTOSConfig.h。FreeRTOS通过这个头文件来配置裁剪、堆大小、时钟节拍等。它通常放在你工程的Core/IncUser/inc下,如果找不到,编译时会报#error "FreeRTOSConfig.h not found"configTOTAL_HEAP_SIZE相关错误。添加路径的时候,确认FreeRTOSConfig.h所在的目录也在include_directories里。

第三方库添加到CMakeLists的方式没有魔法,就是把源文件目录和头文件目录分别加进对应的GLOB和include_directories。但建议单独建一个变量来管理,不要全部塞进SOURCES,这样以后想整体移除某个库,直接删一个变量就行:

set(FREERTOS_SOURCES "Middlewares/Third_Party/FreeRTOS/Source/tasks.c" ... ) list(APPEND SOURCES ${FREERTOS_SOURCES})

5. 高频报错与排查经验速查表

我自己这几年在CMake + STM32开发上,遇到过的报错可以说是五花八门。这里挑几个最高频的,简单说说现象和排查思路,先看速查表。

现象可能原因排查与解决
fatal error: xxx.h: No such file or directory头文件搜索路径缺失检查include_directoriestarget_include_directories是否包含xxx.h所在目录
undefined reference to 'xxx'源文件没有进入构建检查SOURCES是否包含源文件,重新cmake configure,确认编译输出中有该文件
新增文件后编译不变,还是报找不到符号使用file(GLOB_RECURSE)但没重新configure删除build目录重新cmake,或在VSCode中执行CMake: Configure
修改CMakeLists后报CMake Error: xxx路径拼写、括号不匹配看具体报错行,检查路径是否存在,注意相对路径基于CMAKE_CURRENT_SOURCE_DIR
CubeMX重新生成后改动全没主CMakeLists被覆盖把用户代码和用户CMake配置独立出来,重新include用户片段
汇编文件报unknown pseudo-op或预处理指令不展开.s.S后缀用错,或者没开ASM带预处理器的汇编用.S,确认enable_language(ASM)
编译乱码,中文注释全变问号文件编码问题统一用UTF-8,Windows下注意源文件编码,或加编译选项-finput-charset=UTF-8
报固件包版本相关错误(如stm32cube fw_f1 v1.8.7依赖问题)CubeMX固件包版本与版本要求不匹配,和源文件无关到CubeMX的固件包管理器里升级或重新选择匹配的固件包版本

5.1 头文件找不到与未定义引用

这两个是新手最常撞上的。头文件找不到好解决,看报错里是哪个头文件,去目录里找它在哪里,把路径加进include_directories就行。要注意的是,有时候路径写对了但还是找不到,那就是相对路径的位置问题。CMake里相对路径是相对于当前CMakeLists.txt所在目录,你如果把路径写错层级,比如少写了../,就会找不到。实在不行就用${CMAKE_CURRENT_SOURCE_DIR}拼绝对路径,最安全。

未定义引用相对麻烦,因为它不是“找不到文件”而是“文件没被编译”或者“编译了但符号没进链接”。排查步骤我一般这样走:

  1. 先看编译日志里有没有编译这个文件,没有说明源文件没进构建,检查SOURCES
  2. 如果编译了但还是未定义引用,看是不是函数名在不同文件里拼写不一致,比如头文件声明了led_init,源文件里定义成了LedInit,C语言是区分大小写的。
  3. 检查这个函数是不是被编译条件排除了,比如源文件里有#ifdef ENABLE_LED,但宏没定义。

5.2 文件加了但没编译进去:GLOB的时效性

这个坑我用一个真实经历说明。有一次我在User/src下新建了motor.c,然后信心满满地cmake --build build,结果链接器疯狂提示undefined reference to motor_init。我当时怀疑是函数拼写问题,检查了半天没发现错,后来发现CMake根本没编译motor.c

原因前面提过:file(GLOB_RECURSE)只在configure期间扫描文件。我新建了文件,但CMake的配置没重新跑,编译系统根本不知道有motor.c。执行一次cmake -S . -B build,再重新编译,问题立刻消失。

这不是C语言的问题,是CMake的缓存机制。遇到这种情况,先别怀疑代码,重新configure一次,概率直接解决一半。

5.3 CubeMX重新生成把改动冲掉的经典事故

我最初带项目团队时,给团队定过一个规矩:CubeMX生成的CMakeLists.txt,不允许任何人手改。原因就是前面说的,重新生成会被冲掉。这个规矩执行了半年,帮大家避免很多次加班。

实际操作上可以更灵活一点。如果你坚持在主CMakeLists里加东西,每次CubeMX重新生成后,用版本管理工具查看diff,把被冲掉的用户代码片段重新合并回来,其实也花不了几分钟。但时间一长,或者CubeMX频繁重新生成,这种手工合并一定会有遗漏。所以我还是推荐独立用户CMake片段的方式。

一个可落地的做法是:在工程根目录放CMakeLists_user.cmake,CubeMX生成后自动执行一个小脚本,检查主CMakeLists里是否已有include(CMakeLists_user.cmake),没有就自动加在add_executable之前。这个脚本可以用Python或者一个批处理文件搞定。跑一次,所有用户代码路径自动回来,省心很多。

5.4 其他容易误判的问题:固件包版本、编码与IDE索引

有些报错看起来像源文件问题,实际不是。比如CubeMX界面或者构建时提示the firmware package (stm32cube fw_f1 v1.8.7) or one of its dependencies requires a higher version这类,说明你本地固件包版本和CubeMX要求不匹配,需要去CubeMX固件包管理器里更新或重新选择。这种问题跟你的源文件没有任何关系,千万别往CMakeLists上找原因。

还有一个和IDE相关的问题是,在VSCode里CMake Tools的IntelliSense有时候和实际编译结果不一致,明明代码编译通过,编辑器却标红一堆。这时候运行CMake: Delete Cache and Reconfigure,或者把compile_commands.json输出给Clangd用,通常能解决。生成compile_commands.json也很简单:

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

放在CMakeLists里,构建之后就会在build目录生成compile_commands.json,VSCode的clangd插件识别到这个文件后,代码提示和索引会准确很多。

6. 我的工程组织习惯与后续扩展建议

6.1 推荐一套长期好维护的工程组织方式

这套方式我在团队里推行了很久,稳定性和开发效率都不错:

  • CubeMX的工作目录和CMake工程目录分离。CubeMX只管生成初始骨架和外设初始化,生成后锁住,不轻易重新生成。
  • 用户代码统一放User/目录,里面有srcincmodule三个子目录,模块化程度高的项目甚至可以一级模块一个目录。
  • 用户CMake片段独立成文。主CMakeLists只保留CubeMX生成的内容,用户代码、第三方库都通过CMakeLists_user.cmake统一管理。
  • 源文件收集优先用GLOB_RECURSE + CONFIGURE_DEPENDS。如果你的CMake版本不低于3.12,可以在file(GLOB_RECURSE ...)后面加一个CONFIGURE_DEPENDS参数,这样新增文件时CMake会自动检测文件系统变化并重新配置。这个特性不是所有环境都完美,比如在某些网络驱动器上可能无效,但本地开发很省事。
file(GLOB_RECURSE USER_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/User/src/*.c" )
  • 每个模块的头文件路径集中在User/inc,同一层级的include目录保持扁平,避免到处include_directories导致路径管理混乱。

6.2 把CMake工程扩展到多板卡与CI

当你适应了这套CMake组织方式后,它会成为项目释放生产力的很好助力。比如做产品家族时,多块板子共用一套驱动代码,只是外设引脚和数量不同,可以在CMakeLists里加一个BOARD变量,根据板卡决定编译哪些源文件:

if(BOARD STREQUAL "DEV_BOARD") list(APPEND SOURCES "User/src/board_dev.c") elseif(BOARD STREQUAL "PROD_BOARD") list(APPEND SOURCES "User/src/board_prod.c") endif()

命令行编译时指定:

cmake -S . -B build -DBOARD=DEV_BOARD

这套逻辑还可以直接对接CI流水线,代码提交后自动编译所有板卡配置,谁改了驱动影响哪块板子,构建系统立刻告诉团队。这也是我为什么强烈建议花时间把CMake工程结构搞清楚,它不只是“加个源文件”这么简单,而是为整个项目的可维护性打底。

我个人在实际项目里的体会是,CMake工程里的源文件管理并不难,难的是从一开始就养成区分“生成代码”和“用户代码”的习惯。只要你的用户代码永远在自己独立的目录和独立的CMake片段里,后面不管是CubeMX升级、固件包更换、还是从一个人开发变成五个人协作,都不会出现“改一处崩一片”的窘境。如果你刚开始把工程切到CMake,就先按这个思路把目录建好,把源文件挂进去,跑通一次最简单的流程,后面再慢慢加中间件和模块化,完全来得及。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询