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 编写设备树节点的步骤

我个人习惯分三步走:

  1. 确定 compatible 字符串——驱动里用哪个,节点里就写哪个。
  2. 查阅 binding 文档——看哪些属性是 required,哪些是 optional。
  3. 填写属性值——地址、中断、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 属性有:

  • compatible
  • reg(I2C 地址)

optional 属性可能包括 interrupt-parentinterruptsvdd-supply 等。于是节点写成:

&i2c1 {
    status = "okay";

    temperature-sensor@48 {
        compatible = "ti,tmp117";
        reg = <0x48>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
        vdd-supply = <&reg_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 设备树绑定的核心逻辑

说了这么多,其实核心就一句话:绑定文件定义了「驱动需要什么」,设备树节点提供了「硬件有什么」。两者对上号,驱动就能正常工作。

我画了一张图,帮你理清这个关系:

设备树节点 temperature-sensor@48 compatible = "ti,tmp117" reg = <0x48> interrupts = <5 ...> vdd-supply = <&reg_3v3> 绑定文件 gpio-keys.yaml required: - compatible - reg - interrupts optional: - vdd-supply 驱动 probe() of_match i2c_add devm_* 验证 匹配 设备树绑定:节点 → 绑定文件 → 驱动 绑定文件定义规则,节点提供数据,驱动消费数据

从这张图你能看到:设备树节点提供硬件信息,绑定文件定义哪些信息是必须的,驱动则根据这些信息完成初始化。三者缺一不可。

总结一下:写设备树节点,先查 binding 文档,再填属性,最后用 dtbs_check 验证。别凭感觉写,别跳过验证。我见过太多「我觉得没问题」然后翻车的案例了。

我的习惯:每次写完节点,都会在板子上用 ls /sys/firmware/devicetree/base/ 确认节点是否被正确解析。如果节点没出现,说明 dts 编译或加载有问题。如果节点出现了但驱动没 probe,那就是 compatible 或属性对不上。

好了,设备树绑定就讲到这里。记住:绑定是契约,节点是数据,驱动是执行者。把这三者的关系理清楚,你写设备树就不会再迷茫了。


公众号:蓝海资料掘金营,微信deep3321