
1. 项目背景与核心问题在嵌入式开发领域尤其是使用 Nordic 的 NCSnRF Connect SDK框架时很多开发者特别是从传统 IDE如 Keil、IAR或简单 Makefile 项目迁移过来的朋友会遇到一个看似基础却容易卡壳的问题如何把自己写的.c和.h文件添加到工程里让它们能被正确编译和链接。这背后其实是一个构建系统认知的转换。在 NCS 的世界里主宰一切的构建工具是CMake和Kconfig而不是你熟悉的那个图形化界面里的“添加文件”按钮。如果你只是把文件复制到项目目录下然后满怀期待地执行west build大概率会收获一个“未定义的引用”错误或者干脆编译系统就找不到你的源文件。今天我们就来彻底拆解这个问题从 CMake 的底层逻辑出发手把手教你如何正确、优雅地将自定义的.c/.h文件集成到 NCS 工程中并理解每一步操作背后的“为什么”。2. 理解 NCS 的构建骨架CMakeLists.txt 的角色在动手之前我们必须先搞清楚 NCS或者说 Zephyr RTOS的构建体系是如何组织的。这就像盖房子你得先看懂建筑图纸而不是直接去搬砖。NCS 基于 Zephyr而 Zephyr 使用 CMake 作为其构建系统的生成器。整个项目的编译指令、文件包含关系、库的链接最终都通过一系列的CMakeLists.txt文件来定义。你可以把CMakeLists.txt想象成一份给 CMake 这个“总工程师”的施工蓝图。一个典型的 NCS 应用项目目录结构如下your_app/ ├── CMakeLists.txt # 你项目的顶层构建定义文件 ├── prj.conf # 项目的 Kconfig 配置文件 ├── src/ │ └── main.c # 默认的主函数文件 └── build/ # 编译输出目录由 west build 生成当你执行west build -b board .时west命令会调用 CMakeCMake 则会依次解析项目根目录、Zephyr 基础目录以及各个模块目录下的CMakeLists.txt文件最终生成一个适用于你本地编译环境如 Ninja 或 Make的构建脚本。你的.c/.h文件要想参与编译就必须在这个“蓝图”中被明确地登记在册。这里最常见的误区是开发者修改了src/main.c或者在其旁边新建了my_module.c就认为构建系统会自动发现它们。这是传统 IDE 带来的思维定势。在 CMake 体系中源文件不会自动被包含。你必须通过target_sources()等 CMake 指令显式地告诉构建系统“请把这些文件加入编译列表”。3. 实战添加自定义源文件与头文件的三种场景理解了原理我们进入实战环节。根据你的模块是放在项目内部还是作为一个相对独立的库添加方式略有不同。我们分场景讨论。3.1 场景一在项目src/目录内添加新模块这是最常见的情况。假设你的项目叫my_ble_project现在需要添加一个管理温度传感器的模块。第一步创建文件首先在项目根目录下创建一个src文件夹如果不存在的话然后在里面创建你的源文件和头文件。my_ble_project/ ├── CMakeLists.txt ├── prj.conf └── src/ ├── main.c ├── temperature_sensor.c └── temperature_sensor.h第二步编写模块代码在temperature_sensor.h中声明公共接口在.c文件中实现。这是 C 语言编程的基础此处不再赘述。确保头文件有防止重复包含的宏#ifndef ... #define ... #endif。第三步修改 CMakeLists.txt关键步骤这是核心操作。打开项目根目录下的CMakeLists.txt你会看到类似下面的内容# Find Zephyr. This also loads Zephyrs CMake package. find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(my_ble_project) # 你的应用程序源代码 target_sources(app PRIVATE src/main.c)你需要修改target_sources这一行将你的新源文件添加进去target_sources(app PRIVATE src/main.c src/temperature_sensor.c )为什么是app在 Zephyr 的 CMake 体系中app是一个由find_package(Zephyr)自动创建的特殊 CMake 目标target它代表了你的可执行应用程序。target_sources命令就是向这个目标添加私有的源文件。为什么只加.c而不加.h这是 CMake 和 C/C 编译的常识。头文件.h是通过#include预处理指令在源代码中被引用的它们不需要通常也不应该被直接列在target_sources里。构建系统关心的是需要被编译的源文件.c,.cpp等。但是你需要确保头文件的路径对编译器可见。第四步处理头文件路径默认情况下src/目录通常已经在包含路径include path中了。Zephyr 的构建系统会自动将CMAKE_CURRENT_SOURCE_DIR即当前CMakeLists.txt所在目录以及src/等常见目录加入编译器的头文件搜索路径。所以在main.c中你可以直接写#include “temperature_sensor.h”如果头文件放在其他子目录比如src/drivers/你可能需要显式地添加包含路径使用target_include_directories()指令。但为了简单和清晰建议初期将项目相关的头文件都放在src/或项目根目录下。3.2 场景二在项目根目录或其他位置添加文件有时你可能不想把所有文件都塞进src/。比如你把文件直接放在项目根目录下my_ble_project/ ├── CMakeLists.txt ├── prj.conf ├── main.c ├── my_algorithm.c └── my_algorithm.h此时修改CMakeLists.txt时路径需要做相应调整target_sources(app PRIVATE main.c my_algorithm.c )同样在main.c中包含头文件时路径也要匹配#include “my_algorithm.h”注意将源文件直接放在根目录虽然可行但会显得项目结构有些杂乱。通常更推荐使用src/目录来归类所有应用程序源文件这是一种广泛认可的最佳实践能让项目结构更清晰也便于后期维护和与他人协作。3.3 场景三添加一个外部库或模块位于项目外或子模块这是一个更进阶的场景。假设你有一个通用的驱动库my_lib它位于你的 NCS 项目外部或者作为一个 Git 子模块modules/my_lib/引入。它的结构如下modules/my_lib/ ├── CMakeLists.txt # 库自身的构建定义 ├── include/ │ └── my_lib.h └── src/ └── my_lib.c第一步编写库的 CMakeLists.txt在modules/my_lib/CMakeLists.txt中你需要将这个库定义为一个独立的 CMake 目标通常是静态库# 创建一个库目标名字叫 my_lib zephyr_library() zephyr_library_sources(src/my_lib.c) zephyr_library_include_directories(include)这里使用了 Zephyr 提供的便捷函数zephyr_library()它会创建一个适合在 Zephyr 环境中使用的库目标。第二步在主项目的 CMakeLists.txt 中引入这个库回到你的主项目my_ble_project/CMakeLists.txt你需要通过add_subdirectory将这个库的构建纳入主构建系统并将其链接到你的app目标。find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(my_ble_project) # 将外部库目录添加为子目录 add_subdirectory(modules/my_lib) # 你的应用程序源代码 target_sources(app PRIVATE src/main.c) # 将你的 app 目标与 my_lib 库链接 target_link_libraries(app PUBLIC my_lib)关键点解析add_subdirectory(modules/my_lib)这行命令告诉 CMake“请去modules/my_lib目录下找到那里的CMakeLists.txt并执行它。” 执行后my_lib这个库目标就被创建并准备好了。target_link_libraries(app PUBLIC my_lib)这行命令将my_lib库链接到你的应用程序app上。PUBLIC关键字意味着不仅app本身能使用my_lib的接口任何将来链接app的其他目标也能“看到”my_lib。对于简单的应用使用PRIVATE也可以。链接后my_lib的头文件路径会自动对app可见因此在main.c中可以直接#include my_lib.h。这种方式非常适合管理可复用的代码保持了项目的模块化和整洁。4. 避坑指南与高级配置即使按照上述步骤操作你可能还是会遇到一些“坑”。下面是一些常见问题及其解决方案。4.1 坑一修改 CMakeLists.txt 后编译报错依旧现象你已经添加了target_sources但执行west build依然提示undefined reference to ‘your_function’。排查与解决清理构建目录CMake 会缓存配置信息。最彻底的方法是删除整个build/目录然后重新执行west build -b board .。这是解决大多数 CMake 配置更新问题的“万能钥匙”。检查拼写和路径仔细核对CMakeLists.txt中的文件名和路径是否与磁盘上的文件完全一致包括大小写。在 Linux/macOS 系统下MyFile.c和myfile.c是两个不同的文件。验证函数声明确保在头文件中声明的函数在源文件中的实现其函数签名返回值、函数名、参数类型完全一致。一个常见的错误是在头文件中声明了void func(void)但在.c文件中却写成了void func()。4.2 坑二头文件找不到fatal error: .h: No such file or directory现象编译时提示找不到你自定义的头文件。排查与解决确认包含语句检查#include语句使用的是双引号“”还是尖括号。对于项目自身的、位置相对固定的头文件强烈建议使用双引号#include “my_header.h”。双引号会优先在当前文件所在目录和编译器指定的“用户包含目录”中搜索而尖括号通常用于系统库或通过target_include_directories明确添加的路径。手动添加包含路径如果头文件位于一个非标准位置例如includes/或libs/xxx/include你需要在CMakeLists.txt中为app目标显式添加包含目录target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/includes ${CMAKE_CURRENT_SOURCE_DIR}/libs/xxx/include )这行命令告诉编译器在为app目标编译时请额外去这两个目录下寻找头文件。4.3 坑三如何条件化地添加文件基于 Kconfig在复杂的项目中你可能希望某些模块只在特定的配置下才被编译。例如只有当用户配置了CONFIG_TEMP_SENSORy时temperature_sensor.c才需要加入编译。这需要结合 Kconfig 和 CMake 的条件语句来实现。第一步在 Kconfig 中定义配置选项在你的项目目录下创建一个Kconfig文件如果不存在或者修改现有的prj.conf的对应来源。更规范的做法是在模块同级目录创建Kconfig# 在 CMakeLists.txt 同目录下的 Kconfig 文件中 menu “My App Configuration” config TEMP_SENSOR bool “Enable Temperature Sensor Support” help This enables the driver for the XX temperature sensor. endmenu然后在prj.conf中设置CONFIG_TEMP_SENSORy来启用它。第二步在 CMakeLists.txt 中使用条件判断修改你的CMakeLists.txttarget_sources(app PRIVATE src/main.c) if (CONFIG_TEMP_SENSOR) target_sources(app PRIVATE src/temperature_sensor.c) endif()这样只有当CONFIG_TEMP_SENSOR被设置为y时temperature_sensor.c才会被添加到源文件列表中进行编译。这是一种非常强大的功能可以极大地优化最终固件的大小并提高代码的模块化程度。5. 构建流程复盘与最佳实践建议让我们从头到尾复盘一下当你执行west build时背后发生了什么以及你的文件是如何被处理的解析阶段west调用 CMakeCMake 从项目根目录的CMakeLists.txt开始解析。目标创建find_package(Zephyr)执行它设置了 Zephyr 构建环境并创建了名为app的默认可执行目标。源文件收集CMake 读取target_sources(app PRIVATE …)指令将列出的所有.c文件路径记录下来作为app目标的源文件列表。依赖与链接如果存在target_link_libraries(app …)CMake 会处理库之间的依赖关系并确保链接器命令中包含这些库。生成构建系统CMake 根据以上信息为你选择的工具链如 GNU Arm Embedded生成具体的构建脚本如build.ninja。编译与链接west调用生成的构建脚本编译器编译每一个.c文件为.o目标文件链接器将所有.o文件和库链接成最终的.elf或.hex文件。基于此流程我总结出几条最佳实践保持结构清晰坚持使用src/目录存放应用源文件include/或类似目录存放可公开的头文件如果是库。根目录的CMakeLists.txt尽量保持简洁只做“组装”工作。命名规范目标app、库名my_lib使用清晰、不含空格和下划线的名称推荐小写加下划线。路径使用变量对于复杂的项目可以使用CMAKE_CURRENT_SOURCE_DIR等 CMake 变量来构造路径增强可移植性。版本控制忽略务必在你的.gitignore文件中添加build/目录不要将构建产物提交到代码仓库。善用 west 命令除了west build多使用west build -t menuconfig来图形化配置 Kconfig使用west build -t flash来烧录这能极大提升开发效率。将自定义文件添加到 NCS 工程本质上是从“文件思维”切换到“目标与依赖思维”的过程。一旦你习惯了通过CMakeLists.txt这个“总控台”来管理项目的每一个组成部分你会发现它不仅更强大、更灵活而且能更好地适应从简单应用到复杂多模块系统的演变。下次当你再遇到“文件加进去了但没编译”的问题时第一反应就应该是“我的CMakeLists.txt写对了吗”