☰
STM32CubeMX CMake工程添加源文件:从GLOB到CMakeLists配置全解析
2026/10/29 14:06:02 网站建设 项目流程

用STM32CubeMX生成CMake工程,很多朋友遇到的第一道坎往往不在业务代码,而是“我明明加了一个.c文件,编译结果却像没看见一样”。在IDE里点两下就能解决的事情,到了CMake工程里突然变得不透明——文件放哪个目录才对、CMakeLists.txt里该动哪一行、为什么重新生成代码之后我加的东西全没了,这些问题反复出现。LAT1574这个编号背后问的,其实就是这套流程的完整玩法。

这篇文章我把CubeMX生成的CMake工程从“源文件到底是怎么被收集的”讲起,再给出两条落地路线:一条是让CubeMX自己托管目录,另一条是手动改CMakeLists.txt精确控制。后面还会用实际工程演示添加一个自写驱动模块的完整过程,把编译、链接、头文件路径这几个高频报错点全部过一遍。适合用STM32CubeMX生成CMake工程、做命令行构建或者想在工程里集成第三方代码的人参考。

1. CubeMX生成的CMake工程里,源文件清单究竟长什么样

1.1 从CubeMX生成CMake工程的第一步

在STM32CubeMX里新建工程后,Project Manager页面有个Toolchain/IDE下拉框,选CMake,然后点GENERATE CODE,就会得到一个包含CMakeLists.txt的完整工程目录。生成的目录结构大体是这个样子:

my_project/ ├── CMakeLists.txt ├── Core/ │ ├── Inc/ │ ├── Src/ │ └── Startup/ ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/(具体型号对应不同前缀) ├── Middlewares/(选了中间件才会有) └── build/(构建输出目录,由你手动创建)

CubeMX对目录的规划是有固定套路的,Core/Src放用户逻辑,Drivers放HAL库,Middlewares放中间件。CMakeLists.txt在工程根目录,是整个构建系统的入口,里面定义了芯片型号、链接脚本、编译宏、源文件集合、头文件搜索路径,还有最终的固件目标名。

很多人在这一步就开始乱放了。想加一个自己写的驱动,随手创建个新目录,把.c文件丢进去,然后在CubeMX里重新生成代码,发现编译产物里根本没有这个文件。原因很简单——CubeMX生成的CMakeLists.txt只认它模板里写好的那几个目录,你随手建的新目录如果不在它扫描的清单里,就不会被编译。

1.2 GLOB扫描和显式列表:两种不同的“源文件收集”机制

打开新生成的CMakeLists.txt,重点看源文件收集这一段。不同CubeMX版本生成的模板会有差别,但基本就两种风格。

新一点的CubeMX版本(6.x中后期),生成的内容类似这样:

file(GLOB_RECURSE SOURCES "Core/*.c" "Drivers/*.c" "Middlewares/*.c" )

这就是GLOB模式。它不手写文件名,而是让CMake在构建时去扫描指定目录下的所有.c文件,自动拼出一个源文件列表。在这种模式下,只要你的.c文件放进Core下的任意子目录,理论上都会被扫描到并参与编译。注意,我用了“理论上”这个词,因为GLOB有一个非常迷惑的坑。

老一点的CubeMX版本,或者某些定制工程,生成的可能是显式列表模式:

set(SOURCES Core/Src/main.c Core/Src/stm32f1xx_it.c Core/Src/system_stm32f1xx.c Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal.c ... )

这种模式会把每个文件都列出来,新增文件时如果不手动往这个列表里加,编译绝对看不见它。

判断你的工程是哪一种模式,直接打开CMakeLists.txt搜索SOURCES就能看到。这个判断非常重要,因为后面的所有操作都基于它。如果你用GLOB模式却按照显式列表的思路去改CMakeLists,那是在做无用功;反过来也一样。

1.3 GLOB不会自动重新扫描,这才是“加了文件没反应”的根源

GLOB模式看起来省心,但它有个隐蔽行为:CMake在生成构建系统时,会做一次目录扫描,把当时的文件列表固化到构建系统里。之后你往目录里新增文件,如果不重新运行CMake配置步骤,构建系统里的文件列表不会自动更新。

打个比方,这就好比你在餐厅点了菜,菜单是厨师进厨房前定好的。你临时想加一道菜,但厨师手里的单子没改,后厨连菜都没备。

操作上的表现就是:你把新的.c文件放进Core/Src,直接make或点击构建,编译过程里完全没有新文件的身影。必须重新执行一次CMake配置,让GLOB重新扫描一遍,新文件才会进入构建系统。

命令行方式的处理方式一般是:

cmake -S . -B build cmake --build build

在STM32CubeIDE里,IDE有时会帮你自动重新配置,但在纯命令行环境或自定义CI流程里,这就需要你自己记住。这个细节是后面所有排查的基础,先记牢。

2. 最不容易出错的方案:让CubeMX帮你登记源文件

2.1 通过Project Manager把自定义目录交给CubeMX

如果你不想每次手工改CMakeLists.txt,最稳的做法是让CubeMX自己来登记。CubeMX的Project Manager页面里有一个Source/Header管理功能,具体位置在Project Manager -> Project -> Folder区域,不同版本界面略有区别,但核心逻辑一致:你可以添加额外的源文件目录和头文件目录,CubeMX在生成代码时会把这些目录写进CMakeLists.txt。

我在实际工程中的做法是:在工程根目录下新建一个UserCode文件夹,里面按模块分子目录,比如UserCode/led、UserCode/motor,然后在CubeMX的Source目录管理里把UserCode加进去,在Header目录管理里也把UserCode加进去。这样CubeMX生成的CMakeLists.txt会自动在GLOB扫描范围里增加UserCode目录,头文件搜索路径也会自动包含UserCode。

操作完成后重新生成代码,打开CMakeLists.txt看一眼,你会发现类似这样:

file(GLOB_RECURSE SOURCES "Core/*.c" "Drivers/*.c" "UserCode/*.c" ) target_include_directories(${PROJECT_NAME}.elf PUBLIC "Core/Inc" "Drivers/STM32F1xx_HAL_Driver/Inc" "UserCode" )

以后你往UserCode下添加任何.c文件,只要重新跑一次CMake配置,新文件就能被识别。这种方式的好处非常明显:CubeMX不管重新生成多少次代码,这个登记关系都会保留,你不需要在每次生成后手动打补丁。

2.2 重新生成代码后,你的文件去向由CubeMX决定

这种方案的关键在于“不要绕过CubeMX去添加目录”。我看到很多人习惯直接在文件系统里新建目录,把代码放进去,然后跑到CubeMX里点一下生成代码,心里默认CubeMX会扫描整个工程目录。实际不会。

CubeMX只会在CMakeLists.txt里写入它自己配置过的目录,也就是它能识别的那些标准目录加上你通过Project Manager登记的目录。你随手新建的目录,它无从知道,更不会自动加进GLOB扫描范围。

所以流程必须是:先在CubeMX的界面里登记目录,再把文件放进那个目录,最后生成代码。顺序不能反。文件放好再去登记,重新生成后同样有效,但你先登记再放文件,逻辑上更清晰,不容易漏。

2.3 这个方案的边界和隐患

让CubeMX托管目录适合大多数常规场景,但也有局限。首先是CubeMX版本差异,不同版本对Source/Header管理的界面和生成效果不完全一致,有的版本生成的CMakeLists.txt会把登记的目录单独列一组,有的版本则直接合并进默认的GLOB列表。

其次是批量移动文件时会比较麻烦。如果你想重构目录结构,把某个模块从一个目录挪到另一个目录,你不仅要在文件系统里移动文件,还要回到CubeMX里改登记的目录,再重新生成。相比之下,手动改CMakeLists.txt可能更直接。

还有一个隐患容易被忽略:CubeMX重新生成代码时,确实会覆盖CMakeLists.txt,但它只会覆盖自己负责的那部分内容。如果你之前在CMakeLists.txt里手动添加过自定义内容,重新生成后这些手写内容基本都会丢失。这个风险不只在源文件收集区,凡是CubeMX模板关心的区域都是这样。所以我一般不推荐在CubeMX生成物上手工改太多,除非你能接受每次生成后重新补丁。

3. 手动改CMakeLists.txt:需要改哪一行、怎么改才安全

3.1 定位源文件收集区

手动改CMakeLists.txt的前提,是你能准确识别出源文件收集区在哪个位置。打开文件,搜索SOURCES,你就会找到刚才说的两种模式之一。

GLOB模式下,你只需要在file(GLOB_RECURSE SOURCES ...)里追加一行目录路径。

file(GLOB_RECURSE SOURCES "Core/*.c" "Drivers/*.c" "Middlewares/*.c" "UserCode/led/*.c" )

显式列表模式下,在set(SOURCES ...)末尾追加文件路径。

set(SOURCES Core/Src/main.c Core/Src/stm32f1xx_it.c ... UserCode/led/led.c )

注意Windows环境下路径里的斜杠方向,CMake对正斜杠和反斜杠的处理比较宽松,但为了跨平台和可读性,统一用正斜杠。我见过不少人在复制Windows路径时直接把\带进来,构建时出现各种奇怪的找不到文件,改成/就正常了。

3.2 追加路径的推荐写法

手动追加源文件路径时,有两种常见写法:直接填充到已有的GLOB列表里,或者单独新增一个GLOB列表再用list合并。

直接填充最省事,但有个问题:如果你想在CubeMX重新生成后又不需要再补丁,你加的这一行会被覆盖。为了对抗覆盖,我习惯把自定义部分单独写成一段,放在CubeMX生成区域之外,用list(APPEND)方式合并。

# User custom sources - added manually file(GLOB_RECURSE USER_SOURCES "UserCode/*.c" ) list(APPEND SOURCES ${USER_SOURCES})

这样即使CubeMX重新生成覆盖了它自己管理的那段GLOB,我手写的这段代码只要放在CMakeLists.txt的末尾,就不会被CubeMX动到。当然,前提是你不要在CubeMX里改动任何与CMakeLists.txt生成有关的设置后再重新生成,否则整个文件都可能被重写。

这种写法的好处是隔离清晰。CubeMX管它的,你管你的。以后排查问题时,只需要搜索“User custom sources”注释,就知道哪些是人工维护的部分。

3.3 头文件目录千万别漏

源文件加进SOURCES只是第一步,如果源文件里#include了自定义头文件,还需要把头文件所在目录加入include路径。

在CMakeLists.txt中找到target_include_directories相关部分,把目录加进去。

target_include_directories(${PROJECT_NAME}.elf PUBLIC "Core/Inc" "Drivers/STM32F1xx_HAL_Driver/Inc" "UserCode/led" )

C语言的头文件搜索机制是:#include "xxx.h"会先找当前源文件所在目录,然后找include目录列表。如果头文件跟源文件在同一个目录,不加include路径有时也能编译通过,因为GCC会查找源文件所在目录。但如果头文件放在别的目录,不加include路径编译时就会报fatal error: xxx.h: No such file or directory。

这个报错太常见了,很多人以为是文件没加进工程,其实是头文件路径缺失。等下实战部分我会专门演示。

3.4 改完后一定要重新configure

这句话说起来很简单,但真的很多人会在这里栽跟头。你在CMakeLists.txt里改了GLOB路径或新增了源文件,直接执行cmake --build build,有时候看似生效,有时候完全不生效,关键在于CMake是否会检测到CMakeLists.txt变化并自动重新配置。

大多数CMake生成器(包括Unix Makefiles和Ninja)会在构建时自动重新运行CMake配置,前提是CMakeLists.txt被修改了。所以如果你改了CMakeLists.txt,构建时会触发重新配置,这没问题。但如果你只是往目录里新增了一个.c文件,没有改动CMakeLists.txt,那么GLOB模式下的文件列表不会自动更新,直接build不会生效。

这个区别一定要记住。我自己的习惯是,每次新增文件后,无论有没有改动CMakeLists.txt,都手动执行一次cmake -S . -B build,然后再build。多花两秒钟,省掉一整天排错时间。

4. 实战:给工程添加一个自写的驱动模块

4.1 目录组织与代码准备

假设我们的工程基于STM32F103C8T6,CubeMX生成的CMake工程已经能正常编译。现在要添加一个LED驱动模块,文件放在UserCode/led/led.c和UserCode/led/led.h。

为什么新起一个UserCode目录而不是直接塞进Core/Src?因为Core/Src是CubeMX标准管理的目录,它生成的主循环和中断处理文件都在那里。如果把自己模块的代码混进去,以后CubeMX重新生成时万一有同名文件或结构调整,容易出问题。单独起目录,边界清晰,自己的代码和CubeMX生成的代码互不干扰。

led.c和led.h的内容很简单,只是封装GPIO初始化:

// led.h #ifndef __LED_H #define __LED_H #include "stm32f1xx_hal.h" void LED_Init(void); void LED_On(void); void LED_Off(void); #endif
// led.c #include "led.h" void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_13; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); } void LED_On(void) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); } void LED_Off(void) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); }

代码本身不复杂,重点是接下来的构建接入过程。

4.2 改CMakeLists.txt的完整操作

打开CMakeLists.txt,找到源文件收集区。假设你的工程是GLOB模式,在GLOB列表里追加一行:

file(GLOB_RECURSE SOURCES "Core/*.c" "Drivers/*.c" "Middlewares/*.c" "UserCode/*.c" )

然后在include目录区追加:

target_include_directories(${PROJECT_NAME}.elf PUBLIC "Core/Inc" "Drivers/STM32F1xx_HAL_Driver/Inc" "UserCode" )

如果你的工程是显式列表模式,就在set(SOURCES ...)里追加UserCode/led/led.c,同时也要加include目录。

改完之后,命令行执行:

cmake -S . -B build cmake --build build

第一次执行cmake时,如果build目录不存在,CMake会创建并生成构建系统。如果build目录已经存在,它会检查CMakeLists.txt的变化并重新生成。

4.3 编译通过后发现链接报错怎么办

构建成功之后,LED模块理论上已经进入固件。如果你在main.c里调用LED_Init(),可能会遇到两种报错。

第一种:undefined reference to 'LED_Init'。这个报错说明链接器找不到LED_Init的实现。典型原因有三个:源文件没被编译进SOURCES;编译了但函数名拼写不一致(比如led.c里实现的是led_init,main.c里调用的却是LED_Init);或者源文件被某个条件编译指令包住了,实际代码没有被编译进目标文件。

排查时先确认编译过程里有没有led.c。在CMake生成的build目录下,找到对应子目录里的编译产物,比如build/CMakeFiles/xxx.elf.dir/UserCode/led/led.c.obj,如果不存在,说明文件确实没进SOURCES。如果存在,再检查函数名和条件编译。

第二种:undefined reference to 'HAL_GPIO_WritePin'。这个报错说明你的代码用到了HAL库函数,但链接时没找到HAL库的实现。在CubeMX的CMake工程里,HAL源码一般在Drivers目录下,GLOB扫描时应该会被覆盖到。出现这种情况,大概率是Drivers目录的GLOB扫描范围有问题,或者链接配置里漏掉了某些库。先确认Drivers/*.c有没有写进GLOB列表,再看Drivers目录结构有没有变动。

4.4 添加后如何验证它真的进了编译

有时候构建成功,但你觉得固件行为不对,想确认自己的文件到底有没有被编译,最直接的方法是看编译日志里的文件名。CMake构建时,每个.c文件编译都会输出一行以Building C object开头的消息,后面跟着源码路径。

在命令行中可以这样:

cmake --build build --verbose

输出里会显示完整的编译命令,包括每个源文件路径。搜索led.c,如果看到了,说明文件确实进入了编译流程。如果没有,说明文件根本没被GLOB扫描到或没加进SOURCES。

用这个方法排查比看CMakeLists.txt更直观,因为CMakeLists.txt可能因为换行、注释等原因写了目录但实际没生效,而编译日志是最终结果。

5. 第三方代码和子目录:要不要用add_subdirectory

5.1 为什么CubeMX工程默认不用子目录CMakeLists

CubeMX生成的工程是典型的单层CMakeLists结构,所有源码都在根目录的CMakeLists.txt里统一收集。它不会给你搞add_subdirectory那一套,因为对于简单的嵌入式工程,一个文件管完所有路径和源文件,比多层CMakeLists更好维护。

但随着工程变大,比如加入一个带独立构建脚本的第三方库,单层结构就开始难受了。这时候有人会自然想到add_subdirectory,这样可以把第三方库的构建逻辑隔离在它的子目录里,顶层CMakeLists只引用它。

不过要泼盆冷水的是,很多嵌入式第三方库根本不是按CMake组织代码的,它们只是一堆.c和.h文件,并没有自带CMakeLists.txt。这种库用add_subdirectory反而鸡肋,你等于要在它的目录里额外写一个CMakeLists.txt来描述它有哪些文件,说到底还是手动维护源文件列表。

5.2 简单的组件化改造思路

如果真想让工程组件化,我建议的路径不是上来就用add_subdirectory,而是先把自定义代码全部收拢到UserCode目录,用第3章说的list(APPEND)方式管理。当某个模块的文件特别多,导致UserCode里的GLOB列表变得很宽泛时,再考虑给它单独建一个CMakeLists.txt,用add_subdirectory引入。

举个例子,你的UserCode下有一个fatfs库,里面有几十个文件,还有自己的一些配置头文件。这时可以在UserCode/fatfs下写一个CMakeLists.txt:

file(GLOB_RECURSE FATFS_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.c" ) target_include_directories(${PROJECT_NAME}.elf PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) target_sources(${PROJECT_NAME}.elf PRIVATE ${FATFS_SOURCES})

然后在顶层CMakeLists.txt里添加:

add_subdirectory(UserCode/fatfs)

这个写法利用的都是CMake内置命令,target_sources和target_include_directories配合,能在子目录里直接对当前目标添加源文件和头文件路径,不需要额外定义库目标再链接。

5.3 与CubeMX重新生成代码的共存策略

组件化改造和CubeMX重新生成代码是有冲突风险的,因为顶层CMakeLists.txt会被覆盖。我的做法是把add_subdirectory和相关配置集中在CMakeLists.txt的固定区域,放在CubeMX生成内容之后,用一个醒目的注释块隔离。

# ===== User Custom CMake (preserved manually) ===== add_subdirectory(UserCode/fatfs) # ===== End of User Custom CMake =====

每次CubeMX重新生成后,如果CMakeLists.txt被覆盖,我不需要重新写整个组件化配置,只需要把这段注释块和内容加回去就行。你也可以把这个区域单独提取到一个cmake文件里,用include()引入,这样CubeMX重新生成后只需要加一行include(UserCustom.cmake),工作量更小。

这个做法非常实用,推荐给长期迭代的工程。

6. 各种奇怪的报错,按这个顺序排查

6.1 报错类型与根因对照

添加源文件后产生的编译报错虽然千奇百怪,但归类下来就那么几类。我把常见的写成一个表,方便对照。

报错现象根因处理方式
fatal error: xxx.h: No such file or directory头文件路径没加入include目录在target_include_directories里添加对应目录
undefined reference to 'xxx'源文件没参与编译,或函数名不匹配检查SOURCES是否含该文件,检查函数拼写
multiple definition of 'xxx'同一个源文件被重复添加检查GLOB路径是否包含了同一文件的多个副本
No rule to make target 'path/to/file.c'CMakeLists里写的文件路径和实际目录不一致核对路径大小写、目录层级
构建成功但固件行为不变新文件没参与编译或没被链接用verbose编译日志检查文件是否出现
fatal error: stm32f1xx_hal_conf.h: No such fileHAL库配置头文件路径和宏定义不对检查Drivers和CMSIS的include路径,确认HSE_VALUE等宏定义

在这张表里,我特别想强调一个反向问题:有时候不是报错,而是构建成功但你感觉固件里没有新代码。这种比报错更迷惑人,因为系统给了你一种“一切正常”的错觉。这时候你要想到,可能是CMake的GLOB没有重新扫描,你新加的源文件根本没进入构建系统。重新cmake -S . -B build再构建一次,基本就能解决。

6.2 一条完整的排查链路实例

有次我在一个工程里加了几个传感器驱动文件,构建成功,但下载到板子上后传感器数据完全不对。我确认了main.c里调用了传感器读取函数,代码逻辑也检查过了,问题依然存在。

后来用cmake --build build --verbose看编译日志,发现驱动文件的编译命令根本没出现。打开CMakeLists.txt看GLOB列表,发现我新加的传感器文件放在UserCode/sensor目录,但GLOB里只写了"UserCode/*.c"。

问题出在哪?UserCode/*.c只匹配UserCode目录下的直接子文件,不递归匹配子目录里的文件。sensor是子目录,sensor/xxx.c自然不会被这个模式匹配到。

解决办法有两个:要么把GLOB改成"UserCode/*.c"加上"UserCode/sensor/*.c",要么直接用递归的"UserCode/*.c"和"UserCode/sensor/*.c"写全。更简洁的做法是用"UserCode/*.c"配合file(GLOB_RECURSE ...)。GLOB和GLOB_RECURSE的区别在于是否递归子目录,这一点很多时候没注意,就会导致文件“神秘消失”。

改完后重新执行cmake,再看verbose日志,传感器源文件出现在编译命令里了。这个案例说明,源文件没参与编译和写代码写错了,是两种完全不同的失误,排查顺序一定先确认前者。

6.3 容易被忽略的CubeMX版本和固件包差异

不同CubeMX版本生成的CMakeLists模板差异很大,特别是源文件收集方式。我在多个版本上见过:6.5之前倾向于显式列出所有源文件,6.6之后逐步改为GLOB_RECURSE。如果你搜到一篇老教程,照着显式列表的方式去改你新版本生成的CMakeLists,虽然也能用,但怎么看怎么别扭。

另外,CubeMX在生成工程时如果固件包版本不匹配,会在生成阶段直接报错。比如弹出的提示类似“The Firmware Package (STM32Cube FW_F1 V1.8.7) or one of its dependencies requires...”,这种错误发生在生成阶段,还没到CMakeLists.txt的层面。解决方法是先打开Manage embedded software packages,确认本地装的固件包版本和你工程选的型号匹配,把缺失的固件包装好后再重新生成工程。

固件包版本问题容易被新手当成CMake问题来排查,折腾半天发现CMakeLists.txt根本还没生成出来。所以遇到任何和“无法生成工程”相关的问题,先检查固件包,再谈构建配置。

我个人在实际操作中的体会是,源文件管理这件事,真不是改一行CMakeLists那么简单。整个构建链路里,文件是否被扫描、源文件是否参与链接、头文件路径是否完整、宏定义是否匹配,每一环都有各自独立的坑。最大的坑永远是GLOB不重新扫描和路径不递归这两件事。我现在的新习惯是:每次加文件,先加进目录,然后立刻手动跑一次cmake配置,再build;改动CMakeLists.txt时,把自定义内容和CubeMX生成内容用注释隔开,并且只在固定的“User Custom”区域动手;遇到编译问题,先打开verbose日志确认文件进了编译,再去查代码逻辑。这样做的直接好处是,排错时间从半天压缩到十几分钟。

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

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

立即咨询