Linux设备驱动开发之设备树(Device Tree)

点击关注"嵌入式IoT杂谈”公众号,选择“星标公众号”干货福利,第一时间送达!

转发于个人公众号内容:Linux设备驱动开发之设备树(Device Tree)

一、什么是设备树

设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做DTS(Device Tree Source),这个DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU数量、内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。

二、DTS、DTB和DTC

DTS是设备树源码文件,DTB是将DTS编译以后得到的二进制文件。将.c文件编译为.o需要用到gcc编译器,那么将.dts编译为.dtb需要什么工具呢?需要用到DTC工具!DTC工具源码在Linux内核的scripts/dtc目录下,详见scripts/dtc/Makefile文件。

三、DTS语法

1.dtsi头文件

和C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。比如在imx6ull-evk-emmc.dts中有如下所示内容:

  #include <dt-bindings/clock/imx6ul-clock.h> 
  #include <dt-bindings/gpio/gpio.h>
  #include <dt-bindings/input/input.h>
  #include <dt-bindings/interrupt-controller/arm-gic.h> 
  #include "imx6ull-pinfunc.h" 
  #include "imx6ull-pinfunc-snvs.h" 
  #include "skeleton.dtsi"
  #include "imx6ull.dtsi"

.dts文件可以引用C语言中的.h文件,甚至也可以引用.dts文件。引用C语言中的头文件一般是使用到其中的宏定义等,而.dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范围,比如UART、 IIC等等。

2.设备节点

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键值对。从语法上看,跟XML语法有点相类似。例如imx6ull.dtsi文件中缩减出来的设备树文件内容如下:

1 / {
    
     
2   aliases {
    
     
3       can0 = &flexcan1; 
4   }; 
5 
6   cpus {
    
     
7       #address-cells = <1>; 
8       #size-cells = <0>; 
9 
10      cpu0: cpu@0 {
    
     
11          compatible = "arm,cortex-a7"; 
12          device_type = "cpu"; 
13          reg = <0>; 
14      }; 
15  }; 
16 
17  intc: interrupt-controller@00a01000 {
    
     
18      compatible = "arm,cortex-a7-gic"; 
19      #interrupt-cells = <3>; 
20      interrupt-controller; 
21      reg = <0x00a01000 0x1000>, <0x00a02000 0x100>; 
22  }; 
23 }

第1行,“/”是根节点,每个设备树文件只有一个根节点。而在其他文件中也有“/”根节点。其实存在多个根节点语法上的支持的,共存的。在编译设备树的时候会自动把多个根节点合并成一个根节点。

第2、6和17行,aliases、cpus和intc是三个子节点,在设备树中节点命名格式如下:

node-name@unit-address

其中“node-name”是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interrupt-controller@00a01000”。

但是在第10行中,我们看到的节点命名却如下所示:

cpu0:cpu@0

上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”前面的是节点标签(label),“:”后面的才是节点名字,格式如下所示:

label: node-name@unit-address

引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,比如通过&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点“intc: interrupt-controller@00a01000”,节点label是intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc来访问“interrupt-controller@00a01000”这个节点要方便很多!

第10行,cpu0也是一个节点,只是cpu0是cpus的子节点。每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:

①、字符串

compatible = “arm,cortex-a7”;

上述代码设置compatible属性的值为字符串“arm,cortex-a7”。

②、32位无符号整数

reg = <0>;

上述代码设置reg属性的值为0,reg的值也可以设置为一组值,比如:

reg = <0 0x123456 100>;

③、字符串列表

属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:

compatible = “fsl,imx6ull-gpmi-nand”, “fsl, imx6ul-gpmi-nand”;

上述代码设置属性compatible的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。

3.标准属性

节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用这些标准属性,接下来将介绍几个常用的标准属性。

compatible属性

compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值是一个字符串列表,compatible属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible属性的值格式如下所示:

“manufacturer,model”

其中manufacturer表示厂商,model一般是模块对应的驱动名字。例如:

 compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";

属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和fsl,imx-audio-wm8960”,其中fsl表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和imx-audio-wm8960”表示驱动模块名字。sound这个设备首先使用第一个兼容值在 Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在文件imx-wm8960.c中有如下内容:

1    static const struct of_device_id imx_wm8960_dt_ids[] = {
    
     
2        {
    
     .compatible = "fsl,imx-audio-wm8960", }, 
3        {
    
     /* sentinel */ } 
4    }; 
5
6    MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids); 
7    
8    static struct platform_driver imx_wm8960_driver = {
    
     
9         .driver = {
    
     
10        .name = "imx-wm8960", 
11        .pm = &snd_soc_pm_ops, 
12        .of_match_table = imx_wm8960_dt_ids, 
13        }, 
14        .probe = imx_wm8960_probe, 
15        .remove = imx_wm8960_remove, 
16    };
        

其中第1~4行的数组imx_wm8960_dt_ids就是imx-wm8960.c这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的compatible属性值与此相等,那么这个节点就会使用此驱动文件。

第12行,wm8960采用了platform_driver驱动模式,此行设置.of_match_table为imx_wm8960_dt_ids,也就是设置这个platform_driver所使用的OF匹配表。

model属性

model属性值也是一个字符串,一般model属性描述设备模块信息,比如名字等等。

model = “wm8960-audio”;

status属性

status属性和设备状态有关,status属性值也是字符串,即表示设备的状态信息。可选的状态如下:

描述
“okay” 表明设备是可操作的
“disabled” 表明设备当前是不可操作的,但可以变为可操作的,如热插拔设备插入后。具体含义还需看设备的绑定文档。
“fail” 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作
“fail-sss” 含义和“fail”相同,后面的 sss部分是检测到的错误内容。

#address-cells和 #size-cells属性(property)

这两个属性的值都是无符号32位整形,#address-cells和#size-cells这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells属性值决定了子节点reg属性中地址信息所占用的字长(32位),#size-cells属性值决定了子节点reg属性中长度信息所占的字长(32位)。

#address-cells和#size-cells表明了子节点应该如何编写reg属性值,一般 reg属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg属性的格式一为:

reg = <address1 length1 address2 length2 address3 length3…………>

每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长度#address-cells表明address这个数据所占用的字长,#size-cells表明length这个数据所占用的字长。例如:

1 spi4 {
    
     
2   compatible = "spi-gpio"; 
3   #address-cells = <1>; 
4   #size-cells = <0>; 
5 
6   gpio_spi: gpio_spi@0 {
    
     
7       compatible = "fairchild,74hc595"; 
8       reg = <0>; 
9   }; 
10 }; 
11 
12 aips3: aips-bus@02200000 {
    
     
13  compatible = "fsl,aips-bus", "simple-bus"; 
14  #address-cells = <1>; 
15  #size-cells = <1>; 
16 
17  dcp: dcp@02280000 {
    
     
18      compatible = "fsl,imx6sl-dcp"; 
19      reg = <0x02280000 0x4000>; 
20  }; 
21 };

第3、4行,节点spi4的#address-cells = <1>,#size-cells = <0>,说明spi4的子节点reg属性中起始地址所占用的字长为1,地址长度所占用的字长为0。

第8行,子节点gpio_spi: gpio_spi@0的reg属性值为<0>,因为父节点设置了#address-cells = <1>,#size-cells = <0>,因此addres=0,没有length的值,相当于设置了起始地址,而没有设置地址长度。

第14、15行,设置aips3: aips-bus@02200000节点#address-cells = <1>,#size-cells = <1>
说明aips3: aips-bus@02200000节点起始地址长度所占用的字长为1,地址长度所占用的字长也为1。

第19行,子节点 dcp: dcp@02280000的 reg属性值为<0x02280000 0x4000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>; address= 0x02280000 , length= 0x4000,相当于设置了起始地址为0x02280000,地址长度为0x40000。

Example1:

/ {
    
    
        #address-cells = <0x1>;    // 在 root node 下使用 1 个 u32 来代表 address。
        #size-cells = <0x0>;       // 在 root node 下使用 0 个 u32 来代表 size。
        ...
        ...
        memory {
    
            // memory device
                ...
                reg = <0x90000000>;
                // 0x90000000 是存取 memory 的 address
                ...
	};
        ...
        ...
}

Example2:

/ {
    
    
        #address-cells = <0x1>;    // 在 root node 下使用 1 个 u32 来代表 address。
        #size-cells = <0x1>;       // 在 root node 下使用 1 个 u32 来代表 size。
        ...
        ...
        memory {
    
            // memory device
                ...
                reg = <0x90000000 0x800000>;
                // 0x90000000 是存取 memory 的 address
                // 0x800000 是 memory 的 size。
                ...
        };
        ...
        ...
}

Example3:

/ {
    
    
        #address-cells = <0x2>;    // 在 root node 下使用 2 个 u32 来代表 address。
        #size-cells = <0x1>;       // 在 root node 下使用 1 个 u32 来代表 size。
        ...
        ...
        memory {
    
            // memory device
                ...
                reg = <0x90000000 00000000 0x800000>;
                // 0x90000000 00000000 是存取 memory 的 address
                // 0x800000 是 memory 的 size。
                ...
	};
        ...
        ...
}

Example4:

/ {
    
    
        #address-cells = <0x2>;    // 在 root node 下使用 2 个 u32 来代表 address。
        #size-cells = <0x2>;       // 在 root node 下使用 2 个 u32 来代表 size。
        ...
        ...
        memory {
    
            // memory device
                ...
                reg = <0x90000000 00000000 0x800000 00000000>;
                // 0x90000000 00000000 是存取 memory 的 address
                // 0x800000 00000000 是 memory 的 size。
                ...
        };
        ...
        ...
}

reg属性

reg属性的值一般是 (address length)对。reg属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。

ranges属性

ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵, ranges是一个地址映射/转换表,ranges属性每个项目由子地址、父地址和地址空间长度这三部分组成:

child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占用的字长。

parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells确定此物理地址所占用的字长。

length:子地址空间的长度,由父节点的#size-cells确定此地址长度所占用的字长。

如果 ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。例如:

soc {
    
     
	#address-cells = <1>; 
	#size-cells = <1>; 
	compatible = "simple-bus"; 
	interrupt-parent = <&gpc>; 
	ranges; 
	......
} 

定义了ranges属性,但是ranges属性值为空。ranges属性不为空的示例代码如下所示:

soc {
    
     
	#address-cells = <1>; 
	#size-cells = <1>; 
	compatible = "simple-bus"; 
	interrupt-parent = <&gpc>; 
	ranges = <0x0 0xe0000000 0x00100000>; 

	serial {
    
     
		device_type = "serial"; 
		compatible = "ns16550"; 
		reg = <0x4600 0x100>; 
		clock-frequency = <0>; 
		interrupts = <0xA 0x8>; 
		interrupt-parent = <&ipic>; 
	}; 
}

节点soc定义的ranges属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为0x0,父地址空间的物理起始地址为0xe0000000。

serial是串口设备节点,reg属性定义了serial设备寄存器的起始地址为0x4600,寄存器长度为0x100。经过地址转换,serial设备可以从0xe0004600开始进行读写操作,0xe0004600=0x4600+0xe0000000。

name属性

name属性值为字符串,用于记录节点名字,name属性已被弃用,不推荐使用。一些老的设备树文件可能会使用此属性。

device_type属性

device_type属性值为字符串,IEEE 1275会用到此属性,用于描述设备的FCode,但是设备树没有FCode,所以此属性也被抛弃了。此属性只能用于 cpu节点或者 memory节点。例如:

cpu0: cpu@0 {
    
     
  compatible = "arm,cortex-a7"; 
  device_type = "cpu"; 
  reg = <0>; 
  ...... 
};

根节点compatible属性

每个节点都有compatible属性,根节点“/”也不例外。例如:

/ {
    
    
  model = "Freescale i.MX6 ULL 14x14 EVK Board"; 
  compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
  ......
};

在内核中的驱动程序,那么根节点中的compatible属性是为了可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的SOC,比如这里使用的是imx6ull”这颗SOC。Linux内核会通过根节点的compoatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。

4.特殊节点

在根节点“/”中有两个特殊的子节点分别为aliases和chosen。

aliases子节点

aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label的形式来访问节点。

chosen子节点

chosen并不是一个真实的设备,chosen节点主要是为了uboot向Linux内核传递数据,重点是bootargs参数。一般.dts文件中chosen节点通常为空或者内容很少。但是当进入到 /proc/device-tree/chosen目录里面,会发现多了bootargs这个
属性。如图示:

没有在设备树上定义有,而查看的时候存在bootargs参数的原因是uboot在启动Linux内核的时候会将bootargs的值传递给 Linux内核,bootargs会作为Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(也就是uboot传递进来的 bootargs的值)。

在启动Linux内核之前,只有uboot知道bootargs环境变量的值,并且uboot也知道.dtb设备树文件在DRAM中的位置,在uboot源码搜索“chosen”,发现在common/fdt_support.c文件中有“chosen”。fdt_support.c文件中有个fdt_chosen函数。这里就不贴代码了。函数的作用大概为从设备树找到chosen节点,若找不到则创建;读取uboot环境变量的内容;添加环境变量等内容。那么fdt_chosen函数被调用如下图所示:

四、推荐参考文档

1、参考韦东山的设备树讲解。

2、参考设备树的官方文档:Specifications - DeviceTree

3、linux设备树文档:\Documentation\devicetree\

猜你喜欢

转载自blog.csdn.net/Youning_Yim/article/details/127413555