3. Zephyr 构建系统:CMake与west的协作、Kconfig配置系统、设备树(Devicetree)基础

Zephyr RTOS 的构建系统是驱动开发的核心基础设施。它由三个紧密协作的子系统组成:west(元构建工具)、CMake(构建生成器)、Kconfig(配置系统)和 Devicetree(硬件描述)。理解它们的分工与协作,是编写可移植、可配置驱动的第一步。

学习目标:掌握 west 与 CMake 的构建流程、Kconfig 符号定义与菜单结构、Devicetree 节点与绑定的基本语法,能够阅读并修改简单的驱动配置。

3.1 构建系统全景:west 与 CMake 的协作

Zephyr 使用 west(Zephyr 元工具)作为顶层构建协调器,底层依赖 CMake 生成具体的构建文件(如 Makefile 或 Ninja)。west 负责项目管理、多仓库同步、构建命令封装;CMake 负责编译、链接、生成目标文件。

下图展示了 west 与 CMake 在构建流程中的协作关系:

west 与 CMake 协作构建流程 west build -b <board> west 解析 board/应用清单 CMake 配置 (CMakeLists.txt) Ninja/Make 编译链接 zephyr.elf / .bin

3.1.1 west 命令与工作流

west 是 Zephyr 项目的瑞士军刀。驱动开发中最常用的命令如下:

命令 功能 驱动开发场景
west init 初始化 Zephyr 工作区 首次搭建开发环境
west update 同步所有模块仓库 拉取最新驱动框架
west build -b <board> 配置并编译应用 编译驱动测试程序
west flash 烧录固件到目标板 验证驱动功能
west build -t menuconfig 启动图形化 Kconfig 配置 调整驱动参数
实践要点:驱动开发中,west build -b <board> -p always 会强制重新配置并编译,适合在修改 Kconfig 或 Devicetree 后使用。

3.1.2 CMake 在驱动中的角色

每个 Zephyr 驱动目录下都有一个 CMakeLists.txt,它告诉构建系统:

  • 哪些源文件需要编译(target_sources)
  • 依赖哪些头文件路径(zephyr_include_directories)
  • 是否依赖其他驱动模块(depends_on)

一个典型的驱动 CMakeLists.txt 示例:

# drivers/sensor/my_sensor/CMakeLists.txt
zephyr_library()
zephyr_library_sources(my_sensor.c)
zephyr_library_sources(my_sensor_trigger.c)

zephyr_library_include_directories(.)
zephyr_library_link_libraries(interface__sensor)

3.2 Kconfig 配置系统

Kconfig 是 Zephyr 的编译时配置系统,用于开关驱动功能、设置参数、定义依赖关系。驱动开发者通过 Kconfig 符号控制驱动是否被编译、使用哪种模式、配置缓冲区大小等。

3.2.1 Kconfig 符号结构

每个 Kconfig 符号由 类型、提示、默认值 和 依赖 组成。驱动中常见的符号类型:

类型 含义 驱动示例
bool 开关选项(y/n) CONFIG_MY_SENSOR=y
int 整数参数 CONFIG_MY_SENSOR_BUF_SIZE=256
hex 十六进制地址 CONFIG_MY_SENSOR_BASE_ADDR=0x4000
string 字符串配置 CONFIG_MY_SENSOR_MODEL="v2"

下面是一个驱动 Kconfig 文件的典型结构:

# drivers/sensor/my_sensor/Kconfig
menuconfig MY_SENSOR
    bool "My Sensor Driver"
    depends on I2C
    help
      Enable support for My Sensor.

if MY_SENSOR

config MY_SENSOR_TRIGGER
    bool "Enable trigger support"
    depends on GPIO
    default n

config MY_SENSOR_BUF_SIZE
    int "Buffer size"
    range 64 1024
    default 256

endif # MY_SENSOR

3.2.2 Kconfig 依赖与菜单层次

Kconfig 使用 depends on 表达依赖关系,使用 menuconfig / if ... endif 组织菜单层次。驱动开发者必须确保:

  • 驱动符号依赖正确的总线(如 I2C、SPI)
  • 子功能符号依赖父功能符号
  • 默认值符合硬件规格
常见错误:忘记添加 depends on 会导致驱动在未使能总线时被编译,引发链接错误。始终检查驱动 Kconfig 的依赖链。

下图展示了 Kconfig 符号之间的依赖与菜单关系:

Kconfig 依赖与菜单层次示例 CONFIG_I2C (总线) CONFIG_MY_SENSOR CONFIG_MY_SENSOR_TRIGGER CONFIG_MY_SENSOR_BUF_SIZE depends on: 子选项依赖父选项使能后才能显示 menuconfig/if-endif: 组织菜单层次结构

3.3 设备树 (Devicetree) 基础

Devicetree 是 Zephyr 中描述硬件拓扑的标准方式。它将 SoC 外设、总线、引脚、中断、时钟等硬件信息以树状结构表达,驱动通过 Devicetree API 获取硬件参数,实现与板级无关的代码。

3.3.1 Devicetree 文件体系

Zephyr 的 Devicetree 文件分为三个层次:

层次 文件位置 作用
SoC 级 dts/<arch>/<soc>.dtsi 定义 SoC 内置外设、地址、中断
板级 boards/<arch>/<board>.dts 定义板级引脚、外设实例、时钟
应用覆盖 app.overlay 应用层修改或添加设备节点

3.3.2 节点与属性语法

Devicetree 使用节点(node)表示硬件设备,属性(property)描述设备参数。一个典型的 I2C 传感器节点:

// 节点名: 传感器@地址
my_sensor: my-sensor@48 {
    compatible = "vendor,my-sensor";  // 驱动匹配字符串
    reg = <0x48>;                     // I2C 地址
    interrupt-parent = <&gpio0>;       // 中断控制器
    interrupts = <5 1>;               // GPIO 引脚 5, 上升沿触发
    label = "MY_SENSOR";              // 设备标签
    status = "okay";                  // 启用状态
};

关键属性说明:

  • compatible:驱动通过此字符串匹配设备节点
  • reg:总线地址或寄存器基址
  • interrupts:中断号与触发类型
  • status:"okay" 启用,"disabled" 禁用

3.3.3 驱动如何读取 Devicetree

Zephyr 提供一组 Devicetree API 宏,驱动在 C 代码中通过节点标识符获取硬件信息:

#include <zephyr/devicetree.h>

// 获取节点标识符
#define MY_SENSOR_NODE DT_NODELABEL(my_sensor)

// 读取属性
#define MY_SENSOR_I2C_ADDR DT_REG_ADDR(MY_SENSOR_NODE)
#define MY_SENSOR_IRQ_PIN  DT_IRQN(MY_SENSOR_NODE)
#define MY_SENSOR_LABEL    DT_LABEL(MY_SENSOR_NODE)

// 检查状态
#if DT_NODE_HAS_STATUS(MY_SENSOR_NODE, okay)
    // 设备已启用,执行初始化
#endif
最佳实践:驱动代码中尽量使用 Devicetree API 宏,避免硬编码地址和引脚号。这样同一份驱动代码可以适配不同板级,只需修改 .dts 文件即可。

下图展示了 Devicetree 节点与驱动代码之间的数据流:

Devicetree 节点到驱动代码的数据流 board.dts my-sensor@48 { reg = <0x48>; }; 编译时生成 devicetree.h 宏定义展开 驱动 .c DT_REG_ADDR() DT_IRQN() 数据流说明 1. 板级 .dts 定义硬件节点和属性 2. 构建系统将 .dts 编译为 C 头文件宏 3. 驱动代码通过 DT_* 宏读取硬件参数

3.4 三系统协作实例:添加一个虚拟传感器驱动

下面通过一个完整示例,展示 CMake、Kconfig、Devicetree 如何协同工作:

  1. Devicetree:在板级 .dts 中添加传感器节点
  2. Kconfig:定义驱动开关和参数
  3. CMake:添加源文件到构建
  4. 驱动代码:通过 Devicetree API 读取配置

步骤 1:在 boards/arm/my_board.dts 中添加节点:

/ {
    soc {
        i2c0: i2c@40003000 {
            my_sensor: my-sensor@48 {
                compatible = "vendor,my-sensor";
                reg = <0x48>;
                label = "MY_SENSOR";
                status = "okay";
            };
        };
    };
};

步骤 2:在 drivers/sensor/my_sensor/Kconfig 中定义符号:

config MY_SENSOR
    bool "My Sensor Driver"
    depends on I2C
    help
      Enable My Sensor I2C driver.

步骤 3:在 drivers/sensor/my_sensor/CMakeLists.txt 中添加源文件:

zephyr_library()
zephyr_library_sources(my_sensor.c)
zephyr_library_include_directories(.)

步骤 4:在驱动 C 文件中读取 Devicetree 信息:

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/devicetree.h>

#define MY_SENSOR_NODE DT_NODELABEL(my_sensor)

#if DT_NODE_HAS_STATUS(MY_SENSOR_NODE, okay)
static const struct i2c_dt_spec dev_i2c =
    I2C_DT_SPEC_GET(MY_SENSOR_NODE);

int my_sensor_init(const struct device *dev)
{
    if (!device_is_ready(dev_i2c.bus)) {
        return -ENODEV;
    }
    // 使用 dev_i2c.addr 进行 I2C 通信
    return 0;
}
#endif
关键点:I2C_DT_SPEC_GET() 宏自动从 Devicetree 节点中提取 I2C 总线实例和地址,驱动代码无需硬编码任何硬件参数。

3.5 本章小结

子系统 核心职责 驱动开发者关注点
west 项目管理和构建命令封装 使用 west build -b <board> 编译
CMake 编译规则和源文件管理 编写 CMakeLists.txt 添加源文件
Kconfig 编译时配置和参数定义 定义驱动开关、依赖、默认值
Devicetree 硬件拓扑描述 定义节点、属性、compatible 匹配