3. 设备树常用属性:compatible、reg、interrupts、clocks、gpios、status
好,咱们直接进入正题。设备树里属性很多,但说实话,日常开发中翻来覆去用的就是那么几个。我刚开始学设备树那会儿,看着几百页的规范文档头都大了。后来发现,先把这几个核心属性吃透,90%的移植工作就能搞定。
今天我就把这六个属性掰开了讲。每个我都会结合项目里的实际案例,告诉你什么时候用、怎么用、坑在哪。
3.1 compatible:你的设备叫什么名字
这是设备树里最重要的属性,没有之一。它的作用很简单——告诉内核:这个节点对应哪个驱动。
格式长这样:
compatible = "厂商,型号", "通用型号";
注意顺序!越靠前的越具体,越靠后的越通用。内核匹配驱动时,会从左往右找。找到第一个能用的就停。
核心原则:第一个字符串必须精确匹配你的硬件。后面的作为备选方案。
举个例子。我在移植某款国产FPGA时,设备树里写的是:
compatible = "acme,custom-fpga-v2", "simple-bus";
如果内核里没有针对"acme,custom-fpga-v2"的驱动,就会退而求其次,用"simple-bus"的驱动来匹配。嗯,这里要注意,备选方案不能乱写,得确保功能上兼容。
我的经验:写compatible时,最好参考芯片厂商的官方文档。有些厂商喜欢用"厂商,设备名"的格式,有些用"厂商,设备名-版本"。我个人习惯统一用"厂商,设备名"的格式,简洁明了。
3.2 reg:你的设备占哪块地盘
reg属性描述设备的地址空间。说白了,就是告诉内核:我的寄存器在哪个地址范围,占多大地方。
基本格式:
reg = <地址1 长度1 地址2 长度2 ...>;
地址和长度怎么解释?这取决于你的父节点的#address-cells和#size-cells属性。
| 属性 | 含义 | 常见值 |
|---|---|---|
| #address-cells | 地址占几个32位整数 | 1(32位地址)或2(64位地址) |
| #size-cells | 长度占几个32位整数 | 1(32位长度)或2(64位长度) |
举个例子。我做过一个项目,外设的寄存器基地址是0x10000000,范围是0x1000字节。父节点的#address-cells = <1>,#size-cells = <1>,那么reg就写成:
reg = <0x10000000 0x1000>;
如果设备有多个地址段呢?比如一个控制寄存器区和一个数据缓冲区区:
reg = <0x10000000 0x1000 0x20000000 0x10000>;
内核会按顺序解析,第一个地址段叫reg 0,第二个叫reg 1,以此类推。驱动里可以用platform_get_resource(pdev, IORESOURCE_MEM, 0)来获取。
我曾经踩过的坑:有一次我忘了检查父节点的#address-cells,默认用了1。结果芯片是64位地址,reg里的地址被截断了,驱动死活访问不到寄存器。排查了两天才发现是这个问题。所以,写reg之前,一定先看看父节点怎么定义的。
3.3 interrupts:你的设备怎么通知CPU
中断是外设和CPU通信的重要方式。interrupts属性描述的就是这个。
格式:
interrupts = <中断号 触发类型>;
中断号怎么解释?这取决于中断控制器。对于ARM GIC(通用中断控制器),中断号分三种:
- SPI(共享外设中断):中断号从32开始,用于外设
- PPI(私有外设中断):中断号16-31,用于每个CPU核自己的外设
- SGI(软件生成中断):中断号0-15,用于核间通信
触发类型常见的有:
- IRQ_TYPE_EDGE_RISING (1):上升沿触发
- IRQ_TYPE_EDGE_FALLING (2):下降沿触发
- IRQ_TYPE_LEVEL_HIGH (4):高电平触发
- IRQ_TYPE_LEVEL_LOW (8):低电平触发
举个例子。某款网卡芯片使用SPI中断,中断号是68,上升沿触发:
interrupts = <0 68 1>;
等等,这里怎么多了个0?这是因为GIC的interrupts属性里,第一个cell表示中断类型:0表示SPI,1表示PPI。所以完整的解释是:SPI类型,中断号68,上升沿触发。
我建议:写中断属性时,最好打开芯片的参考手册,对照着写。不同厂家的中断号定义可能不一样。有些从0开始,有些从32开始。搞错了,中断就收不到。
3.4 clocks:你的设备需要什么时钟
时钟是嵌入式系统的命脉。clocks属性描述设备需要哪些时钟源。
格式:
clocks = <&时钟控制器1 时钟ID1>, <&时钟控制器2 时钟ID2>;
每个时钟源由两部分组成:时钟控制器的phandle和时钟ID。时钟ID具体是什么,得看时钟控制器的驱动怎么定义。
举个例子。某款UART控制器需要两个时钟:一个总线时钟(baud clock)和一个外设时钟(peripheral clock):
clocks = <&clkc 16>, <&clkc 17>;
clock-names = "baud", "periph";
clock-names属性给每个时钟起了个名字,驱动里可以用devm_clk_get(dev, "baud")来获取对应的时钟。我个人强烈建议加上clock-names,代码可读性会好很多。
关键点:时钟ID的定义通常在时钟控制器的头文件里。比如include/dt-bindings/clock/目录下。写设备树时,记得包含对应的头文件。
3.5 gpios:你的设备怎么控制GPIO
GPIO是最常用的外设接口之一。gpios属性描述设备使用了哪些GPIO引脚。
格式:
gpios = <&gpio控制器 GPIO编号 标志位>;
标志位常见的有:
- GPIO_ACTIVE_HIGH (0):高电平有效
- GPIO_ACTIVE_LOW (1):低电平有效
举个例子。某款LED灯接在GPIO3组的第5个引脚上,高电平点亮:
gpios = <&gpio3 5 GPIO_ACTIVE_HIGH>;
如果设备有多个GPIO,比如一个复位引脚和一个中断引脚:
gpios = <&gpio1 2 GPIO_ACTIVE_LOW>, <&gpio2 7 GPIO_ACTIVE_HIGH>;
gpio-names = "reset", "irq";
同样,gpio-names可以让驱动按名字查找,比按索引更可靠。
我曾经犯过的错:有一次我把GPIO编号写错了,从1开始而不是从0开始。结果驱动控制的引脚完全不对。后来我养成了习惯:写GPIO编号时,一定先看芯片手册里的引脚编号定义,确认是从0还是从1开始。
3.6 status:你的设备是开还是关
status属性控制设备节点的启用状态。别看它简单,用好了能省不少事。
常见值:
| 值 | 含义 |
|---|---|
| "okay" | 设备启用 |
| "disabled" | 设备禁用 |
| "reserved" | 保留,不用于操作系统 |
| "fail" | 设备检测失败 |
我一般在做板级支持包时,会把所有外设节点都写上status = "disabled",然后在具体板卡的设备树里,只把用到的外设改成"okay"。这样既保留了所有外设的定义,又不会因为未使用的外设导致资源冲突。
举个例子。SoC的.dtsi文件里:
uart0: serial@10000000 {
compatible = "my-uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
status = "disabled";
};
uart1: serial@10001000 {
compatible = "my-uart";
reg = <0x10001000 0x1000>;
interrupts = <0 34 4>;
status = "disabled";
};
板卡的.dts文件里:
&uart0 {
status = "okay";
};
这样,只有uart0被启用,uart1保持禁用。干净利落。
我的习惯:调试阶段,我会把status写成"okay"先测试。等确认没问题了,再改回"disabled"。这样能避免因为设备树问题导致系统启动失败。
3.7 实战:把这些属性串起来
说了这么多,咱们来个完整的例子。假设我们要描述一个I2C控制器,它连接了一个温度传感器:
i2c@20000000 {
compatible = "my,i2c-controller";
reg = <0x20000000 0x1000>;
interrupts = <0 45 4>;
clocks = <&clkc 10>;
clock-names = "i2c";
status = "okay";
temperature-sensor@48 {
compatible = "lm75";
reg = <0x48>;
interrupts = <0 50 1>;
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
};
你看,每个属性各司其职:
- compatible告诉内核用哪个驱动
- reg告诉内核寄存器在哪
- interrupts告诉内核中断怎么用
- clocks告诉内核时钟从哪来
- gpios告诉内核控制引脚在哪
- status告诉内核这个设备要不要管
把这六个属性搞明白了,设备树移植就成功了一大半。剩下的就是根据具体硬件,把值填对就行了。
嗯,今天就先讲到这里。下一章咱们聊聊设备树的编译和调试,到时候我会分享一些我常用的调试技巧,保证实用。