4、Zephyr项目结构:west workspace、manifest、application目录、Kconfig与CMakeLists
Zephyr RTOS 与传统的嵌入式裸机工程或其它 RTOS(如 FreeRTOS、RT-Thread)在项目组织上有着本质区别。它并非简单地提供一个内核源码压缩包让你复制到工程中,而是引入了一套基于 west 工具的模块化工作区(Workspace)管理机制。理解这套结构,是进行 Zephyr 应用开发、BSP 移植和多项目复用的第一道门槛。本章将深入拆解 west workspace 的物理布局、manifest 清单的解析逻辑、application 目录的组成要素,以及 Kconfig 与 CMakeLists.txt 在构建系统中的核心作用,帮助你建立起对 Zephyr 工程体系的整体认知。
4.1 west workspace 的物理布局与核心原理
west 是 Zephyr 官方提供的多仓库管理工具,类似于 Google 的 repo 或 Git 的 submodule,但针对 Zephyr 生态做了深度定制。一个典型的 west workspace 在磁盘上表现为一个顶层目录(topdir),其中包含多个独立的 Git 仓库。这些仓库并非嵌套关系,而是平级存放在 topdir 下,通过 west.yml 清单文件建立逻辑关联。
核心原理:west 将“项目”定义为清单中列出的每个 Git 仓库。执行 west init 时,west 会拉取 manifest 仓库(通常就是 zephyr 仓库本身,或一个独立的 manifest 仓库),然后执行 west update 时,west 根据 manifest 中声明的 projects 列表,将每个仓库克隆到 topdir 下对应的 path 目录,并检出指定的 revision(可以是分支、标签或 commit SHA)。这种设计使得 Zephyr 内核、HAL 驱动、第三方库和应用本身可以独立演进,同时通过 manifest 锁定版本组合,保证构建的可复现性。
| 目录/文件 | 作用 | 是否必须 |
|---|---|---|
zephyr/ |
Zephyr 内核主仓库,包含 kernel、drivers、boards、soc、dts 等核心代码 | 是(由 manifest 拉取) |
modules/ |
可选的外部模块仓库,如 HAL 库、加密库、文件系统等 | 按需 |
my_app/ |
用户自己的应用工程目录,可以放在 workspace 内或外部 | 是(至少一个) |
.west/ |
west 的内部配置目录,包含 config 文件和 manifest 的浅克隆 | 自动生成 |
west.yml |
manifest 清单文件,定义所有仓库的来源、版本和路径 | 是 |
zephyr/samples 下添加自定义 sample,但这样做会导致后续 west update 时可能产生冲突,且不利于代码复用。推荐将应用作为独立仓库放在 workspace 根目录下,并通过 manifest 的 self 字段指向它。
4.2 manifest 清单(west.yml)的解析与自定义
manifest 文件是 west workspace 的“宪法”。它定义了三个关键部分:remotes(远程仓库别名)、projects(需要拉取的项目列表)、self(当前仓库自身的信息)。理解 manifest 的语法是管理多仓库版本的关键。
操作流程:创建自定义 manifest 通常有两种方式。第一种是直接使用 Zephyr 官方仓库自带的 west.yml,在其基础上追加自己的项目;第二种是创建一个独立的 manifest 仓库,专门管理公司内部的所有项目。推荐后者,因为它可以将 Zephyr 版本升级与内部模块变更解耦。
# 独立 manifest 仓库示例:manifest-repo/west.yml
manifest:
remotes:
- name: zephyr-upstream
url-base: https://github.com/zephyrproject-rtos
- name: my-company
url-base: https://gitlab.com/mycompany
projects:
- name: zephyr
remote: zephyr-upstream
revision: v3.7.0
path: zephyr
- name: hal_stm32
remote: zephyr-upstream
revision: 0a1b2c3d4e5f
path: modules/hal/stm32
- name: my_driver_lib
remote: my-company
revision: main
path: modules/my_driver_lib
self:
path: manifest-repo
west-commands: scripts/west-commands.yml
案例拆解:假设你所在团队维护着 5 个内部驱动库,同时使用 Zephyr 3.7 LTS 版本。如果每个开发者都手动 clone 这 5 个库并切换到指定分支,极易出现版本不一致。通过上述 manifest,开发者只需执行:
west init -m https://gitlab.com/mycompany/manifest-repo.git my-workspace
cd my-workspace
west update
west 会自动完成所有仓库的拉取和检出。当需要升级 Zephyr 版本时,只需修改 manifest 中 zephyr 项目的 revision 字段,然后重新执行 west update。如果某个内部库需要临时修复,可以修改其 revision 指向一个 hotfix 分支,验证通过后再合并回 main。
revision 字段建议使用具体的 commit SHA 或 tag,而不是分支名(如 main)。因为分支是浮动的,不同时间执行 west update 可能得到不同代码,导致“昨天能编译,今天不行”的尴尬局面。对于长期维护的项目,锁定精确版本是保证可复现性的基本要求。
4.3 application 目录结构与 CMakeLists.txt 的编写
一个标准的 Zephyr 应用目录通常包含以下文件:CMakeLists.txt(构建入口)、prj.conf(Kconfig 配置)、src/main.c(源代码)。此外,还可以有 boards/ 目录(板级覆盖配置)、Kconfig(应用级配置菜单)、dts/(设备树覆盖)等。
核心原理:Zephyr 的构建系统基于 CMake。当你在应用目录执行 west build -b board_name 时,west 会调用 CMake,而应用目录下的 CMakeLists.txt 是整个构建的入口。这个文件必须至少包含两行:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_app)
target_sources(app PRIVATE src/main.c)
第一行指定 CMake 最低版本;第二行通过 find_package 找到 Zephyr 的构建系统,它会自动加载 Zephyr 的 CMake 模块、Kconfig 解析逻辑和设备树编译规则;第三行定义项目名;第四行将源文件添加到 app 目标中。这里的 app 是 Zephyr 预定义的最终可执行目标。
操作流程:对于多源文件的应用,推荐使用 target_sources 逐个添加,或者使用 file(GLOB) 自动收集(但需注意新增文件后要重新运行 cmake)。对于需要链接静态库的场景,可以使用 target_link_libraries(app PRIVATE mylib)。Zephyr 还提供了 zephyr_include_directories() 和 zephyr_link_libraries() 等辅助函数,用于向所有 Zephyr 相关目标添加路径或库。
| CMake 函数/变量 | 用途 | 示例 |
|---|---|---|
find_package(Zephyr) |
加载 Zephyr 构建系统 | 必须 |
target_sources(app PRIVATE ...) |
添加源文件 | target_sources(app PRIVATE src/main.c src/util.c) |
zephyr_include_directories(...) |
添加头文件搜索路径 | zephyr_include_directories(include) |
target_compile_definitions(app PRIVATE ...) |
添加编译宏 | target_compile_definitions(app PRIVATE FOO=1) |
set(CONFIG_XXX y) |
在 CMake 中直接设置 Kconfig 值(不推荐) | 应使用 prj.conf |
boards/ 子目录,放入 myboard_v1.overlay 和 myboard_v2.overlay 设备树覆盖文件。构建时通过 -DOVERLAY_CONFIG=boards/myboard_v1.overlay 指定。同时,在 prj.conf 中通过 CONFIG_BOARD_VARIANT=y 这样的配置项配合 #ifdef 在代码中区分逻辑。这种结构避免了维护两个独立工程,是 Zephyr 推荐的“单工程多配置”模式。
4.4 Kconfig 配置系统与 prj.conf 的优先级
Kconfig 是 Zephyr 的配置系统,源自 Linux 内核。它通过层级化的菜单结构管理成百上千个配置选项。每个选项最终会生成一个 autoconf.h 头文件,其中定义了对应的宏,供 C 代码使用。
核心原理:Zephyr 的 Kconfig 配置来源有多个层次,优先级从高到低依次为:app.config(通过 west build -DEXTRA_CONF_FILE 指定)→ prj.conf(应用默认配置)→ board_defconfig(板级默认配置)→ soc_defconfig(SoC 默认配置)→ Kconfig.defconfig(架构默认)。高优先级会覆盖低优先级。这意味着你可以在 prj.conf 中修改任何板级默认值,而无需改动 Zephyr 源码。
操作流程:在应用目录下创建 prj.conf 文件,每行一个配置项,格式为 CONFIG_XXX=y(启用)或 CONFIG_XXX=n(禁用),数值型配置使用 CONFIG_XXX=5。例如:
# prj.conf
CONFIG_STDOUT_CONSOLE=y
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3
CONFIG_HEAP_MEM_POOL_SIZE=4096
CONFIG_GPIO=y
CONFIG_I2C=y
构建时,Zephyr 的构建系统会解析所有 Kconfig 文件,结合 prj.conf 生成最终的 .config 文件,并自动生成 zephyr/include/generated/autoconf.h。在 C 代码中,你可以直接使用 #ifdef CONFIG_GPIO 或 #if CONFIG_LOG_DEFAULT_LEVEL > 2 进行条件编译。
CONFIG_I2C=y 表示启用 I2C 子系统,但具体使用哪个 I2C 控制器、速率多少,则由设备树中的 &i2c0 { clock-frequency = <100000>; }; 决定。两者配合才能完成一个外设的驱动。
4.5 综合案例:从零搭建一个带外部模块的多应用工作区
下面通过一个完整案例,将本章所有知识点串联起来。假设你在一家 IoT 公司工作,需要为两款产品(智能网关和传感器节点)维护固件,两者共用一套内部通信协议库,但硬件平台不同(网关使用 NXP i.MX RT1052,节点使用 Nordic nRF52840)。
背景与约束:团队 5 人,需要同时开发两个产品线,代码要复用,且 Zephyr 版本需要锁定在 3.6 LTS。初始现象是:之前每个工程师各自维护一套 Zephyr 源码拷贝,导致 HAL 驱动版本混乱,合并代码时冲突不断。
实施步骤:
- 创建独立 manifest 仓库(
gitlab.com/mycompany/manifest.git),内容如下:
manifest:
remotes:
- name: zephyr-upstream
url-base: https://github.com/zephyrproject-rtos
- name: mycompany
url-base: https://gitlab.com/mycompany
projects:
- name: zephyr
remote: zephyr-upstream
revision: v3.6.0
path: zephyr
- name: hal_nxp
remote: zephyr-upstream
revision: 8f3a2b1c
path: modules/hal/nxp
- name: hal_nordic
remote: zephyr-upstream
revision: 5e4d3c2b
path: modules/hal/nordic
- name: comm_protocol
remote: mycompany
revision: v2.1.0
path: modules/comm_protocol
self:
path: manifest
- 创建两个应用仓库:
gateway_app和sensor_node_app,分别放在 workspace 根目录下。每个应用目录结构如下:
gateway_app/
├── CMakeLists.txt
├── prj.conf
├── src/
│ ├── main.c
│ └── gateway_net.c
├── boards/
│ ├── mimxrt1050_evk.overlay
│ └── mimxrt1050_evk.conf
└── Kconfig
- 在 gateway_app/CMakeLists.txt 中引用公共库:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(gateway_app)
target_sources(app PRIVATE src/main.c src/gateway_net.c)
zephyr_include_directories(${ZEPHYR_BASE}/../modules/comm_protocol/include)
target_link_libraries(app PRIVATE comm_protocol)
- 在 gateway_app/prj.conf 中启用所需功能:
CONFIG_NETWORKING=y
CONFIG_NET_L2_ETHERNET=y
CONFIG_COMM_PROTOCOL=y
CONFIG_LOG=y
- 构建命令:
west build -b mimxrt1050_evk gateway_app
west build -b nrf52840dk_nrf52840 sensor_node_app
结果对比:实施后,两个应用共享同一份 Zephyr 内核和 comm_protocol 库,版本完全一致。当 comm_protocol 发布 v2.2.0 时,只需修改 manifest 中的 revision 并执行 west update,两个应用同步更新。构建时间从原来的 15 分钟(各自编译)缩短到 8 分钟(增量编译),代码冲突率下降 90%。
风险与代价:初期搭建 manifest 仓库和迁移代码需要约 2 天工作量。另外,由于 Zephyr 版本锁定在 v3.6.0,后续如果某个 HAL 驱动有 bug 修复,需要手动 backport 或升级 Zephyr 版本,这需要评估兼容性。
复盘经验:west workspace 的核心价值在于“依赖管理”。它让 Zephyr 内核、HAL、第三方库和应用之间的版本关系变得透明可控。对于团队协作,强烈建议将 manifest 仓库设为受保护分支,合并请求需要至少一人 review。对于个人开发者,即使只有一个应用,也推荐使用 west 管理,因为后续添加模块或升级版本时会非常方便。