目录
- 前言
- 一、设备树是个啥?
- 二、设备树编写语法规则
- 1.文件类型
- 2.设备树源文件(DTS)结构
- 3.设备树源文件(DTS)解析
- 三、设备树API函数
- 1.在内核中获取设备树节点(三种)
- 2.获取设备树节点的属性
- 四、应用示例
- 1.设备树
- 2.驱动层
- 3.应用层
前言
本文主要讲解了一下设备树的概念,编写语法规则,API函数和使用流程,最后使用LED灯闪烁,实战验证了一下。
一、设备树是个啥?
在Linux开源初期,Linux 系统因其开源、稳定和安全而受到了各大公司和开发者的欢迎。然后,随着越来越多个人和公司的使用,在受到追捧的同时也让Linux内核变得越来臃肿。由于当时内核中没有规范来引导,最终使之充斥着大量的屎山代码。这些垃圾的设备代码,全都来自于对应公司单板启动或运行细节强绑定,无法复用和移植。
终于,在2012年,因为Tony Lindgren,内核OMAP development tree的维护者,发送了一个邮件给Linus,请求提交OMAP平台代码修改,并附带修改以及如何解决merge conficts的这个事情。让Linus Torvalds 实在受不了了,直接在推特上爆粗口:“Gaah.Guys, this whole ARM thing is a fucking pain in the ass!!!”
这个事件发生之后,Linux 内核社区和维护者采取了一些措施来改善这一问题,其中很关键的一条就是:ARM SOC board specific的代码被移除,由DeviceTree机制来负责传递硬件拓扑和硬件资源信息。其实设备树(Device Tree)在 Linux 内核中早在 2005 年已经引入了,只不过该事件之后,设备树的使用得到了更多关注和改进,之后设备树的推动主要是因为其在管理复杂硬件平台方面的固有优势。
本质上,Device Tree改变了原来用hardcode方式将硬件设备配置信息嵌入到内核代码的方法,改用bootloader传递一个DB的形式。
如上所示就是一个大致的设备树抽象图,设备树的出现大大减小了Linux内核的大小,也简化了你写驱动代码的架构。
驱动代码本来是分为两部分的:
1.设备端:提供硬件设备的资源 — 中断、GPIO 资源等
2.驱动端:驱动代码的框架 — 就是之前说过的杂项和 Linux2.6 — 他会和设备端进行匹配,然后从设备端获取硬件的资源,来驱动对应的硬件。
但是,设备树出来之后就把设备端给替代了,我们只需要简单写几行代码将设备添加到设备树中,然后只写驱动端的代码就行了。简化了硬件描述和配置,使得内核代码中对硬件的硬编码减少了。
设备树存放路径,一般都在 arch/arm/boot/dts/下。
我是用的是:
RK3588S/kernel/arch/arm64/boot/dts/rockchip/rk3588s-yyt.dts
二、设备树编写语法规则
1.文件类型
dts
:写驱动时主要编写修改的文件 — 类似于 C 语言的.c 文件。
dtsi
:设备的原始文件 — 一般是由厂商写 — 类似于 C 语言的.h文件。
dtb
:就是设备树编译生成的二进制文件(可执行文件) — 类似于 C 语言的.o 文件。
dtc
:编译设备树的工具 — 类似于 C 语言的 gcc 文件。
图示:
2.设备树源文件(DTS)结构
直接上图:
3.设备树源文件(DTS)解析
1.属性:
model
:描述开发板信息的属性 – 公司 芯片的型号 — 类型是一个字符串
compatible
:兼容性标识符,设备树属性里最重要的一个属性,用来做匹配的。
驱动代码就是通过compatible找到目标节点的,从而获得该设备的设备信息。类型是字符串,也可以是多个字符串
例: compatible = "xyd-led", "rk-led";
查找顺序是先找 xyd-led,再找 rk-led,找到一个即可。
status
:设备当前的状态 ,类似与开关,四种状态。— 类型也是一个字符串
- “okay”:表示设备启用并且应该被驱动程序使用。设备在系统启动时会被初始化。
- “disabled”:表示设备被禁用。驱动程序不会加载和初始化这个设备,但他仍在设备树里。
- “fail”:表示设备初始化失败,通常用于在设备初始化时出现问题的情况下。设备将不会被驱动程序使用。
- “reserved”:表示设备被保留,通常用于未来的扩展或保留给系统内部用途。
了解:
chosen:就是你 uboot 可以给内核传递的一些参数
bootargs = "earlycon=uart8250,mmio32,0xff690000 console=ttyFIQ0
androidboot.baseband=N/A androidboot.veritymode=enforcing
androidboot.hardware=rk30board androidboot.console=ttyFIQ0
androidboot.selinux=permissive init=/init kpti=0";
bootargs:在 uboot 启动的时,会把后边的这些信息传递给内核,内核就可以带着这些信息去找你的文件系统,这些信息也可以在设备树里设置。
aliases:就是给其他的节点取一个别名,你操作这个别名就相对于操作该节点
#address-cells = <2>: — 设备寄存器的地址
#size-cells = <2>: — 设备寄存器的长度 – 就是大小
以上两个都是一个无符号 32 位数值
address-cells = <1> — 代表这个地址的是 2 – 代表的是 2 个字节
size-cells = <1> — 代表这个地址长度是 2 – 代表的是 2 个字节
其实以上两个值是给 reg 属性使用的
reg = <> — 这里你就可以填写硬件设备的地址以及他的长度
地址和长度具体是几个字节的是有 address-cells 和 size-cells
例:reg = <0xff690000 0x20>
三、设备树API函数
1.在内核中获取设备树节点(三种)
struct device_node *of_find_node_by_name(struct device_node
*from, const char *name);
函数功能:通过节点名获取设备树节点
函数头文件:#include <linux/of.h>
函数参数:
from
:直接写 NULL — 代表从根节点开始找
name
:想要从设备树上获取的节点名
函数返回值:成功返回指向一个 struct device_node 指针,失败返回NULL。
struct device_node *of_find_compatible_node(struct device_node
*from, const char *type, const char *compatible)
函数功能:通过兼容性标识符获取设备树节点
函数头文件:#include <linux/of.h>
函数参数:
device_node
:写 NULL
type
:写 NULL
compatible
:设备节点上的 兼容性标识符属性的值
函数返回值:成功返回指向一个 struct device_node 指针,失败返回NULL。
struct device_node *of_find_node_by_path(const char *path);
函数功能:通过给定路径获取设备树节点
函数头文件:#include <linux/of.h>
函数参数:
path
:写你要查找节点的路径
比如你要找 xyd_led ,就填写 “/xyd_led” —表示从设备树上的根目录开始找 xyd_led 的节点。
函数返回值:成功返回指向一个 struct device_node 指针,失败返回NULL。
struct device_node 是设备树的一个重要结构体,用于描述设备树中的一个节点。一般我们只使用该结构中的const char *name; — 从设备树上找到的设备的节点名。
2.获取设备树节点的属性
int of_get_named_gpio(struct device_node *np,const char*propname, int index);
函数功能:从设备树(Device Tree)中获取 GPIO 的编号
函数头文件:#include <linux/of_gpio.h>
函数参数:
np
:指向 device_node 结构的指针,表示设备树中的设备节点。
propname
:指向包含 GPIO 编号的属性名称的字符串。例:“gpios”。
index
:属性中 GPIO 编号的索引获取— 就是你要获取第几个GPIO口。
函数返回值:成功获取得到的 gpio 口的编号 失败返回负数
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value);
函数功能:从设备树中的设备节点属性中读取一个 32 位无符号整数。
函数头文件:#include <linux/of.h>
函数参数:
np
:指向 device_node 结构的指针,表示设备树中的设备节点。
propname
:节点里具体哪一个属性 — 属性的名字
index
:获取这个属性里的一个值 — 如果一个默认就写 0
out_value
:保存获取得到信息
函数返回值:成功返回 0,失败返回负数。
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string);
函数功能:从设备树中的设备节点属性中读取一个字符串
函数头文件:#include <linux/of.h>
函数参数:
np
:指向 device_node 结构的指针,表示设备树中的设备节点。
propname
:获取的节点属性名。
out_string
:保存获取得到的属性值
函数返回值:成功返回 0,失败返回负数。
四、应用示例
目标:利用设备树+字符设备驱动实现对LED等的控制
1.设备树
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
* Copyright (c) 2021 Rockchip Electronics Co., Ltd.
*
*/
/dts-v1/;
#include "rk3588s-evb4-lp4x-yyt.dtsi"
#include "rk3588-linux.dtsi"
/ {
model = "Rockchip RK3588S EVB4 LP4X V10 Board";
compatible = "rockchip,rk3588s-evb4-lp4x-v10", "rockchip,rk3588";
xydled:xyd_leds {
compatible = "xyd_led";
leds-gpios = <&gpio0 RK_PC5 GPIO_ACTIVE_HIGH>,
<&gpio0 RK_PC6 GPIO_ACTIVE_HIGH>;
status = "okay";
};
};
没什么好说的,就是在一个子节点中添加了两个LED灯的GPIO口,默认高电平。
添加完,需在瑞芯微编写好的脚本去编译烧录。就是在RK3588s文件路径下的中断中使用:./build.sh kernel
命令重新编译boot.img并重新烧录。
使用该命令默认就编译设备树文件了,并且他把编译生成的设备树的 dtb 文件给继承了到 boot.img 镜像里。
开发板设备树节点的位置
/sys/firmware/devicetree/base/xyd_led
/proc/device-tree/xyd_led
为什么有两个路径?
1.不同的访问接口: /sys/firmware/devicetree/base 和 /proc/device-tree 提供不同的接口来访问设备树数据,适用于不同的需求和使用场景。sysfs 更适合用来与用户空间程序交互,而 procfs 更适合获取原始数据。
2.数据更新方式: /sys/firmware/devicetree/base 的数据可能会受到内核的处理和格式化影响,而 /proc/device-tree 则提供的是设备树的原始视图。
3.兼容性: 一些旧的工具或脚本可能仍然依赖于 /proc/device-tree,而较新的工具和机制可能使用 /sys/firmware/devicetree/base。同时,procfs 和 sysfs 具有不同的历史背景和设计目标。
2.驱动层
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include<linux/device.h>
#include <linux/gpio.h>
#include <linux/fs.h>
#include <linux/of_gpio.h>
#include <linux/of.h>
const char *out_string = NULL;
dev_t dev;//设备号
struct cdev mydev;
struct class *myclass = NULL;
int gpio_value[2] = {0};
int i=0;
int my_open (struct inode *inode, struct file *fp)
{
gpio_set_value(21,1);
gpio_set_value(22,1);
printk("led_Open ok\n");
return 0;
}
int my_release (struct inode *inode, struct file *fp)
{
gpio_set_value(21,0);
gpio_set_value(22,0);
printk("led_close ok\n");
return 0;
}
struct file_operations my_filop = {
.open = my_open,
.release = my_release,
};
static int __init my_open_init(void)
{
struct device_node *node=of_find_node_by_name(NULL,"xyd_leds");
//寻找指定的设备节点,并将找到的设备节点返回给node里
if(node == NULL)
{
printk("该节点不存在\n");
return -1;
}
printk("节点的名字:%s\n",node->name);
//获取设备树节点的属性
for(i=0;i<2;i++)
{
gpio_value[i] = of_get_named_gpio(node, "leds-gpios", i);
printk("gpio_value[%d]:%d\n",i,gpio_value[i]);
}
//获取设备节点上的属性
of_property_read_string(node,"status",&out_string);
printk("设备节点上的属性out_string:%s\n",out_string);
if(strcmp("okay", out_string)!=0)
{
printk("设备节点不可用!\n");
return 0;
}
gpio_request(gpio_value[0],"led1");
gpio_request(gpio_value[1],"led2");
gpio_direction_output(gpio_value[0],1);
gpio_direction_output(gpio_value[1],1);
alloc_chrdev_region(&dev,0, 1, "led_tree");//注册索取设备号
printk("设备号注册成功!\n");
printk("主设备号:%d\n",MAJOR(dev));
printk("次设备号:%d\n",MINOR(dev));
cdev_init(&mydev,&my_filop);
cdev_add(&mydev,dev,1);
myclass = class_create(THIS_MODULE, "class_led");
if(myclass == NULL)
{
printk("class_creat error!\n");
return -1;
}
device_create(myclass,NULL,dev,NULL,"led_shine");
return 0;
}
static void __exit my_open_exit(void)
{
device_destroy(myclass,dev);//销毁设备节点
class_destroy(myclass);//销毁设备类
cdev_del(&mydev);//删除字符设备
unregister_chrdev_region(dev,1);//释放设备号
printk("设备注销成功\n");
gpio_free(gpio_value[0]);
gpio_free(gpio_value[1]);
}
module_init(my_open_init);
module_exit(my_open_exit);
MODULE_LICENSE("GPL");
入口函数:
设备树
1.of_find_node_by_name寻找指定的设备节点,并将找到的设备节点返回给node里。
2.of_get_named_gpio获取节点上的属性,本例获取的就是GPIO的编号。
3.of_property_read_string获取节点上的属性,本例目的就是获取status的值。
LED灯gpio配置
1.先使用gpio_request申请GPIO口所需的资源。
2.配置工作模式。由于是控制LED灯,所以这里将模式设置为输出模式gpio_direction_output。
3.使用alloc_chrdev_region注册设备号。
4.使用cdev_init初始化设备。
5.使用cdev_add向内核申请linux2.6字符设备。
下面是自动创建节点的部分:
6.使用class_create创建一个类方便管理注册的设备。
7.使用device_create创建设备节点。
出口函数:
1.device_destroy销毁设备节点。
2.class_destroy销毁设备类。
3.cdev_del删除字符设备。
4.unregister_chrdev_region释放设备号。
5.gpio_free释放GPIO口的资源。
3.应用层
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{
int fd =0;
char buffer[100];
const char *data = "Hello, this is a test write!";
if(argc<2)
{
printf("请输入正确的参数\n");
return -1;
}
fd = open(argv[1],O_RDWR);
if(fd<0)
{
perror("open");
return -1;
}
while(1)
{
fd = open(argv[1],O_RDWR); // --- 底层的open函数
sleep(1);
close(fd);//底层的close
sleep(1);
}
return 0;
}
结果: