3. 设备树常用属性
各位同学,咱们今天来啃几个硬骨头。设备树里属性一大堆,但真正天天打交道的,其实就这几个。我刚开始学设备树那会儿,看着几百行的.dts文件头皮发麻,后来发现——说白了,核心属性就那么五六个,搞懂了它们,剩下的都是变体。
嗯,咱们一个一个来。
3.1 compatible —— 你是谁?
这是设备树里最重要的属性,没有之一。它的作用就是告诉内核:「我是这个硬件,请给我匹配对应的驱动。」
格式很简单:
compatible = "厂商,型号", "通用型号";
举个例子:
uart4: serial@4c000000 {
compatible = "st,stm32mp1-uart", "st,stm32-uart";
reg = <0x4c000000 0x400>;
};
这里有两个字符串。内核匹配驱动时,会从左往右试。第一个最精确,第二个是备胎。我个人习惯把最具体的型号放前面,通用的放后面。为什么?因为万一哪天换了同系列不同型号的芯片,驱动还能用。
核心要点:compatible 字符串的命名规则是「厂商名,器件型号」。别乱写,内核社区有规范。
我的经验:我在项目中遇到过有人把 compatible 写反了,写成 "uart,stm32" 而不是 "st,stm32-uart"。结果驱动死活匹配不上,查了两天才发现是字符串顺序错了。你想想看,这种低级错误多冤。
3.2 reg —— 你在哪?
reg 属性描述设备的地址空间。说白了,就是告诉内核:「我的寄存器从哪个地址开始,占多大地方。」
格式:
reg = <地址1 长度1>, <地址2 长度2>;
看个实际例子:
i2c2: i2c@40012000 {
compatible = "st,stm32f4-i2c";
reg = <0x40012000 0x400>;
interrupts = <33>;
};
这里 0x40012000 是 I2C 外设的基地址,0x400 是地址范围。为什么是 0x400?因为 STM32 的外设地址空间通常按 1KB 对齐。嗯,这是硬件设计决定的,记住就好。
注意:reg 里的地址必须是物理地址,不是虚拟地址。内核启动时会做映射,你别自己换算。
有时候一个设备有多个地址段,比如:
dma: dma@40026000 {
compatible = "st,stm32-dma";
reg = <0x40026000 0x400>,
<0x40026400 0x200>;
};
这种情况我遇到过——DMA 控制器有两组寄存器,物理上不连续,那就用两个地址段描述。
3.3 interrupts —— 你怎么通知我?
中断属性描述设备使用哪个中断号、什么触发方式。格式稍微复杂一点,因为涉及中断控制器。
基本格式:
interrupts = <中断号 触发类型>;
在 STM32 上常见写法:
usart1: serial@40011000 {
compatible = "st,stm32-usart";
reg = <0x40011000 0x400>;
interrupts = <37 0x14>;
};
这里 37 是中断号,0x14 是触发类型。0x14 什么意思?二进制 0001 0100,表示高电平触发 + 使能。每个芯片的编码方式可能不同,得查手册。
避坑指南:我曾经在移植代码时,把中断号写错了。结果一触发中断,系统就跳到了错误的中断服务函数,整个系统卡死。查了三天,最后用示波器量中断引脚才发现问题。所以,中断号一定要对着芯片手册核对。
如果设备有多个中断:
interrupts = <37 0x14>, <38 0x14>;
第一个是发送中断,第二个是接收中断。驱动里通过 platform_get_irq() 按索引获取。
3.4 status —— 你开了吗?
这个属性最简单,但最容易忽略。它控制设备是否启用。
取值只有两个:
| 值 | 含义 |
|---|---|
| "okay" | 启用该设备 |
| "disabled" | 禁用该设备 |
举个例子:
&usart1 {
status = "okay";
pinctrl-0 = <&usart1_pins>;
};
在板级文件中,我们经常这样覆盖芯片级文件里的状态。芯片级文件里默认是 disabled,板级文件里按需打开。
我的习惯:调试新板子时,我会先把所有外设都设成 disabled,然后一个一个打开测试。这样出了问题,能快速定位是哪个外设导致的。
3.5 clocks —— 你用什么时钟?
时钟属性描述设备使用哪些时钟源。在 STM32 上,每个外设都有对应的时钟门控和时钟源选择。
格式:
clocks = <&时钟控制器 时钟ID>;
看个例子:
spi2: spi@40003800 {
compatible = "st,stm32-spi";
reg = <0x40003800 0x400>;
interrupts = <36 0x14>;
clocks = <&rcc 0x8C 0x14>;
};
这里 &rcc 是时钟控制器的 phandle,0x8C 是时钟寄存器偏移,0x14 是时钟位掩码。嗯,STM32 的时钟系统比较复杂,但设备树里就是这么描述的。
注意:时钟 ID 的定义每个芯片都不一样。别想当然地照搬其他芯片的写法。我见过有人把 F4 的时钟 ID 直接用在 F7 上,结果外设时钟没打开,通信全是乱码。
如果一个设备需要多个时钟:
clocks = <&rcc 0x8C 0x14>, <&rcc 0x8C 0x15>;
clock-names = "apb", "spi";
clock-names 给每个时钟起个名字,驱动里通过 clk_get(dev, "apb") 来获取,比用索引更安全。
3.6 知识体系总览
说了这么多,咱们用一张图把关系理清楚:
这张图把五个属性的关系说清楚了。你想想看,一个设备节点,本质上就是回答了五个问题:你是谁?你在哪?你怎么通知我?你用什么时钟?你开了吗?搞懂这五个,设备树就算入门了。
3.7 实战中的组合用法
最后,咱们看一个完整的例子。这是我在 STM32MP1 上实际用过的 UART 节点:
&uart4 {
compatible = "st,stm32mp1-uart", "st,stm32-uart";
reg = <0x4c000000 0x400>;
interrupts = <GIC_SPI 84 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rcc UART4_K>;
status = "okay";
pinctrl-0 = <&uart4_pins_a>;
pinctrl-names = "default";
};
看到没?五个核心属性全用上了。再加上 pinctrl 配置引脚复用,一个完整的外设描述就出来了。
我的建议:刚开始写设备树时,先照着芯片参考手册把 compatible、reg、interrupts 这三个写对。clocks 可以后面再加,status 默认先设成 disabled 调试。等驱动能正常 probe 了,再逐步完善其他属性。
嗯,核心属性就这些。记住,设备树不是写论文,它是给内核看的配置文件。简洁、准确、完整,就够了。