3. Zephyr工程结构与west构建系统

在 nRF Connect SDK(NCS)中,一个可编译、可烧录、可复现的固件工程并不是“一个 main.c 加一个 Makefile”,而是由 west manifest 锁定源码版本、CMake 组织构建目标、Kconfig 裁剪功能、设备树(DTS) 描述硬件、板级配置文件 提供默认参数这五套机制协同工作的结果。理解它们的职责边界与数据流向,是创建自定义应用工程的前提。

一、west manifest:工程版本与模块的“锁文件”

west 是 Zephyr 的元工具,它本身不编译代码,而是负责管理多个 Git 仓库(Zephyr、nrfxlib、mcuboot、hal_nordic 等)的版本与路径。这些信息集中在 west.yml 中,称为 manifest。NCS 安装后,工作区根目录下会有 .west/config 记录 manifest 路径,而真正的 west.yml 位于 nrf/west.yml,它通过 manifest.remotes 和 projects 列表声明每个模块的 URL、revision 与 path。

# 查看当前工作区与 manifest 状态
west topdir
west list
west manifest --path

# 更新所有模块到 manifest 指定版本
west update

# 冻结当前实际版本,生成可复现的 manifest
west manifest --freeze > west-frozen.yml

关键点在于:应用工程不应直接修改 nrf/west.yml。若需要引入额外模块(例如自定义驱动库),正确做法是在应用目录下创建 west.yml,使用 manifest.self.path 与 import 机制叠加到 NCS manifest 之上,再执行 west update。这样既保持 NCS 版本可控,又能扩展私有模块。

二、CMake 与 Kconfig 的协同机制

Zephyr 的构建系统以 CMake 为骨架,但配置项并不写在 CMakeLists.txt 里,而是由 Kconfig 生成 .config,再由 CMake 读取并转成编译宏与源文件列表。整个协同过程可以概括为:

  • Kconfig 阶段:CMake 调用 kconfig.py,从 Kconfig 根文件递归解析各模块的 Kconfig,结合 prj.conf、板级 _defconfig、SoC 默认值,生成 build/zephyr/.config。
  • CMake 阶段:读取 .config 中的 CONFIG_*,通过 zephyr_library()、target_sources() 决定编译哪些文件、链接哪些库。
  • 生成阶段:输出 autoconf.h(供 C 代码使用 CONFIG_XXX 宏)与 devicetree_generated.h(供 DTS 宏使用)。

因此,prj.conf 只负责“选功能”,CMakeLists.txt 只负责“加源文件与包含路径”,两者不要互相越界。例如启用日志系统:

# prj.conf
CONFIG_LOG=y
CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_LOG_DEFAULT_LEVEL=3

# 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)

三、板级设备树与配置文件组织

设备树描述“硬件长什么样”,Kconfig 描述“软件要什么功能”,二者通过 status = "okay" 与 CONFIG_* 配合。以 nRF52840 DK 为例,板级目录结构为:

zephyr/boards/arm/nrf52840dk_nrf52840/
├── board.cmake
├── nrf52840dk_nrf52840.dts
├── nrf52840dk_nrf52840_defconfig
├── Kconfig.defconfig
└── nrf52840dk_nrf52840.yaml

应用工程通过 boards/<board>.overlay 覆盖或追加节点,通过 boards/<board>.conf 追加 Kconfig。例如给自定义应用增加一个 UART1 节点并启用:

/* boards/nrf52840dk_nrf52840.overlay */
&uart1 {
    status = "okay";
    current-speed = <115200>;
};

/* boards/nrf52840dk_nrf52840.conf */
CONFIG_UART_1=y

构建时,west 会自动把 boards/ 下与目标板同名的 overlay 与 conf 合并进主配置,无需手动指定路径。若使用自定义板,则需在 boards/<arch>/<board>/ 下提供完整 dts、defconfig 与 yaml。

Zephyr 应用工程构建协同关系 west manifest nrf/west.yml 锁定 Zephyr / nrfxlib mcuboot / hal_nordic west update 拉取 west manifest --freeze Kconfig prj.conf board_defconfig SoC 默认值 生成 .config autoconf.h CMake CMakeLists.txt find_package(Zephyr) target_sources(app) 读取 .config 生成 build/ 设备树 DTS board.dts app.overlay status = "okay" devicetree_generated.h DT_NODELABEL() 板级配置文件 boards/<board>.conf boards/<board>.overlay board.cmake Kconfig.defconfig board.yaml 构建输出 build/zephyr/ zephyr.elf · zephyr.hex · .config · autoconf.h · devicetree_generated.h 源码 .config DTS 宏 板级默认 编译链接 模块路径

四、创建自定义应用工程的完整流程

下面以在 NCS 工作区中创建一个名为 my_sensor_app 的应用为例,目标板为 nrf52840dk_nrf52840,使用 UART1 输出传感器数据。

  1. 建立目录:在 NCS 工作区根目录下创建 my_sensor_app/,与 zephyr/、nrf/ 平级。
  2. 编写 CMakeLists.txt:调用 find_package(Zephyr),声明 project(my_sensor_app),用 target_sources(app PRIVATE src/main.c) 加入源文件。
  3. 编写 prj.conf:启用 CONFIG_GPIO、CONFIG_UART_1、CONFIG_LOG、CONFIG_SENSOR 等。
  4. 编写 boards/nrf52840dk_nrf52840.overlay:将 uart1 的 status 设为 "okay",并配置引脚。
  5. 编写 src/main.c:使用 DEVICE_DT_GET(DT_NODELABEL(uart1)) 获取设备,调用 uart_poll_out() 输出。
  6. 构建与烧录:执行 west build -b nrf52840dk_nrf52840,再执行 west flash。
# 在 NCS 工作区根目录执行
west build -b nrf52840dk_nrf52840 my_sensor_app
west flash

# 查看 Kconfig 实际生效值
west build -t menuconfig

# 查看设备树展开结果
west build -t devicetree
判断构建是否正确的三个观察点:
  • build/zephyr/.config 中 CONFIG_UART_1=y 是否出现;
  • build/zephyr/include/generated/devicetree_generated.h 中是否存在 DT_N_S_soc_peripheral_s_uart_1 相关节点;
  • 串口终端是否按 115200 波特率输出日志,且无 <err> uart: Device not ready 报错。
常见误区:把 CONFIG_UART_1=y 写进 prj.conf 却忘记在 overlay 中把 uart1 的 status 改为 "okay",会导致设备树节点被禁用,运行时 DEVICE_DT_GET 返回未就绪设备。Kconfig 与 DTS 必须成对修改。

掌握上述五套机制的职责与数据流后,创建自定义应用工程就不再是“复制粘贴样例再改”,而是可以按需裁剪、按板适配、按版本复现的工程化过程。west 负责“拿到正确的源码”,Kconfig 负责“选对功能”,CMake 负责“组织编译”,DTS 与板级配置负责“匹配硬件”,四者各司其职,缺一不可。