掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出 CPU 在系统启动的时候就支持设备树,比如我们的 RK3568 系列、NXP 的 I.MX8 系列等。
设备树
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source)。
这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等
树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。DTS 文件的主要功能就是按照上图所示的结构来描述板子上的设备信息。
在没有设备树的时候 Linux 是如何描述 ARM 架构中的板级信息呢?在 Linux 内核源码中大量的 arch/arm/mach-xxx 和 arch/arm/plat-xxx 文件夹,这些文件夹里面的文件就是对应平台下的板级信息。
- 设备数的源来
Linux 内核中 ARM 架构下有太多的冗余的垃圾板级信息文件,导致 linus 震怒,然后 ARM 社区引入了设备树。将这些描述板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts。
- dts和dtsi文件描述
一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文
件。
- 一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等)
- .dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等
DTS、DTB、DTC三种文件的区别
- DTS 是设备树源码文件
- DTB 是将DTS 编译以后得到的二进制文件
- DTC工具是将dts文件变成编译成dtb的工具,就像.c文件变成成.o需要用到gcc编译器一样。
- “make ARCH=arm64 all”命令是编译 Linux 源码中的所有东西,包括 uImage/zImage,.ko 驱动模块以及设备树
- 如果只是编译设备树的话建议使用“make ARCH=arm64 dtbs”命令,“make ARCH=arm64 dtbs”会编译选中的所有设备树文件。
DTS语法
dtsi 头文件
和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在.dts 设备树文件中,可以通过“#include”来引用.h、.dtsi 和.dts 文件。
一般.dtsi 文件用于描述 SOC 的**内部外设信息****,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等**
设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。
设备节点命名格式:
- node-name@unit-address
- label: node-name@unit-address
引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@f00”这个节点,而不需要输入完整的节点名字。
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。
设备树源码中常用的几种数据形式如下所示:
// 1. 字符串
compatible = "rockchip,rk3568";
// 2. 32位无符号整数
reg = <0>;
// 3. 字符串列表
compatible = "rockchip,rk3568-evb ", "rockchip,rk3568";
标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。也有一些是Linux外设驱动中常用的标准属性。
compatible 属性
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值是一个字符串列表,compatible 属性用于将设备和驱动绑定起来。
compatible 属性值的格式如下所示:
**"manufacturer,model"**
其中 manufacturer 表示厂商,model 一般是模块对应的驱动名字。
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。数组 imx415_of_match 就是 imx415.c 这个驱动文件的匹配表
model 属性
model 属性值也是一个字符串,一般 model 属性描述开发板的名字或者设备模块信息
model = "Rockchip rk3568 EVB DDR4 V10 Board";
status 属性
status 属性看名字就知道是和设备状态有关的。
值 | 描述 |
---|---|
“okay” | 表明设备是可操作的。 |
“disabled” | 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备 |
“fail” | 表明设备不可操作,设备检测到了一系列的错误, |
“fail-sss” | 含义和“fail”相同,后面的 sss 部分是检测到的错误内容。 |
#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形,#address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息
- #address-cells 属性值:决定了子节点 reg 属性中地址信息所占用的字长(32 位)
- #size-cells 属性值:决定了子节点 reg 属性中长度信息所占的字长(32 位)
#address-cells 表明 address 这个数据所占用的字长,#size-cells 表明 length 这个数据所占用
的字长
reg格式:
"<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">reg</font>
"属性用于描述设备的物理地址和大小。它是一个32位或64位的整数数组,每个数组元素对应一个设备资源,如内存区域、寄存器等。数组的第一个元素通常是设备的基础地址,其后紧跟的是设备大小。
reg = <address1 length1 address2 length2 address3 length3……>
reg属性
reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息或者设备地址信息
ranges属性
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges 是一个地址映射/转换表
ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
- child-bus-address:子总线地址空间的物理地址
- parent-bus-address:父总线地址空间的物理地址
- length:子地址空间的长度
如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换
根节点 compatible 属性
- 设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,
- 根节点的 compatible 属性第一个值描述了所使用的硬件设备名字,第二个值描述了设备所使用的 SOC。
Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。
向节点追加或修改内容
一旦硬件修改了,就需要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。
如何向节点追加数据,我们现在要解决的就是如何向i2c5 节点追加一个名为 fxls8471 的子节点,而且不能影响到其他使用到 RK3568 的板子。ATK-DLRK3568 开发板使用的设备树文件为 rk3568-atk-evb1-ddr4-v10.dtsi 和 rk3568-linux.dtsi,因此我们需要在 rk3568-atk-evb1-ddr4-v10.dtsi 文件中完成数据追加的内容
这个就是向节点追加或修改内容,重点就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。
**注意:**Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree 目录下根据节点名字创建不同文件夹
特殊节点
- aliases 子节点
单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label 来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。
- chosen 子节点
chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数
设备树常用 OF 操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。
Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中
查找节点
- of_find_node_by_name 函数通过节点名字查找指定的节点
- of_find_node_by_type 函数通过 device_type 属性查找指定的节点
- of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点
- of_find_matching_node_and_match 函数通过 of_device_id 匹配表来查找指定的节点
- of_find_node_by_path 函数通过路径来查找指定的节点
查找父/子节点
- of_get_parent 函数用于获取指定节点的父节点
- of_get_next_child 函数用迭代的查找子节点
提取属性值的OF函数(重点)
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中,如下:
struct property {
char *name;
int length;
void *value;
struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};
of_find_property 函数
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp);
// 函数参数和返回值含义如下:
// np:设备节点。
// name: 属性名字。
// lenp:属性值的字节数
// 返回值:找到的属性。
of_property_count_elems_of_size 函数
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size);
// np:设备节点。
// proname: 需要统计元素数量的属性名字。
// elem_size:元素长度。
// 返回值:得到的属性元素数量
of_property_read_u32_index 函数
**of_property_read_u32_index **函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value);
// np:设备节点。
// proname: 要读取的属性名字。
// index:要读取的值标号。
// out_value:读取到的值
// 返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有
// 要读取的数据,-EOVERFLOW 表示属性值列表太小。
读取属性中 u8、u16、u32 和 u64 类型的数组数据
用于读取这种只有一个整形值的属性
分别用于读取 u8、u16、u32 和 u64 类型属性值
of_property_read_string 函数
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string);
// np:设备节点。
// proname: 要读取的属性名字。
// out_string:读取到的字符串值。
// 返回值:0,读取成功,负值,读取失败。
of_n_addr_cells 函数
int of_n_addr_cells(struct device_node *np);
// np:设备节点。
// 返回值:获取到的#address-cells 属性值
of_n_size_cells 函数
int of_n_size_cells(struct device_node *np);
np:设备节点。
返回值:获取到的#size-cells 属性值。
常用的 OF 函数
- of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 name 指定的字符
串,也就是检查设备节点的兼容性,
- of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性
值
- of_translate_address 函数负责将从设备树读取到的物理地址转换为虚拟地址
- of_address_to_resource 函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是提取 reg 属性值,然后将其转换为 resource 结构体类型
- of_iomap 函数**用于直接内存映射**,以前我们会通过 ioremap 函数来完成物理地址到虚拟地
址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,
不需要使用 ioremap 函数了
总结
关于设备树就讲解到这里,关于设备树我们重点要了解一下几点内容:
①、DTS、DTB 和 DTC 之间的区别,如何将.dts 文件编译为.dtb 文件。
②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。
③、设备树的几个特殊子节点。
④、关于设备树的 OF 操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、GPIO 信息、中断信息等等。要想在驱动中读取设备树的属性值,那么就必须使用 Linux 内核提供的众多的 OF 函数。