3. 设备树基础语法(二):标准属性详解与实战
各位同学,咱们接着聊设备树。上一章我们把设备树的基本骨架搭起来了,这一章要往骨架上填肉——也就是那些标准属性。说白了,这些属性就是设备树世界的「通用语言」,不管你是做I2C控制器,还是写GPIO驱动,都得跟它们打交道。
我个人习惯把标准属性分成三类:身份标识类(你是谁)、资源描述类(你用啥)、状态控制类(你活没活)。咱们一个一个来啃。
3.1 compatible:你的设备叫什么名字
compatible 属性,我愿称之为设备树的「身份证」。内核通过它来匹配驱动。格式很简单:
compatible = "厂商,型号", "通用型号";
注意顺序:越具体的越靠前。内核匹配时从左往右找,找到第一个能驱动的就停。
实战经验:我在项目中遇到过一个问题——某款LCD屏的compatible写成了"ilitek,ili9341",但驱动里注册的是"ilitek,ili9341-v1"。结果屏死活点不亮。排查了半天,发现就是字符串不匹配。从那以后,我每次写compatible都会去驱动源码里确认一遍。
来看个完整例子:
// 一个SPI Flash节点的compatible写法
spi_flash: flash@0 {
compatible = "winbond,w25q128", "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <104000000>;
};
这里写了两个兼容型号。第一个是精确的Winbond芯片,第二个是通用的JEDEC SPI NOR标准。万一内核没有Winbond的驱动,还能用通用驱动兜底。你想想看,这设计是不是挺巧妙的?
3.2 reg:你的设备占哪些资源
reg 属性描述设备的地址空间。对于内存映射设备,它告诉内核:我的寄存器从哪个地址开始,占多大地方。
格式是:
reg = <地址1 长度1>, <地址2 长度2>, ...;
嗯,这里要注意:地址和长度的位数由父节点的 #address-cells 和 #size-cells 决定。默认都是1个32位单元。
| #address-cells | #size-cells | 含义 |
|---|---|---|
| 1 | 1 | 32位地址,32位长度 |
| 2 | 1 | 64位地址,32位长度 |
| 1 | 0 | 只有地址,没有长度(常用于片选信号) |
避坑指南:我曾经在调试一个FPGA桥接设备时,reg写成了 reg = <0x43C00000 0x10000>,但父节点声明的是 #address-cells = <2>。结果内核解析出来的地址完全不对,设备根本没法访问。后来查了半天才发现是cells数量不匹配。记住:子节点的reg格式必须跟父节点的cells声明一致!
多段地址空间的例子:
// 一个以太网控制器,有两段寄存器空间
ethernet@ff700000 {
compatible = "allwinner,sun8i-emac";
reg = <0xff700000 0x10000>, // 主寄存器
<0xff714000 0x1000>; // DMA描述符区
interrupts = <0 57 4>;
};
3.3 interrupts:你的设备怎么通知CPU
interrupts 属性描述设备的中断信息。它告诉内核:我这个设备用哪个中断号,触发方式是什么。
格式:
interrupts = <中断号 触发类型>;
触发类型的常见值:
- 0:上升沿触发
- 1:下降沿触发
- 2:高电平触发
- 3:低电平触发
- 4:边沿触发(上升沿和下降沿都触发)
但要注意,中断号的编码方式取决于中断控制器。ARM GIC(通用中断控制器)通常用这种方式:
interrupts = <0 30 4>; // 第一个0表示SPI中断,30是中断号,4是触发类型
这里的第一个数字:0=SPI(共享外设中断),1=PPI(私有外设中断)。
小技巧:我建议你在写interrupts之前,先找到中断控制器的设备树绑定文档。不同厂家的GIC实现可能有细微差别。比如有些芯片把中断号从0开始编号,有些从16开始。搞错了,中断就收不到。
来看一个带中断控制器的完整例子:
// 中断控制器
gic: interrupt-controller@1c81000 {
compatible = "arm,gic-400";
reg = <0x1c81000 0x1000>,
<0x1c82000 0x2000>;
interrupt-controller;
#interrupt-cells = <3>;
};
// 使用中断的设备
uart0: serial@1c28000 {
compatible = "snps,dw-apb-uart";
reg = <0x1c28000 0x400>;
interrupts = <0 1 4>; // SPI中断,中断号1,高电平触发
clocks = <&uart_clk>;
status = "okay";
};
3.4 status:你的设备活没活
status 属性最简单,但也最容易忽略。它控制设备是否启用:
- "okay":设备启用
- "disabled":设备禁用
- "reserved":保留(不常用)
- "fail":设备检测失败
为什么需要这个属性?因为很多SoC的.dtsi文件里把所有外设都定义好了,但默认都是disabled。板级.dts文件里再根据需要逐个打开。这样设计的好处是:一个芯片的.dtsi可以被多个板子复用。
实战场景:我记得有一次做项目,客户说SPI接口用不了。我检查了所有配置,时钟、引脚、驱动都没问题。最后发现.dts里spi节点的status写成了"disable"——少了个d!内核解析时直接忽略了。改回"disabled"再改成"okay",问题解决。这种低级错误,排查起来最费时间。
典型用法:
// 芯片级.dtsi中定义,默认关闭
&uart0 {
status = "disabled";
};
// 板级.dts中打开
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
pinctrl-names = "default";
};
3.5 实战:组装一个完整的设备节点
好了,理论讲完了。咱们来动手组装一个真实的设备节点。假设我们要描述一个I2C总线上的温度传感器:
// I2C控制器节点
i2c0: i2c@1c2ac00 {
compatible = "allwinner,sun8i-h3-i2c";
reg = <0x1c2ac00 0x400>;
interrupts = <0 6 4>;
clocks = <&ccu CLK_BUS_I2C0>;
resets = <&ccu RST_BUS_I2C0>;
pinctrl-0 = <&i2c0_pins>;
pinctrl-names = "default";
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
// 温度传感器子节点
temp_sensor: lm75@48 {
compatible = "national,lm75";
reg = <0x48>; // I2C地址
status = "okay";
};
};
这个例子把今天讲的所有属性都用上了。你仔细看看:compatible标识设备类型,reg描述地址空间,interrupts定义中断,status控制启用状态。每个属性各司其职,缺一不可。
我的建议:刚开始写设备树时,别急着从头写。先找一个参考板子的.dts文件,对照着改。把别人的compatible、reg、interrupts抄过来,只改你需要的那部分。等熟练了再自己写。我当年就是这么学的。
3.6 知识体系总览
下面这张图把今天讲的内容串起来了。你可以看到每个属性在设备树中的角色和关系:
这张图把四个属性的关系理清楚了。compatible负责「你是谁」,reg负责「你在哪」,interrupts负责「你怎么叫人」,status负责「你活没活」。四个属性配合起来,一个完整的设备描述就出来了。
好了,这一章的内容就到这。标准属性是设备树的基石,你写多了自然就熟练了。记住:多查文档,多对比参考板,少走弯路。
课后练习:找一个你手头开发板的.dts文件,找到任意一个外设节点,把它的compatible、reg、interrupts、status四个属性抄下来,对照芯片手册理解每个值的含义。这是最快的学习方法。