1. 设备树基础回顾:DTS语法、节点与属性、reg与interrupts详解
各位同学好,我是老李。做嵌入式Linux驱动开发十几年了,设备树这东西,我敢说每个工程师都绕不开。尤其是做版本升级的时候,设备树改不对,内核直接罢工。今天咱们就从最基础的东西开始捋一遍。
你可能会问:都2025年了,还讲DTS语法?嗯,我理解你的想法。但说实话,我在项目里见过太多人栽在基础细节上。有一次客户升级内核,板子死活起不来,最后发现就是一个reg属性少写了一个地址单元。所以,基础不牢,地动山摇。
核心观点:设备树是描述硬件拓扑的数据结构,不是代码。它告诉内核“我有哪些硬件、它们怎么连接、怎么访问”。版本升级时,设备树是内核与板卡之间的“翻译官”,翻译错了,系统就崩。
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 版本升级时最容易犯的错
根据我的经验,版本升级中设备树适配的坑主要集中在以下几点:
- reg 地址单元不匹配:32位升64位时,
#address-cells没从1改成2 - 中断号偏移变化:新内核可能调整了中断映射表
- compatible 字符串变更:驱动改名了,设备树没跟着改
- 属性名被废弃:比如
gpios改成了cs-gpios - status 属性被忽略:新内核要求显式写
status = "okay"
我建议你每次升级内核时,都做一次设备树的“差异分析”。把旧设备树和新内核的绑定文档(Documentation/devicetree/bindings/)逐条对比。别嫌麻烦,这个步骤能省你后面几天的调试时间。
我的习惯:在版本升级前,先用 dtc -I dtb -O dts 把旧 DTB 反编译成 DTS,然后和新内核的 DTS 做 diff。这样能快速发现哪些节点被改了、哪些属性被删了。这个习惯帮我避免了好几次线上事故。