第二讲:设备树(DTS)深入解析
各位同学,今天我们来啃一块硬骨头——设备树。说实话,我刚接触Zephyr那会儿,最头疼的就是DTS。明明写C代码好好的,突然冒出来一堆.dts文件,看着像C又不像C,语法怪得很。但后来我发现,不理解设备树,你根本没法真正驾驭Zephyr的驱动框架。
设备树是什么?说白了,它就是一块「硬件描述板」。你用文本的方式告诉系统:我这块板子上有哪些外设,它们挂在哪个总线上,地址是多少,中断号是多少。Zephyr在编译时会解析这些文件,自动生成C代码,驱动就能直接用了。
一、DTS语法基础
先看一个最简单的例子。我在一个项目里写过这样的设备树节点:
/ {
model = "My Custom Board";
compatible = "mycompany,myboard";
chosen {
zephyr,console = &uart0;
};
soc {
uart0: uart@40002000 {
compatible = "mycompany,uart";
reg = <0x40002000 0x1000>;
interrupts = <10 1>;
status = "okay";
current-speed = <115200>;
};
};
};
这里有几个关键点,我一个个说。
1. 节点与属性
每个设备树文件以根节点 / 开始。节点可以嵌套,就像文件夹一样。每个节点有属性,属性就是键值对。
- 节点名:比如
uart@40002000,@后面是地址,用来区分同名设备 - 标签:比如
uart0:,这是给节点起个别名,方便其他地方引用 - 属性:
compatible、reg、interrupts这些都是属性
重点:compatible 属性是驱动匹配的关键。Zephyr的驱动会通过这个字符串找到对应的设备。我见过有人拼写错误,结果驱动死活加载不上,排查了半天。
2. 地址编码
reg = <0x40002000 0x1000>; 表示这个UART外设的基地址是0x40002000,地址范围是0x1000字节。为什么要有范围?因为有些外设占用的地址空间比较大,比如USB控制器可能占用好几页。
嗯,这里要注意:地址和大小是成对出现的。如果有多个地址段,可以写多组:
reg = <0x40002000 0x1000>, <0x40003000 0x100>;
二、绑定(Binding)—— 连接DTS和驱动的桥梁
你可能会问:设备树里的属性,驱动怎么知道是什么意思?比如 current-speed 这个属性,驱动怎么知道它是波特率?
答案就是绑定文件。绑定文件是YAML格式的,它定义了设备树节点的属性类型和含义。Zephyr在编译时会根据绑定文件生成C结构体,驱动直接访问这些结构体就行了。
举个例子,上面那个UART的绑定文件大概长这样:
description: My Company UART
compatible: "mycompany,uart"
include: [uart-controller.yaml]
properties:
current-speed:
type: int
required: true
description: UART baud rate
看到没?compatible 字符串把设备树节点和绑定文件关联起来了。然后绑定文件告诉系统:current-speed 是一个整型属性,是必填的。
我的习惯:写新驱动时,我会先写绑定文件,再写设备树节点,最后写驱动代码。这样思路更清晰,不容易遗漏属性。
三、设备树覆盖(Overlay)
这是Zephyr里非常实用的功能。你想想看,如果你有一个开发板,想外接一个传感器,难道要修改原厂的设备树文件吗?当然不行。覆盖文件就是用来解决这个问题的。
覆盖文件可以「叠加」到原始设备树上,添加新节点或修改已有节点。比如:
/ {
aliases {
my-sensor = &sensor0;
};
&i2c0 {
sensor0: my-sensor@48 {
compatible = "mycompany,temperature";
reg = <0x48>;
label = "TEMP_SENSOR";
};
};
};
这个覆盖文件做了两件事:
- 在I2C0总线上添加了一个温度传感器节点,地址是0x48
- 给这个传感器起了个别名
my-sensor
我在实际项目中经常用覆盖文件来做产品定制。同一个主控板,不同型号的产品外设不一样,我就写不同的覆盖文件,编译时选不同的配置就行,不用动核心代码。
我曾经踩过的坑:覆盖文件里修改节点时,一定要确保节点路径完全正确。有一次我把 &i2c0 写成了 &i2c1,结果传感器挂在了错误的I2C总线上,读出来的数据全是0xFF。排查了整整一天才发现是路径写错了。
四、设备树宏与API
Zephyr提供了一套宏,让你在C代码里访问设备树信息。常用的有:
| 宏 | 作用 |
|---|---|
DT_NODELABEL(label) |
通过标签获取节点标识符 |
DT_PROP(node, property) |
获取节点的属性值 |
DT_REG_ADDR(node) |
获取节点的寄存器基地址 |
DT_INST(inst, compat) |
获取兼容字符串对应的第inst个实例 |
举个例子,要获取上面那个UART的基地址:
#include <zephyr/devicetree.h>
void *base = (void *)DT_REG_ADDR(DT_NODELABEL(uart0));
是不是很简单?但要注意,这些宏是在编译时计算的,不会产生运行时开销。
五、实战技巧与避坑指南
最后分享几个我这些年积累的经验:
- 善用
status属性:把status设为"disabled"可以临时关闭某个外设,调试时特别有用。我经常把所有外设都禁用,然后逐个打开,看哪个出问题。 - 别滥用覆盖文件:覆盖文件虽好,但太多层覆盖会让设备树变得难以理解。我一般控制在两层以内:一层是板级覆盖,一层是应用级覆盖。
- 编译时检查:Zephyr编译时会生成
build/zephyr/include/generated/devicetree_unfixed.h文件,里面是所有设备树宏展开后的结果。如果驱动行为异常,先去看看这个文件,确认宏的值对不对。 - 命名规范:节点标签用有意义的名称,比如
uart0、i2c1、spi2。别用dev1、dev2这种,三个月后你自己都看不懂。
总结一下:设备树是Zephyr的「硬件配置语言」。掌握DTS语法、理解绑定机制、会用覆盖文件,你就能灵活配置任何外设。下一讲我们会用SPI驱动实战来演示今天的内容,到时候你会看到设备树和驱动代码是如何配合工作的。
好,今天就到这里。有问题随时问我,咱们下节课见。