文章目录
Linux驱动开发之物理地址映射
ioremap函数
使用方式
方式一
方式二
不使用GPIO库函数方式来修改之前操作LED灯代码示例
总结
Linux驱动开发之物理地址映射
如果不采用GPIO库函数,那么我们如何能在底层驱动中访问外设对应的硬件寄存器呢?是像类似单片机编程一样直接对硬件寄存器访问么?
- 在Linux系统中,不管是在用户空间还是内核空间一律不允许直接访问硬件外设的基地址(包括寄存器的基地址)。如果要想访问,必须将外设的基地址映射到用户空间的虚拟地址或者内核控件的虚拟地址,一旦映射完成,将来应用程序或者驱动程序访问映射的用户虚拟地址或者内核虚拟地址就是在访问对应的物理地址。
- 注意:虚拟地址包括用户虚拟地址和内核虚拟地址。
- 用户虚拟地址范围:0x00000000~0xBFFFFFFF
- 内核虚拟地址范围:0xC0000000~0xFFFFFFFF
那么如何能够将外设的物理地址映射到内核空间的虚拟地址上呢?
- 使用函数——ioremap
ioremap函数
void *ioremap(unsigned long phys_addr, int size)
- 函数功能:完成物理地址和内核虚拟地址的映射关系。
- 参数:
- phys_addr:要访问、映射的外设的物理地址起始地址。
- size:要访问、映射的外设的物理地址范围(大小)
- 返回值:返回映射的内存起始虚拟地址。
注意: 将来如果对物理地址不在访问,需要解除映射,使用如下函数
void iounmap(void *vir_addr)
- 函数功能:解除物理地址和内核虚拟地址的映射关系。
- 参数(vir_addr):映射的内核虚拟地址起始地址。
使用方式
方式一
例如LED1相关的寄存器物理地址(gpio_c_12):
GPIOCOUT:0xC001C000 GPIOCOUTENB: 0xC001C004 GPIOCALTFN0:0xC001C020
每一个寄存器的大小为4字节,那么Linux内核访问上述的寄存器的方式就是:
unsigned long *gpiocout, *gpiocoutenb, *gpiocaltfn0; gpiocout = ioremap(0xC001C000, 4); gpiocoutenb = ioremap(0xC001C004, 4); gpiocaltfn0 = ioremap(0xC001C020, 4); //配置GPIO功能,配置输出时: *gpiocaltfn0 &= ~(3 << 24); *gpiocaltfn0 |= (1 << 24); *gpiocoutenb |= (1 << 12);
方式二
由于GPIO相关寄存器在物理上都是连续的,只需要指定一个起始物理地址,然后指定一个大小,大小包括了需要访问的所有寄存器即可。
void *gpiobase; unsigned long *gpiocout, *gpiocoutenb, *gpiocaltfn0; gpiobase = ioremap(0xC001C000, 0x24); gpiocout = (unsigned long *)(gpiobase + 0x00); gpiocoutenb = (unsigned long *)(gpiobase+0x04); gpiocaltfn0 = (unsigned long *)(gpiobase+0x20); //配置GPIO功能,配置为输出: gpiocaltfn0 &= ~(3 << 24); gpiocaltfn0 |= (1 << 24); gpiocoutenb |= (1 << 12);
不使用GPIO库函数方式来修改之前操作LED灯代码示例
将之前的GPIO库函数操作LED灯代码进行修改
- led_drv.c
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <linux/io.h> //ioremap #include <linux/uaccess.h> //记录各个寄存器的内核虚拟地址 static void *gpiobase; static unsigned long *gpiocout; //数据寄存器内核虚拟地址 static unsigned long *gpiocoutenb;//输出使能寄存器内核虚拟地址 static unsigned long *gpiocaltfn0;//复用功能选择寄存器的内核虚拟地址 #define LED_ON 0x100001 //开灯命令 #define LED_OFF 0x100002 //关灯命令 static long led_ioctl(struct file *file, unsigned long cmd, unsigned long arg) { //1.分配内核缓冲区 int kindex; //2.拷贝用户缓冲区数据到内核缓冲区 copy_from_user(&kindex, (int *)arg, sizeof(kindex)); //3.解析用户发送来的命令 switch(cmd) { case LED_ON: if(kindex == 1) *gpiocout &= ~(1 << 12); /* else if(kindex == 2) .... else if(kindex == 3) .... else if(kindex == 4) .... */ break; case LED_OFF: if(kindex == 1) *gpiocout |= (1 << 12); /* else if(kindex == 2) .... else if(kindex == 3) .... else if(kindex == 4) .... */ break; default: return -1; } //4.添加调试打印信息,将操作的寄存器的值打印出来 //如果将来发现寄存器的值是对的,但是灯没有反应 //请问:是谁的问题?此问题势必是硬件问题 printk("GPIOCALTFN0=%#x, GPIOCOUTENB=%#x, GPIOCOUT=%#x\n", *gpiocaltfn0, *gpiocoutenb, *gpiocout); return 0; } //定义初始化硬件操作接口对象 static struct file_operations led_fops = { .owner = THIS_MODULE, .unlocked_ioctl = led_ioctl }; //定义初始化混杂设备对象 static struct miscdevice led_misc = { .minor = MISC_DYNAMIC_MINOR, .name = "myled", .fops = &led_fops }; static int led_init(void) { //1.注册混杂设备到内核 misc_register(&led_misc); //2.将各个寄存器的物理地址映射到内核虚拟地址 //一旦完成映射,驱动访问映射的内核虚拟地址就是 //在访问对应的物理地址 gpiobase = ioremap(0xC001C000, 0x24); gpiocout = (unsigned long *)(gpiobase + 0x00); gpiocoutenb = (unsigned long *)(gpiobase + 0x04); gpiocaltfn0 = (unsigned long *)(gpiobase + 0x20); //3.配置引脚为GPIO功能,配置为输出,输出1(省电) *gpiocaltfn0 &= ~(0x3 << 24); *gpiocaltfn0 |= (1 << 24); *gpiocoutenb |= (1 << 12); *gpiocout |= (1 << 12); return 0; } static void led_exit(void) { //1.输出1,解除地址映射 *gpiocout |= (1 << 12); iounmap(gpiobase); //2.卸载混杂设备 misc_deregister(&led_misc); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
- led_test.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #define LED_ON 0x100001 //开灯命令 #define LED_OFF 0x100002 //关灯命令 int main(int argc, char *argv[]) { int fd; int index; //分配用户缓冲区,保存操作灯的编号 if(argc != 3) { printf("用法:%s <on|off> <1|2|3|4>\n", argv[0]); return -1; } //打开设备 fd = open("/dev/myled", O_RDWR); if (fd < 0) { printf("打开设备失败!\n"); return -1; } //"1"->1 index = strtoul(argv[2], NULL, 0); //应用ioctl->软中断->内核sys_ioctl->驱动led_ioctl if(!strcmp(argv[1], "on")) ioctl(fd, LED_ON, &index); else if(!strcmp(argv[1], "off")) ioctl(fd, LED_OFF, &index); //关闭设备 close(fd); return 0; }
- Makefile
obj-m += led_drv.o all: make -C /opt/kernel SUBDIRS=$(PWD) modules clean: make -C /opt/kernel SUBDIRS=$(PWD) clean
- 执行结果:
总结
通常都能够使用gpio库函数去操作gpio,但是gpio库函数的使用基于platform下对匹配的芯片型号的gpio相关宏的定义准确无误,如果不能确定这点的话,那么还是使用物理地址映射的方式来操作外设是最可靠的,因为芯片手册中都会详细描述相关的物理寄存器的配置和使用。所以,能够轻松使用物理地址映射的方式来自如操作硬件配置是一项必备的能力。