浦东医院网站建设,怎么用div做网站,外贸做编织袋常用网站,国外免费网站空间通过字符设备章节的学习#xff0c;我们已经了解了字符设备驱动程序的基本框架#xff0c;主要是掌握如何申请及释放设备号、添加以及注销设备#xff0c;初始化、添加与删除cdev 结构体#xff0c;并通过cdev_init 函数建立cdev 和file_operations 之间的关联#xff0c;…通过字符设备章节的学习我们已经了解了字符设备驱动程序的基本框架主要是掌握如何申请及释放设备号、添加以及注销设备初始化、添加与删除cdev 结构体并通过cdev_init 函数建立cdev 和file_operations 之间的关联cdev 结构体和file_operations 结构体非常重要希望大家着重掌握。 
本小节我们将带领大家做一个激动人心的小实验–点亮led。前面我们已经通过操作寄存器的方式点亮了LED本节我们将带领大家进入点亮开发板RGB LED 灯的世界学习一下如何在linux环境下驱动RGB LED 灯。 
首先我们需要明白直接操作寄存器和通过驱动程序点亮LED 有什么区别。 
设备驱动的作用与本质 
直接操作寄存器点亮LED 和通过驱动程序点亮LED 最本质的区别就是有无使用操作系统。有操作系统的存在则大大降低了应用软件与硬件平台的耦合度它充当了我们硬件与应用软件之间的纽带使得应用软件只需要调用驱动程序接口API 就可以让硬件去完成要求的开发而应用软件则不需要关心硬件到底是如何工作的。这将大大提高我们应用程序的可移植性和开发效率。 
驱动的作用 
设备驱动与底层硬件直接打交道按照硬件设备的具体工作方式读写设备寄存器完成设备的轮询、中断处理、DMA 通信进行物理内存向虚拟内存的映射最终使通信设备能够收发数据使显示设备能够显示文字和画面使存储设备能够记录文件和数据。 
在系统中没有操作系统的情况下工程师可以根据硬件设备的特点自行定义接口如对LED 定义LightOn()、LightOff() 等。而在有操作系统的情况下设备驱动的架构则由相应的操作系统定义驱动工程师必须按照相应的架构设计设备驱动如在本次实验中必须设计file_operations 的接口。这样设备驱动才能良好地整合到操作系统的内核中。 
有无操作系统的区别 无操作系统即裸机时的设备驱动也就是直接操作寄存器的方式控制硬件在这样的系统中虽然不存在操作系统但是设备驱动是必须存在的。一般情况下对每一种设备驱动都会定义为一个软件模块包含.h 文件和.c 文件前者定义该设备驱动的数据结构并声明外部函数后者进行设备驱动的具体实现。其他模块需要使用这个设备的时候只需要包含设备驱动的头文件然后调用其中的外部接口函数即可。这在STM32 的开发中很常见也相对比较简单。  有操作系统时的设备驱动反观有操作系统时首先驱动硬件工作的的部分仍然是必不可少的其次我们还需要将设备驱动融入内核。为了实现这种融合必须在所有的设备驱动中设计面向操作系统内核的接口这样的接口由操作系统规定对一类设备而言结构一致独立于具体的设备。  
由此可见当系统中存在操作系统的时候设备驱动变成了连接硬件和内核的桥梁。操作系统的存在势必要求设备驱动附加更多的代码和功能把单一的驱动变成了操作系统内与硬件交互的模块它对外呈现为操作系统的API。 
操作系统的存在究竟带来了什么好处呢 
首先操作系统完成了多任务并发其次操作系统为我们提供了内存管理机制32 位Linux 操作系统可以让每个进程都能独立访问4GB 的内存空间对于应用程序来说应用程序将可使用统一的系统调用接口来访问各种设备通过write()、read() 等函数读写文件就可以访问各种字符设备和块设备而不用管设备的具体类型和工作方式。 
内存管理单元MMU 
在linux 环境直接访问物理内存是很危险的如果用户不小心修改了内存中的数据很有可能造成错误甚至系统崩溃。为了解决这些问题内核便引入了MMU 
MMU 的功能 
MMU 为编程提供了方便统一的内存空间抽象其实我们的程序中所写的变量地址是虚拟内存当中的地址倘若处理器想要访问这个地址的时候MMU 便会将此虚拟地址Virtual Address翻译成实际的物理地址Physical Address之后处理器才去操作实际的物理地址。MMU 是一个实际的硬件并不是一个软件程序。他的主要作用是将虚拟地址翻译成真实的物理地址同时管理和保护内存不同的进程有各自的虚拟地址空间某个进程中的程序不能修改另外一个进程所使用的物理地址以此使得进程之间互不干扰相互隔离。而且我们可以使用虚拟地址空间的一段连续的地址去访问物理内存当中零散的大内存缓冲区。很多实时操作系统都可以运行在无MMU的CPU 中比如uCOS、FreeRTOS、uCLinux以前想CPU 也运行linux 系统必须要该CPU 具备MMU但现在Linux 也可以在不带MMU 的CPU 中运行了。总体而言MMU 具有如下功能 保护内存 MMU 给一些指定的内存块设置了读、写以及可执行的权限这些权限存储在页表当中MMU 会检查CPU 当前所处的是特权模式还是用户模式如果和操作系统所设置的权限匹配则可以访问如果CPU 要访问一段虚拟地址则将虚拟地址转换成物理地址否则将产生异常防止内存被恶意地修改。  提供方便统一的内存空间抽象实现虚拟地址到物理地址的转换 CPU 可以运行在虚拟的内存当中虚拟内存一般要比实际的物理内存大很多使得CPU 可以运行比较大的应用程序  
到底什么是虚拟地址什么是物理地址 
当没有启用MMU 的时候CPU 在读取指令或者访问内存时便会将地址直接输出到芯片的引脚上此地址直接被内存接收这段地址称为物理地址如下图所示。 简单地说物理地址就是内存单元的绝对地址好比你电脑上插着一张8G 的内存条则第一个存储单元便是物理地址0x0000内存条的第6 个存储单元便是0x0005无论处理器怎样处理物理地址都是它最终的访问的目标。 
当CPU 开启了MMU 时CPU 发出的地址将被送入到MMU被送入到MMU 的这段地址称为虚拟地址之后MMU 会根据去访问页表地址寄存器然后去内存中找到页表假设只有一级页表的条目从而翻译出实际的物理地址如下图所示。 对于I.MX 6ULL 这种32 位处理器而言其虚拟地址空间共有4G(2^32), 一旦CPU 开启了MMU任何时候CPU 发出的地址都是虚拟地址为了实现虚拟地址到物理地址之间的映射MMU 内部有一个专门存放页表的页表地址寄存器该寄存器存放着页表的具体位置用ioremap 映射一段地址意味着使用户空间的一段地址关联到设备内存上这使得只要程序在被分配的虚拟地址范围内进行读写操作实际上就是对设备寄存器的访问。 
TLB 的作用 
讲到MMU 我又忍不住和大家说下TLBTranslation Lookaside Buffer的作用。由上面的地址转换过程可知当只有一级页表进行地址转换的时候CPU 每次读写数据都需要访问两次内存第一次是访问内存中的页表第二次是根据页表找到真正需要读写数据的内存地址如果使用两级了表那么CPU 每次读写数据都需要访问3 次内存这样岂不是显得非常繁琐且耗费CPU 的性能 
那有什么更好的解决办法呢答案是肯定的为了解决这个问题TLB 便孕育而生。在CPU 传出一个虚拟地址时MMU 最先访问TLB假设TLB 中包含可以直接转换此虚拟地址的地址描述符则会直接使用这个地址描述符检查权限和地址转换如果TLB 中没有这个地址描述符MMU 才会去访问页表并找到地址描述符之后进行权限检查和地址转换然后再将这个描述符填入到TLB 中以便下次使用实际上TLB 并不是很大那TLB 被填满了怎么办呢如果TLB 被填满则会使用round-robin 算法找到一个条目并覆盖此条目。 
由于MMU 非常复杂在此我们不做过于深入的了解大家只要大概知道它的作用即可感兴趣的同学可以到网上查阅相关资料对于初学者还是建议先掌握全局然后再深挖其中重要的细节千万不能在汪洋大海中迷失了方向。本小结我们主要用到的是MMU 的地址转换功能在linux 环境中我们开启了MMU 之后想要读写具体的寄存器物理地址就必须用到物理地址到虚拟地址的转换函数。 
地址转换函数 
上面提到了物理地址到虚拟地址的转换函数。包括ioremap() 地址映射和取消地址映射iounmap()函数。 
ioremap 函数 
列表1: 地址映射函数(内核源码/arch/arc/mm/ioremap.c) 
void __iomem *ioremap(phys_addr_t paddr, unsigned long size)
#define ioremap ioremap函数参数和返回值如下 参数 
paddr被映射的IO 起始地址物理地址size需要映射的空间大小以字节为单位 
返回值一个指向__iomem 类型的指针当映射成功后便返回一段虚拟地址空间的起始地址我们可以通过访问这段虚拟地址来实现实际物理地址的读写操作。 
ioremap 函数是依靠__ioremap 函数来实现的只是在__ioremap 当中其最后一个要映射的I/O 空间和权限有关的标志flag 为0。在使用ioremap 函数将物理地址转换成虚拟地址之后理论上我们便可以直接读写I/O 内存但是为了符合驱动的跨平台以及可移植性我们应该使用linux 中指定的函数如iowrite8()、iowrite16()、iowrite32()、ioread8()、ioread16()、ioread32() 等去读写I/O 内存而非直接通过映射后的指向虚拟地址的指针进行访问。读写I/O 内存的函数如下 
列表2: 读写I/O 函数 
unsigned int ioread8(void __iomem *addr)
unsigned int ioread16(void __iomem *addr)
unsigned int ioread32(void __iomem *addr)void iowrite8(u8 b, void __iomem *addr)
void iowrite16(u16 b, void __iomem *addr)
void iowrite32(u32 b, void __iomem *addr)第1 行读取一个字节8bit第2 行读取一个字16bit第3 行读取一个双字32bit第5 行写入一个字节8bit第6 行写入一个字16bit第7 行写入一个双字32bit 
对于读I/O 而言他们都只有一个__iomem 类型指针的参数指向被映射后的地址返回值为读取到的数据据对于写I/O 而言他们都有两个参数第一个为要写入的数据第二个参数为要写入的地址返回值为空。与这些函数相似的还有writeb、writew、writel、readb、readw、readl 等在ARM 架构下writexreadx函数与iowritexioreadx有一些区别writexreadx不进行端序的检查而iowritexioreadx会进行端序的检查。 
说了这么多大家可能还是不太理解那么我们来举个栗子比如我们需要操作RGB 灯中的蓝色led 中的数据寄存器在51 或者STM32 当中我们是直接看手册查找对应的寄存器然后往寄存器相应的位写入数据0 或1 便可以实现LED 的亮灭假设已配置好了输出模式以及上下拉等。前面我们在不带linux 的环境下也是用的类似的方法但是当我们在linux 环境且开启了MMU 之后我们就要将LED 灯引脚对应的数据寄存器物理地址映射到程序的虚拟地址空间当中然后我们就可以像操作寄存器一样去操作我们的虚拟地址啦其具体代码如下所示。 
列表3: 地址映射 
#define AHB4_PERIPH_BASE (0x50000000)
#define RCC_BASE (AHB4_PERIPH_BASE  0x0000)
#define RCC_MP_GPIOENA (RCC_BASE  0XA28)va_clkaddr  ioremap(RCC_MP_GPIOENA, 4);// 映射物理地址到虚拟地址gpio 时钟rcc寄存器
val | (0x43); // 开启a、b、g 的时钟
writel(val, va_clkaddr);第1-3 行定义寄存器物理地址第5 行将物理地址RCC_MP_GPIOENA映射给虚拟地址指针这段地址大小为4 个字节第6 行将要写入的值写入一个临时变量第7 行把值重新写入到被映射后的虚拟地址当中实际是往寄存器中写入了数据 
iounmap 函数 
iounmap 函数定义如下 
列表4: 取消地址映射函数(内核源码/arch/arc/mm/ioremap.c) 
void iounmap(void *addr)
#define iounmap iounmap函数参数和返回值如下 
参数 
addr需要取消ioremap 映射之后的起始地址虚拟地址。 
返回值无 
例如我们要取消一段被ioremap 映射后的地址可以用下面的写法。 
列表5: 取消ioremap 映射地址:linenos: 
iounmap(va_dr); //释放掉ioremap 映射之后的起始地址虚拟地址点亮LED 灯实验 
从第一章内核模块再到第二章字符设备驱动从理论到实验总算是一切准备就绪让我们开始着手写LED 的驱动代码吧。首先我们需要一个LED 字符设备结构体它应该包含我们要操作的寄存器地址。其次是模块的加载卸载函数加载函数需要注册设备卸载函数则需要释放申请的资源。然后就是file_operations 结构体以及openwriteread 相关接口的实现。 
实验说明 
硬件介绍 
本节实验使用到STM32MP1 开发板上的RGB 彩灯。 
硬件原理图分析 
了解RGB 灯的实物后可打开相应的原理图文档来查看硬件连接具体见下图。   LED_R 的阴极连接到STM32MP1 芯片上GPIO_A13 引脚LED_G 连接到GPIO_G2LED_B 连接到GPIO_B5如下表所示。 
LED 灯原理图的标号GPIO 端口及引脚编号LED 红灯LED_RGPIO_A13LED 绿灯LED_GGPIO_G2LED 蓝灯LED_BGPIO_B5
对于RGB 灯的控制进行控制也就是对上述GPIO 的寄存器进行读写操作。可大致分为以下几个步骤 
使能GPIO 时钟设置引脚复用为GPIO(本节不用)设置引脚属性(上下拉、速率、驱动能力)控制GPIO 引脚输出高低电平 
对RGB 的R 灯进行寄存器配置 
GPIO 时钟 
跟GPIO 相关的时钟主要有Enable For MCU\MPU Set\Clear Register 寄存器。 
查看数据手册P872(参考位置) 可以知道GPIO 控制引脚时钟通过四个寄存器来控制GPIO 的时钟。 
由于STM32MP157 为异构处理器所以GPIO 的时钟控制分为MCU 及MPU 两类寄存器这两类寄存器中每类对时钟的控制又分为使能时钟控制寄存器和失能时钟控制寄存器。 对使能时钟控制寄存器和失能时钟控制寄存器对应的位置1 时设置对应外设的功能 引脚复用GPIO 
对于STM32MP1 系类芯片我们需要通过参考手册以及数据手册来确定引脚的复用功能引脚复用相关的信息可以通过数据手册查询 复用寄存器GPIOx_AFRL(GPIO alternate function low register) 的内容我们可以在参考手册中查看到关于该寄存器的配置见下图 GPIO 外设寄存器地址为0x50002000加上对应的偏移即可访问到复用配置寄存器。通过配置对应端口的寄存器即可设置对应引脚复用功能。 
引脚属性 
寄存器总览为GPIO registers GPIOx_MODER模式寄存器用以设置GPIO 引脚的模式可为输入模式、输出模式、复用模式、模拟模式。GPIOx_OTYPER输出类型寄存器用以设置GPIO 引脚的输出模式可为推挽输出、开漏输出。GPIOx_OSPEEDR速度寄存器用以设置GPIO 引脚的输出速度等级可为低、中、高、非常高。GPIOx_PUPDR上下拉配置寄存器用以设置GPIO 引脚的上下拉状态可为不上下拉、上拉、下拉。GPIOx_IDR输入寄存器用以读取GPIO 引脚的输入状态可读取为0、1。GPIOx_ODR输出寄存器当IO 用作输出的时候此寄存器用来设置IO 输出的电平高低。GPIOx_BSRR置位寄存器当IO 用作输出的时候此寄存器也可用来设置IO 输出的电平高低。 
硬件原理以及寄存器配置到此为止更多硬件上的信息可以查看原理图和芯片手册。 
代码讲解 
本章的示例代码目录为linux_driver/led_cdev/ 
定义GPIO 寄存器物理地址 
列表6: LED 灯用到的GPIO 资源 
#define AHB4_PERIPH_BASE (0x50000000)#define RCC_BASE (AHB4_PERIPH_BASE  0x0000)
#define RCC_MP_GPIOENA (RCC_BASE  0XA28)#define GPIOA_BASE (AHB4_PERIPH_BASE  0x2000)
#define GPIOA_MODER (GPIOA_BASE  0x0000)
#define GPIOA_OTYPER (GPIOA_BASE  0x0004)
#define GPIOA_OSPEEDR (GPIOA_BASE  0x0008)
#define GPIOA_PUPDR (GPIOA_BASE  0x000C)
#define GPIOA_BSRR (GPIOA_BASE  0x0018)#define GPIOG_BASE (AHB4_PERIPH_BASE  0x8000)
#define GPIOG_MODER (GPIOG_BASE  0x0000)
#define GPIOG_OTYPER (GPIOG_BASE  0x0004)
#define GPIOG_OSPEEDR (GPIOG_BASE  0x0008)
#define GPIOG_PUPDR (GPIOG_BASE  0x000C)
#define GPIOG_BSRR (GPIOG_BASE  0x0018)#define GPIOB_BASE (AHB4_PERIPH_BASE  0x3000)
#define GPIOB_MODER (GPIOB_BASE  0x0000)
#define GPIOB_OTYPER (GPIOB_BASE  0x0004)
#define GPIOB_OSPEEDR (GPIOB_BASE  0x0008)
#define GPIOB_PUPDR (GPIOB_BASE  0x000C)
#define GPIOB_BSRR (GPIOB_BASE  0x0018)代码中使用宏定义定义出了LED 灯使用到的GPIO 资源物理地址在后面需要将这些寄存器物理地址映射到虚拟地址上供配置使用。 
编写LED 字符设备结构体且初始化 
列表7: led 字符设备结构体 
struct led_chrdev {
struct cdev dev;unsigned int __iomem *va_moder; // 模式寄存器虚拟地址保存变量unsigned int __iomem *va_otyper; // 输出类型寄存器虚拟地址保存变量unsigned int __iomem *va_ospeedr; // 速度配置寄存器虚拟地址保存变量unsigned int __iomem *va_pupdr; // 上下拉寄存器虚拟地址保存变量unsigned int __iomem *va_bsrr; // 置位寄存器虚拟地址保存变量unsigned int led_pin; // 引脚偏移
};unsigned int __iomem *va_clkaddr;static struct led_chrdev led_cdev[DEV_CNT]  {{.led_pin  13}, // 定义GPIO 引脚号{.led_pin  2},{.led_pin  5},
};在上面的代码中我们定义了一个RGB 灯的结构体并且定义且初始化了一个RGB 灯的结构体数组因为我们开发板上面共有3 个RGB 灯所以代码中DEV_CNT 为3。在初始化结构体的时候我们以“.”“变量名字”的形式来访问且初始化结构体变量的初始化结构体变量的时候要以“”隔开使用这种方式简单明了方便管理数据结构中的成员。 
第2 行定义了保存模式寄存器虚拟地址的变量第3 行定义了保存输出类型寄存器虚拟地址的变量第4 行定义了保存速度配置寄存器虚拟地址的变量第5 行定义了保存上下拉寄存器虚拟地址的变量第6 行定义了保存置位寄存器虚拟地址的变量第9 行LED 的引脚号第12 行定义了保存GPIO 时钟寄存器虚拟地址的变量第15-17 行初始化三个LED 灯结构体成员变量 
内核RGB 模块的加载和卸载函数 
第一部分为内核RGB 模块的加载函数其主要完成了以下任务 
将LED 结构体里的虚拟地址给映射好让虚拟地址与GPIO 的物理寄存器地址对应上调用alloc_chrdev_region() 函数向系统动态申请一个未被占用的设备号 使用alloc_chrdev_region() 相比较于register_chrdev_region() 的好处在于不必自己费时间去查看那些是未被占用的设备号避免了设备号重复问题调用class_create() 函数创建一个RGB 灯的设备类分别给三个LED 建立其对应的字符设备结构体cdev 和led_chrdev_fops 的关联并且初始化字符设备结构体最后注册并创建设备。 
第二部分为内核RGB 模块的卸载函数其主要完成了以下任务 
释放LED 结构体里的映射的虚拟地址调用device_destroy() 函数用于从linux 内核系统设备驱动程序模型中移除一个设备并删除/sys/devices/virtual 目录下对应的设备目录及/dev/目录下对应的设备文件调用cdev_del() 函数来释放散列表中的对象以及cdev 结构本身释放被占用的设备号以及删除设备类。 
从下面代码中我们可以看出这三个LED 都使用的同一个主设备号只是他们的次设备号有所区别而已。 
列表8: 内核RGB 模块的加载和卸载函数 
static __init int led_chrdev_init(void)
{int i  0;dev_t cur_dev;unsigned int val  0;printk(led chrdev init \n);led_cdev[0].va_moder  ioremap(GPIOA_MODER, 4); // 映射模式寄存器物理地址到虚拟地址led_cdev[0].va_otyper  ioremap(GPIOA_OTYPER, 4); // 映射输出类型寄存器物理地址到虚拟地址led_cdev[0].va_ospeedr  ioremap(GPIOA_OSPEEDR, 4); // 映射速度配置寄存器物理地址到虚拟地址led_cdev[0].va_pupdr  ioremap(GPIOA_PUPDR, 4); // 映射上下拉寄存器物理地址到虚拟地址led_cdev[0].va_bsrr  ioremap(GPIOA_BSRR, 4); // 映射置位寄存器物理地址到虚拟地址led_cdev[1].va_moder  ioremap(GPIOG_MODER, 4);led_cdev[1].va_otyper  ioremap(GPIOG_OTYPER, 4);led_cdev[1].va_ospeedr  ioremap(GPIOG_OSPEEDR, 4);led_cdev[1].va_pupdr  ioremap(GPIOG_PUPDR, 4);led_cdev[1].va_bsrr  ioremap(GPIOG_BSRR, 4);led_cdev[2].va_moder  ioremap(GPIOB_MODER, 4);led_cdev[2].va_otyper  ioremap(GPIOB_OTYPER, 4);led_cdev[2].va_ospeedr  ioremap(GPIOB_OSPEEDR, 4);led_cdev[2].va_pupdr  ioremap(GPIOB_PUPDR, 4);led_cdev[2].va_bsrr  ioremap(GPIOB_BSRR, 4);alloc_chrdev_region(devno, 0, DEV_CNT, DEV_NAME);led_chrdev_class  class_create(THIS_MODULE, led_chrdev);for (; i  DEV_CNT; i) {cdev_init(led_cdev[i].dev, led_chrdev_fops);led_cdev[i].dev.owner  THIS_MODULE;cur_dev  MKDEV(MAJOR(devno), MINOR(devno)  i);cdev_add(led_cdev[i].dev, cur_dev, 1);device_create(led_chrdev_class, NULL, cur_dev, NULL,DEV_NAME %d, i);}return 0;
}
module_init(led_chrdev_init);static __exit void led_chrdev_exit(void)
{int i;dev_t cur_dev;printk(led chrdev exit\n);for (i  0; i  DEV_CNT; i) {iounmap(led_cdev[i].va_moder); // 释放模式寄存器虚拟地址iounmap(led_cdev[i].va_otyper); // 释放输出类型寄存器虚拟地址iounmap(led_cdev[i].va_ospeedr); // 释放速度配置寄存器虚拟地址iounmap(led_cdev[i].va_pupdr); // 释放上下拉寄存器虚拟地址iounmap(led_cdev[i].va_bsrr); // 释放置位寄存器虚拟地址}for (i  0; i  DEV_CNT; i) {cur_dev  MKDEV(MAJOR(devno), MINOR(devno)  i);device_destroy(led_chrdev_class, cur_dev);cdev_del(led_cdev[i].dev);}unregister_chrdev_region(devno, DEV_CNT);class_destroy(led_chrdev_class);
}
module_exit(led_chrdev_exit);第9-25 行初始化LED 灯结构体成员将物理寄存器的地址映射到虚拟地址空间第27 行向动态申请一个设备号第29 行创建设备类第32 行绑定led_cdev 与led_chrdev_fops第37 行注册设备第39 行创建设备第45 行模块加载第53-59 行释放在init 函数中申请的虚拟地址空间第62 行计算出设备号第66 行删除设备第69 行注销设备第70 行释放被占用的设备号第72 行模块卸载 
file_operations 结构体成员函数的实现 
列表9: file_operations 中open 函数的实现 
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 | (0x43); // 开启a、b、g 的时钟writel(val, va_clkaddr);// 设置模式寄存器输出模式val  readl(led_cdev-va_moder);val  ~((unsigned int)0X3  (2 * led_cdev-led_pin));val | ((unsigned int)0X1  (2 * led_cdev-led_pin));writel(val,led_cdev-va_moder);// 设置输出类型寄存器推挽模式val  readl(led_cdev-va_otyper);val  ~((unsigned int)0X1  led_cdev-led_pin);writel(val, led_cdev-va_otyper);// 设置输出速度寄存器高速val  readl(led_cdev-va_ospeedr);val  ~((unsigned int)0X3  (2 * led_cdev-led_pin));val | ((unsigned int)0x2  (2 * led_cdev-led_pin));writel(val, led_cdev-va_ospeedr);// 设置上下拉寄存器上拉val  readl(led_cdev-va_pupdr);val  ~((unsigned int)0X3  (2*led_cdev-led_pin));val | ((unsigned int)0x1  (2*led_cdev-led_pin));writel(val,led_cdev-va_pupdr);// 设置置位寄存器默认输出低电平val  readl(led_cdev-va_bsrr);val | ((unsigned int)0x1  (led_cdev-led_pin  16));writel(val, led_cdev-va_bsrr);return 0;
}第4 行通过led_chrdev 结构变量中dev 成员的地址找到这个结构体变量的首地址第5 行把文件的私有数据private_data 指向设备结构体led_cdev第12-14 行实现GPIO 时钟寄存器的地址映射第16-38 行配置寄存器 
file_operations 中open 函数的实现函数很重要下面我们来详细分析一下该函数具体做了哪些工作。 
container_of() 函数: 在Linux 驱动编程当中我们会经常和container_of() 这个函数打交道所以特意拿出来和大家分享一下其实这个函数功能不多但是如果单靠自己去阅读内核源代码分析那可能非常难以理解编写内核源代码的大牛随便两行代码都会让我们看的云深不知处分析内核源代码需要我们有很好的知识积累以及技术沉淀。下面我简单跟大家讲解一下container_of() 函数的大致工作内容其宏定义实现如下所示 
列表10: container_of() 函数位于…/ebf_linux_kernel/driver/gpu/drm/mkregtable.c 
#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) (注强转为该结构体指针) 
通过此函数我们便可以轻松地获取led_chrdev 结构体的首地址了。 
文件私有数据: 
一般很多的linux 驱动都会将文件的私有数据private_data 指向设备结构体其保存了用户自定义设备结构体的地址。自定义结构体的地址被保存在private_data 后可以通过读、写等操作通过该私有数据去访问设备结构体中的成员这样做体现了linux 中面向对象的程序设计思想。 
通过ioremap() 函数实现地址的映射: 
其实ioremap() 函数我们之前分析过了在led_chrdev_open() 函数的作用都是一样的只是对LED灯所用到的时钟控制寄存器做了地址映射这样我们便可以通过操作程序中的虚拟地址来间接的控制物理寄存器我们在驱动程序描述寄存器不利于驱动模块的灵活使用后几个章节我们会带领大家通过设备树设备树插件的方式去描述寄存器及其相关属性在此先埋下伏笔循序渐进顺腾摸瓜使大家能够真正理解并掌握linux 驱动的精髓。 
通过ioread32() 和iowrite32() 等函数操作寄存器: 
和STM32 一样都要开启I/O 引脚对应的时钟、设置其端口的复用在此复用为普通的GPIO 口、电气属性、输入输出方向以及输出的高低电平等等一般我们访问某个地址时都是先将该地址的数据读取到一个变量中然后修改该变量最后再将该变量写入到原来的地址当中。注意我们在操作这段被映射后的地址空间时应该使用linux 提供的I/O 访问函数如iowrite8()、iowrite16()、iowrite32()、ioread8()、ioread16()、ioread32() 等这里再强调一遍即使理论上可以直接操作这段虚拟地址了但是Linux 并不建议这么做。 
下面我们接着分析一下file_operations 中write 函数的实现 
列表11: file_operations 中write 函数的实现 
static ssize_t led_chrdev_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{unsigned long val  0;unsigned long ret  0;int tmp  count;struct led_chrdev *led_cdev  (struct led_chrdev *)filp-private_data;kstrtoul_from_user(buf, tmp, 10, ret);val  ioread32(led_cdev-va_bsrr);if (ret  0){val | (0x01  (led_cdev-led_pin16)); // 设置GPIO 引脚输出低电平}else{val | (0x01  led_cdev-led_pin); // 设置GPIO 引脚输出高电平}iowrite32(val, led_cdev-va_bsrr);*ppos  tmp;return tmp;
}第9 行文件的私有数据地址赋给led_cdev 结构体指针第11 行将用户空间缓存区复制到内核空间第13 行间接读取数据寄存器中的数据第21 行将数据重新写入寄存器中, 控制LED 亮灭 
kstrtoul_from_user() 函数: 
再分析该函数之前我们先分析一下内核中提供的kstrtoul() 函数理解kstrtoul() 函数之后再分析kstrtoul_from_user() 就信手拈来了。 
列表12: kstrtoul() 函数解析内核源码/include/linux/kernel.h 
static inline int __must_check kstrtoul(const char *s, unsigned int base, unsigned long *res)
{/** We want to shortcut function call, but* __builtin_types_compatible_p(unsigned long, unsigned long long)  0.*/if (sizeof(unsigned long)  sizeof(unsigned long long) __alignof__(unsigned long)  __alignof__(unsigned long long))return kstrtoull(s, base, (unsigned long long *)res);elsereturn _kstrtoul(s, base, res);
}该函数的功能是将一个字符串转换成一个无符号长整型的数据。 
函数参数和返回值如下 
参数 • s字符串的起始地址该字符串必须以空字符结尾 
• base转换基数如果base0则函数会自动判断字符串的类型且按十进制输出比如“0xa”就会被当做十进制处理大小写都一样输出为10。如果是以0 开头则会被解析为八进制数否则将会被解析成小数 
• res一个指向被转换成功后的结果的地址。 
返回值该函数转换成功后返回0溢出将返回-ERANGE解析出错返回-EINVAL。理解完kstrtoul()函数后想必大家已经知道kstrtoul_from_user() 函数的大致用法了 
kstrtoul_from_user() 函数定义如下 
列表13: kstrtoul_from_user() 函数内核源码/include/linux/kernel.h 
int __must_check kstrtoul_from_user(const char __user *s, size_t count, unsigned int base, unsigned long *res);函数参数和返回值如下 
参数 
s字符串的起始地址该字符串必须以空字符结尾count count 为要转换数据的大小base转换基数如果base0则函数会自动判断字符串的类型且按十进制输出比如“0xa”就会被当做十进制处理大小写都一样输出为10。如果是以0 开头则会被解析为八进制数否则将会被解析成小数res一个指向被转换成功后的结果的地址。 
返回值 
该函数相比kstrtoul() 多了一个参数count因为用户空间是不可以直接访问内核空间的所以内核提供了kstrtoul_from_user() 函数以实现用户缓冲区到内核缓冲区的拷贝与之相似的还有copy_to_user()copy_to_user() 完成的是内核空间缓冲区到用户空io 间的拷贝。如果你使用的内存类型没那么复杂便可以选择使用put_user() 或者get_user() 函数。 
最后分析一下file_operations 中release 函数的实现 
当最后一个打开设备的用户进程执行close() 系统调用的时候内核将调用驱动程序release() 函数release 函数的主要任务是清理未结束的输入输出操作释放资源用户自定义排他标志的复位等。前面我们用ioremap() 将物理地址空间映射到了虚拟地址空间当我们使用完该虚拟地址空间时应该记得使用iounmap() 函数将它释放掉。不过我们在驱动模块退出的时候才进行释放这里我们不做操作。 
列表14: file_operations 中release 函数的实现 
static int led_chrdev_release(struct inode *inode, struct file *filp)
{return 0;
}LED 驱动完整代码 
到这里我们的代码已经分析完成了下面时本驱动的完整代码由于前面已经带领大家详细的分析了一遍所以我把完整代码的注释给去掉了希望你能够会想起每个函数的具体作用。 
led_cdev.c 
列表15: 完整代码位于…linux_driver/led_cdev/led_cdev.c 
#include linux/init.h
#include linux/module.h
#include linux/cdev.h
#include linux/fs.h
#include linux/uaccess.h
#include linux/io.h#define DEV_NAME led_chrdev
#define DEV_CNT (3)#define AHB4_PERIPH_BASE (0x50000000)#define RCC_BASE (AHB4_PERIPH_BASE  0x0000)
#define RCC_MP_GPIOENA (RCC_BASE  0XA28)
#define GPIOA_BASE (AHB4_PERIPH_BASE  0x2000)
#define GPIOA_MODER (GPIOA_BASE  0x0000)
#define GPIOA_OTYPER (GPIOA_BASE  0x0004)
#define GPIOA_OSPEEDR (GPIOA_BASE  0x0008)
#define GPIOA_PUPDR (GPIOA_BASE  0x000C)
#define GPIOA_BSRR (GPIOA_BASE  0x0018)#define GPIOG_BASE (AHB4_PERIPH_BASE  0x8000)
#define GPIOG_MODER (GPIOG_BASE  0x0000)
#define GPIOG_OTYPER (GPIOG_BASE  0x0004)
#define GPIOG_OSPEEDR (GPIOG_BASE  0x0008)
#define GPIOG_PUPDR (GPIOG_BASE  0x000C)
#define GPIOG_BSRR (GPIOG_BASE  0x0018)#define GPIOB_BASE (AHB4_PERIPH_BASE  0x3000)
#define GPIOB_MODER (GPIOB_BASE  0x0000)
#define GPIOB_OTYPER (GPIOB_BASE  0x0004)
#define GPIOB_OSPEEDR (GPIOB_BASE  0x0008)
#define GPIOB_PUPDR (GPIOB_BASE  0x000C)
#define GPIOB_BSRR (GPIOB_BASE  0x0018)static dev_t devno;
struct class *led_chrdev_class;struct led_chrdev {struct cdev dev;unsigned int __iomem *va_moder; // 模式寄存器虚拟地址保存变量unsigned int __iomem *va_otyper; // 输出类型寄存器虚拟地址保存变量unsigned int __iomem *va_ospeedr; // 速度配置寄存器虚拟地址保存变量unsigned int __iomem *va_pupdr; // 上下拉寄存器虚拟地址保存变量unsigned int __iomem *va_bsrr; // 置位寄存器虚拟地址保存变量unsigned int led_pin; // 引脚偏移
};unsigned int __iomem *va_clkaddr;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);va_clkaddr  ioremap(RCC_MP_GPIOENA, 4);// 映射物理地址到虚拟地址gpio 时钟rcc 寄存器val | (0x43); // 开启a、b、g 的时钟iowrite32(val, va_clkaddr);// 设置模式寄存器输出模式val  ioread32(led_cdev-va_moder);val  ~((unsigned int)0X3  (2 * led_cdev-led_pin));val | ((unsigned int)0X1  (2 * led_cdev-led_pin));iowrite32(val,led_cdev-va_moder);// 设置输出类型寄存器推挽模式val  ioread32(led_cdev-va_otyper);val  ~((unsigned int)0X1  led_cdev-led_pin);iowrite32(val, led_cdev-va_otyper);// 设置输出速度寄存器高速val  ioread32(led_cdev-va_ospeedr);val  ~((unsigned int)0X3  (2 * led_cdev-led_pin));val | ((unsigned int)0x2  (2 * led_cdev-led_pin));iowrite32(val, led_cdev-va_ospeedr);// 设置上下拉寄存器上拉val  ioread32(led_cdev-va_pupdr);val  ~((unsigned int)0X3  (2*led_cdev-led_pin));val | ((unsigned int)0x1  (2*led_cdev-led_pin));iowrite32(val,led_cdev-va_pupdr);// 设置置位寄存器默认输出低电平val  ioread32(led_cdev-va_bsrr);val | ((unsigned int)0x1  (led_cdev-led_pin  16));iowrite32(val, led_cdev-va_bsrr);return 0;
}static int led_chrdev_release(struct inode *inode, struct file *filp)
{iounmap(va_clkaddr); // 释放GPIO 时钟寄存器虚拟地址return 0;
}static ssize_t led_chrdev_write(struct file *filp, const char __user * buf,size_t count, loff_t * ppos)
{unsigned long val  0;unsigned long ret  0;int tmp  count;struct led_chrdev *led_cdev  (struct led_chrdev *)filp-private_data;kstrtoul_from_user(buf, tmp, 10, ret);val  ioread32(led_cdev-va_bsrr);if (ret  0){val | (0x01  (led_cdev-led_pin16)); // 设置GPIO 引脚输出低电平}else{val | (0x01  led_cdev-led_pin); // 设置GPIO 引脚输出高电平}iowrite32(val, led_cdev-va_bsrr);*ppos  tmp;return tmp;
}static struct file_operations led_chrdev_fops  {.owner  THIS_MODULE,.open  led_chrdev_open,.release  led_chrdev_release,.write  led_chrdev_write,
};static struct led_chrdev led_cdev[DEV_CNT]  {{.led_pin  13}, // 定义GPIO 引脚号{.led_pin  2},{.led_pin  5},
};static __init int led_chrdev_init(void)
{int i  0;dev_t cur_dev;printk(led chrdev init \n);va_clkaddr  ioremap(RCC_MP_GPIOENA, 4);// 映射物理地址到虚拟地址gpio 时钟rcc 寄存器led_cdev[0].va_moder  ioremap(GPIOA_MODER, 4); // 映射模式寄存器 物理地址到虚拟地址led_cdev[0].va_otyper  ioremap(GPIOA_OTYPER, 4); // 映射输出类型寄存器 物理地址到虚拟地址led_cdev[0].va_ospeedr  ioremap(GPIOA_OSPEEDR, 4); // 映射速度配置寄存器 物理地址到虚拟地址led_cdev[0].va_pupdr  ioremap(GPIOA_PUPDR, 4); // 映射上下拉寄存器 物理地址到虚拟地址led_cdev[0].va_bsrr  ioremap(GPIOA_BSRR, 4); // 映射置位寄存器 物理地址到虚拟地址led_cdev[1].va_moder  ioremap(GPIOG_MODER, 4);led_cdev[1].va_otyper  ioremap(GPIOG_OTYPER, 4);led_cdev[1].va_ospeedr  ioremap(GPIOG_OSPEEDR, 4);led_cdev[1].va_pupdr  ioremap(GPIOG_PUPDR, 4);led_cdev[1].va_bsrr  ioremap(GPIOG_BSRR, 4);led_cdev[2].va_moder  ioremap(GPIOB_MODER, 4);led_cdev[2].va_otyper  ioremap(GPIOB_OTYPER, 4);led_cdev[2].va_ospeedr  ioremap(GPIOB_OSPEEDR, 4);led_cdev[2].va_pupdr  ioremap(GPIOB_PUPDR, 4);led_cdev[2].va_bsrr  ioremap(GPIOB_BSRR, 4);alloc_chrdev_region(devno, 0, DEV_CNT, DEV_NAME);led_chrdev_class  class_create(THIS_MODULE, led_chrdev);for (; i  DEV_CNT; i) {cdev_init(led_cdev[i].dev, led_chrdev_fops);led_cdev[i].dev.owner  THIS_MODULE;cur_dev  MKDEV(MAJOR(devno), MINOR(devno)  i);cdev_add(led_cdev[i].dev, cur_dev, 1);device_create(led_chrdev_class, NULL, cur_dev, NULL,DEV_NAME %d, i);}return 0;
}module_init(led_chrdev_init);static __exit void led_chrdev_exit(void)
{int i;dev_t cur_dev;printk(led chrdev exit\n);iounmap(va_clkaddr); // 释放GPIO 时钟寄存器虚拟地址for (i  0; i  DEV_CNT; i) {iounmap(led_cdev[i].va_moder); // 释放模式寄存器虚拟地址iounmap(led_cdev[i].va_otyper); // 释放输出类型寄存器虚拟地址iounmap(led_cdev[i].va_ospeedr); // 释放速度配置寄存器虚拟地址iounmap(led_cdev[i].va_pupdr); // 释放上下拉寄存器虚拟地址iounmap(led_cdev[i].va_bsrr); // 释放置位寄存器虚拟地址}for (i  0; i  DEV_CNT; i) {cur_dev  MKDEV(MAJOR(devno), MINOR(devno)  i);device_destroy(led_chrdev_class, cur_dev);cdev_del(led_cdev[i].dev);}unregister_chrdev_region(devno, DEV_CNT);class_destroy(led_chrdev_class);}module_exit(led_chrdev_exit);MODULE_AUTHOR(embedfire);
MODULE_LICENSE(GPL);实验准备 
在板卡上的部分GPIO 可能会被系统占用在使用前请根据需要改/boot/uEnv.txt 文件可注释掉某些设备树插件的加载重启系统释放相应的GPIO 引脚。 
如本节实验中可能在鲁班猫系统中默认使能了LED 的设备功能用在了LED 子系统。引脚被占用后设备树可能无法再加载或驱动中无法再申请对应的资源。 
方法参考如下  
取消LED 设备树插件以释放系统对应LED 资源操作如下 如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象请按上述情况检查并按上述步骤操作。 
如出现Permission denied 或类似字样请注意用户权限大部分操作硬件外设的功能几乎都需要root 用户权限简单的解决方案是在执行语句前加入sudo 或以root 用户运行程序。 
LED 驱动Makefile 
列表16: LED 驱动Makefile 
KERNEL_DIR../ebf_linux_kernel/build_image/build
ARCHarm
CROSS_COMPILEarm-linux-gnueabihf-
export ARCH CROSS_COMPILEobj-m : led_cdev.o
out  led_cdev_testall:$(MAKE) -C $(KERNEL_DIR) M$(CURDIR) modules$(CROSS_COMPILE)gcc -o $(out) led_test.c.PHONE:clean copyclean:$(MAKE) -C $(KERNEL_DIR) M$(CURDIR) cleanrm $(out)Makefile 与前面的相差不大定义了led_cdev 这个内核模组和led_cdev_test 应用程序。 
编译命令说明 
在实验目录下输入如下命令来编译驱动模块 
make编译成功后实验目录下会生成”led_cdev.ko”的驱动模块文件和”led_cdev_test”的应用程序。 程序运行结果 
通过scp 或者nfs 将上面的两个文件拷贝到开发板中执行下面的命令加载驱动 
安装LED 驱动 
sudo insmod led_cdev.ko然后我们可以在/dev/目录下找到led_chrdev0、led_chrdev1、led_chrdev2 这三个设备我们可以通过直接给设备写入1/0 来控制LED 的亮灭也可以通过我们的测试程序来控制LED。 
# 红灯亮
sudo sh -c echo 0 /dev/led_chrdev0
# 红灯灭
sudo sh -c echo 1 /dev/led_chrdev0运行LED 测试程序sudo ./led_cdev_test LED 依次呈现红、绿、蓝三种颜色的灯光。 这个时候我们再回味一下设备驱动的作用。当我们开发一款嵌入式产品时产品的设备硬件发生变动的时候我们就只需要更改驱动程序以提供相同的API而不用去变动应用程序就能达到同样的效果这将减少多少开发成本呢。 参考资料嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列