• 设备树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

  • 相关阅读:
    信息安全系统设计基础第八周期中复习总结
    layui下各种富文本的冲突情况
    TP3.2+find_set_in 以及 find_set_in和like的区别
    tp5+linux+apache php7.1.30环境下,上传图片报错:mkdir():permission denied
    一次基于老古董thinkPHP3.1的修改尝试
    微信网页开发 thinkphp5.0的try-catch和重定向
    CentOS 7.2下服务器配置(linux+apache+php+mysql)
    微信小程序踩坑(不定时更新)
    PHP 定时自动执行代码
    PHP TP5 文章评论+积分+签到
  • 原文地址:https://www.cnblogs.com/schips/p/12208681.html
Copyright © 2020-2023  润新知