设备树DTS 学习:2-设备树语法

背景

通过上一讲了解完设备树DTS有关概念,我们这一讲就来基于设备树例程,学习设备树的语法规则。

参考:设备树详解dts设备树语法详解设备树使用总结

设备树框架

1个dts文件 + n个dtsi文件,它们编译而成的dtb文件就是真正的设备树。

基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分。
为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。
这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 以保证整个设备树的管理更加有序。
以solidrun公司的hummingboard为例,其组成为

imx6dl-hummingboard.dts
        |_imx6dl.dtsi
        |   |_imx6qdl.dtsi
        |_imx6qdl-microsom.dtsi
        |_imx6qdl-microsom-ar8035.dtsi

此外,dts/dtsi兼容c语言的一些语法,能使用宏定义,也能包含.h文件

设备树用树状结构描述设备信息,它有以下几种特性:

  1. 每个设备树文件都有一个根节点,每个设备都是一个节点。
  2. 节点由 节点名 + 属性 组成。
  3. 节点间可以嵌套,形成父子关系,这样就可以方便的描述设备间的关系。
  4. 每个设备的属性都用一组key-value对(键值对)来描述。
  5. 每个属性的描述用;结束

所以,一个设备树的基本框架可以写成下面这个样子,一般来说,/表示板子,它的子节点node1表示SoC上的某个控制器,控制器中的子节点node2表示挂接在这个控制器上的设备(们)

/ {                                 //根节点
    node1{                          //node1是节点名,是/的子节点
        key=value;                  //node1的属性
        ...
        node2{                      //node2是node1的子节点
            key=value;              //node2的属性
            ...
        }
    }                               //node1的描述到此为止
    node3{
        key=value;
        ...
    }
}

以下是一颗最简单的设备树:
注意/dts-v1/;是必须的,有时候正是因为忽略了它而引起了syntax error且没有其他提示。

/dts-v1/;
/ {
 
};

节点node

{}包围起来的结构称之为节点,dts中最开头的/ {},称为根节点。
在节点中,以 key = value 代表节点属性。
树中每个表示一个设备的节点都需要一个 compatible 属性。

节点名 name

  • 节点名称:每个节点名格式为:<name>[@<unit_address>],其中:
    • :设备名,就是一个不超过31位的简单 ascii 字符串,节点的命名应该根据它所体现的是什么样的设备。
    • <unit_address> :设备地址,用来唯一标识一共节点。没有指定<unit_address>时,同级节点命名必须是唯一的;但只要<unit_address>不同,多个节点也可以使用一样的通用名称。

下面是典型节点名的写法:

/ {
        model = "Freescale i.MX23 Evaluation Kit";
        compatible = "fsl,imx23-evk", "fsl,imx23";

        memory {
                reg = <0x40000000 0x08000000>;
        };
        // 注意这里
        apb@80000000 {
                ...
        };
}

上面的节点名是apb,节点路径是/apb@80000000 ,这点要注意,因为根据节点名查找节点的API的参数是不能有"@xxx"这部分的。

Linux中的设备树还包括几个特殊的节点:比如chosen,chosen节点不描述一个真实设备,而是用于firmware传递一些数据给OS,比如bootloader传递内核启动参数给内核

/include/ "zynq-7000.dtsi"

/ {
        model = "Zynq ZC702 Development Board";
        compatible = "xlnx,zynq-zc702", "xlnx,zynq-7000";

        ...

        chosen {
                bootargs = "console=ttyPS1,115200 earlyprintk";
        };
};

引用

当我们找一个节点的时候,我们必须书写完整的节点路径,这样当一个节点嵌套比较深的时候就不是很方便。所以,设备树允许我们用下面的形式为节点标注引用(起别名),借以省去冗长的路径。
标号引用常常还作为节点的重写方式,用于修改节点属性。

  • 格式:
    • 声明别名: 别名 : 节点名
    • 访问 : &别名

编译设备树的时候,相同的节点的不同属性信息都会被合并,相同节点的相同的属性会被重写(覆盖前值),使用引用可以避免移植者四处找节点,直接在板级.dts增改即可。

/include/ "imx53.dtsi"

/ {
        model = "Freescale i.MX53 Automotive Reference Design Board";
        compatible = "fsl,imx53-ard", "fsl,imx53";

        memory {
                reg = <0x70000000 0x40000000>;
        };

        eim-cs1@f4000000 {
                #address-cells = <1>;
                #size-cells = <1>;
                compatible = "fsl,eim-bus", "simple-bus";
                reg = <0xf4000000 0x3ff0000>;

                lan9220@f4000000 {
                        compatible = "smsc,lan9220", "smsc,lan9115";
                        reg = <0xf4000000 0x2000000>;
                        phy-mode = "mii";
                        interrupt-parent = <&gpio2>; // 直接使用引用

                        vdd33a-supply = <&reg_3p3v>;
                };
        };

        regulators {
                compatible = "simple-bus";

                reg_3p3v: 3p3v {                     // 定义一个引用
                        compatible = "regulator-fixed";
                        regulator-name = "3P3V";
                };
        };

        ...
        // 引用一个节点,新增/修改其属性。
        &reg_3p3v {
            regulator-always-on;
        }

节点属性 property

属性一般由 key = value; 键值对构成。
Linux设备树语法中定义了一些具有规范意义的属性,包括:compatible, address, interrupt等,这些信息能够在内核初始化找到节点的时候,自动解析生成相应的设备信息。
此外,还有一些Linux内核定义好的,一类设备通用的有默认意义的属性,这些属性一般不能被内核自动解析生成相应的设备信息,但是内核已经编写的相应的解析提取函数,常见的有 "mac_addr","gpio","clock","power"。"regulator" 等等。

  • 简单的键-值对,它的值可以为空或者包含一个任意字节流。虽然数据类型并没有编码进数据结构,但在设备树源文件中任有几个基本的数据表示形式:
    • 文本字符串(无结束符)可以用双引号表示: string-property = "a string"
    • Cells是 32 位无符号整数,用尖括号限定: cell-property = <0xbeef 123 0xabcd1234>
    • 二进制数据用方括号限定: binary-property = [01 23 45 67];
    • 不同表示形式的数据可以使用逗号连在一起: mixed-property = "a string", [01 23 45 67], <0x12345678>;
    • 逗号也可用于创建字符串列表: string-list = "red fish", "blue fish";
    • 混合形式:上述几种的混合形式

compatible 兼容性

如果一个节点是设备节点,那么它一定要有compatible(兼容性),因为这将作为驱动和设备(设备节点)的匹配依据,compatible(兼容性)的值可以有不止一个字符串以满足不同的需求。(设备节点中对应的节点信息已经被内核构造成struct platform_device。驱动可以通过相应的函数从中提取信息。)
compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。
而根节点的compatible也是非常重要的,一般在系统启动以后,用于识别对应系统一些东西,并由此进行对应的初始化。

  • 格式:compatible = "<manufacturer>,<model>" [, "<manufacturer>,<model>"]
    • manufacturer指定厂家名,model指定特定设备型号;后续的<manufacturer,model>指定兼容的设备型号(其中,后续的<manufacturer> 可空,第二个model也可空)。
      我们来看 compatible 是如何与 驱动捆绑在一起的:
      可以看出,驱动中用于匹配的结构使用的compatible和设备树中一模一样,否则就可能无法匹配,这里另外的一点是struct of_device_id数组的最后一个成员一定是空,因为相关的操作API会读取这个数组直到遇到一个空。

1)先随便在设备树中出一个网卡设备,关键是找到 compatible 属性中的 <model>值。

// 文件节选于:arch/arm/boot/dts/vexpress-v2m-rs1.dtsi
ethernet@2,02000000 {
    compatible = "smsc,lan9118", "smsc,lan9115";
    reg = <2 0x02000000 0x10000>;
    interrupts = <15>;
    phy-mode = "mii";
    reg-io-width = <4>;
    smsc,irq-active-high;
    smsc,irq-push-pull;
    vdd33a-supply = <&v2m_fixed_3v3>;
    vddvario-supply = <&v2m_fixed_3v3>;
};

2)在驱动中(为了方便读者理解,这里在内核源码根目录下查找,实际上就是在driver目录中),找到对应的.compatible 关键字所在的文件以及行数。

$ find . 2>/dev/null | grep lan9115
arch/arm/boot/dts/vexpress-v2m-rs1.dtsi:50:     compatible = "smsc,lan9118", "smsc,lan9115";
arch/arm/boot/dts/vexpress-v2m.dtsi:49:         compatible = "smsc,lan9118", "smsc,lan9115";
drivers/net/ethernet/smsc/smsc911x.c:2578:      { .compatible = "smsc,lan9115", },

3)顺藤摸瓜,找到所在行,也就找到了用来描述设备信息的结构体of_device_id

// 节选于 drivers/net/ethernet/smsc/smsc911x.c
#ifdef CONFIG_OF
static const struct of_device_id smsc911x_dt_ids[] = {
    { .compatible = "smsc,lan9115", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, smsc911x_dt_ids);
#endif

可以看出,驱动中用于匹配的结构使用的compatible和设备树中一模一样,且,字符串需要严格匹配。

注:这里另外的一点是 struct of_device_id 数组的最后一个成员一定是空,因为相关的操作API会读取这个数组直到遇到一个空。
i2c和spi驱动还支持一种“别名匹配”的机制,就以pcf8523为例,假设某程序员在设备树中的pcf8523设备节点中写了compatible = "pcf8523";,显然相对于驱动id_table中的"nxp,pcf8523",他遗漏了nxp字段,但是驱动却仍然可以匹配上,因为别名匹配对compatible中字符串里第二个字段敏感。
驱动程序将直接和设备树里的设备节点进行配对,是通过设备节点中的compatible(兼容性)来与设备节点进行配对的,具体的应用详见 基于i2c子系统的驱动分析基于platform总线的驱动分析

address 地址属性

有关节点的地址,比如i2c@021a0000,虽然它在名字后面跟了地址,但是正式的设置是在reg属性中设置。
(几乎)所有的设备都需要与CPU的IO口相连,所以其IO端口信息就需要在设备节点节点中说明。常用的属性有:

  • #address-cells = <CNT>,用来描述子节点"reg"属性的地址表中用来描述首地址的cell的数量
  • #size-cells = <CNT>, 用来描述子节点"reg"属性的地址表中用来描述地址长度的cell的数量。
  • reg = <address ... length>: address 代表基地址, length 代表长度。基址和长度的格式是可变的,addr由父节点的#address-cells个uint32值组成,len由父节点的#size-cells个uint32值组成。表明了设备使用的一个地址范围。
    例如:
	aips-bus@02000000 { /* AIPS1 */
	    compatible = "fsl,aips-bus", "simple-bus";
	    #address-cells = <1>;
	    #size-cells = <1>;
	    reg = <0x02000000 0x100000>;

	    i2c1: i2c@021a0000 {
	        #address-cells = <1>;
	        #size-cells = <0>;
	        compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
	        reg = <0x021a0000 0x4000>;

			rtc: rtc@68 {
			    compatible = "stm,mt41t62";
			    reg = <0x68>;
			};
	    };
	};

/*
我们知道,aips-bus@02000000 是 i2c@021a0000 的父节点;i2c@021a0000 是 rtc@68 的父节点。

aips-bus@02000000的 #address-cells 和#size-cells均为1,所以 i2c@021a0000 中的 `reg` 格式为: `<address length>`
i2c@021a0000的 #address-cells 和#size-cells分别为1和0,所以 rtc@68 中的 `reg` 格式为: `<address>`

通俗来讲,如果现在有 一个节点A的 #address-cells 和#size-cells分别为2和1;那么A的子节点B 的 `reg`格式为 `<address address length>`
*/

interrupts 中断属性

中断产生设备用interrupts属性描述中断源(interrupt specifier),因为不同的硬件描述中断源需要的数据量不同,所以interrupts属性的类型也是。为了明确表示一个中断由几个u32表示,又引入了#interrupt-cells属性,#interrupt-cells属性的类型是u32,假如一个中断源需要2个u32表示(一个表示中断号,另一个表示中断类型),那么#interrupt-cells就设置成2。
有些情况下,设备树的父节点不是中断的父节点(主要是中断控制器一般不是父节点),为此引入了interrupt-parent属性,该属性的类型是,用来引用中断父节点(我们前边说过,一般用父节点的标签,这个地方说中断父节点而不是中断控制器是有原因的)。如果设备树的父节点就是中断父节点,那么可以不用设置interrupt-parent属性。interrupts属性和interrupt-parent属性都是中断产生设备节点的属性,但是#interrupt-cells属性不是,#interrupt-cells属性是中断控制器节点以及interrupt nexus节点的属性,这两类节点都可能是中断父节点。

一个计算机系统中大量设备都是通过中断请求CPU服务的,所以设备节点中就需要在指定中断号。常用的属性有:

  • interrupt-controller: 一个空属性用来声明这个node接收中断信号,即这个node是一个中断控制器。
  • #interrupt-cells :是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符,用来描述子节点中interrupts属性使用了父节点中的interrupts属性的具体的哪个值。一般,如果父节点的该属性的值是3,则子节点的interrupts一个cell的三个32bits整数值分别为:<中断域 中断 触发方式>,如果父节点的该属性是2,则是<中断 触发方式>
  • interrupt-parent:标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的
  • interrupts:一个中断标识符列表,表示每一个中断输出信号。
  • reg : 在schips todo

这里重点说明一下,interrupts 属性,在ARM GIC(Generic Interrupt Controller)中:

备注:ARM GIC 说明文档位于:Documentation/devicetree/bindings/arm/gic.txt ;此外,本人并没有找到 #interrupt-cells为1个时的文档说明。

interrupt-cells为3时,interrupts包含三个cells,如interrupts = <0 168 4>

第一个cell代表中断类型:0 表示SPI中断,1 表示PPI中断。

第二个cell代表具体的中断类型:、

  • PPI中断:私有外设中断(Private Peripheral Interrupt),是每个CPU私有的中断。最多支持16个PPI中断,范围【0 - 15】。
  • SPI中断类型:公用外设中断(Shared Peripheral Interrupt),最多可以支持988个外设中断,范围【0 - 987】。

第三个cell代表中断触发标志:

  • bits [ 3 :0 ] 触发类型和级别标志:
    1 = 低- 至- 高边沿触发
    2 = 高- 到- 低边沿触发
    4 = 活跃的高水平 - 敏感
    8 = 低电平有效 - 敏感
  • bits [ 15 :8 ] PPI中断cpu掩码。每个位对应于每个位附加到GIC的8个可能的cpu。指示设置为"1"的位中断被连接到该CPU 。只有有效的PPI中断。

interrupt-cells为2时,interrupts包含2个cells,如interrupts = <2 4>

第一个cell代表具体的中断类型:

  • SGI中断:软件触发中断(Software Generated Interrupt),通常用于多核间通讯,最多支持16个SGI中断,硬件中断号从ID0~ID15。
  • PPI中断:私有外设中断(Private Peripheral Interrupt),是每个CPU私有的中断。最多支持16个PPI中断,硬件中断号从ID16~ID31。
  • SPI中断类型:公用外设中断(Shared Peripheral Interrupt),最多可以支持988个外设中断,硬件中断号从ID32~ID1019。

第二个cell代表中断触发标志:

bits [ 3 :0 ] 触发类型和级别标志:

  • 1 = 低- 至- 高边沿触发

  • 2 = 高- 到- 低边沿触发

  • 4 = 活跃的高水平- 敏感

  • 8 = 低电平有效- 敏感

    bits [ 15 :8 ] PPI中断cpu掩码。每个位对应于每个位附加到GIC的8个可能的cpu。指示设置为"1"的位中断被连接到该CPU 。只有有效的PPI中断。

/ {
    compatible = "acme,coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent = <&intc>;//指定依附的中断控制器是intc

    serial@101f0000 {   //子节点:串口设备
        compatible = "arm,pl011";
        reg = <0x101f0000 0x1000 >;
        interrupts = < 1 0 >;
    };

    intc: interrupt-controller@10140000 { //intc中断控制器
        compatible = "arm,pl190";
        reg = <0x10140000 0x1000 >;
        interrupt-controller;//定义为中断控制器设备
        #interrupt-cells = <2>;
    };
}

GPIO 属性

gpio也是最常见的IO口,常用的属性有:

  • "gpio-controller",用来说明该节点描述的是一个gpio控制器
  • "#gpio-cells",用来描述gpio使用节点的属性一个cell的内容,即 `属性 = <&引用GPIO节点别名 GPIO标号 工作模式>

通过上面的属性定义以后,就可以使用它,例如:

  2 &spi_1 {
  1     status = "okay";
388     cs-gpios = <&gpa2 5 GPIO_ACTIVE_HIGH>; // 使用 GPIO A2 第5个引脚,
  1
  2     w25q80bw@0 {
  3         #address-cells = <1>;
  4         #size-cells = <1>;
  5         compatible = "w25x80";
  6         reg = <0>;
  7         spi-max-frequency = <1000000>;
  8
  9         controller-data {
 10             samsung,spi-feedback-delay = <0>;
 11         };
 12

驱动自定义key属性

针对具体的设备,有部分属性很难做到通用,需要驱动自己定义好。
可以在设备树中自定义key属性,再在驱动中通过内核的属性提取解析函数进行值的获取。
比如:

/* 有关的 设备树写法 */
  6         ethernet@2,02000000 {
  5             compatible = "smsc,lan9118", "smsc,lan9115";
  4             reg = <2 0x02000000 0x10000>;
  3             interrupts = <15>;
  2             phy-mode = "mii";
  1             reg-io-width = <4>;
55              smsc,irq-active-high;   // 自定义key
  1             smsc,irq-push-pull;     // 自定义key
  2             vdd33a-supply = <&v2m_fixed_3v3>;
  3             vddvario-supply = <&v2m_fixed_3v3>;
  4         };
  5
  6         usb@2,03000000 {
  7             compatible = "nxp,usb-isp1761";
  8             reg = <2 0x03000000 0x20000>;
  9             interrupts = <16>;
arch/arm/boot/dts/vexpress-v2m-rs1.dtsi 

/* 有关的驱动写法 */

2389     if (of_get_property(np, "smsc,irq-active-high", NULL))
   1         config->irq_polarity = SMSC911X_IRQ_POLARITY_ACTIVE_HIGH;
   2
   3     if (of_get_property(np, "smsc,irq-push-pull", NULL))
   4         config->irq_type = SMSC911X_IRQ_TYPE_PUSH_PULL;
   5
   6     if (of_get_property(np, "smsc,force-internal-phy", NULL))
   7         config->flags |= SMSC911X_FORCE_INTERNAL_PHY;
   8
   9     if (of_get_property(np, "smsc,force-external-phy", NULL))
  10         config->flags |= SMSC911X_FORCE_EXTERNAL_PHY;
  11
  12     if (of_get_property(np, "smsc,save-mac-address", NULL))
  13         config->flags |= SMSC911X_SAVE_MAC_ADDRESS;
  14
  15     return 0;
drivers/net/ethernet/smsc/smsc911x.c 

附录:补充对于interrupt-parent的一些知识点

为什么会有interrupt-parent

首先讲讲Linux设备管理中对中断的设计思路演变。随着linux kernel的发展,在内核中将interrupt controller抽象成irqchip这个概念越来越流行,甚至GPIO controller也可以被看出一个interrupt controller chip,这样,系统中至少有两个中断控制器了。另外,在硬件上,随着系统复杂度加大,外设中断数据增加,在这种趋势下,内核中原本的中断源直接到中断号的方式已经很难继续发展了,为了解决这些问题,linux kernel的大牛们就创造了irq domain(中断域)这个概念。domain在内核中有很多,除了irqdomain,还有power domain,clock 这些domain等等;所谓domain,就是领域,范围的意思(即:任何的定义出了这个范围就没有意义了)。

实际上系统可以需要多个中断控制器进行级联,形成事实上的硬件中断处理结构:

img

如上所述,系统中所有的interrupt controller会形成树状结构,对于每个interrupt controller都可以连接若干个外设的中断请求(interrupt source,中断源),interrupt controller会对连接其上的interrupt source(根据其在Interrupt controller中物理特性)进行编号(也就是HW interrupt ID了)。有了irq domain这个概念之后,这个编号仅仅限制在本interrupt controller范围内。

有了这样的设计,CPU(Linux 内核)就可以根据级联的规则一级一级的找到想要访问的中断。当然,通常我们关心的只是内核中的中断号,具体这个中断号是怎么找到相应的中断源的,我们作为程序员往往不需要关心。

在写设备树的时候,设备树就是要描述嵌入式软件开发中涉及的所有硬件信息。所以,设备树就需要准确描述硬件上处理中断的这种树状结构,如此,就有了我们的interrupt-parant这样的概念:用来连接这样的树状结构的上下级,用于表示这个中断归属于哪个interrupt controller,比如,一个接在GPIO上的按键,它的组织形式就是:

中断源--interrupt parent-->GPIO--interrupt parent-->GIC1--interrupt parent-->GIC2--...-->CPU

有了parant,我们就可以使用一级一级的偏移量来最终获得当前中断的绝对编号。

可以看出,在我板子上的dm9000的的设备节点中,它的"interrupt-parent"引用了"exynos4x12-pinctrl.dtsi"(被板级设备树的exynos4412.dtsi包含)中的gpx0节点:
img

而在gpx0节点中,指定了#interrupt-cells = <2>;,所以在dm9000中的属性interrupts = <6 4>;表示dm9000的的中断在作为irq parant的gpx0中的中断偏移量,即gpx0中的属性interrupts中的<0 22 0>,通过查阅exynos4412的手册知道,对应的中断号是EINT[6]。

img

如果说我的文章对你有用,只不过是我站在巨人的肩膀上再继续努力罢了。
若在页首无特别声明,本篇文章由 Schips 经过整理后发布。
博客地址:https://www.cnblogs.com/schips/
原文地址:https://www.cnblogs.com/schips/p/the_syntax_of_dts.html