代码学习资料来源于:
第4.1讲 Linux LED灯驱动实验(直接操作寄存器)-地址映射_哔哩哔哩_bilibili
仅用于个人学习/复习,侵联删
1、LED驱动原理
IMX6ULL系列芯片是一款基于ARM Cortex A7内核的低功耗高性能且低成本的应用处理器。
linux下任何外设驱动,最终都是要配置相应的硬件寄存器,所以本章的LED灯驱动最终也是对IMX6ULL的IO口进行配置,在linux下编写驱动要符合linux的驱动框架,IMX6U-ALPHA开发板上的LED连接到IMX6ULL的GPIO_IO3的这个引脚上。
cat /proc/devices显示所有的设备,但是操作设备的话需要控制相应的/dev目录下的设备节点
2、地址映射
linux驱动开发不能直接对寄存器地址进行读写操作,比如寄存器A物理地址为0x01010101。裸机开发可以直接对物理地址进行操作,但是linux不行,因为linux会使能MMU,MMU全称为memory manager unit,就是内存管理单元。在老版本的linux下处理器要求有MMU,但是现在linux内核已经支持无MMU的处理器了。MMU主要完成的功能如下:
1)完成虚拟空间到物理空间的映射,也称为地址映射
2)内存保护,设置存储器的保护权限
虚拟地址范围比物理地址范围大的问题不用深究。
在linux里面操作的都是虚拟地址,所以需要先得到0x01010101的虚拟地址。比如IMX6ULL的GPIO_IO03引脚的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的地址为0X020E0068。如果没有开启MMU的话直接向0X020E0068这个寄存器写入数据就可以配置GPIO1_IO03的复用功能。现在开启了MMU,我们就需要先得到0X020E0068这个物理地址在linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要得到两个函数:ioremap和iounmap。
arch/arm64/include/asm/io.h
#define ioremap(addr, size) __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
static inline void __iomem *__ioremap(unsigned long port, unsigned long size,
unsigned long flags)
{
return ioremap(port, size);
}
参数说明:
addr:起始地址
size:要映射的内存空间大小
flags:ioremap类型,选择MT_DEVICE
返回值:__iomem类型的指针,指向映射后的虚拟地址首地址
#define iounmap __iounmap
extern void __iounmap(volatile void __iomem *addr);
举个这两个函数的例子:
#define SW_MUX_GPIO1_IO03_BASE 0X020E0068
static void __iomem *SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
...
iounmap(SW_MUX_GPIO1_IO03_BASE);
宏SW_MUX_GPIO1_IO03_BASE是寄存器物理地址,SW_MUX_GPIO1_IO03是映射后的虚拟地址。对于IMX6ULL的一个寄存器是4字节的,所以映射内存的长度为4,映射完成以后就可以直接对SW_MUX_GPIO1_IO03进行读写操作即可。
卸载驱动时需要iounmap释放虚拟地址映射
3、LED灯字符驱动
在STM32来对IO进行设置时,需要做以下几点:
1)使能指定GPIO的时钟
2)初始化GPIO、比如输出功能、上拉、速度等
3)STM32有的IO口可以作为其他外设的引脚,也就是IO复用,如果要将IO作为其他外设引脚使用的话就需要设置IO的复用功能。
4)最后设置GPIO输出高电平或者低电平
GPIO的各个寄存器含义和作用可参考:
IMX6ULL裸机开发之点亮LED灯_Swiler的博客-CSDN博客
GPIO框图中有8个寄存器,分别是:
Data register (GPIO_DR):数据寄存器(32位),一个 GPIO 组最大只有 32 个 IO,因此 DR 寄存器中的每个位都对应一个 GPIO。
GPIO direction register (GPIO_GDIR):方向寄存器(32位),即配置输入输出。0:输入,1:输出。
Pad sample register (GPIO_PSR):状态寄存器(32位),用于获取GPIO的高低电平值。
Interrupt control registers (GPIO_ICR1, GPIO_ICR2):中断控制寄存器,ICR1配置低16个GPIO,ICR2配置高16位个GPIO。
两个位对应四种触发状态:00:低电平触发,01:高电平触发,10:上升沿触发,11:下降沿触发。
Edge select register (GPIO_EDGE_SEL):配置边沿中断寄存器(32位):配置该寄存器会覆盖 ICR1 和 ICR2 的设置,置1则为双边沿触发。
Interrupt mask register (GPIO_IMR):中断屏蔽寄存器(32位),用于使能或禁止中断。
Interrupt status register (GPIO_ISR):中断标志位寄存器(32位),只要某个 GPIO 的中断发生,那么 ISR 中相应的位就会被置 1。通过读取这个寄存器的值可以判断中断是否产生,但是要记得在处理完中断后,要手动清除中断标志位,即向ISR中对应位写1。
点灯需要控制的寄存器如下:
#define CCM_CCGR1_BASE (0X020C406C) // 时钟寄存器
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068) // 设置 IO 的复用功能,使其复用为 GPIO 功能
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) // 设置 IO 的上下拉、速度等属性
#define GPIO1_DR_BASE (0X0209C000) // 数据寄存器(32位),一个 GPIO 组最大只有 32 个 IO,因此 DR 寄存器中的每个位都对应一个 GPIO
#define GPIO1_GDIR_BASE (0X0209C004) // 配置输入输出。0:输入,1:输出
4、I/O内存访问函数
使用ioremap函数将寄存器的物理地址映射到虚拟地址之后,我们就可以直接通过指针访问这些地址,但是linux不建议这么做,而是推荐使用一组操作函数来将映射后的内存进行读写操作。
1)读操作函数
extern inline u8 readb(const volatile void __iomem *addr)
extern inline u16 readw(const volatile void __iomem *addr)
extern inline u32 readl(const volatile void __iomem *addr)
三个函数分别对应8bit,16bit,32bit读操作,参数addr就是要读取内存地址,返回值为读到的数据
2)写操作函数
extern inline void writeb(u8 b, volatile void __iomem *addr)
extern inline void writew(u16 b, volatile void __iomem *addr)
extern inline void writel(u32 b, volatile void __iomem *addr)
三个函数分别对应8bit,16bit,32bit写操作,参数value表示要写入的数值,addr是写入的地址
5、LED字符驱动demo
led.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched/signal.h>
#include <linux/device.h>
#include <linux/ioctl.h>
#include <linux/parport.h>
#include <linux/ctype.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/major.h>
#include <linux/ppdev.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/compat.h>
#define LED_MAJOR 200
#define LED_NAME "led"
/* register physical address */
#define CCM_CCGR1_BASE (0X020C406C) // 时钟寄存器
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068) // 设置 IO 的复用功能,使其复用为 GPIO 功能
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) // 设置 IO 的上下拉、速度等属性
#define GPIO1_DR_BASE (0X0209C000) // 数据寄存器(32位),一个 GPIO 组最大只有 32 个 IO,因此 DR 寄存器中的每个位都对应一个 GPIO,低电平0 led亮
#define GPIO1_GDIR_BASE (0X0209C004) // 方向寄存器,设置为输入还输出。0:输入,1:输出
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
#define LEDOFF 0; // 关闭
#define LEDON 1; // 打开
static ssize_t led_write(struct file *file,
char __user *buf,
size_t count, loff_t *offset)
{
int retval;
unsigned char databuf[1];
retval = copy_from_user(databuf, buf, count);
if (retval < 0) {
printk("kernel write failed! \n");
return -EFAULT;
}
if (databuf[1] == LEDOFF) {
val = readl(GPIO1_DR);
val |= (1 << 3); // bit3置1,关闭led
writel(val, GPIO1_DR);
}
if (databuf[1] == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3); // bit3置0,打开led
writel(val, GPIO1_DR);
}
return 0;
}
static int led_open(struct inode *inode, struct file *file)
{
printk("led_open success \n");
return 0;
}
static int led_release(struct inode *inode, struct file *file)
{
printk("led close success \n");
return 0;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_write,
.open = led_open,
.release = led_release,
};
static int __init led_init(void)
{
int ret = 0;
int val = 0;
/*init led, address remap*/
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); // 先清除以前的配置bit26和bit27
val |= 3 << 26; // bit26,bit27置1
writel(val, IMX6U_CCM_CCGR1);
writel(0x5, SW_MUX_GPIO1_IO03); // 设置复用
writel(0x1080, SW_PAD_GPIO1_IO03); // 设置电气属性
val = readl(GPIO1_GDIR);
val |= 1 << 3; // bit3置1,设置为输出
writel(val, GPIO1_GDIR);
val = readl(GPIO1_DR);
val &= ~(1 << 3); // bit3清零,打开led
writel(val, GPIO1_DR);
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if (ret < 0) {
printk("led register failed \n");
return -EIO;
}
printk("led_init \n");
return 0;
}
static void __exit led_exit(void)
{
int val = 0;
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
val = readl(GPIO1_DR);
val |= (1 << 3); // bit3置1,关闭led
writel(val, GPIO1_DR);
unregister_chrdev(LED_MAJOR, LED_NAME);
printk("led_exit \n");
}
module_init(led_init);
module_exit(led_exit);
ledAPP.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*
* ./ledAPP <filename> <1/2>
* 1 读数据
* 2 写数据
* */
#define LEDOFF 0
#define LEDON 1
int main(int argc, int *argv[])
{
char *filename;
int fd;
int ret;
unsigned char databuf[1];
filename = argv[1];
if (argc < 3) {
printf("usage error \n");
}
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("open %s failed\n", filename);
return -1;
}
databuf[0] = atoi(argv[2]);
ret = write(fd, databuf, 1); // 打开led灯
if (ret < 0) {
printf("LED control failed \n");
}
ret = close(fd);
if (ret < 0) {
printf("close %s failed\n", filename);
return -1;
}
return 0;
}