前言
掌握设备树是 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;
#endif
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
};
- 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操作函数