4、Zephyr 构建系统详解:CMake 与 Kconfig 的协作机制、prj.conf 配置、设备树(Devicetree)基础

Zephyr RTOS 的构建系统并非简单的“编译脚本集合”,而是一套由 CMake 驱动、Kconfig 配置、Devicetree 描述硬件、三者深度耦合的元构建框架。理解这三者的协作关系,是进行板级移植、驱动开发、内存优化和功能裁剪的前提。本章将拆解 Zephyr 构建系统的核心链路,从配置源头到编译产物,帮助您建立完整的构建心智模型。

Zephyr 构建系统三要素协作链路 prj.conf Kconfig 符号赋值 BOARD.dts / .dtsi 硬件拓扑描述 CMakeLists.txt 构建目标与源文件 CMake 配置引擎 1. 解析 Kconfig → .config 2. 编译 Devicetree → .dts.pre.tmp 3. 生成 generated_dts_board.h 4. 构建静态库 / 可执行文件 zephyr.elf / .hex 最终固件 generated_dts_board.h 设备树宏定义 关键路径: west build -b board app → CMake 读取 prj.conf 与 board 默认配置 → 生成 .config → 编译设备树 → 生成宏定义头文件 → 编译链接 任何配置修改(menuconfig / 设备树 overlay)都会触发自动重编译,无需手动清理。

4.1 核心原理:CMake 与 Kconfig 的分工与协作

Zephyr 的构建系统以 CMake 为“总调度”,负责定义编译目标、源文件、链接脚本、工具链选择;而 Kconfig 则负责“功能开关”和“参数配置”。两者通过 Kconfig.zephyr 文件树和 kconfig.cmake 模块实现数据交换。CMake 在配置阶段会调用 kconfiglib 解析所有 Kconfig 文件,结合 prj.conf 和板级默认配置,生成最终的 .config 文件。随后,CMake 将 .config 中的符号转换为编译宏(-DCONFIG_XXX),并传递给编译器。

这种设计带来的核心优势是:配置与代码分离。开发者无需修改 C 源码即可裁剪内核、启用驱动、调整堆栈大小。同时,Kconfig 的依赖关系(depends on、select)保证了配置的合法性,避免出现“启用驱动但未启用底层总线”的冲突。

组件职责输入输出
CMake构建编排、目标定义、工具链CMakeLists.txt、*.cmakeMakefile / Ninja 规则
Kconfig功能配置、参数定义、依赖管理Kconfig*、prj.conf、*_defconfig.config、autoconf.h
Devicetree硬件描述、地址映射、中断号.dts / .dtsi / .overlaygenerated_dts_board.h
在 Zephyr 中,CONFIG_XXX 宏在编译时通过 -D 选项注入,而设备树生成的宏以 DT_ 前缀开头(如 DT_NODELABEL_UART0)。两者命名空间不同,但共同作用于驱动代码。

4.2 操作流程:prj.conf 配置与 menuconfig 可视化

最常用的配置入口是项目根目录下的 prj.conf 文件。其语法为 CONFIG_符号=值,例如 CONFIG_UART_CONSOLE=y。构建时,Zephyr 会先读取板级默认配置(boards/<arch>/<board>/<board>_defconfig),再用 prj.conf 覆盖同名符号。若需临时修改配置,可运行 west build -t menuconfig 打开图形界面,保存后会自动写回 prj.conf 或生成 merged.conf。

# prj.conf 示例:启用 GPIO 和 I2C 驱动
CONFIG_GPIO=y
CONFIG_I2C=y
# 设置系统主频为 64MHz
CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC=64000000
# 减小主栈大小以节省 RAM
CONFIG_MAIN_STACK_SIZE=2048

对于多配置场景(如测试与生产),可使用 prj_<board>.conf 或通过 west build -- -DCONF_FILE=prj2.conf 指定额外配置文件。注意:prj.conf 中的符号必须存在于 Kconfig 树中,否则构建会报“undefined symbol”错误。

不要直接修改 build/zephyr/.config 文件,该文件是生成产物,每次构建都会被覆盖。如需持久化修改,请编辑 prj.conf 或板级 _defconfig。

4.3 设备树基础:从 .dts 到 generated_dts_board.h

设备树(Devicetree)用于描述硬件拓扑,包括 CPU、内存、外设、中断控制器、GPIO 引脚等。Zephyr 在构建时,会将板级 .dts 文件与 SoC 级 .dtsi 文件合并,并应用 *.overlay 补丁(用于应用级硬件修改),最终通过 dtc 编译生成二进制 DTB,同时生成 C 头文件 generated_dts_board.h。驱动代码通过 DT_NODELABEL()、DT_PROP() 等宏访问设备树信息。

// 设备树节点示例(nRF52840 DK 的 UART0)
&uart0 {
    status = "okay";
    current-speed = <115200>;
    pinctrl-0 = &uart0_default;
    pinctrl-names = "default";
};

在 C 代码中,可通过 DEVICE_DT_GET(DT_NODELABEL(uart0)) 获取设备实例。设备树的关键优势在于:硬件描述与驱动逻辑解耦。更换板卡时,只需更换设备树文件,无需修改驱动源码。

设备树文件类型作用域典型路径
.dtsiSoC 级(芯片内部外设)dts/arm/nxp/nrf52840.dtsi
.dts板级(具体开发板)boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts
.overlay应用级(用户自定义)app/boards/nrf52840dk_nrf52840.overlay

4.4 案例拆解:为自定义板卡添加 LED 驱动

假设我们有一块基于 STM32F103 的自制板卡,板载 LED 连接在 PB0 引脚(低电平点亮)。目标是通过设备树和 Kconfig 配置,使 gpio-leds 驱动正常工作。

第一步:创建设备树 overlay 文件(boards/stm32f103_mini.overlay):

/ {
    leds {
        compatible = "gpio-leds";
        user_led: led_0 {
            gpios = &gpiob, 0, GPIO_ACTIVE_LOW;
            label = "User LED";
        };
    };
};
&gpiob {
    status = "okay";
};

第二步:在 prj.conf 中启用 GPIO 和 LED 驱动:

CONFIG_GPIO=y
CONFIG_LED=y

第三步:构建并验证。运行 west build -b stm32f103_mini 后,检查 build/zephyr/include/generated/generated_dts_board.h,应能看到 DT_NODELABEL_LED_0 相关宏。应用代码中调用 led_on(0) 即可点亮 LED。

排障复盘:若 LED 不亮,首先检查 gpios = &gpiob, 0, GPIO_ACTIVE_LOW 中的引脚号和极性是否正确;其次确认 gpiob 节点在 SoC 级 .dtsi 中已定义且时钟已使能;最后用 west build -t menuconfig 确认 CONFIG_GPIO_STM32 已自动被选中(因为 LED 驱动依赖它)。

使用 west build -- -DEXTRA_DTC_OVERLAY_FILE=my.overlay 可临时指定 overlay 文件,适合快速验证硬件修改,无需改动项目文件。

4.5 常见误区与总结复盘

  • 误区一:混淆 Kconfig 与设备树。Kconfig 管“软件功能”,设备树管“硬件描述”。例如,启用 SPI 驱动用 CONFIG_SPI=y,但 SPI 外设的基地址、中断号必须在设备树中定义。
  • 误区二:修改设备树后未重新构建。设备树变更会触发 CMake 重新生成头文件,但若使用 ninja 增量编译,偶尔会出现缓存未失效的情况。此时执行 west build -t clean 后重建。
  • 误区三:忽略 Kconfig 依赖链。例如,启用 CONFIG_I2C=y 后,还需确保 CONFIG_I2C_STM32=y(具体 SoC 驱动)被选中。通常可通过 depends on 自动处理,但手动配置时需留意。
  • 误区四:在 prj.conf 中设置设备树属性。设备树属性(如 current-speed)只能在 .dts/.overlay 中修改,Kconfig 无法覆盖。

总结:Zephyr 构建系统的核心心智模型是“CMake 编排 + Kconfig 裁剪 + Devicetree 描述”。掌握三者的数据流向(prj.conf → .config → autoconf.h;.dts → generated_dts_board.h),即可高效进行板级适配和功能定制。建议读者在开发新板卡时,先复制官方相近板卡的 .dts 和 _defconfig 作为起点,再逐步修改,这是最稳妥的路径。