我的内核学习笔记14:内核设备树学习

李迟按:
上一篇内核的文章是2年半前,期间因工作转行而停止研究,最近又重新捡起。这个系列从2013年起间断地更新,本来想从系统角度逐步写的,但工作量十分庞大,现在也想通了,在适合的时间写,不带目的,不强迫自己完成什么任务。不强调什么平台/芯片、内核版本。一篇文章针对一、两个知识点即可。
本文是笔者接触设备树的第一个实例,仅仅是验证设备树简单的写法和几个函数的使用。

一、设备树

Linux内核很早就引入设备树了。引入设备树之前,arm架构代码中有大量的硬编码——即外设特性、配置全部写到代码中,不管是时钟配置、引脚复用还是MTD分区、GPIO高低电平。这样的话稍有不同的板级配置(如同一SOC,有的有1个LED,有的有2个LED,flash大小亦会有不同),会使用不同的文件或在代码中用宏区分。总之,代码比较臃肿,设备树的引入,使得代码比较简洁,引脚复用、分区等等都放到设备树文件,不同设备树文件对应不同配置的板子,而代码无须大量修改(甚至不用修改)。
关于设备树的引入历史及linus大神的风范,网上有资料,此处不展开。

二、知识点

设备树有固定的语法,本文也不展开。只说一下简单的知识点。
arm的设备树位于目录arch/arm/boot/dts,该目录有许多不同板子的设备树源文件。
设备树源文件后缀为dts(device tree source),经过编译后生成设备树二进制文件,后缀为dtb(device tree blob)。dtb文件需要传递给内核进行解析。
设备树编译器为dtc,一般在内核源码目录输入make dtbs即可。使用的Makefilearch/arm/boot/dts/Makefile。该Makefile根据make menuconfig选定的芯片类型编译多个设备树,如自行新增的,需要在Makefile对应芯片类型添加,否则不会被编译。
一般不会从头编写设备树,而是使用类似的现成的模板修改。
内核启动参数可以在设备树中使用chosen指定,示例(imx6q)如下:

1
2
3
4
chosen {
bootargs = "earlyprintk=vga log_buf_len=25M console=ttymxc0,115200 rootfstype=ext3 root=/dev/mmcblk1 rw rootwait init=/sbin/init";
stdout-path = &uart2;
};

本文使用的设备树节点如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 专门用于测试dts的示例,没实例用途
// 名称可以有“,”、“-”,如“ll,i2c-enable”
myfoo {
compatible = "ll,jimkent-foo";
status = "okay"; // string
enable; // bool,无须值
myvalue = <250>; // 默认是32位,如果使用8位读取,结果为0
value = /bits/ 8 <88>; // 8位单独赋值
value16 = /bits/ 16 <166>; // 16位单独赋值
a-cell = <1 2 3 4 5>; // 数组
// 子节点
foo {
label = "foo";
note = "this is foo";
};
bar {
label = "bar";
note = "this is bar";
};
};
*/

其中compatible与驱动使用的名称必须一致(这样才能匹配上)。其它内容比较简单,分别是字符串、布尔类型、不同位数的数值、数组、子节点。

三、驱动实例

驱动实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
* @file foo_drv.c
* @author Late Lee <latelee@163.com>
* @date Wed Jun 7 22:21:19 2019
*
* @brief 测试dts示例
*
* @note 读取dts的值,学习dts。代码有部分警告,不影响

设备树节点:
// 专门用于测试dts的示例,没实例用途
// 名称可以有“,”、“-”,如“ll,i2c-enable”
myfoo {
compatible = "ll,jimkent-foo";
status = "okay"; // string
enable; // bool,无须值
myvalue = <250>; // 默认是32位,如果使用8位读取,结果为0
value = /bits/ 8 <88>; // 8位单独赋值
value16 = /bits/ 16 <166>; // 16位单独赋值
a-cell = <1 2 3 4 5>; // 数组
// 子节点
foo {
label = "foo";
note = "this is foo";
};
bar {
label = "bar";
note = "this is bar";
};
};
*/

#include <linux/module.h>
#include <linux/kernel.h> /**< printk() */
#include <linux/init.h>

#include <linux/types.h> /**< size_t */
#include <linux/errno.h> /**< error codes */
#include <linux/string.h>

#include <linux/of.h>
#include <linux/of_device.h>

static int foo_remove(struct platform_device *dev)
{
printk(KERN_NOTICE "remove...\n");

return 0;
}

static int foo_probe(struct platform_device *dev)
{
int ret = 0;
struct device_node* np = dev->dev.of_node;
struct device_node* child = NULL;
const char* str = NULL;
bool enable = false;
u8 value = 0;
u16 value16 = 0;
u32 value32 = 0;

// 测试dts读取API
if(np == NULL)
{
pr_info("of_node is NULL\n");
return 0;
}

of_property_read_string(np, "status", &str); // 读字符串

enable = of_property_read_bool(np, "enable"); // bool类型,可判断某字段存在不存在
of_property_read_u32(np, "myvalue", &value32); // 一般地,都使用u32读取数值
of_property_read_u8(np, "value", &value);
of_property_read_u16(np, "value16", &value16);

u32 data[3] = {0};
u32 tag = 0;
// a-cell是一个数组,默认读第1个。
of_property_read_u32(np, "a-cell", &tag);
// 也可以读取指定大小的数组(不一定是全部的)
of_property_read_u32_array(np, "a-cell", data, ARRAY_SIZE(data));

printk("of read status: %s enable: %d value: %d %d %d\n", str, enable, value, value16, value32);
printk("of read tag: %d data: %d %d %d\n", tag, data[0], data[1], data[2]);

// 获取子节点个数
int count = of_get_available_child_count(np);

// 遍历所有子节点,按格式读取属性
int index = 0;
for_each_available_child_of_node(np,child)
{
const char* label = of_get_property(child,"label",NULL) ? : child->name;
const char* note = of_get_property(child,"note",NULL) ? : child->name;
printk("of read: label: %s note: %s\n", label, note);
}
return ret;
}

static struct of_device_id foo_of_match[] = {
{ .compatible = "ll,jimkent-foo", },
{ /* sentinel */ }
};

static struct platform_driver foo_driver = {
.driver = {
.name = "foo",
.of_match_table = of_match_ptr(foo_of_match),
},
.probe = foo_probe,
.remove = foo_remove,
};

static int __init foo_drv_init(void)
{
int ret = 0;

ret = platform_driver_register(&foo_driver);
if (ret)
{
pr_info("platform_driver_register failed!\n");
return ret;
}

pr_info("Init OK!\n");

return ret;
}

static void __exit foo_drv_exit(void)
{
platform_driver_unregister(&foo_driver);
}

module_init(foo_drv_init);
module_exit(foo_drv_exit);

MODULE_AUTHOR("Late Lee");
MODULE_DESCRIPTION("Simple platform driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:foo");

示例的代码是一个简单的模板,除了学习dts外,没什么用处。但是可以以此展开复杂的、有实际用途的驱动。
与以前的platform驱动不同,platform_driver中指定of_match_tablefoo_of_match结构体的.compatible必须与设备树的compatible一致。
本驱动涉及到的读取设备树节点信息的函数如下,更多函数,参考内核源码的include/linux/of.h头文件:

1
2
3
4
5
of_property_read_string // 读取字符串
of_property_read_bool // 判断某个字段是否存在,无须赋值
of_property_read_u8 // 读取8比特
of_property_read_u16 // 读取16比特
of_property_read_u32 // 读取32比特

如果存在多个子节点,用of_get_available_child_count获取个数(可用于开辟内存),然后调用for_each_available_child_of_node遍历所有子节点,注意,of_get_propertyof_property_read_string有相同效果。只是用法不同而已。

四、小结

设备树有许多用法和功能,本文只是初窥门径而已。从实践角度,笔者建议先用实例来了解设备树的基本用法,熟悉后,再看其它与外设有关的(如时钟、引脚复用)的例子。
网上关于设备树的测试驱动资料比较少,笔者写此文,仅抛砖引玉。
本驱动在qemu的imx6q下测试通过。设备树文件名为imx6q-sabrelite.dts

五、资源


李迟 2019.6.12 周三  晚