【Linux驱动】设备树的开发
前言
掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出的 CPU 其驱动开发也基本都是基于设备树的,比如 ST 新出的 STM32MP157、NXP 的 I.MX8 系列等
本篇文章主要是让我们了解如何通过设备树进行Linux驱动的开发
硬件: RV1126
Linux内核: 4.19
一、设备树
1.设备树介绍
随着智能手机的发展,每年新出的ARM架构芯片少说都在数十、数百款,Linux内核下板级信息文件将会成指数级增长!这些板级信息文件都是.c或.h文件,都会被硬编码进Linux内核中,导致Linux内核“虚胖”。就好比你喜欢吃自助餐,然后花了100多到一家宣传看着很不错的自助餐厅,结果你想吃的牛排、海鲜、烤肉基本没多少,全都是一些凉菜、炒面、西瓜、饮料等小吃,相信你此时肯定会脱口而出一句“骗子!”。同样的,内核也是如此,所以ARM社区就引入了PowerPC等架构已经采用的设备树(FlattenedDeviceTree),将这些描述板级硬件信息的内容都从Linux内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts。
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等,如下图所示:
在设备树中,可描述的信息包括:
a. CPU数量和类型
b.内存基地址和大小
c.总线和桥
d.外设连接
e.中断控制器和中断使用情况
f.GPIO控制器和GPIO使用情况
g.时钟控制器和时钟使用情况
bootload会将这些信息传递给内核,内核开始识别这些树,并解析成linux内核中platform_device,i2c_client,spi_device等设备,而这些设备使用的内存资源,中断等信息也传递给内核。内核会将这些资源绑定给相应的设备。
设备树的主要优势: 对于同一SOC的不同主板,只需更换设备树文件.dtb即可实现不同主板的无差异支持,而无需更换内核文件。
2.设备树组成
DTS文件是一种ASCII文本对Device Tree的描述,放置在内核的/arch/arm/boot/dts目录。一般而言,一个*.dts文件对应一个ARM的machine。·DTSI文件作用:由于一个SOC可能有多个不同的电路板,而每个电路板拥有一个 .dts。这些dts势必会存在许多共同部分,为了减少代码的冗余,设备树将这些共同部分提炼保存在.dtsi文件中,供不同的dts共同使用。*.dtsi的使用方法,类似于C语言的头文件,在dts文件中需要进行include *.dtsi文件。当然,dtsi本身也支持include 另一个dtsi文件。 dtsi由厂商提供·DTC是将.dts编译为.dtb的工具,相当于gcc,
·DTB文件是 dts 被 DTC 编译后的二进制格式的设备树文件,它可以被linux内核解析
设备树文件路径: 、rv1126_rv1109_v2.2.0_20210825/kernel/arch/arm/boot/dts
3.设备树语法
设备树中的基本单元,被称为“node”,其格式为:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
label: 是标号,可以省略。label的作用是为了方便地引用node;
node-name: 是设备节点的名称,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设;
@unit-address: 一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话 “unit-address” 可以不要;
注:根节点没有node-name 或者 unit-address,它被定义为 /
注意:
1》设备树文件都由根节点开始,每个设备只有一个根节点(如果包含多个文件,根节点则会合并),其它所有设备都作为子节点存在,由节点名和一组节点属性构成。
2》节点属性都是有key-value的键值对来描述,并以 ; 结束
3》节点间可以嵌套形成父子关系,这样可以方便描述设备间的关系
3.1标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用这些标准属性。
3.2compatible 属性详解
compatible也叫做“兼容性”属性,这是非常重要的一个属性! compatible 属性的值是一个字符串列表, compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,
compatible 属性的值格式:“manufacturer,model”,其中manufacturer 表示厂商, model 一般是模块对应的驱动名字。
例如:compatible = “fsl,mpc8641”, “ns16550”;
上面的compatible有两个属性,分别是 “fsl,mpc8641” 和 “ns16550”;其中 “fsl,mpc8641” 的厂商是 fsl(飞思卡尔);设备首先会使用第一个属性值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件;
如果没找到,就使用第二个属性值查找,以此类推,直到查到到对应的驱动程序 或者 查找完整个 Linux 内核也没有对应的驱动程序为止。
二、设备树开发
1.设备树节点的操作函数
Linux 驱动程序往往需要去读取到 Linux 内核中附带的 dts 文件,并操作设备树 DTS 的相关节点.
1.查找指定节点名称的节点
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
from:起始节点。从这个节点开始向下搜索,查找具有指定名称的节点。如果传入 NULL,则从设备树的根节点开始搜索。
name:要查找的节点名称。
返回值为找到的设备树节点的指针,如果没有找到匹配的节点,则返回 NULL
2.用于通过设备树路径(path)查找设备树中的节点
struct device_node *of_find_node_by_path(const char *path)
path:设备树节点的路径,是一个以斜杠分隔的字符串,表示设备树中节点的层次结构路径。
返回值为找到的设备树节点的指针,如果没有找到匹配的节点,则返回 NULL
2.向设备树中添加节点
设备树文件路径:kernel/arm/arch/boot/dts
注意设备树节点的添加要在根节点内。
添加完结点之后重新编译内核文件生成dst.bin文件烧录到开发板中。
3.设备树节点的使用
3.1平台总线驱动端的注册
注意:
这里因为我添加的设备是蜂鸣器,在总线上没有对应的物理总线,所以我们用的是虚拟总线(平台设备总线)去挂载我们的设备。
平台设备驱动端注册函数:
User int platform_driver_register(struct platform_driver *drv)
该函数用于将一个 struct platform_driver 结构体中的驱动程序信息注册到内核,使得内核能够正确地与该平台设备驱动程序进行关联。
以下是该函数的参数和简要说明:
drv:指向 struct platform_driver 结构体的指针,其中包含了平台设备驱动程序的相关信息,包括探测、移除等操作的回调函数、设备驱动信息等。
返回值为注册成功时返回0,失败时返回一个负数,表示注册失败的错误码。
该函数通常由平台设备驱动程序的初始化代码调用,以便在系统启动时注册驱动程序,从而使内核能够正确地管理该平台设备。注册成功后,当系统探测到与该平台设备驱动程序匹配的硬件设备时,相应的探测函数 (probe 字段指定的函数) 将被调用,完成设备的初始化工作。
struct platform_driver *drv
结构体详解
struct platform_driver *drv
结构体内struct device_driver driver
结构体详解
struct device_driver driver
结构体内struct of_device_id *of_match_table
详解
3.2字符设备的注册
这里我们采用杂项字符设备的注册方式
杂项字符设备注册函数:int misc_register(struct miscdevice * misc)
misc:指向 struct miscdevice 结构体的指针,包含了与杂项字符设备相关的信息,如设备号、设备名称、设备操作函数等。
返回值为注册成功时返回0,失败时返回一个负数,表示注册失败的错误码。
4.代码示例
代码如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/of_gpio.h>
int my_probe(struct platform_device *);
int my_remove(struct platform_device *);
ssize_t my_write (struct file *, const char __user *, size_t, loff_t *);
struct of_device_id match_table={
.compatible="TestBeep"//设备名,与设备树中添加的名字保持一致,也就是compatible的属性
};//设备树匹配结构体
struct platform_driver mydrv={
.probe=my_probe,//设备探测函数
.remove=my_remove,//设备移除函数
.driver={
.name="test",//在这里这个名字不用于设备名的匹配,用compatible属性去匹配设备
.of_match_table=&match_table//用于设备名的匹配
}//设备驱动结构体,存放设备信息
};//平台驱动端核心结构体
struct file_operations ops={
.write=my_write//写函数接口,为应用层提供write接口
};//字符指针操作集核心结构体
struct miscdevice my_misc={
.minor=255,//设备号,255默认由内核分配次设备节点号,杂项主设备号均为10。
.name="dev_beep",//设备名
.fops=&ops//文件操作指针集结构体
};//杂项字符设备设备核心结构体
int beepgpio;//定义一个全局变量去承接GPIO号
ssize_t my_write (struct file *f, const char __user *buf, size_t s, loff_t *l)
{
char data;
int ret;
/*我们不能直接对用户层数据进行操作,
所以我们这里用copy_from_user函数安全地从用户空间复制数据到内核空间。*/
ret = copy_from_user(&data,buf,s);
gpio_set_value(beepgpio,data);/*通过获取到用户空间的数据对gpio口进行设置*/
return 0;
}
int my_probe(struct platform_device *dev)//设备探测函数
{
printk("设备探测函数执行......\n");
//字符设备注册得到设备节点
misc_register(&my_misc);
beepgpio = of_get_named_gpio(dev->dev.of_node,"beep_gpio",0);//获取gpio号
gpio_request(beepgpio,"beep");//请求gpio
gpio_direction_output(beepgpio,0);//设置gpio方向
return 0;
}
int my_remove(struct platform_device *dev)//设备移除函数
{
printk("设备移除函数执行......\n");
misc_deregister(&my_misc);//注销字符设备
gpio_free(beepgpio);//释放gpio口
return 0;
}
static int __init My_Enter(void)
{
printk("模块加载成功!\n");
//1.平台设备驱动端注册
if(platform_driver_register(&mydrv) == 0)
{
printk("平台设备驱动端注册成功!\n");
}
return 0;
}
static void __exit My_Exit(void)
{
printk("模块卸载成功!\n");
platform_driver_unregister(&mydrv);
}
module_init(My_Enter);
module_exit(My_Exit);
MODULE_LICENSE("GPL");
总结
通过上面的代码示例我们可以简单的了解设备树的开发流程,设备树的开发是每个 Linux 驱动开发工程师必须掌握的技能,特别是如今的 Linux 内核版本都是支持设备树的。设备树的出现极大的方便了各种设备的驱动代码编写,同时降低了 Linux 内核的冗余。