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告诉内核这个设备要不要管

把这六个属性搞明白了,设备树移植就成功了一大半。剩下的就是根据具体硬件,把值填对就行了。

嗯,今天就先讲到这里。下一章咱们聊聊设备树的编译和调试,到时候我会分享一些我常用的调试技巧,保证实用。