【Leo的手记】Linux设备驱动程序手记5-设备树

由着之前的平台设备中的platform_device的实现我们可以得知,在平台设备总线的抽象下,设备驱动程序被分离为了设备驱动业务和设备描述信息。而对于主要用于进行设备信息描述的platform_device,如果全部按照内核模块的方式以内核源码的形式放置于Linux内核中,则由于不同且多样的板级硬件实现,Linux内核驱动的源码将飞速膨胀并严重拉低内核源码质量。而对于主要包含设备信息描述的platform_device,将其作为程序代码的组成部分实在是没有必要。因此,LInux内核引入了设备树,通过设备树,Linux可以方便地自行组织相应的设备结构而无需将代码固化到内核代码中。需要注意的是设备树并非Linux内核所独有,在多种实时嵌入式操作系统,Bootloader等项目代码中也都引入了设备树。例如U-Boot同样引入了设备树进行设备的描述。
1. 设备树
设备树顾名思义,它是一种树的数据结构。其包含一个根节点,并对于每一个子节点,都有其父节点(根节点除外,因为根节点在常见的系统解析时是被解析为通过一全局变量指向的数据结构)。系统通过树状结构,描述了系统的各个设备以及设备之间的依赖关系。需要注意的是,对于设备的描述不一定非得是物理设备,也可以是逻辑设备或者是数据结构抽象。
通过设备树,不仅精简了系统的代码,而且更集中高效地组织了系统设备的描述信息,将系统的设备移植成本和裁剪精简成本大大降低。
我们一般说编写设备树,是指编写设备树的源文件,设备树的源文件是以设备树的语法逻辑编写的以dts为拓展名的文本文件。在系统内核编译时,会将所指定的设备树源文件编译为设备树的二进制文件,其拓展名为dtb。在制作系统启动镜像时,往往需要将内核镜像文件和设备树文件放在一起。
2. 设备树的语法
2.1 设备树结构的定义
对于树状结构,设备树的语法主要是需要提供对于节点的描述。
设备树通过如下语法描述设备的节点。
其中,每一个节点,都由节点名称后跟着一对花括号所定义。节点中包含了对当前节点属性的描述,以及子节点的描述。
需要注意的是,对于任何一条完整的语义(包含节点描述,属性描述),都需要以花括号结尾。
而对于节点名node-name,可以在其前方指定label作为别名,中间以半角分号分隔。而我们也可以在node-name后指定该节点的地址。该地址需要和节点的reg属性起始地址匹配。
根节点作为顶层节点,其节点名固定为/,同Linux文件系统结构的根目录路径。
2.2 设备树引用
我们可以在根节点外部引用一些节点并修改其中的部分属性。
通过使用&加别名或全路径的方式,可以进行节点的引用。
在设备树中,通常需要包含硬件厂商所提供的设备树文件。该类设备树文件由厂商负责编写好各硬件资源的定义。在我们使用时,只需要进行裁剪等。我们通常不会直接修改厂商提供的设备树源码。而因此,他们所提供的源码也通常以dtsi为拓展名,意为设备树的包含头文件。该文件在设备树中可以通过#include <>的预处理指令进行包含(需要注意的是,在Linux内核对设备树的编译过程中会调用gcc编译器的-E选项进行预处理,因此其支持C语言的预处理指令。但并不代表所有的项目代码都一定使用这种编译流程。)。而我们在包含相应的头文件后,便可以通过引用节点的方式对设备进行定制。
2.3 设备树的常用节点
2.3.1 根节点
一个设备树结构必须要有一个根节点。
2.3.2 cpu节点
用于对单核或多核处理器的描述。其不同的子节点用于描述不同的cpu核心。
2.3.3 memory节点
用于描述系统内存资源的节点。
2.3.4 chosen节点
虚拟节点,用于指定内核启动参数,标准输出路径和标准输入路径等信息。
2.4 设备树属性的定义
定义一个节点的属性,使用如下格式
其中属性值property-value包括三种大类型,分别是字符串,32位整数和字节。
分别用双引号",尖括号和方括号定义。具体如下
字符串 :name = "LeoBunny";
字符串列表:compatible = "LeoBunny","LeoIsaacBunny";
32位整数列表(64位整数使用两个32位整数表示,0x开头表示16进制,无前缀默认10进制):reg = < 0x12345678 100 >;
单字节列表(16进制表示):data = [12 34 56 78];
组合:sample = "LeoBunny",<0x12345678>,[9a bc];
此外,可以仅仅定义属性的名称,默认代表布尔类型。作为标志。
也可以使用其他节点的句柄作为属性值。
2.5 常用的属性
2.5.1 #address-cells,#size-cells
在设备树的语法中,默认一个cell是代表32位数据。但是设备树支持32位和64位系统,因此对于地址,数据长度等的表示,则需要指定一个字段到底需要多长的位数对齐进行表示。
#address-cells :指定地址需要用多少个cell进行表示
#size-cells :指定长度需要用多少个cell进行表示
例如
上述设备树描述了对于memory节点,使用1个cell进行地址的表示,1个cell进行长度的表示。
2.5.2 compatible
兼容属性,用于指定该设备兼容何种驱动,用于匹配平台驱动。优先级最高。
对于根节点的compatible属性,用于选择对应的machine desc结构体,进行平台初始化。
而对于设备节点,则根据兼容属性查找对应的平台驱动程序。
例如
从前向后依次匹配。
2.5.3 model
用于指定该设备的具体型号描述。
需要注意的是对于compatible属性和model属性,官方给出的命名参考都是"厂商名,产品名"
2.5.4 status
用于指定设备的状态。有如下取值
"okay" : 表示设备良好并使用设备。
"disabled" : 表示设备不可用。
"reserved" : 表示设备可以运行但是不建议使用,通常该类设备被诸如平台固件等其他的软件组件所控制。
“fail” : 表示设备不可用,并且检测到了严重错误。
"fail-sss" : 同上,sss指示具体的错误类型
可以结合status属性和节点引用实现对硬件资源的裁剪。
2.5.5 reg
表示寄存器地址,是一系列的“地址 长度“对。长度由其父节点的#address-cells和#size-cells决定。
例同#address-cells与#size-cells。
此外reg,也可以用于指定资源标号,此时的reg不再使用地址长度对而是仅仅指定标号。例如多核cpu系统中的cpu标号。
2.5.6 name (过时,不建议使用)
指定设备名称,用于匹配平台驱动,优先级最低。
2.5.7 device_type (过时,不建议使用)
指定设备类型,用于匹配平台驱动,优先级中。
3. Linux对设备树的解析
在内核编译的过程中,设备树源文件会被编译为dtb文件,dtb文件随着系统的加载被载入内存,之后Linux内核将其每一个节点都解析为device_node结构。然后根据指定的规则,将某些device_node结构体转换为platform_device结构体。
device_node结构体的原型如下
根节点将会被保存在全局变量of_root中。相关内核代码如下(base.c 外部声明位于of.h)
对于设备树的树状结构,其很好地描述了设备间的依赖关系。而对于平台设备驱动模型。往往并不需要使用总线模型对一些设备的子节点进行处理,这些节点应该交由其父节点所指定的驱动程序进行处理。因此并非所有的设备树节点都会被转换为platform_device。
以下设备树节点将会被转换为platform_device。
根节点下,包含有compatible属性的子节点。
在某个节点的compatible属性中包含有“simple-bus”,"simple-mfd","isa","arm,amba-bus"其中之一,则其包含有compatible属性的子节点将会被转换为platform_device。
3.1 Linux匹配设备树节点与platform_driver
对于能够支持设备树的platform_driver,需要定义其of_match_table成员。
of_match_table为struct of_device_id数组。该数组必须以空项结尾。而struct of_device_id原型如下
对于某个可以转换为platform_device的设备树节点。其匹配流程如下。
遍历整个of_match_table
检查of_match_table中的某项的compatible成员是否和设备树节点中的compatible属性匹配,如果匹配则匹配成功。
如果不匹配,检查type成员是否和设备的type属性匹配。
如果不匹配,检查name成员是否和设备的name属性匹配。
而对于匹配成功的设备节点,platform_driver则需要获取其属性来得到资源定义。
3.2 Linux获取设备树属性
对于转换成platform_device的设备节点,其reg属性将会被转换为IORESOURCE_MEM类型的资源。而interrupts将会被转换为IORESOURCE_IRQ类型的资源。
而对于非官方指定的标准属性或者是不会被转换为platform_device的设备节点属性,Linux内核提供了一系列方法用于读取其属性。
这一系列的操作通常可以由查找节点,查找节点属性,获取节点属性值三步完成。
3.2.1 查找设备树节点
of_find_node_by_path方法
该方法可以根据设备树路径获取设备节点。
of_find_node_by_name方法
该方法可以根据设备节点名称找到节点。其中from参数指定从哪一个节点开始寻找。如果传入NULL则从根节点开始(下同)。
of_find_node_by_type方法
该方法可以根据类型(即设备节点的device_type属性)查找节点。
of_find_compatible_node方法
该方法可以根据compatible属性查找节点。
of_find_node_by_phandle方法
phandle是dts文件被编译为dtb文件时,为每一个节点指派的唯一的数字ID,可以使用这些数字ID来查找device_node。
of_get_parent方法
获取某一节点的父节点
of_get_next_parent方法
返回结果同of_get_parent,区别在于其在执行的过程中将参数node作为参数调用了一次of_node_put方法减少了node节点的引用计数。
of_get_next_child方法
查找下一个子节点,node传入父节点,prev表示上一个子节点,prev传入NULL表示寻找第一个子节点。
of_get_next_available_child方法
与of_get_next_child方法的区别在于它通过判断status属性只查找可用的节点,如果节点的status属性为"disabled",会跳过。
of_get_child_by_name方法
根据名字获取节点的子节点,node表示父节点。
3.2.2 获取节点属性
of_find_property方法
np:要获取属性的节点。
name:属性的名称。
lenp:用来保存属性长度的变量指针。
struct property原型
name :属性名。
length :属性长度。
value :属性内容(指针)。
3.2.3 获取属性值
of_get_property方法
np:要获取属性的节点。
name:属性的名称。
lenp:用来保存属性长度的变量指针。
of_property_count_elems_of_size方法
获取当前属性按照属性的元素长度计算得到的元素个数。需要注意的是上文中所说的属性长度均以字节为单位。
np :节点。
propname :属性名。
elem_size :单位长度。
(1) 读取整数
of_property_read_u32方法
读取32位整数。
np : 节点。
propname :属性值。
out_value :用于存放输出值的指针。
of_property_read_u64方法
读取64位整数,参数定义同上。
of_property_read_u32_index方法
读取指定索引处的32位整数。index参数为索引号。
of_property_read_u64_index方法
读取指定索引处的64位整数。
of_property_read_variable_u8_array方法
用u8数组的方式获取参数的全部数据。sz_min和sz_max指定长度的范围,如果长度没有落在由其指定的范围内,则不返回属性值。
of_property_read_variable_u16_array方法
of_property_read_variable_u32_array方法
of_property_read_variable_u64_array方法
of_property_read_string方法
读取到的字符串属性的指针会被存放在out_string指向的字符串的指针中。
4. 实例代码
我们在原有的设备树中添加如下的设备节点。
重新编译设备树并将设备树替换原有的设备树。
然后编写platform_driver对应的驱动代码。如下
编译,在系统启动后加载内核模块,得到如下的输出。

可以看到,驱动程序在加载时匹配到了设备树生成的设备。也能获得其对应的资源。
需要注意的是,对于自动转换为IORESOURCE_MEM和IORESOURCE_IRQ的资源,会通过系统的处理转化为系统对应的资源表示形式。这也说明了为什么打印的Reg和IRQ和设备树中所编写的有所区别。而通过直接获取资源得到的数值则同设备树源文件中的内容。