[ARM Linux 驱动开发] linux 设备树
在没有引入设备树之前,每一个具体的硬件平台都会在 linux 源码包的 arch/arm/mach-xxx/ 目录下存在一个硬件信息描述的源码包,其中定义了 GPIO、I2C 等外设和系统信息。这些信息也定义为 .h、.c 文件,内容多为描述设备信息的结构体,同驱动源码一起编译。
由此可见,在没有设备树的情况下,随着新硬件平台的不断产生(特别是手机行业,更新特别迭代快),linux 驱动代码中会增加越来越多的板级描述 .h、.c 文件。而 linux 源码中应该更多关注和体现的是处理驱动的逻辑,而不是这些“没有的”板级描述文件:也就是 Linus 所说的 "this whole ARM thing is a f*cking pain in the ass"。
在 x86 体系下没有特别强调设备树的概念。
应该是 PC 兼容机有组织标准规定,或者是 PCI 总线也有类似的“设备树”机制。
导致以上问题的主要原因就是驱动代码和设备信息没有分离,而设备树就是为了解决这个问题的。由于目前对设备树没有很好的总体观念,所以接下来的行文组织会不是很有条例,基本算是知识的“收集和记录”。
大致感受设备树
设备树一般位于 linux 源码的 arch/arm/boot/dts 目录,对应 .dts(device tree source) 后缀的文件。dts 文件可以通过
- > make dtbs
命令编译成 .dtb(device tree blob) 后缀的二进制文件,以供内核驱动使用。图 1 为一个 dts 文件的一部分,可以看到它也是类似 xml 格式的层级结构。

linux 内核在启动的时候会解析设备树中的各个节点信息,并在 /proc/device-tree/ 目录下根据节点名字创建不同的文件夹或文件。如图 2 所示,可以看到创建的目录层级结构和 dts 中指定的层级结构是完全对应的。

我们这篇文章先不深究设备树加载等详细逻辑流程,但是可以大抵推测设备的相关操作:编译后的 dtb 文件会被内核存放在某处以供查询,而查询的操作会和 xml 文件类似,提供一系列层级相关的查询函数。
在大致感受了一下什么是设备树之后,文章后续会简单介绍一下设备树文本的内容语法和语义;然后会编写一个简单驱动,了解一下相关的查询函数。
设备树标准
设备树的标准文档可参考:
图 3 就是我们上节所说的设备树的层级结构,根节点为 /。

术语节点(node)的结构如下,我们依次进行说明,同时涉及术语属性(property)、标签(label)。
- [label:] node-name[@unit-address] {
- [properties definitions]
- [child nodes]
- };
node-name
node-name 组件指定节点的名称。长度应为 1 至 31 个字符,并且需要描述设备的一般类型。
unit-address
unit-address 特别针对于节点所在的总线类型。unit-address 必须与此节点的 reg 属性中指定的第一个地址匹配。如果该节点没有 reg 属性,则必须省略 @unit-address。仅 node-name 就可以将该节点与书中同一级别的其他节点区分开。特定总线的绑定可以为 reg 和 unit-address 指定额外的、更加具体的要求。
properties
设备树中的每个节点都有描述节点特征的属性。属性由名称和值组成。
属性的值是零个或多个字节的数组,包含与属性相关联的信息。如果传递的是 true-false 信息,那么属性可以有空值。在这种情况下,属性的存在与否就已经足够说明问题了。
label
设备树的源格式中允许将标签附加到任何节点和属性值上。phandle 和路径引用可以通过引用标签自动生成,而不是显式指定 phandle 的值或节点的完整路径。标签仅在设备树的源格式中使用,不会编码进 DTB 二进制文件中。
标签是通过在标签名称后面附加冒号(':')来创建的。引用是通过在标签前面加取值符号('&')来创建的。
节点名称和属性名称都分标准规定和私有定义。标准定义的内容含义可在 Devicetree specification 中查阅;私有特定的内容含义可在源码目录下的 Documentation/devicetree/bindings/ 目录下查阅。这些内容就不赘述了,准备之后用到什么再查阅什么。
设备树操作函数
最后我们编写一个简单的驱动,借此了解一下设备树相关的操作函数。设备树相关的操作函数在 linux/of.h 头文件中定义。相关操作函数都是以 of 打头的。
我们以根节点下的 backlight 节点为例,依次读取其 compatible、brightness-levels、default-brightness-level 和 status 的属性值。
- / {
- …………
- backlight {
- compatible = "pwm-backlight";
- pwms = <&pwm1 0 5000000>;
- brightness-levels = <0 4 8 16 32 64 128 255>;
- default-brightness-level = <6>;
- status = "okay";
- };
of_find_node_by_path
of_find_node_by_path 函数可以根据节点的路径获取节点结构体。此例中节点路径为 /backlight。这个函数的使用见代码 1 中的第 13 至 19 行,其中保存了返回的节点结构体指针。
of_find_property
of_find_property 函数可以获取指定节点下的某一属性结构体。见代码 1 的第 21 至 29 行,此例中我们想获取 compatible 属性结构体。见第 30 行,属性结构体中的 value 成员保存了属性的值,这边我们将其打印出来。
有特别针对的函数用于获取不同类型的属性值,我们接着看。
of_property_read_string
of_property_read_string 函数用于获取类型为字符串的属性值。例子中 status 属性值为字符串类型,代码 1 中的第 31 至 35 行对其进行了读取。需要注意的是,返回的是一个字符串指针,其指向已分配内容的地址,而不是一份拷贝。因此,我们并不需要额外为字符串分配空间。
of_property_read_u32
of_property_read_u32 函数用于获取类型为 4 字节整形的属性值。代码 1 中的第 54 至 58 行,对 default-brightness-level 属性进行了读取。
of_property_count_elems_of_size
of_property_read_u32 函数针对于类型为数组的属性值,它可以获取数组的长度。我们可以根据获取到的长度,动态分配需要的空间。代码 1 中的第 54 至 58 行,获取了 brightness-levels 属性值的数组长度。然后第 66 行使用 kmalloc 分配了指定大小的空间,用于存储后续读取的值。
of_property_read_u32_array
of_property_read_u32_array 函数用于获取类型为数组的属性值。此例中存放读取数组的内容是 kmalloc 分配的,最后不要忘记使用 kfree 释放(代码 1 第 90 行)。
- #include <linux/module.h>
- #include <linux/of.h>
- #include <linux/slab.h>
- static int __init dtsof_init(void)
- {
- struct device_node* backlight_node = NULL;
- struct property* compatible_prop = NULL;
- const char* str;
- u32 u32_val;
- u32* u32_arr;
- int res, cnt, i;
- /* 'backlight' node */
- backlight_node = of_find_node_by_path("/backlight");
- if (backlight_node == NULL)
- {
- printk("of_find_node_by_path /backlight failed\n");
- return -EINVAL;
- }
- /* 'compatible' property */
- compatible_prop = of_find_property(
- backlight_node,
- "compatible",
- NULL);
- if (compatible_prop == NULL)
- {
- printk("of_find_property compatible failed\n");
- return -EINVAL;
- }
- printk("compatible = %s\n", (char*)compatible_prop->value);
- /* 'status' property - string value */
- res = of_property_read_string(
- backlight_node,
- "status",
- &str);
- if (res < 0)
- {
- printk("of_property_read_string status failed\n");
- return res;
- }
- printk("status = %s\n", str);
- /* 'default-brightness-level' property - u32 value */
- res = of_property_read_u32(
- backlight_node,
- "default-brightness-level",
- &u32_val);
- if (res < 0)
- {
- printk("of_property_read_u32 default-brightness-level failed\n");
- return res;
- }
- printk("default-brightness-level = %u\n", u32_val);
- /* 'brightness-levels' property - u32 array value */
- cnt = of_property_count_elems_of_size(
- backlight_node,
- "brightness-levels",
- sizeof(u32));
- if (cnt < 0)
- {
- printk("of_property_count_elems_of_size brightness-levels failed\n");
- return cnt;
- }
- printk("brightness-levels has %u elements\n", cnt);
- u32_arr = kmalloc(sizeof(u32) * cnt, GFP_KERNEL);
- if (u32_arr == NULL)
- {
- printk("kmalloc failed\n");
- return -EINVAL;
- }
- res = of_property_read_u32_array(
- backlight_node,
- "brightness-levels",
- u32_arr,
- cnt);
- if (res < 0)
- {
- printk("of_property_read_u32_array brightness-levels failed\n");
- goto fail_free;
- }
- for (i = 0; i < cnt; i++)
- {
- printk("brightness-levels[%d] = %u\n", i, u32_arr[i]);
- }
- fail_free:
- kfree(u32_arr);
- return res;
- }
- static void __exit dtsof_exit(void)
- {
- }
- module_init(dtsof_init);
- module_exit(dtsof_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("hanhanblog");
我们开发板上验证所写的程序,如图 4 所示,参照 dts 文件进行核对,打印的内容正确无误。

总结
这是第一次接触设备树,每次接触一块新知识,进展就会变慢。“本着用到时再深入的原则”,本篇文章也算是对设备树的一个初步认识和总结,希望后续和设备树相关的内容能越学越快。