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 文件各自承担的职责,并给出实战中的组织方法与排障思路。
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 工作区内。初始化一个新工作区的标准流程为:
- 安装 west:
pip install west - 初始化 manifest 仓库:
west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.7.0 zephyrproject(指定版本分支) - 同步所有仓库:
cd zephyrproject && west update - 导出 Zephyr 构建辅助文件:
west zephyr-export(可选,用于 CMake 包查找)
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 和按键节点,但编译后按键功能异常。排查步骤:
- 使用
west build -t dts查看合并后的设备树,确认两个节点是否都指向 PB0。 - 检查
gpio-hogs或pinctrl配置,确认引脚复用是否正确。 - 查看
build/zephyr/.config中CONFIG_GPIO是否开启。 - 在代码中打印 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/ 下的自定义驱动,但核心组织方式不变。