Linux驱动(五):Linux2.6驱动编写之设备树

2024-09-05 08:44

本文主要是介绍Linux驱动(五):Linux2.6驱动编写之设备树,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 前言
  • 一、设备树是个啥?
  • 二、设备树编写语法规则
    • 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:设备当前的状态 ,类似与开关,四种状态。— 类型也是一个字符串

  1. “okay”:表示设备启用并且应该被驱动程序使用。设备在系统启动时会被初始化。
  2. “disabled”:表示设备被禁用。驱动程序不会加载和初始化这个设备,但他仍在设备树里。
  3. “fail”:表示设备初始化失败,通常用于在设备初始化时出现问题的情况下。设备将不会被驱动程序使用。
  4. “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);//底层的closesleep(1);}	return 0;
}

结果:
在这里插入图片描述

在这里插入图片描述


这篇关于Linux驱动(五):Linux2.6驱动编写之设备树的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1138479

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo