轻松学习Zephyr BSP: 17-从 west build 追踪 Devicetree 生成过程

摘要:本文从一次 west build 出发,完整追踪 Zephyr 中 Devicetree 从 DTS 源文件到最终 C 宏的生成链路。你将看到:west build 如何进入 CMake 构建系统,Board/SoC/Overlay/Binding 如何被合并成最终 Devicetree,zephyr.dtsdevicetree_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 = &lt;0x40004400 0x400&gt;;
	interrupts = &lt;38 0&gt;;
	clocks = &lt;&rcc ...&gt;;
	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(
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值