第2章 设备树基础:从DTS语法到实战编译

设备树(Device Tree)这东西,刚接触时觉得挺玄乎。说白了,它就是一套描述硬件信息的“说明书”。Linux内核通过它才知道你板子上接了哪些外设、内存多大、中断怎么连。我在Tina Linux项目里调过不下二十块板子,每次第一件事就是翻设备树——硬件怎么接的,全写在这里了。

核心认知:设备树 = 硬件拓扑的文本化描述。内核不直接读原理图,它只认.dts文件。

2.1 DTS语法入门——先看懂再说

DTS(Device Tree Source)的语法其实很简单。它用树形结构描述硬件,根节点是“/”,下面挂各种外设节点。每个节点可以包含属性和子节点。

来看一个最简模板:

/dts-v1/;

/ {
    compatible = "allwinner,sun8i";
    #address-cells = <1>;
    #size-cells = <1>;

    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x0>;
    };

    memory@40000000 {
        device_type = "memory";
        reg = <0x40000000 0x20000000>;
    };
};

注意几个要点:

  • /dts-v1/; 是版本声明,必须写在第一行
  • 每个节点用 节点名@地址 格式,地址部分可以省略
  • 属性值用 = 赋值,字符串加引号,数值用尖括号
  • 分号结尾,花括号表示层级——跟C语言很像

我的习惯:写设备树时,我会先把根节点下的 #address-cells 和 #size-cells 写好。这两个决定了reg属性的地址宽度,忘写了后面编译会报一堆错。

2.2 节点与属性——设备树的“零件”

每个设备树节点代表一个硬件设备或总线。节点里最重要的就是 compatible 属性——内核靠它匹配驱动。

属性名 作用 示例
compatible 驱动匹配字符串 "allwinner,i2c0"
reg 寄存器地址/长度 <0x01c2b000 0x400>
interrupts 中断号/触发方式 <0 5 4>
status 设备使能状态 "okay" 或 "disabled"
clocks 时钟引用 <&ccu CLK_I2C0>

我遇到过最坑的事:某次I2C驱动死活不工作,查了两天才发现 status = "disabled" 没改成 "okay"。嗯,这种低级错误,犯过一次就记住了。

2.3 reg属性详解——地址怎么描述

reg 属性描述设备的地址空间。格式是 <地址1 长度1 地址2 长度2 ...>。每个地址和长度占几个cell,由父节点的 #address-cells 和 #size-cells 决定。

// 父节点定义地址宽度
soc@01c00000 {
    compatible = "simple-bus";
    #address-cells = <1>;   // 地址占1个cell(32位)
    #size-cells = <1>;      // 长度占1个cell(32位)

    uart0@01c28000 {
        compatible = "allwinner,sun8i-uart";
        reg = <0x01c28000 0x400>;  // 基址0x01c28000,长度0x400
        interrupts = <0 1 4>;
    };
};

如果 #address-cells = <2>,那地址就要写两个数,比如 <0x0 0x40000000 0x0 0x20000000>。64位地址空间常见这种写法。

避坑指南:我曾经在移植全志V3s的SDK时,把 #address-cells 写成了2,但reg里只给了一个地址值。编译不报错,但内核解析出来的地址完全不对,外设根本访问不到。记住:address-cells和reg里的地址数量必须匹配。

2.4 interrupts属性详解——中断怎么配

中断属性稍微复杂点。它通常包含三个值:中断类型、中断号、触发方式。具体含义依赖 interrupt-parent 指定的中断控制器。

// 中断控制器定义
gic: interrupt-controller@01c81000 {
    compatible = "arm,cortex-a7-gic";
    #interrupt-cells = <3>;   // 每个中断占3个cell
    interrupt-controller;
    reg = <0x01c81000 0x1000>;
};

// 外设引用中断
uart0@01c28000 {
    compatible = "allwinner,sun8i-uart";
    reg = <0x01c28000 0x400>;
    interrupt-parent = <&gic>;   // 指定中断控制器
    interrupts = <0 1 4>;         // 类型0(SPI) 中断号1 触发方式4(高电平)
};

三个cell的含义(以GIC为例):

  • 第一个cell:中断类型。0=SPI(共享外设中断),1=PPI(私有外设中断)
  • 第二个cell:中断号。SPI从32开始编号,PPI从16开始
  • 第三个cell:触发方式。1=上升沿,2=下降沿,4=高电平,8=低电平

你想想看,如果触发方式配错了会怎样?中断要么不触发,要么疯狂触发。我在调试GPIO按键中断时就遇到过——配成了电平触发,结果按键一直按着,中断一直进,系统直接卡死。

2.5 设备树编译与反编译——实战工具链

.dts文件不能直接给内核用,得编译成二进制.dtb。反过来,拿到别人的.dtb也可以反编译成.dts看看人家怎么配的。

常用命令就三个:

# 编译:dts → dtb
dtc -I dts -O dtb -o sun8i-v3s.dtb sun8i-v3s.dts

# 反编译:dtb → dts
dtc -I dtb -O dts -o sun8i-v3s.dts sun8i-v3s.dtb

# 带调试信息的编译(推荐)
dtc -I dts -O dtb -o output.dtb -@ input.dts

-@ 参数会保留符号信息,方便后续动态加载overlay。我在调试Tina Linux的LCD驱动时,经常用反编译看别人板子的时序参数——比自己对着手册算快多了。

实用技巧:编译报错时,先检查括号是否匹配、分号是否漏了。DTC的报错信息有时候不太友好,但90%的错误都是语法问题。我习惯用 dtc -O dts -o /dev/null input.dts 只做语法检查,不生成文件。

2.6 知识结构总览

下面这张图把设备树的核心流程串起来了。从.dts源文件开始,经过编译生成.dtb,内核启动时解析dtb,最终驱动通过 compatible 匹配到设备节点。

设备树知识结构图 设备树知识结构图 .dts 源文件 语法要素 核心属性 节点名@地址 { } 属性 = 值; 标签: 节点@地址 compatible = "厂商,设备" reg = <地址 长度> interrupts = <类型 号 触发> DTC 编译 .dtb 二进制 内核解析匹配 关键路径:.dts → DTC编译 → .dtb → 内核Bootloader加载 → 驱动匹配 反编译:.dtb → DTC -I dtb -O dts → .dts(调试利器)

2.7 实战要点总结

最后列几个我反复踩过的坑,你写设备树时多留个心眼:

  • 地址对齐:reg里的基地址必须是4字节对齐,有些外设还要求页对齐(4KB)。不对齐的话,内核ioremap会失败
  • 中断号偏移:全志平台的中断号在设备树里通常要减32(因为SPI从32开始编号)。我见过新手直接抄数据手册的原始号,结果中断永远不触发
  • 引用标签:用 &标签 引用节点时,确保标签在dtsi头文件里定义过。编译时不会报错,但链接阶段会生成空节点
  • 头文件包含:.dts文件可以 #include .dtsi文件。我习惯把SoC公共部分放.dtsi,板级差异放.dts,这样复用性最好

血的教训:有一次我改完设备树,只编译了.dtb没拷到boot分区,重启后还是旧配置。折腾了半天才发现是忘了更新boot分区里的dtb文件。记住:编译完一定要确认dtb被正确部署到了启动分区。