序言
上一篇我们分享了字符设备驱动框架:Linux驱动基础篇:hello驱动,当时分享的是hello驱动程序。学STM32我们从点灯开始,学Linux驱动我们自然也要点个灯来玩儿,尽量在从这种基础类库中攫取知识,细抠、细抠,为以后更复杂的知识打好基础。
与硬件无关的LED驱动
回顾hello驱动程序,我们的依据实际需求对其进行写字符串与读字符串操作。这儿我们其实也要按照实际来思索我们的LED驱动程序。在STM32点灯的时侯,通常输出低电平点灯,输出高电平灭灯。在嵌入Linux操作系统的情况下,我们自然也要想到有个写1/0的思想。类比我们上一篇的hello程序:
我们的LED程序自然要写入的数据为0/1来照亮、熄灭LED。这儿我们做的实验室与硬件无关的LED实验:我们的驱动程序在收到应用程序发送过来的0时复印ledon、收到1时复印ledoff。模仿上一篇的hello程序,我们更改得到的与硬件无关的LED程序(核心部份)如下:
LED应用程序:
LED驱动程序:
加载led驱动模块及运行应用程序:
与硬件有关的LED驱动
前面那一节分享的是与硬件无关的LED驱动实验,主要是为了理清LED驱动的大体思路。这儿我们再加入与硬件有关的相关操作以构造与硬件有关的LED驱动程序。
我们在进行STM32的裸机编程的时侯,对一些外设进行配置虽然就是操作一些地址的过程,这种外设地址在芯片指南中可以见到:
这是地址映射图,这儿图中只是列举的外设的边界地址,每位外设又有好多寄存器,这种寄存器的地址都是对外设基地址进行偏斜得到的。同样的,对于NXP的IMX6ULL芯片来说,也是有类似这样的地址的:
此时我们要编撰Linux系统下的led驱动,涉及到硬件操作的地方操作的并不是那些地址(化学地址)linux驱动教程,而是操作系统给我们提供的地址(虚拟地址)。操作系统按照化学地址来给我们生成一个虚拟地址,我们的led驱动操控这个地址就是间接的操控化学地址。至于这两个地址是如何联系上去的,上面个原理我们姑且不展开。我们从函数层面来看,内核给我们提供了ioremap函数,这个函数可以把数学地址映射为虚拟地址。这个函数在内核文件arch/arm/include/asm/io.h中:
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
与ioremap函数相对应的函数为:
void iounmap (volatile void __iomem *addr)
地址映射完成以后,我们可以直接通过表针来访问虚拟地址,如:
*GPIO5_DR &= ~(1 << 3); /* GPIO5_IO03输出低电平 */
*GPIO5_DR |= (1 << 3); /* GPIO5_IO03输出高电平 */
这儿简单介绍一下i.MX6ULL的GPIO。对于i.MX6ULL来说,以数字来给IO端口(组别)命令,GPIO5为第五组,所以GPIO5_IO03为第五组端口的第3个引脚。而STM32中是以小写字母来表示端口(组别),如PA3表示A端口的第3个引脚。
i.MX6ULL有5组GPIO(GPIO1~GPIO5),每组引脚最多有32个:
GPIO1 有 32 个引脚: GPIO1_IO0~GPIO1_IO31;
GPIO2 有 22 个引脚: GPIO2_IO0~GPIO2_IO21;
GPIO3 有 29 个引脚: GPIO3_IO0~GPIO3_IO28;
GPIO4 有 29 个引脚: GPIO4_IO0~GPIO4_IO28;
GPIO5 有 12 个引脚: GPIO5_IO0~GPIO5_IO11;
地址映射完成以后,我们除了可以通过表针来访问虚拟地址,并且还可以使用内核给我们提供的一些读写函数:
/* 写操作函数 */
void writeb(u8 value, volatile void __iomem *addr);
void writew(u16 value, volatile void __iomem *addr);
void writel(u32 value, volatile void __iomem *addr);
/* 读操作函数 */
u8 readb(const volatile void __iomem *addr);
u16 readw(const volatile void __iomem *addr);
u32 readl(const volatile void __iomem *addr);
writeb、writew和writel这三个函数分别对应8bit、16bit和32bit写操作,参数value是要写入的数值,addr是要写入的地址。
readb、readw和readl这三个函数分别对应8bit、16bit和32bit读操作,参数addr就是要读取写显存地址,返回值就是读取到的数据。
此时我们可以把上一节的led_init函数led_drv_write函数进行更改:
与STM32一样linux运维面试题,对于i.MX6ULL的GPIO外设来说,也有好多寄存器:
前面我们只是点一个灯,倘若是要点多个灯呢?那就得操控多个GPIO。假如进行地址映射的写法还像前面那样,代码都会变得很臃肿。回想一下我们STM32,GPIO外设通过结构体来管理它的寄存器:
这儿的__IO是个宏,代表C语言的关键字volatile,为了避免编译器对我们的一些硬件操作进行优化linux驱动教程,进而得不到想要的结果。诸如:
/* 假设REG为寄存器的地址 */
uint32 *REG;
*REG = 0; /* 点灯 */
*REG = 1; /* 灭灯 */
此时若是REG不加volatile进行修饰,则点灯操作将被优化掉,只执行灭灯操作。
在这儿,我们也可以模仿STM32那样子,用一个结构体来对i.MX6ULL的GPIO的寄存器进行管理,如:
struct GPIO_RegDef
{
volatile unsigned int DR;
volatile unsigned int GDIR;
volatile unsigned int PSR;
volatile unsigned int ICR1;
volatile unsigned int ICR2;
volatile unsigned int IMR;
volatile unsigned int ISR;
volatile unsigned int EDGE_SEL;
};
结构体里的成员排序是要根据特定次序来的:
由于这种寄存器都是相对于GPIO外设的基地址作偏斜得到的,例如:
不能搅乱次序,否则就不能正确访问到对应的寄存器了。用结构体进行管理以后,我们就可以用类似下边的方法进行映射:
struct GPIO_RegDef *GPIO5 = ioremap(0x20AC000, sizeof(struct GPIO_RegDef));
之后就可以向STM32那样来操控GPIO寄存器,如:
GPIO5->DR &= ~(1 <DR |= (1 << 3); /* GPIO5_IO03输出高电平 */
与硬件有关的LED驱动(升级版)
上一节我们分享的LED驱动是一个常规的LED驱动,只能适用于我们当前的开发版linux驱动下载,所以是一个专用的LED驱动程序。若是换了另一块板,led所联接的gpio引脚可能不一样了,我们就更改我们的驱动程序led_drv.c里与寄存器相关的操作。有没有更好的办法不用再更改我们的led_drv.c驱动程序了?
若是led_drv.c不用再更改了,这么这个led_drv.c驱动就是一个通用的驱动程序了。具体可查看韦东山老师的《嵌入式Linux应用开发完全指南第2版》第五篇第3~7节进行学习。
下边来简单地梳理一下:
因为篇幅问题,具体的部份就不贴下来了。
之前的笔记中:C语言、嵌入式重点知识:反弹函数中我也有提及通用与专用的含意,可以了解了解加深对这两个词的认识。
这儿我们学到了很重要的思想软件分层的思想及方法,但也只是点了一下,未来的路还很长,须要持续学习,继续提升。