第四章 构建系统详解:west构建命令、CMake构建流程、设备树(DTS)与硬件抽象层(HAL)

好,咱们今天来聊聊Zephyr的构建系统。说实话,我刚接触Zephyr那会儿,最头疼的就是这一块。west命令、CMake、设备树、HAL……这些东西搅在一起,很容易让人摸不着头脑。但你别怕,我带你一层层剥开它。

4.1 west构建命令:不只是个包装器

很多人以为west就是个简单的命令包装器,其实不然。我个人的习惯是,把west看作Zephyr项目的「项目经理」——它不光管构建,还管拉代码、管理模块、甚至帮你处理多仓库的依赖关系。

先看最常用的几个命令:

# 初始化工作空间
west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.5.0

# 更新所有模块
west update

# 构建一个示例
west build -b nrf52840dk_nrf52840 samples/basic/blinky

# 构建并烧录
west build -b nrf52840dk_nrf52840 samples/basic/blinky -t flash

这里有个坑,我踩过好几次。你执行west update的时候,它会根据manifest文件拉取所有依赖模块。如果你网络不好,或者某个仓库挂了,整个流程就卡住了。我曾经在客户现场演示时遇到这个问题,场面一度很尴尬……

我的建议: 在稳定的网络环境下先跑一次 west update,之后可以加 --narrow 参数只更新需要的模块,省时间也省带宽。

4.2 CMake构建流程:从源码到固件

west build底层调用的其实是CMake。说白了,Zephyr的构建系统就是一套CMake脚本的集合。你想想看,嵌入式项目要支持上百种开发板、几十种架构,没有一套灵活的构建系统怎么行?

整个流程大致分三步:

  1. 配置阶段(Configuration):CMake读取 CMakeLists.txt,解析设备树,生成 zephyr.dts 和一堆头文件。
  2. 生成阶段(Generation):根据配置生成Makefile或Ninja构建文件。
  3. 编译阶段(Build):调用编译器、链接器,最终生成 .elf.hex.bin 等固件文件。

嗯,这里要注意一个关键点:设备树的解析是在配置阶段完成的。也就是说,你改了DTS文件,必须重新执行 west build 的配置步骤,光重新编译是不够的。

# 强制重新配置并构建
west build -b nrf52840dk_nrf52840 samples/basic/blinky --force
避坑指南: 我曾经在调试一个I2C驱动时,改了DTS里的地址,结果只重新编译没重新配置,折腾了整整一个下午才发现问题。所以,改DTS后一定要加 --force 参数。

4.3 设备树(DTS):硬件描述的语言

设备树,说白了就是一份描述硬件资源的「说明书」。它告诉Zephyr:你的芯片有哪些外设、引脚怎么连、中断号是多少、时钟频率是多少……

一个典型的DTS文件长这样:

/ {
    model = "Nordic nRF52840 DK";
    compatible = "nordic,nrf52840-dk";

    chosen {
        zephyr,console = &uart0;
        zephyr,flash = &flash0;
    };

    &uart0 {
        status = "okay";
        current-speed = <115200>;
        pinctrl-0 = <&uart0_default>;
        pinctrl-names = "default";
    };

    &i2c0 {
        status = "okay";
        clock-frequency = <100000>;
    };
};

你看,chosen 节点指定了控制台用哪个UART,uart0 节点配置了波特率和引脚复用。这些信息最终会被CMake解析成C语言宏定义,供驱动代码使用。

我个人觉得,理解设备树的关键在于记住一句话:DTS是描述,不是配置。它只告诉系统硬件长什么样,至于怎么用,那是驱动代码的事。

4.4 硬件抽象层(HAL):屏蔽差异的利器

HAL,全称Hardware Abstraction Layer。说白了,就是给上层驱动提供一套统一的接口,让它们不用关心底层寄存器怎么操作。

Zephyr的HAL分两层:

  • SoC级HAL:由芯片厂商提供,比如Nordic的nrfx、ST的STM32 HAL。这些代码直接操作寄存器。
  • Zephyr级HAL:Zephyr自己封装的一层,通过 soc.hdevice.h 等头文件提供统一接口。

举个例子,你要操作一个GPIO:

#include <zephyr/drivers/gpio.h>

const struct device *dev;
int ret;

dev = DEVICE_DT_GET(DT_NODELABEL(led0));
ret = gpio_pin_configure(dev, 13, GPIO_OUTPUT_ACTIVE);

这段代码在nRF52840上能跑,在STM32上也能跑。为什么?因为 gpio_pin_configure 这个函数在底层调用了不同的HAL实现——nRF平台调用nrfx,STM32平台调用STM32 HAL。这就是HAL的威力。

关键理解: 设备树告诉系统「有什么硬件」,HAL告诉系统「怎么操作硬件」。两者配合,才能让上层驱动做到平台无关。

4.5 三者如何协同工作

好,现在我们把west、CMake、DTS、HAL串起来,看看一个完整的构建流程:

  1. 你执行 west build -b nrf52840dk_nrf52840
  2. west调用CMake,CMake读取 boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts
  3. CMake解析DTS,生成 zephyr.dts_compiledinclude/generated/devicetree.h
  4. CMake根据 Kconfig 配置,决定启用哪些HAL模块
  5. 编译器编译源码,其中驱动代码通过 devicetree.h 获取硬件信息,通过HAL操作寄存器
  6. 链接器生成最终固件

你看,每一步都环环相扣。我刚开始学的时候,总觉得这些工具是独立的,后来才明白它们是一个整体。你想想看,如果没有设备树,你要为每个开发板写一套不同的驱动代码,那得多累?

调试小技巧: 如果你不确定设备树解析是否正确,可以执行 west build -t guiconfig 打开图形化配置界面,里面能看到所有解析后的硬件信息。

好了,这一章的内容就到这里。构建系统是Zephyr的骨架,理解了它,你就能更好地驾驭整个框架。下一章我们会深入驱动开发,到时候你会看到今天讲的东西是如何在实际代码中发挥作用的。