1、什么是设备树?
设备树是一种描述硬件资源的数据结构。他通过bootloader将硬件资源传给内核,使得内核和硬件资源 描述相对独立。
2、设备树的由来
2.1 平台总线的由来
要想了解为什么会有设备树,设备树是怎么来的,我们就要先来回顾以下在没有设备树之前我们是怎么来写一个驱动程序的。以字符设备驱动代码框架为例,我们一起一起来回顾下。任何的设备驱动的编写,Linux已经为我们打好了框架,我们只需要做完形填空一样填写就可以了。
下面是注册字符设备驱动框架图:
具体过程可以参考下面这篇文章:
Linux驱动学习—字符设备驱动注册详解-CSDN博客
下面是注册杂项设备驱动框架图:
有关杂项设备驱动注册流程可以参考下面这篇:
Linux驱动学习—杂项设备驱动注册-CSDN博客
通过这些框架,我们可以很容易编写我们 的驱动代码,但是,当我们用这个框架非常熟练的时候,我们就会发现虽然这个方法很简单,但是非常不容易扩展,当有很多很多类似的设备的时候,如果我们都是按照这个框架来完成,那就要写很多遍这个流程,但是多个相似设备之间真正差异的地方只有初始化硬件部分,其他步骤的代码基本都是一样的。这样就会造成大量的重复代码。但是,我们在编写驱动代码的时候,要尽量做到代码的复用,也就是一套驱动尽量可以兼容很多设备,如果我们还按照这个来编写就不太符合我们的规则了。
为了实现这个目标,我们就要吧通用的代码和有差异的代码分离出来,来增强我们驱动代码的可移植性。所以,设备驱动分离的思想就应运而生了,在Linux中,我们是在写代码的时候进行分离。分离是把一些不相似的东西放到device.c,把相似的东西放到driver.c,如果有很多相似的设备或者平台,我们只要修改device.c就可以了,这样我们重复性的工作就大大的减少了。这就是平台总线的由来。
2.2 平台总线这个方法有什么弊端呢?(设备树由来)
当我们用这个方法用习惯以后就会发现,假如soc不变,我们每换一个平台,都要修改C文件,并且还要重新编译。而且会在arch/arm/plat-xxx和arch/arm/mach-xxx下面留下大量关于板级细节的代码。并不是说这个方法不好,只是从Linux的发展来看,这些代码相对于Linux内核来说就是“垃圾代码”,而且这些“垃圾代码”非常多。
为了改变这个现状,设备树也就被引进到Linux上了,用来剔除相对内核来说的“垃圾代码”,即用设备树文件来描述这些设备信息,也就是代替device.c文件,虽然拿到了内核外面,但是platform匹配基本不变,并且相比于之前的方法,使用设备树不仅可以去掉大量的“垃圾代码”,并且采用文本格式,方便阅读和修改,如果需要修改部分资源,我们也不用在重新编译内核了,只需要把设备树源文件编译成二进制文件,在通过bootloader传递给内核就可以了。内核对其进行解析和展开得到关于硬件的拓扑图。我们通过内核提供的接口剖获取设备树的节点和属性就可以了。即内核对于同一soc的不同主板,只需要换设备树文件dtb即可实现不同主板的无差异支持,而无需更换内核文件。
3、设备树的基本概念
3.1 为啥叫设备树呢?
因为他的语法结构像树一样,所以管它叫设备树
3.2常用名词解释
<1>DT:Device Tree //设备树 <2>FDT:Flattened Device Tree//展开设备树//开放固件,设备树起源于OF,所以我们在设备树中可以看到很多of字母的函数 <3>device tree source(dts) //设备树代码 <4>device tree source includeDTB(dtsi) //更通用的设备树代码,也就是相同芯片但不同平台都可以使用的代码 <5>device tree blob(dtb) //DTS编译后得到的DTB文件 <6>device tree complier(dtc) //设备树编译器
dtsi:一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息, 将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。
DTS,DTSI,DTB,DTC他们之间的关系如下:
4、设备树基本语法
4.1 设备树基本框架
<1>设备从根节点开始,每个设备都是节点。 <2>节点和节点之间可以相互嵌套,形成父子关系。 <3>设备的属性用key-value对(键值对)来描述,每个属性用分号结束
4.2 设备树语法
4.2.1节点
什么是节点呢?节点就好比一颗大树,从树的主干开始,然后有一节一节的树枝,这个就叫节点。在代码中的节点是什么样子的呢。我们把上面模板的根节点摘出来,如下所示,这个就是根节点。相当于大树的树干。
/{
};//分号
而树枝就相当于设备树的子节点,同样我们把子节点摘出来就是根节点里面的node1和node2,如下所示:
/{ //根节点
node1//子节点node1
{
};
node2//子节点node2
{
};
};//分号
一个树枝是不是也可以继续分成好几个树枝呢,也就是说子节点里面可以包含子子节点。所以child-node1和child-node2是node1和node2的子节点,如下所示:
/{ //根节点
node1//子节点node1
{
child-node1//子子节点
{
};
};
node2//子节点node2
{
child-node2//子子节点
{
};
};
};//分号
4.2.2 节点名称
节点的命名有一个固定的格式。
格式:<名称>[@<设备地址>]
(1)<名称>节点的名称也不是任意起的,一般要体现设备的类型而不是特点的型号,比如网口,应该命名为ethernet,而不是随意起一个,比如111。
(2)<设备地址>就是用来访问该设备的基地址。但并不是说在操作过程中来描述一个地址,其主要用来区分用。
(3)注意事项:A、同一级的节点只要地址不一样,名称是可以不唯一的。
B、设备地址是一个可选选项,可以不写。但为了任意区分和理解,一般是都写的。
4.2.3 节点别名
当我们找一个节点的时候,必须书写完整的节点路径,如果节点名很长,那么我们在引用的时候就十分不方便,所以,设备树允许我们用下面的形式为节点标注引用(起别名)。举例:
uart8:serial@02288000
其中uart8就是这个节点名称的别名,serial@02288000就是节点名称。
4.2.4 节点引用
一般往节点里面添加内容的时候,不会直接把直接添加的内容写到节点里面,而是通过节点的引用来添加。
举例:
&uart8{
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart8>;
status = "okay";
};
&uart8表示引用节点别名为uart8的节点。并往节点添加以下内容:
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart8>;
status = "okay";
注意事项:编译设备树的时候,相同的节点的不同属性信息都会被合并,相同节点的先相同的属性会被重写,使用引用可以避免移植者四处找节点。如dts和dtsi里面都有根节点,但最终会合并成一个根节点。
4.2.5属性
(1)reg属性
reg属性用来描述一个设备的地址范围。格式:
reg=<add1 lenth1 [add2 length2]...>
举例:
serial@02288000{
reg=<101F2000 0x1000>;//101F2000是起始地址,0x1000是长度
};
(2)#address-cells和#size-cells属性
#address-cells用来设置子节点中reg地址的数量
#size-cells用来设置子节点中reg地址长度的数量
举例:
cpu{
#address-cells = <1>;//用来设置子节点中reg地址的数量
#size-cells = <1>;//用来设置子节点中reg地址长度的数量
serial@02288000{
reg=<101F2000 0x1000>;//101F2000是起始地址,0x1000是长度
};
};
其中#address-cells和#size-cells均为1,也就是说我们子节点里面的reg属性里这个寄存器组的起始地址只有一个,长度也只有一个。所以101F2000是起始地址,0x1000是长度。
(3)compatible属性
compatible是一个自负床列表,可以在代码中进行匹配。
举例:
compatible = “led";
(4)status属性
status属性的值类型是字符串,这里我们只要记住两个常用的即可,一个是okay,表示涉笔可以正常使用,一个disable,表示设备不能正常使用。
5、在设备树中添加自定义节点
5.1 命令查看设备树节点
<1> cd /proc/device-tree/下就可看到
<2> cd /sys/firmware/devicetree/base/下就可看到
这是设置uboot环境变量的:
5.2 安装dtc工具
(1)直接make dtbs出现这种情况,说明环境没有配置对,则需要安装dtc工具
(2)安装dtc工具
apt-get install device-tre-compiler
5.3 实验:添加一个节点
如下图,添加一个节点test,对这个节点取别名为test1,然后节点引用&test1,一般往节点里面添加内容的时候,不会直接把直接添加的内容写到节点里面,而是通过节点的引用来添加。所以最终的compatible和status属性是节点引用里面的内容。
添加完之后编译dts,在内核源码路径下输入以下命令即可编译:
make ARCH=arm CROSS_COMPILE=arm-linux-guneabihf- dtbs
把编译的dtb烧录到开发板,cd /proc/device-tree/目录下可以看到test节点已经生成。cat /proc/devicetree/test/compatible发现是test1234,cat /proc/devicetree/test/status发现是okay。
6、设备树中常见的of操作函数
设备都是以节点的形式“挂”到设备树上的,因此姚秀昂获取这个设备的其他属性信息,必须先获取到这个设备的节点。linux内核实验device_node结构体来描述一个节点,此结构体的第一在文件include/linux/of.h中,如下:
struct device_node {
const char *name;//节点名字
const char *type;//设备类型
phandle phandle;
const char *full_name;//节点全名
struct fwnode_handle fwnode;
struct property *properties;//属性
struct property *deadprops; /* removed properties */
struct device_node *parent;//父节点
struct device_node *child;//子节点
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在include/linux/of.h中,如下:
struct property {
char *name;//属性名字
int length;//属性长度
void *value;//属性值
struct property *next;//下一个属性
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
6.1获取设备树文件节点里面资源的步骤
<1>步骤一:查找我们要找的节点。
<2>步骤一:查找我们要找的属性值。
6.2 查找节点常用的of函数
<1>of_find_node_by_path函数
作用:函数通过路径来查找指定的节点。
函数原型:
static inline struct device_node *of_find_node_by_path(const char *path)
参数:
path:带有全路径的节点名,可以使用节点的别名,比如"/test"就是test这个节点的全路径。使用节点别名的路径是"/test1".
返回值:成功就返回找到的节点。失败则返回NULL。
<2>of_get_parent函数
作用:用于获取节点的父节点(如果有父节点的话)。
struct device_node *of_get_parent(const struct device_node *node);
node:要查找的父节点的节点
返回值:找到的父节点。
<3>of_get_next_child函数
作用:用于迭代的查找子节点。
static inline struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)
参数如下:
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL表示从第一个子节点开始。
返回值:找到的下一个子节点。
6.3 查找节点属性常用的of函数
<1>of_get_property函数
作用:用于查找指定的属性。
static inline const void *of_get_property(const struct device_node *node,
const char *name,
int *lenp)
参数如下:
np:设备节点。
name:属性名字。
lenp:属性值的字节数。
返回值:找到的属性。
<2> of_property_read_u8、of_property_read_u16、of_property_read_u32、of_property_read_u64
有些属性只有一个整型值,者四个函数就是用于读取这种只有一个整型值的属性,分别用于读取u8、u16、u32、u64类型属性值,函数原型如下:
static inline int of_property_read_u8(const struct device_node *np,
const char *propname,
u8 *out_value);
static inline int of_property_read_u16(const struct device_node *np,
const char *propname,
u16 *out_value);
static inline int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value);
static inline int of_property_read_u64(const struct device_node *np,
const char *propname, u64 *out_value);
参数如下:
np:设备节点。
proname:要读取的属性名字。
out_value:读取的值。
返回值:0,读取成功,负值,读取失败。
<3>of_property_read_u8_array、of_property_read_u16_array、of_property_read_u32_array、of_property_read_u64_array
这四个函数分别是读取属性中u8、u16、u32、u64类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这4个函数一次读取reg属性中的所有数据。这四个函数的原型如下:
static inline int of_property_read_u8_array(const struct device_node *np,
const char *propname,u8 *out_values, size_t sz);
static inline int of_property_read_u16_array(const struct device_node *np,
const char *propname, u16 *out_values, size_t sz);
static inline int of_property_read_u32_array(const struct device_node *np,
const char *propname, u32 *out_values, size_t sz);
static inline int of_property_read_u64_array(const struct device_node *np,
const char *propname, u64 *out_values, size_t sz);
参数:
np:设备节点。
proname:要读取的属性名字。
out_values:读取的数组值。
返回值:0,读取成功,负值,读取失败。
<4>of_property_read_string
作用:用于读取属性只呢个字符串值
int of_property_read_string(const struct device_node *np,
const char *propname,
const char **out_string);
参数:
np:设备节点。
proname:要读取的属性名字。
out_values:读取的字符串值。
返回值:0,读取成功,负值,读取失败。
6.4 实验:把5.3添加的一个节点的值和属性读取出来
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
struct device_node *test_device_node;
struct property *test_node_property;
int size;
u32 out_values[2]={0};
const char *str=NULL;
static int hello_init(void)
{
int ret = 0;
printk("hello_init\n");
//查找要查找的节点
test_device_node = of_find_node_by_path("/test");
if(test_device_node == NULL) {
printk("test_device_node find error\n");
return -1;
}
printk("test_device_node name is %s\n",test_device_node->name);//test
//获取compatible属性内容
test_node_property = of_find_property(test_device_node, "compatible", &size);
if(test_node_property == NULL) {
printk("test_node_property find error\n");
return -1;
}
printk("test_node_property name is %s\n",test_node_property->name);//compatible
printk("test_node_property->value is %s\n",test_node_property->value);//test1234
//获取reg属性内容
ret = of_property_read_u32_array(test_device_node, "reg", out_values, 2);
if(ret < 0) {
printk("of_property_read_u32_array is error\n");
return -1;
}
printk("out_values[0] is 0x%08x\n",out_values[0]);//0x020ac000
printk("out_values[1] is 0x%08x\n",out_values[1]);//0x00000004
//获取status属性内容
ret = of_property_read_string(test_device_node, "status", &str);
if(ret < 0) {
printk("of_property_read_string is error\n");
return -1;
}
printk("status is %s\n",str);//okay
return 0;
}
static void hello_exit(void)
{
printk("hello_exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
加载驱动,打印如下:
7、设备树下的platform总线
7.1 传统方法下的platform总线
Linux驱动学习—平台总线模型-CSDN博客
之前这篇文章中是使用传统的方法对平台总线进行学习,什么是传统方法呢,就是硬件设备信息部分写在device.c,驱动部分写在driver.c中。而设备树下的platform总线 则是用设备树文件代替device.c。所以使用设备树的方法在配置好设备树文件后,只需编写driver.c。
7.2 of_iomap函数
作用:of_iomap函数用于直接内存映射,以前我们会通过ioremap函数来完成物理地址到虚拟地址的映射。
函数原型:
void __iomem *of_iomap(struct device_node *node, int index);
参数:
np:设备节点
index:reg属性中要完成内存映射的段,如果reg属性只有一段的话inbdex就设置0。
返回值:经过内存映射后的虚拟内存首地址,如果为NULL的话就表示内存映射失败。
7.3 实验代码
Linux驱动学习—平台总线模型-CSDN博客
直接在上面文章的4.3小节platform driver.c上修改,主要实现的功能就是映射GPIO5的数据寄存器的内存地址,实现对数据寄存器的操作,从而实现对蜂鸣器引脚控制。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
struct device_node *test_device_node;
struct property *test_node_property;
int size;
u32 out_values[2]={0};
const char *str=NULL;
unsigned int *vir_gpio_dr;
static const of_device_id of_match_table_test[] = {//匹配表
{.compatible = "test1234"},
};
static const platform_device_id beep_id_table ={
.name = "beep_test",
};
/*设备树节点compatible属性与of_match_table_test的compatible相匹配就会进入该函数,pdev是匹配成功后传入的设备树节点*/
int beep_probe(struct platform_device *pdev)
{
int ret = 0;
printk("beep_probe\n");
/*
//查找要查找的节点 pdev是匹配成功后传入的设备树节点,所以不需要用之前的方法进行查找了
test_device_node = of_find_node_by_path("/test");
if(test_device_node == NULL) {
printk("test_device_node find error\n");
return -1;
}
printk("test_device_node name is %s\n",test_device_node->name);//test
//获取compatible属性内容
test_node_property = of_find_property(test_device_node, "compatible", &size);
if(test_node_property == NULL) {
printk("test_node_property find error\n");
return -1;
}
printk("test_node_property name is %s\n",test_node_property->name);//compatible
printk("test_node_property->value is %s\n",test_node_property->value);//test1234
*/
//获取reg属性内容
ret = of_property_read_u32_array(pdev->dev.of_node, "reg", out_values, 2);
if(ret < 0) {
printk("of_property_read_u32_array is error\n");
return -1;
}
printk("out_values[0] is 0x%08x\n",out_values[0]);//0x020ac000
printk("out_values[1] is 0x%08x\n",out_values[1]);//0x00000004
vir_gpio_dr = of_iomap(pdev->dev.of_node, 0);
if(vir_gpio_dr == NULL) {
printk("of_iomap error\n");
return -1;
}
return 0;
}
int beep_remove(struct platform_device *pdev)
{
pritnk("beep_remove \n");
return 0;
}
strcut platform_driver beep_device = {
.probe = beep_probe,
.remove = beep_remove,
.driver = {
.owner = THIS_MODULE,
.name = "123",
.of_match_table = of_match_table_test,//匹配表
},
.id_table = &beep_id_table,
};
static int beep_driver_init(void)
{
int ret = -1;
ret = platform_driver_register(&beep_device);
if(ret < 0) {
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok\n");
return 0;
}
static void beep_driver_exit(void)
{
platform_driver_unregister(&beep_device);
printk("beep_driver_exit \n");
}
module_init(beep_driver_init);
module_exit(beep_driver_exit);
MODULE_LICENSE("GPL");
编译加载驱动,这样寄存器地址就获取成功了,我们可以注册一个杂项设备对数据引脚进行操作高低电平,从而实现对蜂鸣器的操作。
*vir_gpio_dr |= (1<<1);
*vir_gpio_dr &= ~(1<<1);
7.3.1 匹配优先级
先是platform_driver.driver.of_match_table.compatible,然后是platform_driver.driver.id_table,最后是platform_driver.driver.name
注意:设备树节点compatible属性与of_match_table_test的compatible相匹配就会进入probe函数,其函数参数pdev是匹配成功后传入的设备树节点
7.3.2 reg有多组参数时,of_iomap如何传参
当然,如果reg有多组参数的话,这一个是不一样的,举个例子: