第四章 构建系统详解: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脚本的集合。你想想看,嵌入式项目要支持上百种开发板、几十种架构,没有一套灵活的构建系统怎么行?
整个流程大致分三步:
- 配置阶段(Configuration):CMake读取
CMakeLists.txt,解析设备树,生成zephyr.dts和一堆头文件。 - 生成阶段(Generation):根据配置生成Makefile或Ninja构建文件。
- 编译阶段(Build):调用编译器、链接器,最终生成
.elf、.hex、.bin等固件文件。
嗯,这里要注意一个关键点:设备树的解析是在配置阶段完成的。也就是说,你改了DTS文件,必须重新执行 west build 的配置步骤,光重新编译是不够的。
# 强制重新配置并构建
west build -b nrf52840dk_nrf52840 samples/basic/blinky --force
--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.h、device.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的威力。
4.5 三者如何协同工作
好,现在我们把west、CMake、DTS、HAL串起来,看看一个完整的构建流程:
- 你执行
west build -b nrf52840dk_nrf52840 - west调用CMake,CMake读取
boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts - CMake解析DTS,生成
zephyr.dts_compiled和include/generated/devicetree.h - CMake根据
Kconfig配置,决定启用哪些HAL模块 - 编译器编译源码,其中驱动代码通过
devicetree.h获取硬件信息,通过HAL操作寄存器 - 链接器生成最终固件
你看,每一步都环环相扣。我刚开始学的时候,总觉得这些工具是独立的,后来才明白它们是一个整体。你想想看,如果没有设备树,你要为每个开发板写一套不同的驱动代码,那得多累?
west build -t guiconfig 打开图形化配置界面,里面能看到所有解析后的硬件信息。
好了,这一章的内容就到这里。构建系统是Zephyr的骨架,理解了它,你就能更好地驾驭整个框架。下一章我们会深入驱动开发,到时候你会看到今天讲的东西是如何在实际代码中发挥作用的。