目录
一、设备树的引入
首先需要了解Linux系统平台总线设备驱动模型的框架在这篇博客中我概述了如何快速通过平台总线设备驱动模型来编写驱动程序。在这个模型中,我们提供了通用的硬件驱动程序,对于不同的单板,我们只需要提供对应的board_xxx.c提供对应的硬件资源即可。那么出现一个问题,对于同一款芯片我们有成千上万的单板,因此就会存在成千上万的单板程序在内核当中。另外一个问题是,当你需要在单板中更改硬件资源,就需要重新编译对应的单板文件。总结起来:
这些缺点的根源在于我们使用.c文件来指定这些硬件资源。
解决方法:
用配置文件来指定这些硬件资源。配置文件通过设备树文件DTS来写 。
二、设备树语法
为啥叫树:

2.1 如何描述树
拿上图举例来讲
/{
cpu{
name = val; // name的名字可以随便取,但是val的取值不可以随便取。val 有最基本的三种取值方式
};// CPU节点
i2c{
at24c02{
}; // 子节点
}; // I2C节点
}; // 描述根节点
属性中val的取值有三种方式
2.2 DTS文件格式
/{
[property definitions]
[child nodes]
};
2.3 node 的格式
设备树中的基本单元被成为node,其格式为:
[label:]node-name[@unit-address]{
[property definitions]
[chiled nodes]
}
label是标号,可以省略,label的作用是为了方便的引用node。
/{
uart0:uart@fe001000{
compatible = "ns16550"
reg = <0xfe001000 01000>
};
};
// 日后想要修改上边定义的串口,可以采用label直接引用
&uart0{
status = "disabled";
}
2.4 常用的节点
2.4.1、根节点
dts文件中必须有根节点。
根节点中必须有的属性
#address-cells //在它的子节点的reg属性中,用多少个32位整数来描述地址。
#size-cells // 在它的子节点的reg属性中,用多少个u32来描述大小。
#compatible // 定义一系列的字符串,用来表示这个板子兼容哪些平台。
#model // 当前的这个板子是什么
2.4.2、CPU节点
一般不需要设置在dtsi文件中定义好了。
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
.......
}
};
2.4.3、memory节点
芯片厂家不能事先确定你的板子使用多大的内存,因此memory节点需要板厂设置,比如:
memory{
reg = <0X80000000,0X20000000>;
};
2.4.4、chosen节点
通过设备树文件给内核传入一些参数,需要在chosen节点中设置bootargs属性:
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
2.5 常用的属性
2.5.1、#address-cells、#size-cells
#addrdss-cells :用多少个32位的数据来表示地址
#size-cells:用多少个32位的数据来表示大小。
这两个属性影响到的是节点里边的reg属性
2.5.2、compatible
表示兼容属性,用来寻找驱动程序。
led{
compatible = "A","B","C";
}; // 表示这个LED或者硬件兼容驱动A、B、C
2.5.3、model
model属性和compatible属性类似,compatible属性表示这个硬件可以兼容一些驱动。
model用来准确的定义这个硬件是什么。
/{
compatible = "sasung,smdk2440","samsung,mini2440";
model = "jz2440_v3";
};
表示这个单板,可以兼容内核中smdk2440,也兼容mini2440,但是他是jz2440_v3。它可以用内核中的smdk2440,也可以用内核中mini2440来进行初始化。
2.5.4、status
status的常见用法如下

&uart0{
status = "disabled";
};
4.1dts文件包含dtsi文件
设备树文件不需要我们从零写出来,如果内核支持某款芯片,在内核的arch/arm/boot/dts目录下就有能用的设备树模板,一般命名为xxx.dtsi。i表示include ,表示被别的文件引用。
dtsi文件跟dts文件的语法是完全相同的。
dts中包含.h头文件,也可以包含dtsi文件。
/dts-v1/;
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
/ {
……
};
2.4.5、reg
在设备树里用来描述一段空间。reg属性的值,是一系列的address size,用多少个32位的数来表示address和size由其父节点的#address-cells、#size-cells决定。
/{
#address-cells = <1>;
#size-cells = <1>;
memory{
reg = <0x80000000,0x20000000>;
};
};
三、 编译、更换设备树
程序员一般不需要从零写dts文件,而是修改,改的对不对,就需要编译一下。内核中直接使用dts文件太过于低效,因此需要使用二进制格式的dtb文件。
1、在内核目录中直接make
设置好ARCH、CROSS_COMPILE、PATH这三个环境变量后,进入ubuntu上板子内核源码目录,执行如下命令即可编译得到dtb文件。
make dtbs V=1
2、手工编译
除非对设备树非常了解,否则不建议手工使用dtc工具直接编译。
内核目录下 scripts/dtc/dtc 是设备树的编译工具,直接使用它编译dts文件时,dts文件包含其他文件时不能使用"#include",而必须使用"/include"
编译、反编译实例命令如下,"-I"指定输入格式,"-O"指定输出格式,"-o"指定输出文件。
./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts // 编译dts为dtb
./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb//反编译dtb为dts
3、给板子更换设备树文件
基本方法都是:设置好ARCH、CROSS_COMPILE、PATH三个环境变量后,在内核源码目录中执行:
make dtbs
启动板子后,更换板子上的dtb文件
四、内核对设备树的处理
从源代码文件dts文件开始,设备树的处理过程为:

4.1 dtb中每一个节点都被转换为device_node结构体
struct device_node {
const char *name; // 设备树中常用属性name(因为已经过时,因此在设备树常用属性中没有做介绍)
const char *type; // 设备树常用属性type(因为已经过时,因此在设备树常用属性中没有做介绍)
phandle phandle;
const char *full_name;
struct fwnode_handle fwnode;
struct property *properties; // 节点属性
struct property *deadprops; /* removed properties */
struct device_node *parent; // 父亲节点
struct device_node *child; // 孩子节点
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
struct property {
char *name; // 属性的名字
int length; // 属性值的长度
void *value; // 属性的值
struct property *next;
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
根节点被保存在全局变量of_root中,从of_root开始可以访问到任意节点。
4.2 哪些设备树节点会被转化为platform_device
转化规则
规则 |
---|
根节点下含有compatible属性的子节点 |
含有特定compatible属性的节点的子节点。如果一个节点的compatible属性,它的值是下列之一: “simple-bus”、“simple-mfd”、“isa”、“arm,amba-bus” ,那么它的子节点(需要含compatible属性)也可以转化为platform_device |
总线I2C、SPI节点下的子节点:不转化为platform_device |
举例说明:
/ {
mytest { // mytest属于根节点下的节点,并且含有compatible属性,并且兼容simple-bus,因此可以转化为platform_device。
compatile = "mytest", "simple-bus";
mytest@0 { // mytest@0 属于根节点下的子节点,其父节点兼容“simple-bus”
compatile = "mytest_0";
};
i2c { // i2c可以转换为platform_device
compatile = "samsung,i2c";
at24c02 { // at24c02 不可以转化,如何处理由父节点的Platfom_driver决定
compatile = "at24c02";
};
};
spi { // spi可以转化为platform_device
compatile = "samsung,spi";
flash@0 { //flash@0 不可以转换为platform_device
compatible = "winbond,w25q32dw";
spi-max-frequency = <25000000>;
reg = <0>;
};
};
};
4.3 platform_device配对platform_driver规则
platform_device 会被注册进内核中,当我们注册一个platform_driver时,它们就会两两确定能否配对,如果能配对成功,就去调用platform_driver的prob函数。下边介绍匹配规则

4.3.1、最先比较是否强制选择某个driver。
比较 platform_device. driver_override 和 platform_driver.driver.name可以设置 platform_device 的 driver_override,强制选择某个 platform_driver。
4.3.2、比较设备树信息
比较platform_device. dev.of_node和platform_driver.driver.of_match_table。
由设备树节点转换得来的 platform_device 中,含有一个结构体:of_node。它的类型如下
struct device_node {
const char *name;// 来自节点的name属性
const char *type;// 来自节点的type属性
phandle phandle;
const char *full_name;
struct fwnode_handle fwnode;
struct property *properties;//含有compatible属性
struct property *deadprops; /* removed properties */
struct device_node *parent;
struct device_node *child;
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
struct property {
char *name; // 属性的名字
int length; // 属性值的长度
void *value; // 属性的值
struct property *next;
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
如果一个 platform_driver 支持设备树,它的 platform_driver.driver.of_match_table是一个数组,类型如下:
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
使用设备树信息来判断dev和drv是否配对
1、首先,如果 of_match_table 中含有 compatible 值,就跟 dev 的 compatile 属性比较,若一致则成功,否则返回失败;
2、其次,如果 of_match_table 中含有 type 值,就跟 dev 的 device_type 属性比较,若一致则成功,否则返回失败;
3、最后,如果 of_match_table 中含有 name 值,就跟 dev 的 name 属性比较,若一致则成功,否则返回失败。
而设备树中建议不再使用 devcie_type 和 name 属性,所以基本上只使用设备节点的 compatible 属性来寻找匹配的platform_driver。
4.3.3、比较:platform_device_id
比较 platform_device. name 和platform_driver.id_table[i].name,id_table 中可能有多项。platform_driver.id_table 是“platform_device_id”指针,表示该 drv 支持若干个 device,它里面列出了各个 device 的{.name, .driver_data},其中的“name”表示该 drv 支持的设备的名字,driver_data是些提供给该 device 的私有数据。
4.4.4、最后比较platform_device.name 和 platform_driver.driver.name
platform_driver.id_table 可能为空,这时可以根据 platform_driver.driver.name 来寻找同名的 platform_device。
4.4 设备树节点转换成的platform_device和platform_driver匹配成功后,driver如何获得device中的硬件资源
这些硬件资源是从设备节点中来的。
举例:
/{
mytest{
compatible = "mytest";
reg = ...;
interrupts = ...;
}
}
如果节点中含有reg属性,它就会转化为IORESOURCE_MEM类型的资源。
如果节点中含有interrupts属性,它就会转化为IORESOURCE_IRQ类型的资源。
然后就可以使用platform_get_resource函数,来获得这些资源。
对于其他的属性,比如pin属性,他不是标准的属性,内核有提供其他函数。
4.5 没有转化为platform_device的设备树节点的访问方法
内核提供相应的函数帮助我们找到节点、找到属性、获取属性的值。
找到节点 |
---|
of_find_node_by_path // 根据路径找到节点 |
of_find_node_by_name // 根据名字找到节点 |
of_find_compatible_node // 根据属性找到节点 |
of_find_compatible_node // 找到父节点 |
of_get_next_child // 取出下一个节点 |
of_get_next_available_child // 取出下一个可用的节点 |
of_get_child_by_name // 根据名字取出子节点 |
找到属性 |
---|
of_find_property // 找到节点中的属性 |
获取属性的值 |
---|
of_get_property // 根据名字找到节点的属性,并且返回它的值。 |
五、总结
内核对于DTB文件的每一个节点都会构造出一个device_node。对于某些device_node会被构造成platform_device。这些platform_device和platform_driver匹配有相应的规则。当platform_driver和platform_device匹配成功后,platform_driver的probe函数就会被调用,在probe函数,我们需要获取device中的资源。可以使用platform_get_resources获取里边的memory和IRQ资源。这些memory资源和IRQ资源来自设备树节点中两个比较特殊的属性:reg 和 interrupts。如果获得其他的非标准属性,内核也有提供相应的函数。对于某些节点不能转换为platform_device,也可以使用内核提供的函数来找到这些节点,来得到它们的属性。