本文主要带新手体会使用设备树,与之前讲的总线设别驱动模型和基本框架驱动之间的区别。
1. 什么是设备树
还是以 LED 驱动为例,如果你要更换 LED 所用的 GPIO 引脚,该怎么办?
在我们学了第一篇基本框架,应该知道就是把对应引脚值修改,然后整个.c文件重新编译,但缺点就是修改麻烦;
学了第二篇总线设备驱动模型后,我们把设备(资源)和驱动分离开,dev.c和driver.c分别编译加载进内核,那么修改引脚只需要修改设备(资源)部分。
但是问题来了,Linux内核支持的板子太多了,每一个设计都有不同,以led为例,在A板子上用GPF4,在B板子上用GPF5......那样在内核文件中为了匹配板子,就会有大量.c文件。
linux 内核 arm 架构下添加了很多开发板的适配文件,打开内核源码,和arm开发板相关的文件放在arch/arm/mxch-xxx
目录下,这些 c 文件仅仅用来适配某款开发板,对于 Linux 内核来说并没有提交什么新功能,但是每适配一款新的开发板就需要一堆文件,导致 Linux 内核越来越臃肿
Linux的创始人Linus 大发雷霆: "this whole ARM thing is a f*cking pain in theass"。
于是, Linux 内核开始引入设备树(device tree)。
我们编写一个设备树文件(dts: device tree source),然后编译为dtb(device tree blob)文件,内核解析dtb,dts的本质是用来描述板级设备信息的一种数据结构。内核使用一个dtb 文件,就可以代替一堆.c文件。
设备树的结构是基于第二篇中所讲的总线设备驱动模型的,区别只是在于对设备(资源)表示方式的不同。
如图,系统总线为树干,各个设备控制器为节点(也可以是设备),各设备为子节点,是不是特别像树。
其实dts的编写结构和这张图也有神似。
2. dts编写语法
先放一个简单的dts,然后一点点介绍:
/dts-v1/;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <0x1>;
#size-cells = <0x1>;
memory@30000000 {
device_type = "memory";
reg = <0x30000000 0x4000000>;
};
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
led {
compatible = "jz2440_led";
pin = <0x50006>;
};
};
2.1 设备树版本与保留内存
设备树版本
/dts-v1/
保留内存
/memreserve/<address><length>;
设备树版本的下一行可以加保留内存选项,让内核保留一段内存不用,如果让内核使用全部内存可省略,上文中的例子就没有该项。
2.2 设备树节点
设备树是由一个个节点组成的,每个节点相当于树上的一片叶子。
2.2.1 节点
/ {
...
};
最外面的叫根节点。
memory@30000000 {
...
};
chosen {
...
};
led {
...
};
里面的是节点,用{};分隔,这里就表示了三个节点,节点中还可以有节点。
节点语法:
[label:] node-name[@unit-address] {
[properties definitions];
[child nodes];
};
1)label: 节点别名(标签),可写可不写,用:
隔开,为了方便访问节点,可用直接通过&lable
来访问节点
2)node-name: 节点名
3)unit-address: 设备地址,可写可不写。因为同级别节点名字不能一样,用设备地址可区分。例如:
memory@30000000{
device_type="memory";
reg=<0x30000000 0x4000000>;
}
memory@0{
device_type="memory";
reg=<0 4096>;
}
4)properties definitions:属性定义
5)child nodes:子节点
2.2.2 节点属性
[label:] property-name = value;
[label:] property-name;
属性分为有属性值和无属性值
属性值有三种取值:
1.arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示),用尖括号表示< >,如:
example=<0x11223344 123>;
2.string(字符串), 用双引号表示" ",如:
example="hello";
3.bytestring(1个或多个字节),用方括号表示[ ],一个字节必须用两位16进制数表示,比如0必须写成[00],例如[00 11 22],空格可以省略,如[001122]
三种可以组合,用,号隔开,如:
example=<0x10101010 11>,"hello",[001122];
一般不会这么做。
2.2.2.1 compatible
compatible 属性值由 string list (字符串列表)组成,以"",符号分隔几个字符串,定义了设备的兼容性,推荐格式为manufacturer,model
,manufacturer 描述了生产商,model 描述了型号。
compatible = "samsung,smdk2440","samsung,smdk2410";
这句表示兼容2440和2410两个板子。compatible属性在根节点下就是用来寻找对应的machine_decs,执行对应的初始化函数,在节点下就是用来寻找对应的驱动程序,归根结底就是匹配作用。
驱动程序先使用第一个兼容值在 Linux 内核中查找,看看能不能找到对应的驱动文件;如果没有找到的话,就使用第二个兼容值查找。
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值核 OF 匹配表中的任何一个值相等,那么就表示这个设备可以使用这个驱动。
2.2.2.2 model
model 属性值是一个 string,指明了设备的厂商和型号,推荐格式为manufacturer,model
。
model = "samsung smdk2440";
compatible用来指明兼容哪些,model表示他到底是什么东西。
2.2.2.4 reg
reg 的本意是 register,用来表示寄存器地址。但是在设备树里,它可以用来描述一段空间。反正对于 ARM 系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。
reg 属性值用来描述设备地址空间资源信息,一般是某个外设的寄存器地址范围信息,包括起始地址和地址长度。
reg = <address1 length1 address2 length2 address3 length3……>
如示例中:
reg = <0x30000000 0x4000000>;
代表了0x30000000地址开始的0x4000000字节内存。再如:
reg = <0x30000000 0x4000000 0 4096>;
代表了两段内存:代表了0x30000000地址开始的0x4000000字节内存,和0地址开始的4096字节内存。
如果是64位地址呢?那就需要2个32位数字才能表示一个地址了,怎么分辨?用到下文的#address-cells 和 #size-cells。
2.2.2.3 #address-cells 和 #size-cells
#address-cells and #size-cells 属性值是一个 u32,可以用在任何拥有子节点的设备中,并描述子设备节点应该如何寻址。
#address-cells
属性定义子节点 reg 属性中地址字段所占用的字长,也就是占用 u32 单元格的数量。
#size-cells
属性定义子节点 reg 属性值的长度所占用的 u32 单元格的数量。
#address-cells = <0x1>;
#size-cells = <0x1>;
表示用1个32位数表示地址,用1个32位数表示长度。
2.2.2.5 chosen
chosen 节点是为了uboot 向 Linux 内核传递数据,重点是 bootargs 参数,这个涉及内核启动,不在本文讲。
2.2.3 引用其他节点
(1) phandle属性引用
节点中的phandle属性, 它的取值必须是唯一的(不要跟其他的phandle值一样)
pic@10000000 {
phandle = <1>;
interrupt-controller;
};
another-device-node {
interrupt-parent = <1>; // 使用phandle值为1来引用上述节点
};
pic代表中断控制器,interrupt-parent指名节点another-device-node的中断父亲是pic@100000000
(2)使用别名label(本质还是phandle)
PIC: pic@10000000 {
interrupt-controller;
};
another-device-node {
interrupt-parent = <&PIC>; // 使用label来引用上述节点,
// 使用lable时实际上也是使用phandle来引用,
// 在编译dts文件为dtb文件时, 编译器dtc会在dtb中插入phandle属性
};
2.2.4 包含dtsi文件
#include "2440.dtsi"
公共部分可以写成dtsi文件,语法和dts一模一样。
在每个.dsti和.dts中都会存在一个“/”根节点,那么如果在一个设备树文件中include一个.dtsi文件,那么岂不是存在多个“/”根节点了么?编译器DTC在对.dts进行编译生成dtb时,会对node进行合并操作,最终生成的dtb只有一个root node。
注意dts中同名的节点中的属性可以覆盖dtsi中的属性。
如:
dts:
/dts-v1/;
#include<2440.dtsi>
/{
model = "SMDK2440";
compatible = "samsung,smdk2440";
#address-cells = <0x1>;
#size-cells = <0x1>;
/{
led{
pin=<3>;
};
}
2440.dtsi:
/dts-v1/;
/{
model = "SMDK2440";
compatible = "samsung,smdk2440";
#address-cells = <0x1>;
#size-cells = <0x1>;
led {
pin=<6>;
};
};
最后led的pin属性是3,正是这个属性可以使公共部分写在dtsi,在dts中写一些各板子的差异。
2.2.5 查看设备树
板子启动后查看设备树,板子启动后执行下面的命令:
# ls /sys/firmware/
得到:devicetree fdt
/sys/firmware/devicetree 目录下是以目录结构呈现的 dtb 文件, 根节点对应 base 目录, 每一个节点对应一个目录, 每一个属性对应一个文件。
这些属性的值如果是字符串,可以使用 cat 命令把它打印出来;对于数值,可以用 hexdump 把它打印出来。还可以看到/sys/firmware/fdt 文件,它就是 dtb 格式的设备树文件,可以把它复制出来放到 ubuntu 上,执行下面的命令反编译出来(-I dtb:输入格式是 dtb, -O dts:输出格式是 dts):cd 板子所用的内核源码目录
./scripts/dtc/dtc -I dtb -O dts /从板子上/复制出来的/fdt -o tmp
3. 设备树驱动
3.1 内核对设备树的处理
内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;对于部分device_node 结构体,会被转换为 platform_device 结构体。
哪些设备树节点会被转换为 platform_device?
1)根节点下含有 compatile 属性的子节点
2)含有特定 compatile 属性的节点的子节点
如果一个节点的 compatile 属性,它的值是这 4 者之一:
"simplebus","simplemfd","isa","arm,amba-bus", 那 么 它 的 子 结 点 ( 需 含compatile 属性)也可以转换为 platform_device。
3)总线 I2C、 SPI 节点下的子节点: 不转换为 platform_device
某个总线下到子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。
比如以下的节点中:
{
mytest {
compatile = "mytest", "simple-bus";
mytest@0 {
compatile = "mytest_0";
};
};
i2c {
compatile = "samsung,i2c";
at24c02 {
compatile = "at24c02";
};
};
spi {
compatile = "samsung,spi";
flash@0 {
compatible = "winbond,w25q32dw";
spi-max-frequency = <25000000>;
reg = <0>;
};
};
};
- /mytest 会被转换为 platform_device, 因为它兼容"simple-bus";它的子节点/mytest/mytest@0 也会被转换为 platform_device
- /i2c 节点一般表示 i2c 控制器, 它会被转换为 platform_device, 在内核中有对应的platform_driver;
- /i2c/at24c02 节点不会被转换为 platform_device, 它被如何处理完全由父节点platform_driver 决定, 一般是被创建为一个 i2c_client。
- 类似的也有/spi 节点, 它一般也是用来表示 SPI 控制器, 它会被转换为platform_device, 在内核中有对应的 platform_driver;
- /spi/flash@0 节点不会被转换为 platform_device, 它被如何处理完全由父节点的platform_driver 决定, 一般是被创建为一个 spi_device。
platform_device 中含有 resource 数组, 它来自 device_node 的 reg,interrupts 属性;
platform_device.dev.of_node 指向 device_node, 可以通过它获得其他属性
platform_dev和platform_drv如何匹配这里不讲,直接去源码搜索platform_match函数,特别简单肯定能看明白。
匹配过程按优先顺序如下:
a. 比较 platform_dev.driver_override 和 platform_driver.drv->name
b. 比较 platform_dev.dev.of_node的compatible属性 和 platform_driver.drv->of_match_table
c. 比较 platform_dev.name 和 platform_driver.id_table
d. 比较 platform_dev.name 和 platform_driver.drv->name
platform_get_resource函数
设 备 树 中 的 节 点 被 转 换 为platform_device 后,设备树中的 reg 属性、 interrupts 属性也会被转换为“ resource”。这时,你可以使用这个函数取出这些资源。
函数原型为:
struct resource *platform_get_resource(struct platform_device *dev,unsigned int type, unsigned int num);
对于设备树节点中的 reg 属性,它对应 IORESOURCE_MEM 类型的资源;对于设备树节点中的 interrupts 属性,它对应 IORESOURCE_IRQ 类型的资源。
of_property_read_u32函数
从device_node节点熟悉中读取u32数,np为节点指针,propname为属性名字
static inline int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value)
{
return of_property_read_u32_array(np, propname, out_value, 1);
}
3.2 修改设备树
直接反编译原来的设备树文件,在内核目录下执行:
./scripts/dtc/dtc -I dtb -O dts /从板子上/复制出来的/fdt -o tmp
然后加上myled0和myled1节点。
myled0 {
compatible = "my_led";
pin = <5>;
};
myled1 {
compatible = "my_led";
pin = <6>;
};
3.3 修改驱动
和第二篇一样要定义platform结构体,区别是要加上of_match_table,匹配列表,用来和节点的compatible属性匹配。
static struct platform_driver myled_drv = {
.probe = myled_probe,
.remove = myled_remove,
.driver = {
.name = "myled",
.of_match_table = of_match_leds,
}
};
static const struct of_device_id of_match_leds[] = {
{ .compatible = "my_led", .data = NULL },
};
在probe函数中register_chrdev ,构造类,构造设备
static int myled_probe(struct platform_device * pdev)
{
struct device_node *np;
int err = 0;
np = pdev->dev.of_node;
if (!np)
return -1;
if(major == 0){
gpio_con = ioremap(0x56000050, 8);
gpio_dat = gpio_con + 1;
major=register_chrdev(0,"myled",&myled_opr);
myled_class = class_create(THIS_MODULE, "myled_class");
if(myled_class==NULL){
printk("class_create error \n");
return -1;
}
}
err = of_property_read_u32(np, "pin", &led_pin[led_num]);
device_create(myled_class,NULL, MKDEV(major, led_num), NULL,"myled%d",led_num);
led_num++;
return 0;
}
of_property_read_u32(np, "pin", &led_pin[led_num]);这就是读取pin属性函数,读出来放到led_pin数组里。
设备树里每有一个节点和驱动匹配上就会调用一次probe函数,就会在/dev下创建一个设备,
但是register_chrdev和class_create只需要一次,所以用major来判断一下是不是第一次进入probe。
完整代码:https://download.csdn.net/download/freestep96/86743909
显然使用了设备树后极其方便,甚至可以使用uboot直接修改设备树文件。
当然这里的代码为了新手可以明白,写得非常简略,学会后可以去看看内核源码其他的驱动是怎么写得,从成熟的代码中学习是十分好的方式,一个优秀的驱动,设计了分层、分离,面向对象的设计思想,不过越容易移植,就越复杂,还需要在工作中权衡。