嵌入式Linux中的LED驱动控制(续)

嵌入式Linux中的LED驱动控制”一文实现了在野火STM32MP157开发板上对三个LED灯的控制,这里来讨论一下该驱动程序具体实现的原理。由于实例使用的是STM32MP157这款芯片,所以先来看一下与该芯片端口操作相关的寄存器。

先看端口模式寄存器MODER,该类型的寄存器在STM32MP157中有11个,即x的值从A到K。它们分别针对11组不同的I/O端口,其结构完全相同(下同)。下面是该寄存器的全部位结构。它的偏移地址为:0x00。

第31~0位分别配置第15~第0引脚的具体模式,每两位配置一个引脚。当值为00时,引脚为输入模式,值为01时,引脚为通用输出模式,值为10时,引脚为多功能模式,值为11时,引脚为模拟模式。默认为模拟模式。

接着看端口输出类型寄存器OTYPER,下面是该寄存器的全部位结构。它的偏移地址为:0x04。

第15~0位分别配置第15~第0引脚的输出类型,每一位配置一个引脚,第31~16位未使用。当值为0时,引脚为推挽模式,值为1时,引脚为开漏模式。默认为推挽模式。

接下来是端口输出速度寄存器OSPEEDR,下面是该寄存器的全部位结构。它的偏移地址为:0x08。

第31~0位分别配置第15~第0引脚的具体模式,每两位配置一个引脚。当值为00时,引脚为低速模式,值为01时,引脚为中速模式,值为10时,引脚为高速模式,值为11时,引脚为超高速模式。默认为低速模式。

然后看端口上/下拉寄存器PUPDR,下面是该寄存器的全部位结构。它的偏移地址为:0x0C。

第31~0位分别配置第15~第0引脚的具体模式,每两位配置一个引脚。当值为00时,引脚为无上下拉(浮空)模式,值为01时,引脚为上拉模式,值为10时,引脚为下拉模式,值为11时保留。默认为浮空模式。

接着看端口输入数据寄存器IDR,下面是该寄存器的全部位结构。它的偏移地址为:0x10。

第15~0位分别提供第15~第0引脚的输入数据,每一位对应一个引脚,第31~16位未使用。当值为0时,引脚输入为低电平,值为1时,引脚输入为高电平。该寄存器为只读。

然后是端口输出数据寄存器ODR,下面是该寄存器的全部位结构。它的偏移地址为:0x14。

第15~0位分别提供第15~第0引脚的输出数据,每一位对应一个引脚,第31~16位未使用。当值为0时,引脚输出低电平,值为1时,引脚输出高电平。默认为低电平。

最后看端口位配置寄存器BSRR,下面是该寄存器的全部位结构。它的偏移地址为:0x18。

第31~16位分别提供第15~第0引脚的清零,每一位对应一个引脚,写1时引脚清零(输出低电平),写0无效。第15~0位分别提供第15~第0引脚的置位,每一位对应一个引脚,写1时引脚置位(输出高电平),写0无效。默认为无效。

在操作端口的输出电平时,可以有ODR和BSRR两个寄存器选择,虽然两者都能实现端口电平的输出,但还是有所区别的。当要求操作某一引脚的电平而不影响其他引脚的电平时,若使用ODR寄存器则要实现“读——改——写”的过程,即先把ODR数据读出,然后通过逻辑与或进行修改,再写入到ODR寄存器中。若使用BSRR寄存器则可直接写入,非常方便。

除了上述对端口控制的寄存器之外,还有一个时钟使能寄存器也需要讨论(只有一个),它寄存器名称为RCC_MP_AHB4ENSETR,下面是该寄存器的全部位结构。它的偏移地址为:0xA28。

第0~10位分别控制第A~第K组端口的时钟,值为1时使能该组端口时钟,值为0时禁止时钟。默认为禁止。

以上就是本例中使用到的寄存器,他们都位于AHB4总线之中,这些寄存器的地址都是以AHB4的基址为偏移的,具体地址如下表所示。

在Linux系统中,并不能直接使用物理地址对寄存器进行操作,必须先把寄存器的物理地址映射到操作系统中来形成虚拟地址,然后才能进行相应地操作。实现映射功能的函数名为ioremap,其原型为:void __iomem *ioremap(phys_addr_t paddr, unsigned long size)。第一个参数为物理地址,第二个参数为长度,返回值是一个指向__iomem类型的指针。__iomem是一个宏,它表示返回的地址是一个IO存储空间的有效地址。相应的解除映射函数iounmap,其原型为:void iounmap(void *addr)。只有一个参数,为前面映射时的返回指针,该函数没有返回值。

下面以映射GPIO_MODER_PA寄存器为例进行说明,先对GPIO_MODER_PA寄存器的物理地址进行一个宏定义,定义采取基址+偏移量的方式,如下。

#define AHB4_PERIPH_BASE (0x50000000)
#define GPIOA_BASE (AHB4_PERIPH_BASE + 0x2000)
#define GPIOA_MODER (GPIOA_BASE + 0x0000)

然后定义一个用于接收映射的返回值的指针变量,如下。

volatile void __iomem *GPIO_MODER_PA

变量加上前缀volatile关键字,表示它不接受编译器优化(一般定义寄存器都要如此)。

然后就可以进行地址映射了,如下。

GPIO_MODER_PA = ioremap(GPIOA_MODER, 4);

映射成功后,就可以使用GPIO_MODER_PA来进行赋值了,如下。

tmp = ioread32(GPIO_MODER_PA);
tmp &= ~(0x3 << 26);
tmp |= (0x1 << 26);
iowrite32(tmp, GPIO_MODER_PA);

上面的赋值采用了“读——改——写”的方式,所以只改变寄存器的第26、27两位,其余位不变。经过赋值后,GPIOA的第13引脚就被配置成了通用输出模式。

在Linux中,对端口的读写有专用的函数,一般不推荐直接对指针进行操作。ioread32函数用于读取一个32位的值,iowrite32函数用于写入一个32位的值。(早期可能会使用readl和writel函数,现在不推荐使用)

本例把寄存器的地址映射放在了入口函数(led_init)中,同时也把端口配置放在了入口中。把解除映射函数放在出口函数(led_exit)中进行。对芯片寄存器的读、写等操作放在了文件操作接口里面(file_operations结构体成员函数中)。在使用设备时,应用程序会打开设备节点,并通过设备节点的inode结构体、file结构体最终找到file_operations结构体,然后从file_operations结构体中得到操作设备的具体方法。 这部分的具体内容可参见“嵌入式Linux中字符型驱动程序的基本框架”一文。在函数alloc_chrdev_region(&devno, 0, 3, "led")执行后,字符串led会出现在/proc/devices文件中。函数class_create(THIS_MODULE,  "led_dev")执行后,字符串led_dev会出现在/sys/class目录下。函数device_create(led_chrdev_class, Null, devid, Null, "led%d", i)执行后,字符串ledi会出现在/dev目录下。可在开发板上自行查看。

在实例中,把设备号、字符型结构体、类结构体和设备结构体又封装在了一个名为led_dev的结构体中,并定义了一个名为led的该类结构体,后面的程序来引用该led的成员。这样做的好处是逻辑性强,容易区别不同的设备,当然,也可以不用结构体,直接声明变量,看自己的喜好而定。此外,在该实例中,把打开端口时钟的操作放在了open函数中,然后在release函数中关闭端口时钟。其意义在于,当应用程序执行完成后,端口时钟处于关闭状态,这样可以节约一点功耗。(一般的做法是在移除驱动模块后才关闭端口时钟)

posted @ 2024-06-11 21:09  fxzq  阅读(75)  评论(0编辑  收藏  举报