前言
掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树。本篇博客重点介绍一下设备树与设备树语法。
嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!
目录
- 前言
- 1. 设备树简介
- 1.1 设备树介绍
- 1.2 dtb、dts、dtc、dtsi文件的关系
- 1.3 编译设备树
- 1.4 设备树特点
- 2. 设备树语法
- 2.1 基本构成
- 2.2 节点的格式
- 2.3 节点属性
- 2.3.1 compatible属性
- 2.3.2 model属性
- 2.3.3 status属性
- 2.3.4 #address-cells和#size-cell
- 2.3.5 reg属性
- 2.3.6 ranges属性
- 2.3.7 name和device_type
- 2.3.8 特殊节点
- 2.3.8.1 aliases子节点
- 2.3.8.2 chosen子节点
- 3. 获取设备树节点信息
- 3.1 查找节点
- 3.1.1 根据节点路径:
- 3.1.2 根据节点类型和compatible属性寻找节点函数
- 3.1.3 其他方式
- 3.2 获取属性值
- 3.2.1 device_node结构体
- 3.2.2 获取节点属性
- 3.2.3 其他of函数
- 4. Linux设备树调试
- 参考资料
1. 设备树简介
1.1 设备树介绍
描述设备树的文件叫DTS,该文件采用树形结构描述板级设备即开发板上的设备信息:CPU数量,内存基地址,IIC接口上接了哪些设备,如下所示:
设备树是一种描述硬件的数据结构,在Linux3.x版本上才开始使引入,采用了设备树之后,许多硬件的细节可以直接通过它传递给Linux,而不再需要在内核中进行大量的冗余编码,它通过bootloader将硬件资源传给内核,使得内核和硬件资源描述相对独立。
ARM 社区引入了 PowerPC 等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts。一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。
1.2 dtb、dts、dtc、dtsi文件的关系
这四个代表四种不同的文件格式,可以类比到C语言中的相关知识来理解。
我们都知道,C语言编写.c文件的时候需要添加C库.h文件来添加我们所需要用到的函数,宏等。然后要用编译器将.c文件编译成计算机能理解的二进制文件。
同样的,在设备树中,dts是设备树源码,相当于.c文件,是我们编写和能看懂的文件,然后需要添加.dtsi文件来得到一些板级信息,相当于.h文件。最后要将这个文本文件编译成计算机理解的二进制文件,即用dtc编译工具编译成.dtb这个二进制文件。总结下来,对应关系如下所示:
.dts --> .c文件
.dtsi --> .h文件
.dtb --> .exe文件
dtc --> 编译器
dtc的源码存放于scripts/dtc目录中,对应于该目录下Makefile中hostprogs-y:=dtc
这一编译目标
生成dts文件对应的dtb文件
dtc -I dts -O dtb -o xxx.dtb xxxdts
反过来生成dts文件
dtc -I dtb -O dts -o xxx.dts xxxdtb
1.3 编译设备树
进入到Linux源码根目录下,然后执行如下指令进行编译:
make dtbs (这个指令只编译设备树)
或者
make all (这个指令是编译所有的东西,包括.ko,zImage)
1.4 设备树特点
设备树可以用树状结构描述硬件资源,如上图所示,在根节点/
下,挂载本地总线的SPI总线,UART总线等的树干为根节点的子节点。若是SPI下的设备不止一个,那么又可以从这根树枝下分出枝干
设备树可以复用,例如多个硬件平台都使用i.MX6ULL作为主控芯片, 那么我们可以将i.MX6ULL芯片的硬件资源写到一个单独的设备树文件里面一般使用“.dtsi”后缀, 其他设备树文件直接使用“# includexxx”引用即可。
2. 设备树语法
设备树文件存放地址:
源码地址/arch/arm/boot/boot/dts
此处打开正点原子的imx6ull-alientek-emmc.dts
文件来学习一下设备树语法,此处提一个事情,设备树语法中的注释用/* ... */
表示
2.1 基本构成
首先来看以下一段:
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
这一段首先是包含头文件,设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。 imx6ull.dtsi由NXP官方提供,是一个imx6ull平台“共用”的设备树文件。.dts 文件引用 C 语言中的.h 文件,甚至也可以引用.dts 文件。
/ {model = "Freescale i.MX6 ULL 14x14 EVK Board";compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";key {#address-cells = <1>;#size-cells = <1>;compatible = "atkmini-key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_FALLING>;status = "okay";};gpioled {#address-cells = <1>;#size-cells = <1>;compatible = "atkmini-gpioled";pinctrl-names = "default";pinctrl-0 = <&pinctrl_led>;led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;status = "okay";};mini {#address-cells = <1>;#size-cells = <0>;compatible = "atkmini-led";status = "okay";reg = < 0x020c406c 0x04 /* CCM_CCGR1_BASE */0x020e0068 0x04 /* SW_MUX_GPIO1_IO03_BASE */0x020e02f4 0x04 /* SW_PAD_GPIO1_IO03_BASE */0x0209c000 0x04 /* GPIO1_DR_BASE */0x0209c004 0x04>; /* GPIO1_GDIR_BASE */};chosen {stdout-path = &uart1;};dht11 {compatible = "alientek, dht11";pinctrl-names = "default";pinctrl-0 = <&pinctrl_dht11>;dht11-gpio = <&gpio1 1 GPIO_ACTIVE_LOW>;status = "okay";};ds18b20 {compatible = "alientek, ds18b20";pinctrl-names = "default";pinctrl-0 = <&pinctrl_ds18b20>;ds18b20-gpio = <&gpio1 1 GPIO_ACTIVE_LOW>;status = "okay";};memory {reg = <0x80000000 0x20000000>;};reserved-memory {#address-cells = <1>;#size-cells = <1>;ranges;linux,cma {compatible = "shared-dma-pool";reusable;size = <0x8000000>;linux,cma-default;};};/* 以下内容省略
}
这一段是设备树节点,每个{}
就是一个节点,最外面的/{...}
就是根节点,每个设备树只有一个根节点。但是如果打开imx6ull.dtsi
文件可以发现它也有一个根节点,虽然imx6ull-alientek-emmc.dts
引用了imx6ull.dtsi
文件, 但这并不代表imx6ull-alientek-emmc.dts
设备树有两个根节点,因为不同文件的根节点最终会合并为一个。
然后我们可以看到根节点内部也有很多{...}
,比如ds18b20 {...}
、memory {}
这些都是根节点的子节点。
最后来看下一段:
&cpu0 {arm-supply = <®_arm>;soc-supply = <®_soc>;dc-supply = <®_gpio_dvfs>;
};&clks {assigned-clocks = <&clks IMX6UL_CLK_PLL4_AUDIO_DIV>;assigned-clock-rates = <722534400>;
};&csi {status = "okay";port {csi_ep: endpoint {remote-endpoint = <&camera_ep>;};};
};
这一部分是设备树节点的追加内容,最明显的特点就是添加了一个&
符号。该符号表示向已经存在的子节点追加数据,这些已经存在的节点可以是本文件中的,也可以是#include中定义的。
2.2 节点的格式
知道了设备树的组成后,来具体看看一个节点如何定义:
node-name@unit-address {属性1 = ""属性2 = ""属性3 = ""子节点
}
node-name
是节点名称,长度为1~31个字符,最好使用大写或小写字母开头,且能描述设备类别。根节点是一个特殊的节点,其用/
指代。
@unit-address
是指定单元地址,@可理解为分隔符,unit-address
的值要与节点“reg”属性的第一个地址一致,如果没有reg节点,可以省略,这时就要求同级设备树下,节点名唯一。因此要么节点名唯一,要么节点名重复单单元地址不同,总之就是node-name@unit-address
这个整体要求同级唯一。
还有一种方式就是添加了节点标签:
label:node-name@unit-address
比如:
cpu0:cpu@0
引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。此外还有一个很重要的作用就是对接点进行扩展,当其他位置需要引用时可以使用节点标签来向该节点中追加内容。
2.3 节点属性
设备树源码中常用的几种数据形式:字符串、32位无符号整数
2.3.1 compatible属性
属性值类型:字符串
一般compatible属性的格式如下所示,manufacturer 表示厂商,model 一般是模块对应的驱动名字。
compatible = "manufacturer,model"
例如:
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
设备树中的每一个代表了一个设备的节点都要有一个compatible属性。 compatible是系统用来决定绑定到设备的设备驱动的关键。 compatible属性是用来查找节点的方法之一。
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。如下:
static const struct of_device_id imx6ull_of_match {{.compatible = "fsl,imx6ull-14x14-evk"},{ //Sentinel }
};static struct platform_driver imx6ull_driver = {.driver = {.name = "xxx",.of_match_table = imx6ull_of_match,},.probe = imx6ull_probe,.remove = imx6ull_remove,
};
2.3.2 model属性
属性值类型:字符串
一般 model 属性描述设备模块信息,比如名字什么的。
model = "wm8960-audio";
2.3.3 status属性
属性值类型:字符串
该属性是设备的状态信息,可选状态如表所示:
status值 | 描述 |
---|---|
“okay” | 表示设备可操作 |
“disable” | 表示设备当前是不可操作的,但在未来可以变为可操作,比如热插拔设备插入后 |
“fail” | 表明设备不可操作,设备检测到了一系列错误,且设备不大可能变得可操作 |
“fail-sss” | 含义与"fail"相同,后面的sss是检测到的错误内容 |
2.3.4 #address-cells和#size-cell
属性值类型:整数
#address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值。具体的结合下面的reg属性讲解。
2.3.5 reg属性
属性值类型:整数(表示地址)
reg的形式如下:
reg = <address1 length1 address2 length2 address3 length3……>
#address-cells控制address的数量,#size-cells控制length的数量。如下:
spi4 {compatible = "spi-gpio";pinctrl-names = "default";pinctrl-0 = <&pinctrl_spi4>;status = "disabled";gpio-sck = <&gpio5 11 0>;gpio-mosi = <&gpio5 10 0>;num-chipselects = <1>;#address-cells = <1>;#size-cells = <0>;gpio_spi: gpio_spi@0 {compatible = "fairchild,74hc595";gpio-controller;#gpio-cells = <2>;reg = <0>;registers-number = <1>;registers-default = /bits/ 8 <0x57>;spi-max-frequency = <100000>;};
};
父节点设置了#address-cells = <1>以及#size-cells = <0>,于是在子节点中就是reg<0>,表示只设置了起始地址,没有设置地址长度。
2.3.6 ranges属性
属性值类型:任意数量的 <子地址、父地址、地址长度>编码
比如对于#address-cells和#size-cells都为1的话,以ranges=<0x0 0x10 0x20>为例,表示将子地址的从0x0~(0x0 + 0x20)
的地址空间映射到父地址的0x10~(0x10 + 0x20)
。
可以为空,如下所示:
soc {...ranges;...
}
不为空时如下所示:
soc {compatible = "simple-bus";#address-cells = <1>;#size-cells = <1>;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。
2.3.7 name和device_type
属性值类型:字符串
这两个属性值已经被抛弃了。
2.3.8 特殊节点
2.3.8.1 aliases子节点
aliases
子节点的作用就是为其他节点起一个别名,如下所示:
aliases {can0 = &flexcan1;can1 = &flexcan2;ethernet0 = &fec1;ethernet1 = &fec2;gpio0 = &gpio1;gpio1 = &gpio2;gpio2 = &gpio3;gpio3 = &gpio4;gpio4 = &gpio5;i2c0 = &i2c1;i2c1 = &i2c2;/*----------- 以下省略------------*/
}
以can0 = &flexcan1;
为例。flexcan1是一个节点的名字, 设置别名后我们可以使用can0来指代flexcan1节点,与节点标签类似。 在设备树中更多的是为节点添加标签,没有使用节点别名,别名的作用是“快速找到设备树节点”。 在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。 也可以使用别名“一步到位”找到节点。
2.3.8.2 chosen子节点
chosen子节点不代表实际硬件,它主要用于给内核传递参数,此外这个节点还用作uboot向linux内核传递配置参数的“通道”, 我们在Uboot中设置的参数就是通过这个节点传递到内核的, 这部分内容是uboot和内核自动完成的。
3. 获取设备树节点信息
3.1 查找节点
3.1.1 根据节点路径:
就和windows下查找文件一样,我们也可以通过节点路径查找节点。
/** @description: 根据节点路径查找节点* @param-path : 指定节点在设备树中的路径* @return : 返回device_node结构体指针,如果查找失败返回NULL,否则返回device_node类型的结构体指针,保存设备节点的信息。*/
struct device_node *of_find_node_by_path(const char *path)
得到device_node结构体之后我们就可以使用其他of 函数获取节点的详细信息。
3.1.2 根据节点类型和compatible属性寻找节点函数
/** @description : 根据节点类型和compatible属性寻找节点函数* @param-from : 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找* @param-type : 要查找节点的类型,这个类型就是device_node-> type* @param-compatible: 要查找节点的compatible属性* @return : device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL*/
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
3.1.3 其他方式
常用的为以上几种,如果想看其他的话可以看野火的文档。
3.2 获取属性值
找到一个设备节点就会返回这个设备节点对应的结构体指针(device_node*)。这个过程可以理解为把设备树中的设备节点“获取”到驱动中。“获取”成功后我们再通过一组of函数从设备节点结构体(device_node)中获取我们想要的设备节点属 性信息。其of函数存放在以下目录下:
内核源码/include/linux/of.h
3.2.1 device_node结构体
struct device_node {const char *name;const char *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;
#if defined(CONFIG_OF_KOBJ)struct kobject kobj;
#endifunsigned long _flags;void *data;
#if defined(CONFIG_SPARC)const char *path_component_name;unsigned int unique_id;struct of_irq_controller *irq_trans;
#endif
};
- name: 节点中属性为name的值
- type: 节点中属性为device_type的值
- full_name: 节点的名字,在device_node结构体后面放一个字符串,full_name指向它
- properties: 链表,连接该节点的所有属性
- parent: 指向父节点
- child: 指向子节点
- sibling: 指向兄弟节点
3.2.2 获取节点属性
/** @description: 寻找指定属性* @param-np : 设备节点* @param-name : 属性名字* @param-lenp : 属性值的字节数* @return : 找到的属性
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
属性的property结构体:
struct property {char *name; // 属性名字int length; // 属性长度void *value; // 属性值struct property *next; // 下一个属性unsigned long _flags;unsigned int unique_id;struct bin_attribute attr;
};
3.2.3 其他of函数
太多了,可以看原子的资料,或者查看这位博主的博客:Linux 学习笔记: 设备树—常用OF操作函数
4. Linux设备树调试
我们可以在Linux下查看设备树信息:
cd /proc/device-tree
ls
Linux内核在启动时会解析设备树的各个节点信息,并在/proc/device-tree
目录下根据节点名字创建不同的文件或文件夹
如果要查看其下面有哪些属性和节点,cd进去即可,比如我要看最后一个spi4:
cd spi4
ls
参考资料
[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第四十三章
[2] 【野火】嵌入式Linux驱动开发实战指南——基于I.MX6ULL系列
[3] Device Tree -----设备树
[4] Linux 学习笔记: 设备树—常用OF操作函数