3. Zephyr项目结构:理解west工作区、manifest仓库、应用目录、CMakeLists.txt、prj.conf、dts文件的作用与组织方式

Zephyr RTOS 的项目组织方式与传统的嵌入式工程(如 Keil、IAR 或 STM32CubeIDE 单工程)有本质区别。Zephyr 采用 west 多仓库管理工具与 CMake 构建系统,将内核、驱动、第三方库、应用代码和硬件描述文件分层组织。理解这套结构是进行 Zephyr 开发的第一道门槛——如果只把 Zephyr 当作一个“带 RTOS 的 SDK”来使用,很快会在多仓库同步、设备树覆盖、Kconfig 配置冲突等问题上陷入困境。本章将系统拆解 west 工作区、manifest 仓库、应用目录、CMakeLists.txt、prj.conf 与 dts 文件各自承担的职责,并给出实战中的组织方法与排障思路。

Zephyr 项目结构总览 west 工作区(zephyrproject/) west topdir:包含 .west/config、manifest 仓库、zephyr 主仓库、模块仓库、应用目录 manifest 仓库 west.yml:声明所有仓库版本 west 命令的同步依据 zephyr 主仓库 内核、驱动、CMake 模块、dts 基础文件 模块仓库(modules/) hal、cmsis、tee、第三方库 由 west.yml 按需拉取 应用目录(app/) CMakeLists.txt(构建入口) + prj.conf(Kconfig 配置) + app.overlay(设备树覆盖) + src/main.c CMakeLists.txt prj.conf app.overlay CMake + Ninja 构建流程 west build → cmake 解析 → 生成 build/ 目录 → ninja 编译链接 → zephyr.elf / zephyr.hex build/ zephyr.elf zephyr.hex west 工作区 = manifest 仓库 + zephyr 主仓库 + 模块仓库 + 应用目录 构建时:CMakeLists.txt 定义构建目标,prj.conf 配置内核功能,dts 描述硬件资源

3.1 west 工作区与 manifest 仓库:多仓库协同的基石

核心原理

Zephyr 不是单一代码仓库,而是由 zephyr 主仓库、hal 仓库(如 hal_stm32、hal_nxp)、第三方模块(如 cmsis、tee、mbedtls)以及应用代码共同组成的多仓库工程。west 是 Zephyr 官方提供的多仓库管理工具,它通过一个 manifest 仓库(通常名为 manifest 或 zephyr)中的 west.yml 文件,声明所有仓库的 Git 地址、分支和版本号。执行 west update 时,west 会读取 west.yml,将每个仓库同步到指定版本。

west 工作区的典型布局如下:

zephyrproject/                    <-- west topdir
├── .west/                        <-- west 内部配置
│   └── config                    <-- 记录 manifest 路径与工作区根目录
├── manifest/                     <-- manifest 仓库(可自定义名称)
│   └── west.yml                  <-- 仓库清单与版本锁定
├── zephyr/                       <-- Zephyr 主仓库
│   ├── CMakeLists.txt
│   ├── Kconfig
│   ├── dts/
│   ├── drivers/
│   ├── kernel/
│   └── ...
├── modules/                      <-- 第三方模块仓库
│   ├── hal/stm32/
│   ├── cmsis/
│   └── ...
├── app/                          <-- 应用目录(可放在任意位置)
│   ├── CMakeLists.txt
│   ├── prj.conf
│   ├── app.overlay
│   └── src/main.c
└── build/                        <-- 构建输出目录(由 west build 生成)

判断方法与操作流程

要判断当前是否处于一个有效的 west 工作区,可以执行 west topdir 命令,它会输出工作区根目录路径。若提示错误,说明当前目录不在 west 工作区内。初始化一个新工作区的标准流程为:

  1. 安装 west:pip install west
  2. 初始化 manifest 仓库:west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.7.0 zephyrproject(指定版本分支)
  3. 同步所有仓库:cd zephyrproject && west update
  4. 导出 Zephyr 构建辅助文件:west zephyr-export(可选,用于 CMake 包查找)
经验提醒:在团队协作中,manifest 仓库应纳入版本控制,并锁定到经过验证的版本号。不要直接修改 west.yml 中的 Git 分支为 main 或 master,否则不同开发者的仓库版本可能不一致,导致“在我机器上能编译”的尴尬局面。建议使用 west update --narrow -o=--depth=1 进行浅克隆以节省磁盘空间。

案例拆解:多板卡项目中的 manifest 管理

假设团队同时维护两个产品线:A 产品使用 STM32F4 系列,B 产品使用 nRF52840。两个产品需要不同版本的 Zephyr 内核和 HAL 驱动。此时可以创建两个 manifest 分支:manifest-v3.7-stm32 和 manifest-v3.7-nrf,分别锁定对应的 HAL 仓库版本。开发者在各自的工作区中执行 west init -m <manifest-url> --mr manifest-v3.7-stm32 即可获得一致的依赖版本。这样既保证了代码隔离,又避免了在同一个工作区中频繁切换分支带来的编译缓存失效问题。

常见误区与复盘

  • 误区一:把 west update 当作 git pull 的替代品。实际上 west update 会强制将每个仓库切换到 west.yml 指定的提交,本地未提交的修改可能被覆盖。执行前应使用 git status 检查各仓库状态。
  • 误区二:在应用目录中直接修改 zephyr/ 主仓库的代码。这种做法会导致主仓库变脏,后续 west update 时产生冲突。正确的做法是将驱动或内核修改以补丁形式提交到 manifest 仓库,或使用 modules/ 下的扩展模块。
  • 误区三:忽略 .west/config 文件。该文件记录了 manifest 仓库的路径,如果移动了工作区目录,需要更新此文件中的路径,否则 west 命令会报错。

3.2 应用目录与 CMakeLists.txt:构建入口的定义

核心原理

Zephyr 的应用目录是一个独立的 CMake 项目,它通过 CMakeLists.txt 文件声明应用名称、源文件、依赖的 Zephyr 模块以及额外的配置。Zephyr 的构建系统在编译时,会先加载 Zephyr 主仓库的 CMakeLists.txt,然后通过 find_package(Zephyr) 机制将应用目录与 Zephyr 内核、驱动链接起来。应用目录可以放在 west 工作区内部(如 zephyrproject/app/),也可以放在工作区外部,只要在构建时通过 -d 参数指定应用路径即可。

一个最小化的应用 CMakeLists.txt 如下:

cmake_minimum_required(VERSION 3.20.0)

# 指定应用名称
project(my_app)

# 加载 Zephyr 构建系统
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

# 指定应用源文件
target_sources(app PRIVATE src/main.c)

# 可选:添加额外的包含路径
target_include_directories(app PRIVATE include)

操作流程与关键参数

构建一个 Zephyr 应用的标准命令是:

west build -b <board_name> -d build/app /path/to/app

其中 -b 指定目标板卡(如 stm32f4_disco、nrf52840dk_nrf52840),-d 指定构建输出目录。构建完成后,生成的固件位于 build/zephyr/zephyr.elf 和 build/zephyr/zephyr.hex。若需要修改构建配置,可以在应用目录下创建 CMakeLists.txt 中通过 set(KCONFIG_ROOT ...) 指定额外的 Kconfig 文件,或使用 prj.conf 进行配置。

风险提示:应用目录的 CMakeLists.txt 中 find_package(Zephyr) 的 HINTS 路径必须正确。如果 Zephyr 主仓库不在环境变量 ZEPHYR_BASE 指向的位置,构建会失败。建议在 ~/.zephyrrc 文件中设置 export ZEPHYR_BASE=/path/to/zephyrproject/zephyr,或使用 west build 时自动检测。

案例拆解:多源文件应用的 CMake 组织

假设一个应用包含 main.c、sensor.c、display.c 以及一个静态库 lib/utils。合理的 CMakeLists.txt 组织方式为:

cmake_minimum_required(VERSION 3.20.0)
project(smart_device)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

# 添加静态库子目录
add_subdirectory(lib/utils)

# 应用源文件
target_sources(app PRIVATE
    src/main.c
    src/sensor.c
    src/display.c
)

# 链接静态库
target_link_libraries(app PRIVATE utils)

# 添加全局包含路径
target_include_directories(app PRIVATE include)

这种组织方式将业务代码与工具库分离,便于单元测试和代码复用。在构建时,Zephyr 的构建系统会自动处理 add_subdirectory 中的 CMake 逻辑,并将静态库链接进最终固件。

常见误区与复盘

  • 误区一:在 CMakeLists.txt 中直接使用 add_executable() 创建可执行文件。Zephyr 构建系统已经预定义了 app 这个可执行目标,应用代码应通过 target_sources(app ...) 添加源文件,而不是新建目标。
  • 误区二:忘记在 CMakeLists.txt 中声明 project()。CMake 要求必须声明项目名称,否则 find_package 可能无法正常工作。
  • 误区三:将构建输出目录放在应用源码目录内。这会导致源码目录被构建产物污染,且 west build 在清理时可能误删源码。应始终使用独立的 build/ 目录。

3.3 prj.conf 与 Kconfig:内核功能的可视化配置

核心原理

prj.conf 是 Zephyr 应用级别的 Kconfig 配置文件,它通过 CONFIG_XXX=y 或 CONFIG_XXX=n 的形式,开启或关闭内核功能、驱动、协议栈等。Kconfig 是 Linux 内核使用的配置系统,Zephyr 沿用了这一机制,并扩展了 Zephyr 特有的配置项。构建时,Zephyr 的构建系统会读取 prj.conf,与 Zephyr 主仓库中的 Kconfig 文件合并,生成最终的 .config 文件,并据此编译。

常见的配置项示例:

# 开启 UART 驱动
CONFIG_SERIAL=y
# 开启 GPIO 驱动
CONFIG_GPIO=y
# 开启 I2C 驱动
CONFIG_I2C=y
# 设置系统主频
CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC=16000000
# 开启日志系统
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3

判断方法与操作流程

要查看某个配置项是否可用,可以使用 west build -t menuconfig 打开图形化配置界面,或使用 west build -t guiconfig 打开 Qt 界面。在终端中,也可以使用 grep 搜索 Zephyr 源码中的 Kconfig 文件:

grep -r "CONFIG_I2C" zephyr/drivers/i2c/Kconfig

当需要为不同板卡或不同产品变体提供不同配置时,可以使用 prj_<board>.conf 文件(如 prj_stm32f4_disco.conf),构建系统会自动选择匹配的配置文件。此外,west build 还支持通过 -D 参数临时覆盖配置项:

west build -b stm32f4_disco -d build/app app -DCONFIG_LOG_DEFAULT_LEVEL=4
经验提醒:在调试阶段,建议开启 CONFIG_ASSERT=y 和 CONFIG_DEBUG_THREAD_INFO=y,以便在异常时输出更多诊断信息。但在发布版本中应关闭这些选项以减小固件体积。另外,CONFIG_PRINTK 是 Zephyr 的轻量级打印函数,默认开启,但 CONFIG_LOG 提供了更强大的日志过滤功能,推荐在复杂项目中使用。

案例拆解:从零配置一个 I2C 传感器应用

假设要在 nRF52840 DK 上驱动一个 I2C 温湿度传感器(如 SHT30)。初始 prj.conf 配置如下:

CONFIG_I2C=y
CONFIG_SENSOR=y
CONFIG_SHT3X=y
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3

构建后,如果传感器驱动没有正常工作,首先检查 build/zephyr/.config 文件确认 CONFIG_SHT3X 是否被正确设置为 y。然后检查设备树中 I2C 总线的地址是否与传感器硬件地址匹配(SHT30 默认地址为 0x44)。如果地址不匹配,需要在 app.overlay 中覆盖设备树节点。这个案例说明,prj.conf 只是第一步,硬件资源的描述还需要设备树配合。

常见误区与复盘

  • 误区一:在 prj.conf 中配置了驱动,但忘记在设备树中使能对应的总线节点。例如,配置了 CONFIG_I2C=y,但设备树中 I2C 节点被禁用(status = "disabled"),驱动无法工作。
  • 误区二:修改 prj.conf 后没有重新构建。Zephyr 的构建系统会检测 prj.conf 的变化并自动重新生成 .config,但如果使用 ninja 直接编译,可能不会触发重新配置。建议始终使用 west build 进行构建。
  • 误区三:在 prj.conf 中设置无效的配置项。Zephyr 在配置阶段会检查配置项是否存在,如果拼写错误或该配置项在当前板卡上不可用,构建会报错。此时应检查 Kconfig 文件确认配置项名称。

3.4 dts 文件:硬件资源的描述与覆盖

核心原理

设备树(Device Tree)是一种描述硬件资源的数据结构,Zephyr 使用设备树来定义板卡上的外设、内存映射、中断、GPIO 引脚等功能。设备树源文件(.dts)和头文件(.dtsi)位于 Zephyr 主仓库的 dts/ 目录下。每个板卡都有一个对应的 <board>.dts 文件,描述了该板卡的硬件布局。应用层可以通过 app.overlay 文件对设备树进行覆盖(overlay),而无需修改板卡原始文件。

设备树的基本结构如下:

/ {
    model = "My Board";
    compatible = "my,board";

    chosen {
        zephyr,console = &uart0;
    };

    soc {
        uart0: serial@40001000 {
            compatible = "arm,pl011";
            reg = <0x40001000 0x1000>;
            interrupts = <5>;
            status = "okay";
        };

        i2c0: i2c@40002000 {
            compatible = "nordic,nrf-twim";
            reg = <0x40002000 0x1000>;
            interrupts = <6>;
            status = "okay";
            clock-frequency = <100000>;
        };
    };
};

判断方法与操作流程

在应用开发中,最常用的操作是添加或修改外设节点。例如,要在 I2C0 总线上挂载一个 SHT30 传感器,可以在 app.overlay 中写入:

/ {
    soc {
        i2c0: i2c@40002000 {
            status = "okay";
            sht30@44 {
                compatible = "sensirion,sht3x";
                reg = <0x44>;
            };
        };
    };
};

构建时,Zephyr 的构建系统会将 app.overlay 与板卡的 .dts 文件合并,生成最终的设备树二进制(zephyr.dts)。可以通过 west build -t dts 查看合并后的设备树文件,或使用 west build -t devicetree 输出设备树信息。

风险提示:设备树覆盖时,节点路径必须与原始 .dts 文件中的路径完全一致,否则覆盖不会生效。建议先查看板卡的 .dts 文件确认节点路径。另外,status = "okay" 是使能节点的关键,如果节点默认是 disabled,必须显式设置为 okay。

案例拆解:GPIO 引脚功能冲突排查

假设在 STM32F4 Discovery 板上,LED 使用 PB0 引脚,同时用户想用 PB0 作为按键输入。在 app.overlay 中分别配置了 LED 和按键节点,但编译后按键功能异常。排查步骤:

  1. 使用 west build -t dts 查看合并后的设备树,确认两个节点是否都指向 PB0。
  2. 检查 gpio-hogs 或 pinctrl 配置,确认引脚复用是否正确。
  3. 查看 build/zephyr/.config 中 CONFIG_GPIO 是否开启。
  4. 在代码中打印 GPIO 状态,确认驱动是否成功初始化。

最终发现,LED 节点将 PB0 配置为输出模式,而按键节点需要输入模式,两者冲突。解决方案是更换按键引脚,或在设备树中显式配置引脚复用为 GPIO 输入。

常见误区与复盘

  • 误区一:直接修改板卡的 .dts 文件。这种做法会导致板卡文件被污染,且升级 Zephyr 版本时修改会丢失。应始终使用 app.overlay 进行覆盖。
  • 误区二:在设备树中使用不存在的 compatible 字符串。Zephyr 驱动通过 compatible 匹配设备树节点,如果拼写错误,驱动无法绑定到节点。
  • 误区三:忽略 chosen 节点。Zephyr 使用 chosen 节点指定默认控制台、默认传感器等,如果 chosen 中的引用无效,系统可能无法启动。

3.5 综合实战:从零搭建一个完整的 Zephyr 应用

下面通过一个完整的示例,串联本章所有知识点。假设我们要在 nRF52840 DK 上实现一个通过 UART 打印传感器数据的应用。

步骤一:初始化工作区

west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.7.0 zephyrproject
cd zephyrproject
west update

步骤二:创建应用目录

mkdir -p app/src
cd app
touch CMakeLists.txt prj.conf app.overlay

步骤三:编写 CMakeLists.txt

cmake_minimum_required(VERSION 3.20.0)
project(sensor_logger)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

target_sources(app PRIVATE src/main.c)

步骤四:配置 prj.conf

CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_I2C=y
CONFIG_SENSOR=y
CONFIG_SHT3X=y
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3

步骤五:编写 app.overlay

/ {
    soc {
        i2c0: i2c@40003000 {
            status = "okay";
            sht30@44 {
                compatible = "sensirion,sht3x";
                reg = <0x44>;
            };
        };
    };
};

步骤六:编写 main.c

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(main);

void main(void)
{
    const struct device *sht = DEVICE_DT_GET_ANY(sensirion_sht3x);
    if (!sht) {
        LOG_ERR("SHT30 device not found");
        return;
    }

    while (1) {
        struct sensor_value temp, humidity;
        sensor_sample_fetch(sht);
        sensor_channel_get(sht, SENSOR_CHAN_AMBIENT_TEMP, &temp);
        sensor_channel_get(sht, SENSOR_CHAN_HUMIDITY, &humidity);
        LOG_INF("Temp: %d.%d C, Humidity: %d.%d %%",
                temp.val1, temp.val2, humidity.val1, humidity.val2);
        k_sleep(K_SECONDS(2));
    }
}

步骤七:构建与烧录

west build -b nrf52840dk_nrf52840 -d build/app app
west flash -d build/app

这个案例完整展示了 west 工作区、应用目录、CMakeLists.txt、prj.conf 和 dts 文件如何协同工作。在实际项目中,可能还需要添加 boards/ 目录下的自定义板卡定义、drivers/ 下的自定义驱动,但核心组织方式不变。

总结复盘:Zephyr 项目结构的核心思想是分层解耦——west 管理多仓库版本,CMake 管理构建流程,Kconfig 管理功能配置,设备树管理硬件描述。掌握这四者的关系,就能在复杂的嵌入式项目中游刃有余。建议初学者先从一个简单的 blinky 示例开始,逐步增加外设和驱动,理解每个文件的作用后再进行复杂项目开发。