做钢材都有什么网站,中标查询,常州建设银行网站,做爰小视频网站文章目录 前言一、设备驱动的作用与本质1. 驱动的作用2. 有无操作系统的区别 二、内存管理单元MMU三、相关函数1. ioremap( )2. iounmap( )3. class_create( )4. class_destroy( ) 四、GPIO的基本知识1. GPIO的寄存器进行读写操作流程2. 引脚复用2. 定义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_BASE0x0000)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); \
})参数 ownerTHIS_MODULE (struct module结构体的首地址这个结构体存放了驱动的出口入口)namekobject对象名称 返回值 成功返回结构体首地址失败返回错误码指针  
注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 0xfdc600082. 定义GPIO寄存器物理地址 需要设置的寄存器的地址为baseoffset由下图可以知道GPIO1的基地址为0xFE740000    接下来就是确定GPIO的是输入还是输出我们这里需要的是GPIO_SWPORT_DDR_L。    可以看到GPIO_SWPORT_DDR_L的定义情况这里我们可以重复上面提到的命令行查看寄存器的设置情况我们的b0应当是第1x718位。    同样查看可以看到这里的值为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的偏移为808},
};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_pin16));	//设置数据方向寄存器为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_pin16));//设置数据寄存器为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需要源码可私聊笔者 
免责声明本程序参考了野火和北京讯为科技的部分视频资料不作商用仅供学习若有侵权和错误请联系笔者删除