Linux驱动开发笔记(二) 基于字符设备驱动的GPIO操作
文章目录
- 前言
- 一、设备驱动的作用与本质
- 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驱动开发笔记(二) 基于字符设备驱动的GPIO操作
文章目录 前言一、设备驱动的作用与本质1. 驱动的作用2. 有无操作系统的区别 二、内存管理单元MMU三、相关函数1. ioremap( )2. iounmap( )3. class_create( )4. class_destroy( ) 四、GPIO的基本知识1. GPIO的寄存器进行读写操作流程2. 引脚复用2. 定义GPIO寄存器物理地址 五、…...
【ESP32之旅】ESP32 PlatformIO 固件单独烧录
背景 有时候使用PIO编写的代码需要发给客户去验证,相比较于发送源码直接发送bin文件,更加的安全而且高效。不用担心源码的泄漏,也不用帮客户配置PIO环境。 操作方法 1.编译 首先进行代码编译,如编译成功会在 .pio\build\airm2…...
视频监控业务平台LntonCVS运用国标协议对接视频汇聚管理综合平台应用方案
为了实现“以信息化推动应急管理能力现代化”的目标,应急管理部提出了加速现代信息技术与应急管理业务深度融合的计划。这一计划是国家加强和改进应急管理工作的关键举措,也是满足日益严峻的应急管理形势和人民群众不断增长的公共安全需求的紧迫需求。 为…...
【Linux 网络编程】协议的分层知识!
文章目录 1. 计算机网络背景2. 认识 "协议"3. 协议分层 1. 计算机网络背景 网络互联: 多台计算机连接在一起, 完成数据共享; 🍎局域网(LAN----Local Area Network): 计算机数量更多了, 通过交换机和路由器连接。 🍎 广…...
Firefox国际版
Firefox国际版官方网址: Download the Firefox Browser in English (US) and more than 90 other languagesEveryone deserves access to the internet — your language should never be a barrier. That’s why — with the help of dedicated volunteers around…...
封装和解构是 Python 中常用的技术
目录 前言 一、封装(Packing): 二、解构(Unpacking): 2.1 解构元组或列表: 2.2 解构字典: 2.3 使用*进行解构: 2.4 解构函数返回值 总结 前言 提示:这…...
理解OAuth:服务间的授权机制
理解OAuth:服务间的授权机制 好的,让我来教你一下关于这个奇怪的东西。 在不同的项目中,认证有很多不同的方式。但在我们深入探讨它的使用方式之前,让我们先来看看它最初的用途。 首先,我们可以从名称中得到一些线索。“auth”这个词与什么有关呢?问题是,这里的“aut…...
JRT性能演示
演示视频 君生我未生,我生君已老,这里是java信创频道JRT,真信创-不糊弄。 基础架构决定上层建筑,和给有些品种的植物种植一样,品种不对,施肥浇水再多,也是不可能长成参天大树的。JRT吸收了各方…...
React 使用JSX或者TSX渲染页面
02 Rendering with JSX Your first JSX content In this section, we’ll implement the obligatory " Hello, World " JSX application. At this point, we’re just dipping our toes in the water; more in-depth examples will follow. We’ll also discuss wh…...
【Linux】Socket中的心跳机制(心跳包)
Socket中的心跳机制(心跳包) 1. 什么是心跳机制?(心跳包) 在客户端和服务端长时间没有相互发送数据的情况下,我们需要一种机制来判断连接是否依然存在。直接发送任何数据包可以实现这一点,但为了效率和简洁,通常发送一个空包&am…...
Java怎样动态给对象添加属性并赋值【代码实现】
本篇文章主要介绍Java如何给已有实体类动态的添加字段并返回新的实体对象且不影响原来的实体对象结构。 参考代码如下: 引入依赖包 <dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>2.2.2</…...
C#多线程同步lock、Mutex
C#使用多线程可以通过System.Threading命名空间下的Thread类来实现 lock和Mutex用于实现线程同步的机制: 上代码: class People{public People(int idd){id idd;}public int id;public int age;}class TestHelper{public TestHelper() { }List<Peo…...
模型实战(21)之 C++ - tensorRT部署yolov8-det 目标检测
C++ - tensorRT部署yolov8-det 目标检测 python环境下如何直接调用推理模型转换并导出:pt -> onnx ->.engineC++ tensorrt 部署检测模型不写废话了,直接上具体实现过程+all代码 1.Python环境下推理 直接命令行推理,巨简单yolo detect predict model=yolov8n.pt source…...
Linux完整版命令大全(二十一)
mkswap 功能说明:设置交换区(swap area)。语 法:mkswap [-cf][-v0][-v1][设备名称或文件][交换区大小]补充说明:mkswap可将磁盘分区或文件设为Linux的交换区。参 数: -c 建立交换区前,先检查是否有损坏的区…...
Elasticsearch集群许可证过期问题解决方法汇总
最近在使用elasticsearch的过程中,使用elastic-head进行可视化展示集群的状态和信息,从2024年5月18日突然elastic-head无法现在集群的状态界面啦,elasticsearch集群状态是正常,命令如下: curl -X GET "localhost:9200/_cluster/health?pretty" 在google页面上通过…...
Golang实现根据文件后缀删除文件和递归删除文件
概述 这个功能会非常强大,因为在日常工作中,我通常会遇到需要批量删除文件的场景,通过这个方法,再结合我的另一个 命令行开发框架,能够很轻松的开发出这个功能。 代码 package zdpgo_fileimport ("errors"…...
Python OCR 文字识别使用模型:读光-文字识别-行识别模型-中英-通用领域
介绍 什么是OCR? OCR是“Optical Character Recognition”的缩写,中文意为“光学字符识别”。它是一种技术,可以识别和转换打印在纸张或图像上的文字和字符为机器可处理的格式,如计算机文本文件。通过使用OCR技术,可…...
Python | Leetcode Python题解之第105题从前序与中序遍历序列构造二叉树
题目: 题解: class Solution:def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:if not preorder:return Noneroot TreeNode(preorder[0])stack [root]inorderIndex 0for i in range(1, len(preorder)):preorderVal pr…...
经典面试题:什么是事物的隔离级别?什么是MVCC?
目录 前言一、事物的隔离级别二、隔离级别是如何实现的1. 锁定技术2. 多版本并发控制(MVCC)基本原理实现机制优缺点 3. 乐观锁与悲观锁 三、MySQL设置隔离级别MySQL数据库Java (使用JDBC) 前言 事务的四大特性(ACID特性)…...
Java程序中,不同jar包,有2个完全相同的类,运行时的加载顺序
在Java程序中,如果有两个完全相同的类存在于不同的jar包中,那么在运行时究竟加载哪个类,主要取决于类加载器的类路径(Classpath)搜索顺序。 Java使用类加载器(ClassLoader)来动态加载类。当Jav…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...
Spring Security 认证流程——补充
一、认证流程概述 Spring Security 的认证流程基于 过滤器链(Filter Chain),核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤: 用户提交登录请求拦…...
微服务通信安全:深入解析mTLS的原理与实践
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、引言:微服务时代的通信安全挑战 随着云原生和微服务架构的普及,服务间的通信安全成为系统设计的核心议题。传统的单体架构中&…...
《信号与系统》第 6 章 信号与系统的时域和频域特性
目录 6.0 引言 6.1 傅里叶变换的模和相位表示 6.2 线性时不变系统频率响应的模和相位表示 6.2.1 线性与非线性相位 6.2.2 群时延 6.2.3 对数模和相位图 6.3 理想频率选择性滤波器的时域特性 6.4 非理想滤波器的时域和频域特性讨论 6.5 一阶与二阶连续时间系统 6.5.1 …...
对象回调初步研究
_OBJECT_TYPE结构分析 在介绍什么是对象回调前,首先要熟悉下结构 以我们上篇线程回调介绍过的导出的PsProcessType 结构为例,用_OBJECT_TYPE这个结构来解析它,0x80处就是今天要介绍的回调链表,但是先不着急,先把目光…...
