3. Zephyr工程结构

好,咱们进入第三章。这一章我打算把Zephyr的构建系统彻底讲透。说实话,很多初学者甚至一些有经验的开发者,都被West和Kconfig搞得一头雾水。我当年刚接触Zephyr时,也在这上面栽过跟头——明明代码逻辑没问题,编译出来就是跑不起来,最后发现是Kconfig配置漏了一项。

所以这一章,咱们从工具链到底层配置,一步步拆解。你跟着我走一遍,以后遇到构建问题,心里就有底了。

3.1 West:不只是构建工具

West是Zephyr的元工具。说白了,它既管代码拉取,又管构建和刷写。很多人以为West就是个make的包装,其实远不止如此。

核心认知:West是一个多仓库管理+构建调度的统一入口。它通过manifest文件管理整个Zephyr生态的版本依赖。

3.1.1 West的工作流

我习惯把West的工作流分成三个阶段:

  1. 初始化(west init):拉取manifest仓库,建立项目骨架
  2. 更新(west update):根据manifest同步所有子模块
  3. 构建(west build):调用CMake/Ninja完成实际编译

你看,前两步解决的是“代码从哪里来”,第三步才是“怎么编译”。这种分离设计,让Zephyr能灵活支持成百上千种板子和配置。

3.1.2 常用命令速查

命令 作用 我常用的场景
west init -m <url> 初始化工作区 新项目开始,指定自己的manifest仓库
west update 同步所有模块 每天上班第一件事,拉取最新代码
west build -b <board> 构建指定板子 切换目标板时,一键重建
west flash 刷写固件 调试时,改完代码直接烧录
west config 查看/修改配置 排查构建路径问题时,查west的全局设置

我的小技巧:用west build -t menuconfig可以图形化配置Kconfig。比手动翻文件直观多了,尤其适合刚接触新平台时快速浏览所有选项。

3.2 Kconfig配置系统:Zephyr的“开关总闸”

Kconfig是Zephyr的配置核心。每个功能、每个驱动、每个内核选项,都由一个Kconfig符号控制。你想想看,一个RTOS要支持几十种架构、上百种板子、上千种外设,如果没有一套统一的配置体系,那代码得乱成什么样?

Kconfig的语法其实不复杂,但它的依赖和选择机制,才是真正的精髓。

3.2.1 基本语法

一个典型的Kconfig条目长这样:

config MY_FEATURE
    bool "Enable my custom feature"
    default n
    depends on SOME_OTHER_FEATURE
    help
      This enables my custom feature for demonstration.

这里bool表示开关型选项,default n默认关闭,depends on表示依赖关系。嗯,要注意的是,如果依赖不满足,这个选项即使被选中也不会生效。

3.2.2 配置层次与覆盖

Zephyr的配置有明确的优先级:

  1. 板级默认配置:boards/<arch>/<board>/<board>_defconfig
  2. 应用配置:prj.conf(或prj_<board>.conf)
  3. 覆盖配置:通过overlay文件或命令行参数

我曾经在一个项目里,板子默认开启了UART,但我的应用不需要。我就在prj.conf里把它关了。结果发现还是被打开了——查了半天,原来是某个驱动模块的select语句强制依赖了UART。这就是Kconfig的“隐式依赖”陷阱。

避坑指南:我曾经因为select和depends on搞混,导致配置冲突。记住:depends on是“如果A不满足,B不可选”;select是“如果选了A,强制选B”。前者是约束,后者是强制。

3.3 多板支持与覆盖配置

Zephyr支持上百种开发板。但实际项目中,我们往往只需要针对一两块板子做开发。怎么管理不同板子的差异?覆盖配置就是答案。

3.3.1 板级覆盖

假设你的应用在nrf52840dk和stm32f4_disco上都要跑,但外设配置不同。你可以这样组织:

app/
├── src/
├── prj.conf              # 通用配置
├── prj_nrf52840dk.conf   # nRF52840专用配置
└── prj_stm32f4_disco.conf # STM32F4专用配置

构建时,West会自动合并通用配置和板级配置。如果冲突,板级配置覆盖通用配置。

3.3.2 设备树覆盖

除了Kconfig,设备树(Device Tree)也支持覆盖。这是Zephyr的一大特色——硬件描述和软件配置分离。

我习惯在app/boards/目录下放自定义的设备树覆盖文件:

app/
└── boards/
    └── nrf52840dk.overlay

这个文件里可以修改管脚分配、外设参数,甚至添加新的设备节点。构建系统会自动识别并应用。

关键点:Kconfig管“软件功能开关”,设备树管“硬件资源描述”。两者配合,才能精准控制你的目标板。

3.4 自定义构建目标

当标准构建流程满足不了你的需求时,就该自定义构建目标上场了。比如,我想在构建完成后自动运行单元测试,或者生成一个固件大小报告。

3.4.1 在CMakeLists.txt中添加自定义目标

Zephyr的构建系统基于CMake。你可以在应用的CMakeLists.txt里添加自定义目标:

add_custom_target(firmware_size
    COMMAND ${CMAKE_SIZE_UTIL} ${PROJECT_BINARY_DIR}/zephyr/zephyr.elf
    COMMENT "Calculating firmware size..."
)

add_dependencies(firmware_size app)

然后你就可以用west build -t firmware_size来执行这个目标了。

3.4.2 使用West扩展命令

如果你想让自定义目标像west flash一样优雅,可以写一个West扩展。在项目根目录下创建scripts/west-commands.yml:

west-commands:
  - file: scripts/my_command.py
    commands:
      - name: my-build
        class: MyBuildCommand

然后在my_command.py里实现你的逻辑。这样,你就可以用west my-build来调用它了。

我的经验:自定义构建目标最适合做“构建后处理”。比如自动生成文档、打包固件、上传到服务器。把这些重复劳动自动化,能省下大量时间。

3.5 知识体系总览

为了让你对整个构建系统有个全局认识,我画了张图。你看一眼,就能明白各个组件之间的关系。

Zephyr构建系统知识体系 Zephyr构建系统核心组件 West 元工具 Manifest管理 构建调度(CMake/Ninja) 刷写与调试 Kconfig配置系统 设备树(硬件描述) 板级支持包(BSP) 最终固件 (zephyr.elf/.hex/.bin) West统一调度 → Kconfig+设备树+BSP协同配置 → 生成目标固件

这张图把整个构建流程串起来了。你看,West在最上层,负责调度;中间是三大配置支柱;最终输出固件。每个环节都可以自定义,这就是Zephyr灵活性的来源。

3.6 实战:从零构建一个自定义板子

光说不练假把式。咱们来个实战——为一个不存在的“虚拟板子”创建构建环境。

3.6.1 创建板级目录

mkdir -p boards/arm/my_virtual_board
cd boards/arm/my_virtual_board

3.6.2 编写必要文件

至少需要三个文件:

  • Kconfig.board:板级Kconfig选项
  • Kconfig.defconfig:默认配置
  • my_virtual_board.dts:设备树文件

一个最简单的Kconfig.board示例:

config BOARD_MY_VIRTUAL_BOARD
    bool "My Virtual Board"
    depends on SOC_SERIES_CUSTOM

3.6.3 构建测试

west build -b my_virtual_board app/

如果一切顺利,你会看到构建系统自动识别了你的板子,并应用了对应的配置。

注意:自定义板子需要确保SoC、架构等依赖链完整。我曾经漏了depends on,结果构建时Kconfig直接报“未定义的符号”。排查起来挺费劲的。