Linux驱动开发笔记(二) 基于字符设备驱动的I/O操作
文章目录
- 前言
- 一、设备驱动的作用与本质
- 1. 驱动的作用
- 2. 有无操作系统的区别
- 二、内存管理单元MMU
- 三、相关函数
- 1. ioremap( )
- 2. iounmap( )
- 3. class_create( )
- 4. class_destroy( )
- 四、GPIO的基本知识
- 1. GPIO的寄存器进行读写操作流程
- 2. 引脚复用
- 2. 定义GPIO寄存器物理地址
- 五、实验代码
- 1. 宏定义出需要的地址
- 2. 编写LED字符设备结构体且初始化
- 3. container_of( )函数
- 4. file_operations结构体成员函数的实现
- 5. 实验效果
前言
前段时间我们学习了字符驱动,并实现了字符的回环发送,这部分我们将进行I/O的操作学习,以万能的点亮LED为例。
一、设备驱动的作用与本质
直接操作寄存器点亮LED和通过驱动程序点亮LED最本质的区别就是有无使用操作系统。 有操作系统的存在则大大降低了应用软件与硬件平台的耦合度,它充当了我们硬件与应用软件之间的纽带, 使得应用软件只需要调用驱动程序接口API就可以让硬件去完成要求的开发,而应用软件则不需要关心硬件到底是如何工作的。
1. 驱动的作用
设备驱动与底层硬件直接打交道,按照硬件设备的具体工作方式读写设备寄存器, 完成设备的轮询、中断处理、DMA通信,进行物理内存向虚拟内存的映射,最终使通信设备能够收发数据, 使显示设备能够显示文字和画面,使存储设备能够记录文件和数据。
2. 有无操作系统的区别
无操作系统(即裸机)时的设备驱动也就是直接操作寄存器的方式控制硬件,在这样的系统中,虽然不存在操作系统,但是设备驱动是必须存在的。 一般情况下,对每一种设备驱动都会定义为一个软件模块,包含.h文件和.c文件,前者定义该设备驱动的数据结构并声明外部函数, 后者进行设备驱动的具体实现。其他模块需要使用这个设备的时候,只需要包含设备驱动的头文件然后调用其中的外部接口函数即可。 比如我们在51或者STM32中直接看手册查找对应的寄存器,然后往寄存器相应的位写入数据0或1便可以实现LED的亮灭。
有操作系统时的设备驱动反观有操作系统。首先,驱动硬件工作的的部分仍然是必不可少的,其次,我们还需要将设备驱动融入内核。 为了实现这种融合,必须在所有的设备驱动中设计面向操作系统内核的接口,这样的接口由操作系统规定,对一类设备而言结构一致,独立于具体的设备,还是以led为例,我们就要将LED灯引脚对应的数据寄存器(物理地址)映射到程序的虚拟地址空间当中,然后我们就可以像操作寄存器一样去操作我们的虚拟地址啦!
二、内存管理单元MMU
MMU是一个实际的硬件,为编程提供了方便统一的内存空间抽象,MMU内部有一个专门存放页表的页表地址寄存器,该寄存器存放着页表的具体位置,这使得只要程序在被分配的虚拟地址范围内进行读写操作,实际上就是对设备(寄存器)的访问,如下图所示。他的主要作用是将虚拟地址翻译成真实的物理地址同时管理和保护内存, 不同的进程有各自的虚拟地址空间,某个进程中的程序不能修改另外一个进程所使用的物理地址,以此使得进程之间互不干扰,相互隔离。 总体而言MMU具有如下功能:
- 保护内存: MMU给一些指定的内存块设置了读、写以及可执行的权限,这些权限存储在页表当中,MMU会检查CPU当前所处的是特权模式还是用户模式,如果和操作系统所设置的权限匹配则可以访问,如果CPU要访问一段虚拟地址,则将虚拟地址转换成物理地址,否则将产生异常,防止内存被恶意地修改。
- 提供方便统一的内存空间抽象,实现虚拟地址到物理地址的转换: CPU可以运行在虚拟的内存当中,虚拟内存一般要比实际的物理内存大很多,使得CPU可以运行比较大的应用程序。


三、相关函数
上面提到了物理地址到虚拟地址的转换函数。包括ioremap()地址映射和取消地址映射iounmap()函数。
1. ioremap( )
//用于将物理内存地址映射到内核的虚拟地址空间
void __iomem *ioremap(phys_addr_t phys_addr, unsigned long size)//定义寄存器物理地址
#define GPIO0_BASE (0xFDD60000)
#define GPIO0_DR (GPIO0_BASE+0x0000)va_dr = ioremap(GPIO0_DR, 4); // 将物理地址GPIO0_DR,映射给虚拟地址指针,这段地址大小为4个字节
val = ioread32(va_dr); //读取该地址的值,保存到临时变量,重新赋值
val |= (0x00400000); // 设置GPIO0_A6引脚低电平
writel(val, va_dr); //把值重新写入到被映射后的虚拟地址当中,实际是往寄存器中写入了数据
- 参数:
- phys_addr:要映射的物理地址的起始地址
- size:要映射的内存区域的大小(以字节为单位)
- 返回值:
- 如果成功,ioremap返回一个指向映射区域的虚拟地址的指针
- 如果失败,返回NULL
在使用ioremap函数将物理地址转换成虚拟地址之后,理论上我们便可以直接读写I/O内存,但是为了符合驱动的跨平台以及可移植性, 我们应该使用linux中指定的函数(如:iowrite8()、iowrite16()、iowrite32()、ioread8()、ioread16()、ioread32()等)去读写I/O内存,如下表所示:
| 函数名 | 功能 |
|---|---|
| unsigned int ioread8(void __iomem *addr) | 读取一个字节(8bit) |
| unsigned int ioread16(void __iomem *addr) | 读取一个字(16bit) |
| unsigned int ioread32(void __iomem *addr) | 读取一个双字(32bit) |
| void iowrite8(u8 data, void __iomem *addr) | 写入一个字节(8bit) |
| void iowrite16(u16 data, void __iomem *addr) | 写入一个字(16bit) |
| void iowrite32(u32 data, void __iomem *addr) | 写入一个双字(32bit) |
2. iounmap( )
//取消地址映射
void iounmap(void *addr)iounmap(va_dr); //释放掉ioremap映射之后的起始地址(虚拟地址)
- 参数
- addr: 需要取消ioremap映射之后的起始地址(虚拟地址)。
- 返回值: 无
3. class_create( )
//提交目录信息
#define class_create(owner, name) \
({static struct lock_class_key _key; \_class_create(owner, name, &_key); \
})
- 参数
- owner:THIS_MODULE (struct module结构体的首地址这个结构体存放了驱动的出口入口)
- name:kobject对象名称
- 返回值
- 成功:返回结构体首地址
- 失败:返回错误码指针
注:IS_ERR(cls); 判断是否为错误指针
PTR_ERR(cls); 将错误码指针转换为错误码
4. class_destroy( )
//注销目录信息
void class_destroy(struct class *cls);
- 参数
- cls:结构体首地址
- 返回值:无
四、GPIO的基本知识
1. GPIO的寄存器进行读写操作流程
- 使能GPIO时钟(默认开启,不用设置)
- 设置引脚复用为GPIO(复位默认为GPIO,不用配置)
- 设置引脚属性(上下拉、速率、驱动能力,默认)
- 控制GPIO引脚为输出,并输出高低电平
2. 引脚复用
对于rockchip系类芯片,我们需要通过参考手册以及数据手册来确定引脚的复用功能。首先可以看到泰山派的小灯连接引脚,这里我们选择GPIO1_B0_d。

通过查询rk3568官方资料,可以看到该引脚的复用功能如下所示。
再查找其复用功能存在于SYS_GRF寄存器,和复用相关的总共8个寄存器,如下图所示:

查询 Rockchip_RK3568_TRM_Part1 手册,GRF_GPIO1B_IOMUX_L寄存器(由于GPIO1_b0是在低八位,下同),如下图所示:

寄存器总共32位,高16位都是使能位,控制低16位的写使能,低16位对应4个引脚,每个引脚占用3bits,不同的值引脚复用为不同功能。与此同时由[14:12]进行具体功能的设定。
我们可以查看到SYS_GRF寄存器的复用功能基地址为0xFDC60000。

此时通过命令行输入可以查询到该寄存器的设置情况,可以看到默认为GPIO口模式。
//目标地址为Address Base(0xfdc60000)+offset(0x0008)
io -r -4 0xfdc60008

2. 定义GPIO寄存器物理地址
需要设置的寄存器的地址为base+offset,由下图可以知道GPIO1的基地址为:0xFE740000

接下来就是确定GPIO的是输入还是输出,我们这里需要的是GPIO_SWPORT_DDR_L。


可以看到GPIO_SWPORT_DDR_L的定义情况,这里我们可以重复上面提到的命令行,查看寄存器的设置情况,我们的b0应当是第1x7+1=8位。

同样查看可以看到这里的值为0x00000700。

数据寄存器选择GPIO_SWPORT_DR_L,大致流程和上面一样就不再赘述了。这里便完成了对GPIO的设置。
五、实验代码

1. 宏定义出需要的地址
#define GPIO1_BASE (0xFE740000)//一个寄存器32位,其中高16位都是写使能位,控制低16位的写使能;低16位对应16个引脚,控制引脚的输出电平
#define GPIO1_DR_L (GPIO0_BASE + 0x0000) // GPIO0的低十六位引脚的数据寄存器地址
#define GPIO1_DR_H (GPIO0_BASE + 0x0004) // GPIO0的高十六位引脚的数据寄存器地址//一个寄存器32位,其中高16位都是写使能位,控制低16位的写使能;低16位对应16个引脚,控制引脚的输入输出模式
#define GPIO1_DDR_L (GPIO0_BASE + 0x0008) // GPIO0的低十六位引脚的数据方向寄存器地址
#define GPIO1_DDR_H (GPIO0_BASE + 0x000C) // GPIO0的低十六位引脚的数据方向寄存器地址
2. 编写LED字符设备结构体且初始化
//led字符设备结构体
struct led_chrdev {struct cdev dev;unsigned int __iomem *va_dr; // 数据寄存器虚拟地址保存变量unsigned int __iomem *va_ddr; // 数据方向寄存器虚拟地址保存变量unsigned int led_pin; // 引脚
};static struct led_chrdev led_cdev[DEV_CNT] = {{.led_pin = 8 //CPIO1_B0的偏移为8+0=8},
};
3. container_of( )函数
在Linux驱动编程当中我们会经常和container_of()这个函数打交道,其宏定义实现如下所示:
#define container_of(ptr, type, member) ({ \const typeof( ((type *)0)->member ) *__mptr = (ptr); \(type *)( (char *)__mptr - offsetof(type,member) );})
- 参数:
- ptr: 结构体变量中某个成员的地址
- type: 结构体类型
- member: 该结构体变量的具体名字
- 返回值: 结构体type的首地址
原理其实很简单,就是通过已知类型type的成员member的地址ptr,计算出结构体type的首地址。 type的首地址 = ptr - size ,需要注意的是它们的大小都是以字节为单位计算的,container_of( )函数的主要作用如下:
- 判断ptr 与 member 是否为同一类型
- 计算size大小,结构体的起始地址 = (type *)((char *)ptr - size) (注:强转为该结构体指针)
注:文件私有数据
一般很多的linux驱动都会将文件的私有数据private_data指向设备结构体,其保存了用户自定义设备结构体的地址。 自定义结构体的地址被保存在private_data后,可以通过读、写等操作通过该私有数据去访问设备结构体中的成员, 这样做体现了linux中面向对象的程序设计思想。
4. file_operations结构体成员函数的实现
static int led_chrdev_open(struct inode *inode, struct file *filp)
{unsigned int val = 0;struct led_chrdev *led_cdev = (struct led_chrdev *)container_of(inode->i_cdev, struct led_chrdev, dev);filp->private_data = container_of(inode->i_cdev, struct led_chrdev, dev);printk("open\n");//读取数据方向寄存器val = ioread32(led_cdev->va_ddr);//设置数据方向寄存器为pin位可写val |= ((unsigned int)0x1 << (led_cdev->led_pin+16)); //设置数据方向寄存器为pin位输出val |= ((unsigned int)0X1 << (led_cdev->led_pin));//写入数据方向寄存器iowrite32(val,led_cdev->va_ddr);//读取数据寄存器val = ioread32(led_cdev->va_dr);//设置数据寄存器为pin位可写val |= ((unsigned int)0x1 << (led_cdev->led_pin+16));//设置数据寄存器为pin位高电平val |= ((unsigned int)0x1 << (led_cdev->led_pin));//写入数据寄存器iowrite32(val, led_cdev->va_dr);return 0;
}
这部分代码位open_operations结构体的设置,其中container_of()函数和寄存器设置部分需要联系前节4.2的介绍反复理解(笔者这里看了很久才顿悟)。
5. 实验效果
#蓝灯亮
sudo sh -c 'echo 0 >/dev/led_chrdev0'
#蓝灯灭
sudo sh -c 'echo 1 >/dev/led_chrdev0'


需要源码可私聊笔者
免责声明:本程序参考了野火和北京讯为科技的部分视频资料,不作商用仅供学习,若有侵权和错误请联系笔者删除
相关文章:
Linux驱动开发笔记(二) 基于字符设备驱动的I/O操作
文章目录 前言一、设备驱动的作用与本质1. 驱动的作用2. 有无操作系统的区别 二、内存管理单元MMU三、相关函数1. ioremap( )2. iounmap( )3. class_create( )4. class_destroy( ) 四、GPIO的基本知识1. GPIO的寄存器进行读写操作流程2. 引脚复用2. 定义GPIO寄存器物理地址 五、…...
三品软件:打造高效安全的图文档管理体系
在数字化转型的浪潮中,工程设计单位和企业设计部门面临着电子图文档管理的巨大挑战。随着电子图纸和文档数量的激增,如何有效组织、管理和共享这些资源,成为提升工作效率和保障信息安全的关键。本文将探讨当前图文档管理面临的问题࿰…...
N1 one-hot编码
🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊# 前言 前言 onehot编码在机器学习比较常见,例如推荐系统中类别变量的处理等。 onehot 编码简介 One-hot编码(one-hot encoding&…...
数据库基础+增删查改初阶
数据库基础增删查改初阶 一。数据库操作 1.概念: 一个mysql服务器上有很多的表,把有关系的表放在一起就构成了一个数据集合,此时称为“数据库”,一个mysql1服务器上可以有多个这样的数据库 2.创建数据库: create …...
大模型日报2024-05-29
大模型日报 2024-05-29 大模型资讯 大型语言模型在金融预测中将超越人类分析师 摘要: 新研究表明,大型语言模型如ChatGPT在金融预测方面表现优于人类专家,为交易策略提供了宝贵的见解。这意味着未来这些模型将在金融领域发挥更重要的作用,提升…...
如何摆脱打工人任人宰割的命运
那就是为自己打工。 要有自己的思想,自己的目标,有自己的方向,坚决的非常自信的去执行它。 这样才是活出属于自己的人生,活出自己的精彩。 当然,这是在你已经比周围人优秀的情况下,至少是你觉得你比他们…...
“图片在哪”、“我是temunx”、“变成思维导图用xmindparser”gpt给出文本变字典
需求 我的意思是什么 分类清单“图片在哪 我是temunx变成思维导图 用xmindparser用 shell 画思维导图 x mind,可以 /storage/emulated/0/字体/黑体.ttf 保存/storage/emulated/0/print/图片/input图纸/完整代码 给个文本内容”任务清单 调整语言顺序文不对题的…...
【LeetCode】【5】最长回文子串
文章目录 [toc]题目描述样例输入输出与解释样例1样例2 提示Python实现动态规划 个人主页:丷从心 系列专栏:LeetCode 刷题指南:LeetCode刷题指南 题目描述 给一个字符串s,找到s中最长的回文子串 样例输入输出与解释 样例1 输入…...
主播们直播时的美颜是如何实现的?集成第三方美颜SDK方案详解
很多人问小编,主播们直播时的美颜效果是如何实现的呢?接下来,我将为您详细介绍美颜功能的实现原理。 一、美颜功能的基本原理 通过对图像进行实时处理,达到美化人脸的效果。其主要技术包括: 1.人脸检测与关键点定位 …...
Leetcode - 131双周赛
一,3158. 求出出现两次数字的 XOR 值 本题是一道纯模拟题,直接暴力。 代码如下: class Solution {public int duplicateNumbersXOR(int[] nums) {int ans 0;long t 0;for(int x : nums){if(((t>>x)&1) 1){ans ^ x;}else{t | (…...
【CSharp】判断目录以及文件是否存在
【CSharp】判断目录以及文件是否存在 1.背景2.判断目录3.判断文件1.背景 我们在进行磁盘IO的时候进行需要判断目录、文件是否存在,根据判断结果再做进一步的操作。 其中判断目录是否存在,涉及Directory.Exists(String) 方法; 命名空间:System.IO 方法功能:确定给定路径是…...
kali基本扫描工具(自带)
免责声明:本文仅做技术交流与学习...请勿非法破坏... 详细用法: 命令 -h/百度/翻译 fping 用法 hostlist 文件里面为ip fping -a -q -f hostlist -a 只看存活的 fping -g 202.100.1.1 202.100.1.255 -a -q > Ahost 输出到Ahost文件上 nping nping -c 1 201.100.2.155-244 …...
与MySQL的初相遇
🌎初识MySQL 注:本文SQL语句只为了验证猜想,不会也不要紧。 文章目录: MySql开端 认识数据库 什么是数据库 主流数据库 MySQL的本质 MySQL基础使用 连接mysql服务器 …...
详解Spring IoCDI(一)
目录 1.什么是IoC 2.IoC应用场景(案例分析) 2.1传统程序开发 2.2问题分析 2.3解决方案 2.4IoC 优势 3. DI概念 4.IoC详解 4.1Bean的存储 4.2Controller(控制器存储) 4.3获取Bean 4.4Bean相关注解 1.什么是IoC Spring…...
Android 14 - 绘制体系 - 概览
从Android 12开始,Android的绘制系统有结构性变化, 在绘制的生产消费者模式中,新增BLASTBufferQueue,客户端进程自行进行queue的生产和消费,随后通过Transation提交到SurfaceFlinger,如此可以使得各进程将缓…...
【RAG论文】文档树:如何提升长上下文、非连续文档、跨文档主题时的检索效果
RAPTOR Recursive Abstractive Processing for Tree-Organized RetrievalICLR 2024 Stanfordhttps://arxiv.org/pdf/2401.18059 RAPTOR(Recursive Abstractive Processing for Tree-Organized Retrieval)是一种创建新的检索增强型语言模型,它…...
【前端每日基础】day27——小程序开发
小程序开发详细介绍 基本概念 小程序:小程序是一种无需下载安装即可使用的应用。用户通过微信搜索或扫描二维码即可打开小程序。小程序具有触手可及、用完即走、体验良好的特点。 组成部分: WXML:用于描述页面的结构。 WXSS:用于…...
【C语言】指针速览
指针速览 指针1.野指针与空指针2. 空类型指针 void *3. 指针常量4. 常量指针5. 指向常量的指针常量6. 指针操作数组6.1 数组名作为函数参数 7. 多级指针8. 函数指针8.1 函数指针数组 最后 指针 指针就是内存的字节单元编号地址,指针变量就是存放地址的变量。 1.野…...
Java基础学习:深入解析Java中的位运算符
在Java中,位运算符用于对整数类型的值进行位运算。以下是Java中的位运算符: 位与(&):两位都为1时,结果为1,否则为0。 位或(|):两位中有1个为1,结果为1。 位非(~):位的反&#…...
9.Redis之list类型
list相当于链表、数据表 1.list类型基本介绍 列表中的元素是有序的"有序"的含义,要根据上下文区分~~有的时候,谈到有序,指的是"升序","降序”有的时候,谈到的有序,指的是, 顺序很关键~~如果把元素位置颠倒,顺序调换.此时得到的新的 List 和之前的 Li…...
观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...
MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
初学 pytest 记录
安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…...
用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...
人机融合智能 | “人智交互”跨学科新领域
本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...
SpringAI实战:ChatModel智能对话全解
一、引言:Spring AI 与 Chat Model 的核心价值 🚀 在 Java 生态中集成大模型能力,Spring AI 提供了高效的解决方案 🤖。其中 Chat Model 作为核心交互组件,通过标准化接口简化了与大语言模型(LLM࿰…...
