2、设备树基础:DTS语法、节点与属性、reg与interrupts属性详解

设备树(Device Tree)是什么?说白了,它就是一块「硬件描述板」。Linux 内核通过它来知道板子上接了哪些外设、地址是多少、中断怎么连。没有设备树之前,内核里全是板级文件,改个 GPIO 都要重新编译内核,那叫一个痛苦。

我个人习惯把设备树比作硬件的「身份证」。内核启动时读这张身份证,就知道该给谁装驱动、该给谁分配资源。今天我们就来拆解这张身份证的写法。

核心认知:设备树是描述硬件的结构化文本,不是代码。它不执行逻辑,只提供数据。内核解析它,然后根据数据初始化驱动。

2.1 DTS 文件长什么样?

DTS(Device Tree Source)是设备树的源文件。编译后变成 DTB(Device Tree Blob),内核才能吃。我们先看一个最简单的例子:

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";

    chosen {
        stdout-path = &uart0;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  /* 256 MB */
    };

    leds {
        compatible = "gpio-leds";
        led0 {
            label = "beaglebone:green:usr0";
            gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };
};

嗯,这里要注意几个点:

  • /dts-v1/; 是版本声明,必须写在第一行。
  • / 是根节点,整棵树从这里开始。
  • 每个节点用花括号 { } 包裹,最后用分号结束。
  • 属性就是 key = value; 的形式。

我在项目中遇到过有人把分号漏了,编译报错找了半天。DTC 编译器报错信息有时候不太友好,养成检查分号的好习惯能省很多时间。

2.2 节点与属性:树的骨架

设备树的每个节点代表一个硬件设备或总线。节点可以嵌套,形成树形结构。节点名一般写成 名称@地址 的形式,比如 uart@44e09000。地址部分不是必须的,但如果有 reg 属性,建议加上,方便阅读。

属性就是节点的「特征描述」。常见的属性类型有:

属性类型 写法示例 说明
字符串 model = "TI AM335x"; 用双引号包裹
32位整数 reg = <0x80000000 0x10000000>; 尖括号内,空格分隔
64位整数 reg = <0x00000000 0x80000000 0x00000000 0x10000000>; 两个 32 位拼一个 64 位
布尔值 status = "disabled"; 用字符串模拟,常见 "okay" 和 "disabled"
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号内,十六进制
字符串列表 compatible = "ti,am335x-bone", "ti,am335x"; 逗号分隔多个字符串
引用(phandle) &gpio1 & 符号引用其他节点

你想想看,为什么 compatible 属性要写多个值?因为内核匹配驱动时,会从左到右尝试。第一个最精确,后面的作为 fallback。我调试过一块板子,驱动没加载,最后发现是 compatible 字符串写反了顺序,内核匹配到了错误的驱动。

个人经验:写 compatible 时,遵循「厂商,型号」的命名规范。比如 "ti,am335x-uart"。这样既避免冲突,也方便别人一眼看出是谁家的设备。

2.3 reg 属性:地址与大小

reg 属性描述设备的地址空间。格式是 reg = <地址1 大小1 地址2 大小2 ...>;。每个地址-大小对称为一个「region」。对于 32 位系统,一个 region 占两个 32 位整数;对于 64 位系统,占四个。

举个例子:

uart0: serial@44e09000 {
    compatible = "ti,am3352-uart", "ti,omap3-uart";
    reg = <0x44e09000 0x2000>;
    interrupts = <72>;
};

这里 reg 表示 UART 模块的寄存器基地址是 0x44e09000,地址范围是 0x2000 字节。内核驱动通过 platform_get_resource() 拿到这个地址,然后 ioremap() 映射到虚拟地址空间。

我曾经在移植驱动时,把 reg 的大小写错了。本来寄存器只占 0x1000,我写了 0x10000。结果 ioremap 映射了过大的空间,导致后续其他设备映射失败。这种 bug 很难查,因为系统不会立刻崩溃,而是等到访问其他外设时才出问题。

对于 SPI 或 I2C 设备,reg 可能表示片选号或设备地址:

&spi0 {
    status = "okay";
    spidev@0 {
        compatible = "spidev";
        reg = <0>;  /* 片选 CS0 */
        spi-max-frequency = <1000000>;
    };
};

这里 reg = <0> 表示该设备挂在 SPI 总线的片选 0 上。内核 SPI 子系统会解析这个值来配置片选信号。

2.4 interrupts 属性:中断连接

中断属性描述设备如何向 CPU 发中断。格式取决于中断控制器。最常见的是 interrupts = <中断号 触发类型>;。

触发类型常用的宏定义:

宏 值 含义
IRQ_TYPE_EDGE_RISING 1 上升沿触发
IRQ_TYPE_EDGE_FALLING 2 下降沿触发
IRQ_TYPE_EDGE_BOTH 3 双边沿触发
IRQ_TYPE_LEVEL_HIGH 4 高电平触发
IRQ_TYPE_LEVEL_LOW 8 低电平触发

看一个实际例子:

gpio0: gpio@44e07000 {
    compatible = "ti,omap4-gpio";
    reg = <0x44e07000 0x1000>;
    interrupts = <96 IRQ_TYPE_LEVEL_HIGH>;
    #gpio-cells = <2>;
    gpio-controller;
    #interrupt-cells = <2>;
    interrupt-controller;
};

这里 GPIO 控制器本身也是一个中断控制器。它用 interrupts 告诉 CPU:我自己的中断线是 96 号,高电平触发。同时它声明了 interrupt-controller 和 #interrupt-cells = <2>,表示它可以接收其他设备的中断,每个中断描述需要 2 个 cell(通常是 GPIO 引脚号和触发类型)。

那么其他设备怎么用这个 GPIO 的中断呢?

button@0 {
    compatible = "gpio-keys";
    label = "user_button";
    gpios = <&gpio0 7 GPIO_ACTIVE_LOW>;
    interrupts-extended = <&gpio0 7 IRQ_TYPE_EDGE_FALLING>;
};

这里 interrupts-extended 明确指定了中断控制器是 &gpio0,引脚是 7,下降沿触发。内核会把这个中断映射到 GPIO 控制器的中断处理链上。

避坑指南:我曾经在项目中把 interrupts 和 interrupts-extended 混用。如果一个设备有多个中断源,或者中断控制器不是父节点,必须用 interrupts-extended。否则内核解析时会出错,中断根本注册不上。

2.5 设备树的核心结构图

下面这张图展示了设备树从源文件到内核使用的完整链路,以及节点、属性、reg、interrupts 之间的关系。

设备树核心结构图 根节点 / memory@80000000 uart0: serial@44e09000 gpio0: gpio@44e07000 属性示例 device_type = "memory" reg = <0x80000000 0x10000000> 属性示例 compatible = "ti,am3352-uart" reg = <0x44e09000 0x2000> 属性示例 interrupts = <96 LEVEL_HIGH> gpio-controller; button: 中断设备 中断控制器 (GPIO) interrupts-extended DTS 源文件 DTB 二进制 DTC 编译 内核解析驱动匹配

从图中可以看到,设备树的根节点下挂载了多个子节点,每个子节点携带自己的属性。reg 描述地址资源,interrupts 描述中断连接。最终 DTS 通过 DTC 编译器生成 DTB,内核在启动时解析 DTB,匹配驱动并初始化硬件。

2.6 常用技巧与避坑

  • 引用节点:用 &节点标签 的方式引用其他节点。比如 &uart0 可以在任何地方修改 uart0 的属性。我习惯把板级特有的修改放在 .dts 文件里,SoC 通用定义放在 .dtsi 里,这样复用性高。
  • 覆盖属性:同一个属性在 .dtsi 和 .dts 中都出现时,后面的覆盖前面的。利用这个特性,可以在 .dts 中只修改需要改的部分。
  • status 属性:默认设为 "disabled",在板级文件中改为 "okay"。这样 SoC 的 .dtsi 可以通用,每个板子按需开启外设。
  • 编译检查:用 dtc -I dts -O dtb -o output.dtb input.dts 编译。加上 -@ 选项可以生成符号表,方便调试。

调试建议:内核启动时加上 earlycon 和 dtoc 参数,可以打印设备树解析过程。如果某个设备没注册,先看内核日志里有没有 "not found" 或 "failed to map" 之类的信息。我遇到过很多次,问题出在 reg 地址写错了,或者中断号对不上芯片手册。

设备树看起来语法简单,但实际项目中坑不少。多读芯片厂商提供的 .dtsi 文件,那是最好的学习材料。把 reg 和 interrupts 这两个属性吃透,设备树就算入门了。