4、设备树绑定:如何为特定外设编写设备树节点
设备树绑定,说白了就是给内核写一份「外设说明书」。
内核拿到设备树后,得知道某个节点对应什么硬件、怎么初始化、用哪个驱动。这份「说明书」就是 binding 文档。我刚开始写设备树时,总觉得随便写几个属性就能跑起来。结果呢?驱动加载失败,日志里一堆「not matched」。嗯,后来才明白——绑定没写对,内核根本不认你。
4.1 绑定文件长什么样?
每个外设驱动,都会在 Documentation/devicetree/bindings/ 下有一个对应的 YAML 文件。比如一个简单的 GPIO 按键:
# Documentation/devicetree/bindings/input/gpio-keys.yaml
$id: http://devicetree.org/schemas/input/gpio-keys.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#
title: GPIO Key Device Tree Bindings
maintainers:
- Your Name <your@email.com>
properties:
compatible:
const: gpio-keys
gpios:
description: GPIO specifier for the key
maxItems: 1
linux,code:
description: Linux input event code
$ref: /schemas/types.yaml#/definitions/uint32
required:
- compatible
- gpios
- linux,code
additionalProperties: false
这个文件告诉内核:一个合法的 gpio-keys 节点,必须包含 compatible、gpios 和 linux,code 三个属性。少了任何一个,dtbs_check 就会报错。
核心原则:绑定文件就是驱动与设备树之间的「契约」。驱动通过 compatible 字符串找到节点,再通过属性名读取配置。
4.2 编写设备树节点的步骤
我个人习惯分三步走:
- 确定 compatible 字符串——驱动里用哪个,节点里就写哪个。
- 查阅 binding 文档——看哪些属性是 required,哪些是 optional。
- 填写属性值——地址、中断、GPIO、时钟等,一个都不能少。
举个例子。假设我们要给一个 I2C 温度传感器写节点,型号是 TMP117。先看驱动里的 compatible:
static const struct i2c_device_id tmp117_id[] = {
{ "tmp117", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, tmp117_id);
static const struct of_device_id tmp117_of_match[] = {
{ .compatible = "ti,tmp117" },
{ }
};
MODULE_DEVICE_TABLE(of, tmp117_of_match);
嗯,compatible 是 "ti,tmp117"。然后查 binding 文档,发现 required 属性有:
compatiblereg(I2C 地址)
optional 属性可能包括 interrupt-parent、interrupts、vdd-supply 等。于是节点写成:
&i2c1 {
status = "okay";
temperature-sensor@48 {
compatible = "ti,tmp117";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
vdd-supply = <®_3v3>;
};
};
小技巧:I2C 地址通常用 7 位,写在 reg 里。注意不要和总线上其他设备冲突。我遇到过两次地址冲突,排查了半天才发现是同一个地址被两个设备用了。
4.3 常见属性详解
写设备树节点,其实就是填属性。我把最常见的几类属性列出来:
| 属性类别 | 示例 | 说明 |
|---|---|---|
| compatible | "ti,tmp117" |
驱动匹配的关键,格式通常是「厂商,型号」 |
| reg | <0x48> |
地址,I2C 是 7 位地址,SPI 是片选号,内存映射外设是基地址 |
| interrupts | <5 IRQ_TYPE_EDGE_FALLING> |
中断号 + 触发类型 |
| clocks | <&clk_uart0> |
时钟句柄,驱动通过 clk_get 获取 |
| gpios | <&gpio0 10 GPIO_ACTIVE_HIGH> |
GPIO 句柄 + 引脚号 + 有效电平 |
| status | "okay" 或 "disabled" |
控制节点是否启用 |
你想想看,这些属性其实就对应了驱动里调用的那些 API。比如 of_get_property() 读 compatible,of_iomap() 读 reg,irq_of_parse_and_map() 读 interrupts。一一对应,非常直接。
4.4 避坑指南:我踩过的几个坑
写设备树节点,看着简单,但细节决定成败。我分享几个真实案例:
坑一:compatible 字符串大小写不一致
我曾经把 "ti,tmp117" 写成了 "TI,TMP117"。内核匹配是大小写敏感的,驱动直接不 probe。排查了一下午,最后用 dtc -I dtb -O dts 反编译才发现。嗯,从那以后我每次都复制粘贴驱动里的字符串。
坑二:reg 属性格式错误
I2C 设备的 reg 是单个 32 位值,但有些新手写成 reg = <0x48 0x10>,多了一个地址长度。内核解析时会认为设备地址是 0x48,长度是 0x10,导致驱动读取寄存器时偏移全乱掉。这个坑我帮同事排查过两次。
坑三:忘记加 status = "okay"
很多 SoC 的 dtsi 文件里,外设节点默认 status = "disabled"。如果你在板级 dts 里只写了子节点,没写 status = "okay",整个外设都不会使能。我刚开始做项目时,I2C 控制器怎么都调不通,最后发现是父节点没使能。
4.5 用 dtbs_check 验证绑定
写完节点别急着编译烧录。先跑一遍 dtbs_check:
make dtbs_check DT_SCHEMA_FILES=ti,tmp117.yaml
这个命令会用你写的 binding 文件去检查所有设备树,看有没有节点违反了绑定规则。比如你漏了 required 属性,它会直接报错:
arch/arm/boot/dts/myboard.dtb: temperature-sensor@48: 'reg' is a required property
我个人习惯在提交代码前,一定跑一遍 dtbs_check。它能帮你发现 90% 的低级错误。
4.6 设备树绑定的核心逻辑
说了这么多,其实核心就一句话:绑定文件定义了「驱动需要什么」,设备树节点提供了「硬件有什么」。两者对上号,驱动就能正常工作。
我画了一张图,帮你理清这个关系:
从这张图你能看到:设备树节点提供硬件信息,绑定文件定义哪些信息是必须的,驱动则根据这些信息完成初始化。三者缺一不可。
总结一下:写设备树节点,先查 binding 文档,再填属性,最后用 dtbs_check 验证。别凭感觉写,别跳过验证。我见过太多「我觉得没问题」然后翻车的案例了。
我的习惯:每次写完节点,都会在板子上用 ls /sys/firmware/devicetree/base/ 确认节点是否被正确解析。如果节点没出现,说明 dts 编译或加载有问题。如果节点出现了但驱动没 probe,那就是 compatible 或属性对不上。
好了,设备树绑定就讲到这里。记住:绑定是契约,节点是数据,驱动是执行者。把这三者的关系理清楚,你写设备树就不会再迷茫了。
公众号:蓝海资料掘金营,微信deep3321