第三章 设备树绑定文档:从规范到实战
设备树绑定文档,说白了就是一份“说明书”。它告诉内核:某个外设该怎么配置、哪些属性是必须的、哪些是可选的。我刚开始接触设备树时,总觉得写绑定文档是件麻烦事——反正代码能跑就行呗。直到有一次,我接手一个同事离职留下的项目,设备树里一堆自定义属性,没有任何文档说明……嗯,从那以后,我再也不敢轻视绑定文档了。
3.1 为什么需要绑定文档?
设备树本身只是一棵描述硬件的数据树。没有绑定文档,内核驱动就不知道如何解析这棵树。绑定文档起到了三个关键作用:
- 契约:驱动开发者与设备树编写者之间的约定
- 规范:定义属性名、类型、值的合法范围
- 文档:让后来者(包括三个月后的你自己)能看懂配置意图
核心原则:绑定文档不是写给编译器看的,是写给人看的。好的绑定文档,能让一个不熟悉该外设的工程师在10分钟内完成配置。
3.2 绑定文档的格式规范
Linux内核社区对设备树绑定文档有明确的格式要求。我个人习惯遵循以下结构:
| 章节 | 内容 | 是否必须 |
|---|---|---|
| 标题 | 设备名称与简述 | 是 |
| 描述 | 硬件功能概述、适用场景 | 推荐 |
| 属性 | 必选属性、可选属性列表 | 是 |
| 示例 | 完整的设备树节点示例 | 是 |
| 注意事项 | 兼容性、中断号、电源域等 | 推荐 |
绑定文档通常以YAML格式编写,存放在内核源码的 Documentation/devicetree/bindings/ 目录下。YAML格式的好处是:机器可解析、人类可阅读、易于校验。
小技巧:写绑定文档时,先用 make dt_binding_check 验证格式,再用 make dtbs_check 验证实际设备树。这两个命令能帮你省下不少调试时间。
3.3 常用外设绑定示例
接下来,我们逐个看几个最常用的外设绑定。这些都是我在项目中反复用到的,你迟早也会遇到。
3.3.1 GPIO 绑定
GPIO是最基础的外设。绑定文档需要描述:哪个控制器、哪个引脚、输入还是输出、是否需要上拉。
# gpio-led.yaml
description: 一个简单的GPIO LED控制器
properties:
compatible:
const: gpio-leds
gpios:
description: GPIO引脚描述,格式为<phandle gpio-specifier>
maxItems: 1
label:
description: LED的名称,用于用户空间识别
$ref: /schemas/types.yaml#/definitions/string
required:
- compatible
- gpios
examples:
- |
leds {
compatible = "gpio-leds";
led-0 {
gpios = &gpio1 5 GPIO_ACTIVE_HIGH;
label = "power-led";
};
};
我在项目中遇到过一个问题:某个GPIO LED在系统休眠后无法唤醒。查了半天,发现是绑定文档里漏写了 wakeup-source 属性。嗯,这种坑踩过一次就记住了。
3.3.2 I2C 绑定
I2C设备绑定需要特别注意地址和时钟频率。你想想看,I2C总线上挂多个设备时,地址冲突是常见问题。
# i2c-sensor.yaml
description: I2C温湿度传感器
properties:
compatible:
items:
- enum:
- ti,hdc1080
- sensirion,sht30
reg:
description: I2C从设备地址
maxItems: 1
measurement-interval:
description: 测量间隔,单位毫秒
$ref: /schemas/types.yaml#/definitions/uint32
default: 2000
required:
- compatible
- reg
examples:
- |
i2c {
#address-cells = <1>;
#size-cells = <0>;
temp-sensor@40 {
compatible = "ti,hdc1080";
reg = <0x40>;
measurement-interval = <5000>;
};
};
注意:I2C地址必须是7位或10位格式,且不能与总线上的其他设备冲突。我曾经见过一个案例,两个设备用了同一个地址,结果总线直接锁死——连SCL都被拉低了。
3.3.3 SPI 绑定
SPI设备的绑定比I2C复杂一些,因为涉及时钟极性(CPOL)和时钟相位(CPHA)。这两个参数配错了,数据根本读不对。
# spi-flash.yaml
description: SPI NOR Flash存储器
properties:
compatible:
items:
- pattern: "^jedec,spi-nor$"
reg:
description: SPI片选编号
maxItems: 1
spi-max-frequency:
description: SPI时钟最大频率,单位Hz
$ref: /schemas/types.yaml#/definitions/uint32
spi-cpol:
description: 时钟极性,1表示空闲时为高电平
type: boolean
spi-cpha:
description: 时钟相位,1表示第二个边沿采样
type: boolean
required:
- compatible
- reg
- spi-max-frequency
examples:
|
&spi1 {
status = "okay";
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <50000000>;
spi-cpol;
spi-cpha;
};
};
我记得有一次调试SPI Flash,读出来的数据全是0xFF。折腾了两天,最后发现是SPI的MISO和MOSI接反了。硬件工程师和软件工程师互相甩锅的场景,至今记忆犹新。
3.3.4 UART 绑定
UART绑定相对简单,但流控和波特率是容易出问题的地方。
# uart-bluetooth.yaml
description: UART蓝牙模块
properties:
compatible:
items:
- const: ti,wl1837-st
device-wakeup-gpios:
description: 设备唤醒GPIO
maxItems: 1
enable-gpios:
description: 使能GPIO
maxItems: 1
max-speed:
description: 最大波特率
$ref: /schemas/types.yaml#/definitions/uint32
default: 115200
required:
- compatible
- device-wakeup-gpios
examples:
|
&uart3 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart3_pins>;
bluetooth {
compatible = "ti,wl1837-st";
device-wakeup-gpios = <&gpio2 7 GPIO_ACTIVE_HIGH>;
enable-gpios = <&gpio2 8 GPIO_ACTIVE_HIGH>;
max-speed = <3000000>;
};
};
避坑指南:我曾经在UART绑定里忘了配pinctrl,结果蓝牙模块的TX/RX引脚被复用成了GPIO。系统启动后蓝牙能识别,但一收发数据就崩溃。所以,pinctrl一定要写,而且要跟原理图对清楚。
3.4 绑定文档的整体知识体系
下面这张图,是我自己整理的一个绑定文档知识框架。你可以把它当作写绑定文档时的检查清单。
3.5 写绑定文档的几个习惯
最后,分享几个我个人的习惯,希望能帮你少走弯路:
- 先写文档,再写代码:绑定文档写清楚了,驱动实现就清晰了。反过来,驱动写完了再补文档,很容易遗漏细节。
- 属性名要见名知意:别用
foo,bar这种名字。我见过一个项目,属性名叫vendor,magic,代码里注释都没写,后来谁也看不懂。 - 示例要完整可运行:示例不是摆设,是要能直接复制到设备树里用的。包含必要的父节点、地址、状态等。
- 用工具自动校验:
dt-schema工具可以自动检查绑定文档和设备树的一致性。我每次提交代码前都会跑一遍。
血的教训:我曾经在一个量产项目里,因为绑定文档里写错了GPIO的active电平——文档写的是 GPIO_ACTIVE_HIGH,实际硬件是低电平有效。结果产线上一批设备都点不亮LED。从那以后,我每次写完绑定文档,都会拿着原理图逐行核对。
绑定文档看似是“文档工作”,实际上是嵌入式开发中承上启下的关键环节。它连接了硬件设计、设备树配置和内核驱动。写好了,整个项目都顺畅;写不好,调试时处处是坑。