4. 设备树(Devicetree)基础:语法、节点、compatible 与 reg
设备树,说白了就是一块「硬件描述板」。它用树形结构告诉 Zephyr 内核:你有哪些外设、它们挂在哪条总线上、地址是多少、用什么驱动来管。我刚开始接触 Zephyr 时,总觉得设备树很玄乎,后来发现它其实就是一套标准化的「硬件说明书」。
嗯,咱们今天就把这块硬骨头啃下来。你只要掌握四个核心点:语法怎么写、节点怎么组织、compatible 怎么匹配驱动、reg 怎么描述地址。后面写传感器驱动,你天天都要跟它打交道。
4.1 设备树语法速览
设备树文件通常以 .dts 或 .dtsi 结尾。.dtsi 是公共头文件,.dts 是具体板级文件。语法其实很简单,我总结成三句话:
- 节点用花括号:每个设备就是一个节点,用
{ }包裹。 - 属性用键值对:
属性名 = 值;或属性名;(布尔型)。 - 引用用 & 符号:通过
&节点标签来追加或修改已有节点。
核心原则:设备树只描述硬件「有什么、在哪、怎么配」,不描述驱动怎么跑。驱动怎么跑,那是 compatible 匹配之后的事。
来看一个最简单的节点示例:
/dts-v1/;
/ {
model = "My Sensor Board";
compatible = "mycompany,myboard";
soc {
i2c0: i2c@40003000 {
compatible = "mycompany,i2c";
reg = <0x40003000 0x1000>;
status = "okay";
clock-frequency = <100000>;
};
};
};
这里 i2c0 是节点名,i2c@40003000 是完整名称(@后面是地址)。compatible 告诉系统用哪个驱动,reg 告诉系统地址范围。后面咱们一个一个拆开讲。
4.2 节点定义与层级结构
设备树的节点,就像文件系统的目录。根节点 / 下面挂 soc、chosen、aliases 等子节点。每个子节点又可以挂自己的子节点。我习惯把这种结构想象成一棵倒挂的树——根在上,叶子在下。
节点定义的基本格式:
[标签:]节点名[@单元地址] {
[属性定义];
[子节点];
};
- 标签(label):可选,用于在别处引用。比如
i2c0:,后面可以用&i2c0来引用它。 - 节点名:必填,一般用设备类型,如
i2c、spi、gpio。 - 单元地址:可选,当同类型设备有多个时,用地址区分。比如
i2c@40003000和i2c@40004000。
我的习惯:写传感器驱动时,我通常会在 soc 节点下找到对应的 I2C 或 SPI 总线节点,然后在里面追加传感器子节点。这样层级清晰,也方便后续维护。
举个例子,假设你的板子上有一颗温度传感器挂在 I2C0 总线上,地址是 0x48。你会在 i2c0 节点下追加一个子节点:
&i2c0 {
status = "okay";
temp_sensor: temperature@48 {
compatible = "mycompany,tmp117";
reg = <0x48>;
label = "TEMP_SENSOR";
};
};
这里 temperature@48 就是传感器节点,reg = <0x48> 表示它的 I2C 从设备地址是 0x48。嗯,这个写法你后面写驱动时会反复用到。
4.3 compatible 属性:驱动匹配的关键
compatible 是设备树里最重要的属性,没有之一。它就像一把钥匙,用来匹配设备树节点和驱动代码。Zephyr 的驱动框架会遍历所有设备树节点,找到 compatible 字符串与驱动注册时声明一致的节点,然后调用驱动的初始化函数。
compatible 的命名规范:
| 格式 | 示例 | 说明 |
|---|---|---|
厂商,设备型号 |
ti,tmp117 |
标准格式,厂商前缀 + 设备型号 |
厂商,设备型号-版本 |
bosch,bme280-v1 |
带版本号,用于区分不同硬件版本 |
通用类型 |
gpio-leds |
通用设备类型,不指定厂商 |
我曾经踩过的坑:compatible 字符串必须与驱动中 DT_DRV_COMPAT 宏定义的值完全一致,大小写、连字符、下划线都不能错。有一次我写驱动时把 tmp117 写成了 tmp117(少了个字母),结果设备树节点死活匹配不上驱动,查了半天才发现是拼写错误。
在 Zephyr 驱动中,匹配过程是这样的:
- 设备树编译时,每个节点生成一个
DEVICE_DT_DEFINE宏。 - 驱动注册时,通过
DT_DRV_COMPAT声明自己支持的 compatible。 - 系统启动时,遍历所有设备树节点,找到 compatible 匹配的节点,调用驱动的初始化函数。
说白了,compatible 就是设备树和驱动之间的「接头暗号」。暗号对上了,驱动就接管这个设备。
4.4 reg 属性:地址与大小描述
reg 属性描述设备的地址空间。它的格式是:
reg = <地址1 长度1>, <地址2 长度2>, ...;
每个 <地址 长度> 对称为一个「reg 单元」。对于不同的总线,reg 的含义不同:
| 总线类型 | reg 含义 | 示例 |
|---|---|---|
| 内存映射外设 | 基地址 + 寄存器区域大小 | reg = <0x40003000 0x1000> |
| I2C 设备 | 从设备地址(长度通常为 1) | reg = <0x48> |
| SPI 设备 | 片选编号(长度通常为 1) | reg = <0> |
你想想看,为什么 I2C 设备的 reg 只有一个地址值?因为 I2C 总线上每个设备只有一个 7 位或 10 位从地址,不需要长度信息。而内存映射外设需要知道寄存器区域的起始地址和大小,所以需要两个值。
关键点:reg 属性的解析方式由 #address-cells 和 #size-cells 决定。前者表示地址占几个 32 位整数,后者表示长度占几个 32 位整数。默认情况下,#address-cells = <1>,#size-cells = <1>。
举个例子,如果你的传感器是 SPI 设备,片选为 0,reg 就写成:
spi_sensor: sensor@0 {
compatible = "mycompany,spi-sensor";
reg = <0>; /* 片选 0 */
spi-max-frequency = <1000000>;
};
如果是内存映射的 GPIO 控制器,reg 就写成:
gpio0: gpio@50000000 {
compatible = "mycompany,gpio";
reg = <0x50000000 0x1000>; /* 基地址 0x50000000,大小 4KB */
};
4.5 知识结构总览
下面这张图把设备树的核心概念串起来了。你可以把它当作一张「思维导图」,后面写驱动时随时回来对照。
4.6 实战:写一个传感器设备树节点
咱们把前面学的知识串起来,写一个完整的传感器设备树节点。假设你有一颗 BME280 温湿度气压传感器,挂在 I2C1 总线上,地址是 0x76。
第一步,找到板级设备树文件(比如 boards/arm/myboard/myboard.dts),在 I2C1 节点下追加:
&i2c1 {
status = "okay";
clock-frequency = <400000>; /* 400kHz 快速模式 */
bme280: bme280@76 {
compatible = "bosch,bme280";
reg = <0x76>;
label = "BME280";
};
};
第二步,在驱动代码中,通过 DT_DRV_COMPAT 声明匹配:
#define DT_DRV_COMPAT bosch_bme280
/* 驱动初始化函数 */
static int bme280_init(const struct device *dev)
{
/* 获取设备树配置 */
uint16_t addr = DT_INST_REG_ADDR(0);
/* ... 初始化代码 ... */
return 0;
}
DEVICE_DT_DEFINE(DT_NODELABEL(bme280),
bme280_init,
NULL,
NULL,
NULL,
POST_KERNEL,
CONFIG_SENSOR_INIT_PRIORITY,
&bme280_driver_api);
小技巧:Zephyr 提供了大量 DT_* 宏来读取设备树属性。比如 DT_REG_ADDR(node_id) 读取 reg 地址,DT_PROP(node_id, label) 读取 label 属性。写驱动时多用这些宏,少写硬编码。
嗯,到这里你应该对设备树有了一个整体的认识。说白了,它就是硬件和驱动之间的「翻译官」。你只要把设备树节点写对,compatible 和 reg 填准确,Zephyr 就能自动帮你把驱动挂载上去。后面写传感器驱动时,你会反复用到这些知识。