摘要:本文从一次
west build出发,完整追踪 Zephyr 中 Devicetree 从 DTS 源文件到最终 C 宏的生成链路。你将看到:west build如何进入 CMake 构建系统,Board/SoC/Overlay/Binding 如何被合并成最终 Devicetree,zephyr.dts与devicetree_generated.h分别在哪个阶段产生,以及DT_PROP()、DT_NODELABEL()等宏如何被展开为编译期常量。核心结论:Zephyr Devicetree 是典型的 build-time hardware description,整个过程中不存在运行时 DTS 解析器。本文还给出了亲手验证这条链路的实操命令,并串联了前 17 篇内容,为后续手动拆解devicetree_generated.h打下基础。
从 west build 追踪 Devicetree 生成过程
这一篇非常关键。
前面你已经把:
phandle
dependency ordinal
device handle
struct device
DEVICE_DT_DEFINE()
DT_INST_FOREACH_STATUS_OKAY()
Driver Init
这些东西串起来了。
现在我们反过来追踪:
你执行一次 west build,Zephyr 到底什么时候读取 DTS,什么时候生成 C 代码,最后这些 DT_* 宏又是怎么进入 C 编译器的?
最终你应该能建立这样一条完整链路:
west build
│
▼
CMake
│
├── 找到 BOARD
├── 找到 SoC
├── 找到 DTS
├── 找到 Binding
│
▼
Devicetree Compiler / Zephyr scripts
│
▼
zephyr.dts
│
▼
devicetree_generated.h
│
▼
DT_* 宏
│
▼
Driver C/C++ source
│
▼
DEVICE_DT_DEFINE()
│
▼
struct device
一、先不要急着看 CMake
我们先看一个最简单的 Zephyr 项目。
例如:
workspace/
└── hello/
├── CMakeLists.txt
├── prj.conf
├── src/
│ └── main.c
└── boards/
└── ...
执行:
west build -b nucleo_f303re .
表面上只是:
west build
但实际上发生了很多事情。
可以把它理解成:
west
│
▼
CMake
│
├── Application configuration
│
├── Board selection
│
├── SoC selection
│
├── Devicetree processing
│
├── Kconfig
│
└── Build system generation
│
▼
Ninja / Make
│
▼
C compiler
所以:
west 本身不是 Devicetree 编译器。
west 更像一个入口/封装工具。
真正负责构建流程组织的是:
CMake
而 Devicetree 的处理由 Zephyr 的构建系统调用相关脚本完成。
二、第一站:west build
执行:
west build -b nucleo_f303re
West 首先确定:
BOARD = nucleo_f303re
然后进入 Zephyr 的 CMake 构建系统。
如果你使用:
west build -p always -b nucleo_f303re
还会强制重新配置。
三、第二站:CMake 找 Board
Zephyr 的 Board 并不是简单的一块 PCB 名字。
例如:
boards/st/nucleo_f303re/
里面通常可以看到类似:
board.yml
Kconfig.board
Kconfig.defconfig
nucleo_f303re.dts
nucleo_f303re.yaml
最重要的是:
nucleo_f303re.dts
它告诉 Zephyr:
这块 Board 上有哪些硬件,以及这些硬件如何连接。
例如:
&usart2 {
status = "okay";
};
或者:
&gpioa {
status = "okay";
};
但是这里有一个非常重要的问题:
Board DTS 并不是完整 DTS
它通常只是:
在 SoC DTS 基础上做 Board 层修改。
四、第三站:SoC DTS 加进来
例如 STM32F303。
Zephyr 需要知道:
STM32F303
├── CPU
├── RCC
├── GPIOA
├── GPIOB
├── USART1
├── USART2
├── SPI
├── I2C
└── ...
这些定义来自 SoC / SoC family 的 DTS 文件。
最终可以理解成:
SoC DTS
+
Board DTS
+
Overlay
+
chosen
+
aliases
+
bindings
↓
最终 Devicetree
五、Overlay 在什么时候加入?
假设你的项目有:
app.overlay
里面:
&usart2 {
status = "okay";
current-speed = <115200>;
};
那么构建时会把它加入最终 Devicetree。
所以:
nucleo_f303re.dts
+
app.overlay
↓
最终 DTS
注意:
Overlay 不是单独运行的设备树。
它是:
对已有 Devicetree 的修改。
六、Binding 又是什么?
这是非常容易混淆的地方。
例如:
uart@40004400 {
compatible = "st,stm32-usart";
reg = <0x40004400 0x400>;
interrupts = <37 0>;
};
DTS 告诉我们:
这里有一个设备
但是:
compatible = "st,stm32-usart"
需要告诉 Zephyr:
这个设备的 properties 是什么类型?
这就是 Binding。
例如:
dts/bindings/serial/st,stm32-usart.yaml
Binding 可以描述:
compatible: "st,stm32-usart"
properties:
current-speed:
type: int
clocks:
type: phandle-array
pinctrl-0:
type: phandles
所以可以把两者区别记成:
DTS
│
│ 描述“我的硬件是什么”
▼
Node
Binding
│
│ 描述“这个 Node 应该怎么解释”
▼
Property 类型
七、现在进入真正关键部分
CMake 得到:
Board
SoC
DTS
Overlay
Binding
之后,Zephyr 开始生成最终 Devicetree。
最终会产生一个非常重要的文件:
build/zephyr/zephyr.dts
你可以直接看:
less build/zephyr/zephyr.dts
或者:
grep -n "usart2" build/zephyr/zephyr.dts
Windows PowerShell:
Get-Content build\\zephyr\\zephyr.dts
八、zephyr.dts 是什么?
它是:
经过 Board + SoC + Overlay 等处理之后的最终 Devicetree。
例如原来的 Board DTS 可能只有:
&usart2 {
status = "okay";
};
但是最终:
build/zephyr/zephyr.dts
里面会变成完整 Node:
usart2: serial@40004400 {
compatible = "st,stm32-usart";
reg = <0x40004400 0x400>;
interrupts = <38 0>;
clocks = <&rcc ...>;
status = "okay";
...
};
这时候已经不再是:
“某个 Board DTS 文件”。
而是:
整个系统最终确定下来的硬件描述。
九、下一步:生成 devicetree_generated.h
这是整个流程最重要的一步。
Zephyr 会继续把 Devicetree 转换成:
build/zephyr/include/generated/zephyr/devicetree_generated.h
不同 Zephyr 版本/构建配置下具体目录层级可能略有变化,但核心文件就是:
devicetree_generated.h
你可以搜索:
find build -name devicetree_generated.h
Windows:
Get-ChildItem -Recurse -Filter devicetree_generated.h
十、这个 .h 文件到底是什么?
这是很多人第一次学习 Zephyr 时最容易忽略的东西。
假设 DTS:
&usart2 {
status = "okay";
current-speed = <115200>;
};
最终会产生大量类似:
# define DT_N_S_soc_S_usart_40004400 ...
以及:
# define DT_N_S_soc_S_usart_40004400_P_current_speed 115200
等等。
你在 Driver 里面看到的:
DT_PROP(node, current_speed)
最终就是通过这些生成出来的宏工作。
十一、所以 DT_* 不是魔法
例如:
# define UART_NODE DT_NODELABEL(usart2)
看起来很神奇。
实际上它最终会展开到一个非常具体的 Devicetree Node identifier。
然后:
DT_PROP(UART_NODE, current_speed)
继续展开。
概念上:
DT_PROP()
│
▼
生成的 DT node identifier
│
▼
devicetree_generated.h
│
▼
115200
因此:
uint32_t baudrate = DT_PROP(UART_NODE, current_speed);
编译器最终看到的可能就是类似:
uint32_t baudrate = 115200;
十二、这里有一个非常重要的认识
Devicetree 本身不是运行时数据
例如:
current-speed = <115200>;
不是程序启动以后再去解析 DTS。
不是:
程序运行
↓
读取 DTS 文件
↓
解析 current-speed
而是:
Build time
↓
DTS
↓
生成 C 宏
↓
C compiler
↓
机器代码
因此:
Zephyr Devicetree 是典型的 build-time hardware description。
这也是为什么 Zephyr 可以做到大量:
DT_REG_ADDR()
DT_REG_SIZE()
DT_IRQ()
DT_PROP()
DT_CLOCKS_CTLR()
DT_GPIO_CTLR()
在编译阶段直接得到常量。
十三、继续追踪:Driver 怎么拿到这些宏?
假设 STM32 UART Driver 有:
# define DT_DRV_COMPAT st_stm32_usart
然后:
DT_INST_FOREACH_STATUS_OKAY(


7889

被折叠的 条评论
为什么被折叠?



