3. 设备树常用属性详解:compatible、reg、interrupts、status、clocks
设备树里的属性,说白了就是给内核递小纸条。告诉它:「我这块硬件长什么样,怎么用,现在能不能干活。」
我刚开始接触设备树时,总觉得属性名太多记不住。后来发现,真正天天打交道的,也就五六个核心属性。今天咱们就把它们掰开揉碎了讲清楚。
核心原则:设备树属性是硬件描述语言,不是驱动代码。它只描述「有什么」,不描述「怎么干」。
3.1 compatible —— 硬件身份证
这是最重要的属性,没有之一。内核通过它找到对应的驱动。
compatible = "vendor,device", "generic-device";
格式很简单:厂商前缀 + 设备型号。我习惯把最具体的型号放前面,通用的放后面。内核匹配时,会从左往右试,直到找到能驱动的。
举个例子:
uart0: serial@10000000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x10000000 0x4000>;
interrupts = <0 58 4>;
};
这里写了两个兼容字符串。为什么?
我在项目中遇到过这种情况:新芯片的 UART 和上一代完全兼容,但驱动里有些小优化。写两个字符串,新内核用新驱动,旧内核也能用旧驱动兜底。这叫向前兼容。
我的习惯:每个设备至少写两个 compatible 字符串。第一个精确到 SoC 型号,第二个用系列名。这样既保证精确匹配,又保留兼容性。
3.2 reg —— 硬件地址空间
reg 描述设备占用的地址范围。格式是:
reg = <address1 length1 address2 length2 ...>;
每个地址-长度对,描述一段连续的物理地址空间。我见过不少新手只写地址不写长度,结果驱动访问越界,系统直接挂掉。
来看个正经的例子:
gpu@18000000 {
compatible = "arm,mali-450";
reg = <0x18000000 0x40000>,
<0x1c000000 0x100000>;
reg-names = "gpu_core", "gpu_memory";
};
这里 GPU 有两段地址空间。核心寄存器在 0x18000000,长度 256KB;显存在 0x1c000000,长度 1MB。配合 reg-names,驱动里可以直接用名字访问,不用记硬编码地址。
我曾经踩过的坑:reg 里的地址是物理地址,不是虚拟地址。内核启动后,驱动需要自己 ioremap 才能用。别想着直接拿 reg 里的值当指针用,那是找死。
3.3 interrupts —— 中断连接线
中断属性描述设备怎么通知 CPU。格式取决于中断控制器:
interrupts = <中断号 触发类型>;
interrupt-parent = <&gic>;
触发类型常见值:
| 值 | 含义 | 适用场景 |
|---|---|---|
| 0 | 上升沿触发 | 边沿敏感信号 |
| 1 | 下降沿触发 | 边沿敏感信号 |
| 4 | 高电平触发 | 电平敏感信号 |
| 8 | 低电平触发 | 电平敏感信号 |
你想想看,为什么要有 interrupt-parent?因为设备树是树形结构,中断控制器可能挂在另一个节点下。显式指定 parent,内核才知道找谁要中断号映射。
我记得有一次调试网卡中断,死活不触发。查了半天,发现 interrupt-parent 写错了,指向了 GPIO 控制器而不是 GIC。这种错误,编译不报错,运行不报错,就是设备不干活。
调试技巧:在 /proc/interrupts 里看中断是否注册成功。如果设备节点没出现,八成是 interrupt-parent 或中断号写错了。
3.4 status —— 设备开关
status 属性控制设备是否启用。就这么几个值:
| 值 | 含义 |
|---|---|
| okay | 设备可用,驱动正常加载 |
| disabled | 设备禁用,驱动不加载 |
| reserved | 保留给固件使用 |
| fail | 设备检测失败 |
我建议所有设备默认写 disabled,然后在具体板级 dts 里按需打开。为什么?
因为 SoC 里集成了太多外设,一块板子可能只用其中一部分。默认全关,用哪个开哪个,干净利落。我曾经接手过一个项目,所有设备默认 okay,结果内核启动时加载了十几个用不到的驱动,白白浪费内存和启动时间。
&uart1 {
status = "okay";
pinctrl-0 = <&pinctrl_uart1>;
};
&uart2 {
status = "disabled";
};
注意:status 只影响驱动加载,不影响设备树解析。即使 disabled,其他节点仍然可以通过 phandle 引用它。别指望用 status 来节省设备树解析时间。
3.5 clocks —— 时钟配置
时钟属性描述设备需要哪些时钟源。格式比较灵活:
clocks = <&clk_gate 10>, <&clk_pll 2>;
clock-names = "bus", "core";
每个时钟条目包含两个信息:时钟控制器 phandle 和时钟索引。clock-names 给每个时钟起个名字,驱动里用名字找时钟,不用记索引。
我见过最坑爹的写法:
clocks = <&clk 0>, <&clk 1>, <&clk 2>;
没有 clock-names,驱动里全靠硬编码索引。后来时钟顺序变了,驱动全废。你想想看,这种代码谁敢维护?
我的建议:永远写 clock-names。哪怕只有一个时钟,也给它起个名字。这是给自己留后路,也是给接手的人留活路。
3.6 属性关系总览
这几个属性不是孤立的。它们共同描述一个完整的硬件设备:
3.7 实战经验总结
说了这么多,我总结几条实战经验:
- compatible 写两个以上:精确型号 + 兼容型号,给自己留退路
- reg 必须写长度:只写地址不写长度,驱动没法做边界检查
- interrupt-parent 别省略:即使只有一个中断控制器,也显式写出来
- status 默认 disabled:按需开启,省资源省麻烦
- clock-names 必须写:别偷懒,这是给未来自己写的注释
一句话总结:设备树属性是硬件和驱动之间的契约。写清楚,写规范,能省掉你未来无数调试时间。