4. 设备树基础:DTS语法入门、节点与属性
设备树(Device Tree)这东西,说白了就是一块「硬件描述板」。你告诉内核:我板子上接了哪些外设,地址是多少,中断挂在哪条线上。内核不再靠一堆硬编码的板级文件去猜,而是直接读设备树。
我在Tina Linux上最早碰设备树时,也觉得不就是个文本文件嘛。后来踩了几个坑才明白——设备树写错了,驱动根本 probe 不起来,连 log 都不给你留一句。嗯,咱们今天就把这块啃透。
4.1 设备树长什么样?——DTS 语法入门
设备树源文件(.dts)的语法非常直观。它用树形结构描述硬件,根节点是 /,下面挂 CPU、内存、总线、外设。每个节点用 node-name@unit-address 命名,花括号里放属性和子节点。
/dts-v1/;
/ {
model = "Allwinner H3 Tina Board";
compatible = "allwinner,sun8i-h3";
chosen {
stdout-path = &uart0;
};
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x20000000>;
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@1c28000 {
compatible = "allwinner,sun8i-h3-uart";
reg = <0x01c28000 0x400>;
interrupts = <0 0 4>;
status = "disabled";
};
};
};
你看,根节点下直接挂 chosen、memory、soc。每个节点用 标签:节点名@地址 的格式,标签(如 uart0)方便其他地方引用。
4.2 节点与属性——设备树的「零件」
节点就是硬件模块,属性就是模块的参数。常用的属性有这些:
| 属性名 | 作用 | 示例 |
|---|---|---|
compatible |
驱动匹配的关键字,内核靠它找驱动 | "allwinner,sun8i-h3-uart" |
reg |
寄存器地址和长度 | <0x01c28000 0x400> |
interrupts |
中断号、触发类型 | <0 0 4> |
status |
设备状态:okay 或 disabled |
"okay" |
clocks |
时钟源引用 | <&ccu CLK_UART0> |
pinctrl-0 |
引脚复用配置 | <&uart0_pins> |
这里有个关键点:compatible 的命名规则是 "厂商,芯片-外设"。内核的 of_match_table 就是拿这个字符串去匹配的。我见过有人把 compatible 写反了顺序,驱动死活不干活——查了两天才发现是字符串没对上。
4.3 reg 属性——地址怎么描述?
reg 描述设备的地址空间。格式是 reg = <地址1 长度1 地址2 长度2 ...>。地址和长度各占几个 cell(32位),由父节点的 #address-cells 和 #size-cells 决定。
// 父节点定义
soc {
#address-cells = <1>; // 地址用1个32位
#size-cells = <1>; // 长度用1个32位
uart0: serial@1c28000 {
reg = <0x01c28000 0x400>; // 基址0x01c28000,长度0x400
};
emac: ethernet@1c30000 {
reg = <0x01c30000 0x1000>;
};
};
4.4 interrupts 属性——中断怎么配?
中断描述比 reg 稍微复杂一点,因为它依赖中断控制器。常见格式:
interrupts = <中断类型 中断号 触发方式>;
在全志平台上,GIC(通用中断控制器)通常用 3 个 cell:
- 第一个 cell:0 表示 SPI(共享外设中断),1 表示 PPI(私有外设中断)
- 第二个 cell:中断号(从 32 开始偏移)
- 第三个 cell:触发类型——1 上升沿,4 高电平有效
uart0: serial@1c28000 {
interrupts = <0 0 4>;
// 解释:SPI类型,中断号32+0=32,高电平触发
};
gmac: ethernet@1c30000 {
interrupts = <0 5 4>;
// 解释:SPI类型,中断号32+5=37,高电平触发
};
你想想看,如果中断号写错了,驱动注册中断时要么冲突,要么根本收不到中断。我调试 GMAC 网卡时,就遇到过中断号偏移算错一位,结果收包中断一直不触发——查了半天的波形才发现是设备树写错了。
4.5 设备树在 Tina 中的位置
Tina Linux 的设备树文件放在 target/allwinner/<芯片系列>/ 目录下。以 H3 为例:
target/allwinner/h3/
├── config-4.9
├── linux/
│ └── linux-4.9/
│ └── arch/arm/boot/dts/
│ ├── sun8i-h3.dtsi // SoC 级通用定义
│ ├── sun8i-h3-orangepi-pc.dts // 具体板级
│ └── sun8i-h3-nanopi-m1.dts
└── board/
└── h3_xxx/
└── board.dts // 板级覆盖
这里有个重要的分层设计:
- .dtsi 文件:SoC 级通用描述,所有同系列芯片共用。定义了 CPU、GIC、时钟、所有外设节点,但 status 默认设为 disabled。
- .dts 文件:具体板级描述,通过
#include包含 .dtsi,然后使能需要用到的外设,配置引脚、时钟等。 - board.dts:Tina 特有的板级覆盖文件,编译时会跟 .dtsi 合并成最终的 .dtb。
4.6 知识结构总览
下面这张图把设备树的核心逻辑串起来了——从 DTS 源文件到内核驱动匹配,再到外设寄存器操作:
4.7 实战建议:从零开始写一个外设节点
假设你要在 H3 板子上添加一个 I2C 触摸屏。步骤很简单:
- 找到 SoC 的 .dtsi,确认 I2C 控制器节点已经定义(通常 status = "disabled")。
- 在板级 .dts 中使能:
&i2c0 { status = "okay"; }; - 在 I2C 节点下挂子节点:触摸屏作为 I2C 从设备,用 reg 指定地址。
- 配置中断和引脚:触摸屏的 INT 脚接到哪个 GPIO,就在 pinctrl 里配好。
// 在 board.dts 中添加
&i2c0 {
status = "okay";
clock-frequency = <400000>;
touchscreen@38 {
compatible = "goodix,gt911";
reg = <0x38>; // I2C 从设备地址
interrupt-parent = <&pio>; // 中断父节点为 GPIO 控制器
interrupts = <1 5 2>; // PH5,下降沿触发
irq-gpios = <&pio 1 5 GPIO_ACTIVE_LOW>;
reset-gpios = <&pio 1 4 GPIO_ACTIVE_LOW>;
};
};
make dtbs 编译,再用 dtc -I dtb -O dts xxx.dtb 反编译看看最终生成的设备树对不对。这一步能发现很多 include 和覆盖的问题。
设备树这东西,初看觉得啰嗦,用熟了会发现它让驱动开发变得特别干净。你换板子时,改改 .dts 就行,驱动代码一行都不用动。嗯,这就是设备树的魅力。