STM32CubeMX CMake工程添加源文件:从GLOB到CMakeLists配置全解析 📅 发布时间:2026/8/30 3:55:31 👁 浏览次数: 用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 目录组织与代码准备假设我们的工程基于STM32F103C8T6CubeMX生成的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_initmain.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_subdirectory5.1 为什么CubeMX工程默认不用子目录CMakeListsCubeMX生成的工程是典型的单层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.txtfile(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.cCMakeLists里写的文件路径和实际目录不一致核对路径大小写、目录层级构建成功但固件行为不变新文件没参与编译或没被链接用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日志确认文件进了编译再去查代码逻辑。这样做的直接好处是排错时间从半天压缩到十几分钟。