1. 设备树基础回顾:DTS语法、节点与属性、reg与interrupts详解

各位同学好,我是老李。做嵌入式Linux驱动开发十几年了,设备树这东西,我敢说每个工程师都绕不开。尤其是做版本升级的时候,设备树改不对,内核直接罢工。今天咱们就从最基础的东西开始捋一遍。

你可能会问:都2025年了,还讲DTS语法?嗯,我理解你的想法。但说实话,我在项目里见过太多人栽在基础细节上。有一次客户升级内核,板子死活起不来,最后发现就是一个reg属性少写了一个地址单元。所以,基础不牢,地动山摇。

核心观点:设备树是描述硬件拓扑的数据结构,不是代码。它告诉内核“我有哪些硬件、它们怎么连接、怎么访问”。版本升级时,设备树是内核与板卡之间的“翻译官”,翻译错了,系统就崩。

设备树基础知识体系 设备树基础核心要素 设备树源文件 .dts DTS 语法结构 节点与属性 reg / interrupts /dts-v1/; include / 宏 节点名 / 标签 属性 = 值; reg 地址映射 interrupts 中断 版本升级时,设备树必须与内核驱动对 reg 和 interrupts 的解析方式保持一致,否则外设无法工作

1.1 DTS 文件长什么样?

先看一个最简单的例子。我习惯把设备树想象成一棵倒着长的树——根节点是 /,下面挂各种外设节点。

/dts-v1/;

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

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
            reg = <0x00>;
        };
    };

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

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

你看,结构很清晰。根节点下面有 cpus、memory、uart0。每个节点都有自己的属性。我个人写设备树时,喜欢把节点名写成 功能名@基地址 的格式,这样一眼就能看出这个外设的地址范围。

小技巧:节点名后面的 @地址 不是必须的,但强烈建议加上。我在调试时经常靠这个快速定位外设,尤其是当你有多个同类型设备时,比如 uart@44e09000 和 uart@48020000,一看就知道谁是谁。

1.2 节点与属性——设备树的“骨架”和“血肉”

节点就是设备树的骨架,属性就是血肉。每个节点描述一个硬件设备或总线,属性描述这个设备的特征。

常用的属性就那么几个,我列个表给你看:

属性名 含义 常见值示例
compatible 设备兼容性字符串,驱动匹配的关键 "ti,am3352-uart"
reg 设备地址空间描述 <0x44e09000 0x2000>
interrupts 中断号及触发方式 <72> 或 <0 72 4>
status 设备状态:okay 或 disabled "okay"
#address-cells reg 中地址字段占几个 cell(32位) <1>
#size-cells reg 中长度字段占几个 cell <1>

这里有个坑,我当年踩过。 #address-cells 和 #size-cells 这两个属性,决定了 reg 怎么解析。如果父节点写的是 #address-cells = <2>,那子节点的 reg 里每个地址就要写两个 32 位数。版本升级时,如果内核改了地址模型,而你的设备树没跟着改,那驱动拿到的地址就是错的。

警告:版本升级时,务必检查 #address-cells 和 #size-cells 是否与内核期望的一致。尤其是从 32 位 SoC 升级到 64 位 SoC 时,地址单元可能从 1 变成 2,不改的话,驱动读到的寄存器地址全是错的。

1.3 reg 属性——告诉驱动“你的硬件在哪”

reg 属性是设备树里最重要的属性之一。它描述设备的地址空间。格式很简单:

reg = <address1 length1 address2 length2 ...>;

每个 address 和 length 占几个 32 位整数,由父节点的 #address-cells 和 #size-cells 决定。

举个例子:

/* #address-cells = <1>; #size-cells = <1>; */
reg = <0x44e09000 0x2000>;
/* 基地址 0x44e09000,长度 0x2000(8KB) */

如果是 64 位地址:

/* #address-cells = <2>; #size-cells = <2>; */
reg = <0x00 0x44e09000 0x00 0x2000>;
/* 基地址 0x0000000044e09000,长度 0x0000000000002000 */

我在项目中遇到过一件事:某次内核从 4.19 升级到 5.10,平台总线驱动对 reg 的解析方式变了,原来只取第一个地址,新内核要求把所有地址段都注册。结果设备树没改,驱动只映射了第一段内存,第二段没映射,外设 DMA 直接写飞了。嗯,查了两天才找到原因。

1.4 interrupts 属性——告诉驱动“中断来了怎么通知你”

interrupts 属性描述设备的中断信息。它的格式取决于中断控制器。最常见的是 ARM GIC(通用中断控制器)的格式:

interrupts = <中断类型 中断号 触发方式>;

其中:

  • 中断类型:0 表示 SPI(共享外设中断),1 表示 PPI(私有外设中断)
  • 中断号:从 32 开始算(GIC 会把前 32 个留给内部使用)
  • 触发方式:1 表示上升沿触发,4 表示高电平触发

看个实际例子:

uart0: serial@44e09000 {
    compatible = "ti,am3352-uart";
    reg = <0x44e09000 0x2000>;
    interrupts = <72>;  /* 在 AM335x 里,UART0 中断号是 72 */
    interrupt-parent = <&intc>;  /* 指定中断控制器 */
};

这里 interrupt-parent 指向中断控制器节点。如果没写,就继承父节点的中断控制器。我建议你每次都显式写出来,这样代码更清晰,也避免继承链出错。

重点:版本升级时,中断号可能会变。比如某 SoC 的 V2 版本把 UART 中断从 72 改成了 76。如果你只升级内核不更新设备树,驱动注册的中断号是错的,外设永远收不到中断。我曾经在调试一个网卡驱动时遇到过类似问题,中断一直不触发,最后发现是设备树里的中断号跟新内核的 GIC 映射对不上。

1.5 版本升级时最容易犯的错

根据我的经验,版本升级中设备树适配的坑主要集中在以下几点:

  1. reg 地址单元不匹配:32位升64位时,#address-cells 没从1改成2
  2. 中断号偏移变化:新内核可能调整了中断映射表
  3. compatible 字符串变更:驱动改名了,设备树没跟着改
  4. 属性名被废弃:比如 gpios 改成了 cs-gpios
  5. status 属性被忽略:新内核要求显式写 status = "okay"

我建议你每次升级内核时,都做一次设备树的“差异分析”。把旧设备树和新内核的绑定文档(Documentation/devicetree/bindings/)逐条对比。别嫌麻烦,这个步骤能省你后面几天的调试时间。

我的习惯:在版本升级前,先用 dtc -I dtb -O dts 把旧 DTB 反编译成 DTS,然后和新内核的 DTS 做 diff。这样能快速发现哪些节点被改了、哪些属性被删了。这个习惯帮我避免了好几次线上事故。