以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
补充内容:字符设备驱动高级篇5——静态映射表、动态映射结构体方式操作寄存器
前言
上节字符设备驱动基础4——读写接口的操作实践中,驱动源代码中的test_chrdev_open()、test_chrdev_read()、test_chrdev_write()等函数,其内部只是一些简单的输出语句,以表示它们得到调用,从而演示应用层读写操作与驱动层的读写操作如何关联的。这些驱动源代码中的函数没有涉及到具体的硬件操作(比如寄存器操作这些行为)。
本节内容,将以“动静态映射操作LED”为例,细化驱动源代码中的读写操作函数,使其涉及到实际意义的硬件操作。这里的“映射”,是指物理地址和虚拟地址的对应关系;这里的“动静态”,是指映射的建立是一直存在的,还是根据需要随时建立与销毁的。
一、驱动如何操控硬件
1、OS与裸机操作硬件的异同点
(1)相同点
硬件物理原理不变;硬件操作接口(寄存器)不变;硬件操作代码不变。
(2)不同点
寄存器地址不同
原来是直接用物理地址,现在需要用(该物理地址相对应的)虚拟地址。
寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
编程方法不同
裸机中习惯直接用函数指针操作寄存器地址。
kernel为了实现最大程序的可移植性,习惯用封装好的io读写函数来操作寄存器。
2、内核的虚拟地址映射方法
(1)虚拟地址映射的意义
见博文:为什么要使用虚拟地址空间与物理地址空间映射?
(2)虚拟地址映射方法
内核中有两套虚拟地址映射方法:静态映射、动态映射。
1)静态映射
内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核。
在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效。
对于移植好的内核,你用不用它都在那里。
2)动态映射
驱动程序根据需要随时动态的建立映射、使用、销毁映射。
映射是短期临时的。
(3)如何选择虚拟地址映射方法
两种映射并不排他,可以同时使用。
静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存。
静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间。
动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射、销毁映射。
二、静态映射操作LED
1、静态映射表
静态映射表,其实就是头文件中的宏定义。
不同版本内核中静态映射表所在的目录位置、文件名可能不同。
不同SoC的静态映射表所在的目录位置、文件名可能不同。
2、三星版本内核中的静态映射表
CPU在安排寄存器地址时不是随意分布的,而是按照模块来组织的,比如GPIO模块、UART模块等等。每个模块里,寄存器的地址一般是连续的,所以内核在定义寄存器地址时,都是先定义某个模块的基地址,然后再用“基地址+偏移量”来表示具体的一个寄存器地址。
驱动源代码因为要操作LED,涉及到GPIO模块,因此这里讲述下GPIO的相关内容。
(1)GPIO模块的虚拟基地址
文件arch/arm/plat-s5p/include/plat/map-s5p.h中定义了几个模块(GPIO、UART等)的虚拟基地址,其中GPIO模块的虚拟基地址定义如下:
//…… #define S5P_VA_GPIO S3C_ADDR(0x00500000) //……
其中S3C_ADDR定义在arch/arm/plat-samsung/include/plat/map-base.h。
#define S3C_ADDR_BASE (0xFD000000)#ifndef __ASSEMBLY__ #define S3C_ADDR(x) ((void __iomem __force *)S3C_ADDR_BASE + (x)) #else #define S3C_ADDR(x) (S3C_ADDR_BASE + (x)) #endif
其中 #define S3C_ADDR_BASE (0xFD000000) , S3C_ADDR_BASE 是三星移植时确定的静态映射表的基地址,所有模块的虚拟基地址都是以这个地址+偏移量来指定的。
(2)GPIO模块中每个端口的虚拟基地址
GPIO模块包含许多端口,比如GPA0、GPJ0等(这些端口包含着许多IO口,比如GPA0_1,GPA0_2等),每个端口都有一个虚拟基地址,定义在arch/arm/mach-s5pv210/include/mach/regs-gpio.h文件中,如下所示。
由此可知,端口的虚拟基地址,都是是由“GPIO模块的虚拟基地址 + 偏移量”组成的。
/* Base addresses for each of the banks */#define S5PV210_GPA0_BASE (S5P_VA_GPIO + 0x000) #define S5PV210_GPA1_BASE (S5P_VA_GPIO + 0x020) #define S5PV210_GPB_BASE (S5P_VA_GPIO + 0x040) #define S5PV210_GPC0_BASE (S5P_VA_GPIO + 0x060) #define S5PV210_GPC1_BASE (S5P_VA_GPIO + 0x080) #define S5PV210_GPD0_BASE (S5P_VA_GPIO + 0x0A0) #define S5PV210_GPD1_BASE (S5P_VA_GPIO + 0x0C0) #define S5PV210_GPE0_BASE (S5P_VA_GPIO + 0x0E0) #define S5PV210_GPE1_BASE (S5P_VA_GPIO + 0x100) #define S5PV210_GPF0_BASE (S5P_VA_GPIO + 0x120) //省略……
(3)GPIO具体寄存器的虚拟地址
GPIO具体寄存器的虚拟地址,定义在arch/arm/mach-s5pv210/include/mach/gpio-bank.h文件中。它们都是以“GPIO端口的虚拟基地址+偏移量”组成。如下所示。
//省略…… #define S5PV210_GPJ0CON (S5PV210_GPJ0_BASE + 0x00) #define S5PV210_GPJ0DAT (S5PV210_GPJ0_BASE + 0x04) //省略……
3、静态映射操作LED实践
步骤1:添加LED亮灭操作代码
参考裸机内容,添加LED亮、灭操作的代码。为了测试代码的可行性,先在chrdev_init()和chrdev_exit()init函数中分别添加点亮和熄灭LED的代码,如下。
#include <linux/module.h> // module_init module_exit #include <linux/init.h> // __init __exit #include <linux/fs.h> #include <asm/uaccess.h> #include <mach/regs-gpio.h> #include <mach/gpio-bank.h> // arch/arm/mach-s5pv210/include/mach/gpio-bank.h #include <linux/string.h>#define GPJ0CON S5PV210_GPJ0CON #define GPJ0DAT S5PV210_GPJ0DAT#define rGPJ0CON *((volatile unsigned int *)GPJ0CON) #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)// 模块安装函数 static int __init chrdev_init(void) { printk(KERN_INFO "chrdev_init helloworld init\n");rGPJ0CON = 0x11111111;rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮return 0; }// 模块卸载载函数 static void __exit chrdev_exit(void) {printk(KERN_INFO "chrdev_exit helloworld init\n"); rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭 }module_init(chrdev_init); module_exit(chrdev_exit);MODULE_LICENSE("GPL"); // 描述模块的许可证 MODULE_AUTHOR("aston"); // 描述模块的作者 MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息 MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
[root@xjh mnt]# insmod module_test.ko [19261.861366] chrdev_init helloworld init //此时三盏LED亮 [root@xjh mnt]# rmmod module_test.ko [19307.270997] chrdev_exit helloworld init [root@xjh mnt]# rmmod module_test.ko //此时三盏LED由亮转灭
步骤2:分别在test_chrdev_open(release)函数里添加LED亮(灭)代码
见最后的代码。
步骤3:在驱动源代码中细化test_chrdev_write()写函数
1)先定义应用和驱动之间的控制接口,比如应用写"1"表示灯亮,写"0"表示让灯灭。
2)见最后的代码。
步骤4:写应用来测试写函数
因为针对LED,在应用层里不好表示读操作,所以在应用层只测试了写函数。
另外,在运行应用层app.c文件前,先安装驱动获得主设备号,然后以“mknod /dev/test c 主设备 次设备号”的方式创建设备文件。
应用层app.c代码如下:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h>#define FILE "/dev/test" char buf[100];int main(void) {int fd = -1;int i = 0;fd = open(FILE, O_RDWR);if (fd < 0){printf("open %s error.\n", FILE);return -1;}printf("open %s success..\n", FILE);while (1){memset(buf, 0 , sizeof(buf));printf("请输入 on 或者 off 或者 flash 或者 quit :\n");scanf("%s", buf);if (!strcmp(buf, "on")){write(fd, "1", 1);}else if (!strcmp(buf, "off")){write(fd, "0", 1);}else if (!strcmp(buf, "flash")){for (i=0; i<3; i++){write(fd, "1", 1);sleep(1);write(fd, "0", 1);sleep(1);}} else if (!strcmp(buf, "quit")){break;}elsebreak;}close(fd); return 0; }
步骤5:在驱动源代码中添加读函数
见最后的代码。
步骤6:填充struct file_operations变量
见最后的代码。
附录:最后的代码
#include <linux/module.h> // module_init module_exit #include <linux/init.h> // __init __exit #include <linux/fs.h> #include <asm/uaccess.h> #include <mach/regs-gpio.h> #include <mach/gpio-bank.h> // arch/arm/mach-s5pv210/include/mach/gpio-bank.h #include <linux/string.h>#define MYMAJOR 200 #define MYNAME "testchar"#define GPJ0CON S5PV210_GPJ0CON #define GPJ0DAT S5PV210_GPJ0DAT //这里已经是虚拟地址了,在内核中已经定义好S5PV210_GPJ0CON //所谓的静态映射就体现在这里! //这个是在内核中写死的,如果要修改,就要把内核代码相关部分修改,然后重新编译#define rGPJ0CON *((volatile unsigned int *)GPJ0CON) #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)int mymajor; char kbuf[100]; // 内核空间的bufstatic int test_chrdev_open(struct inode *inode, struct file *file) {// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分printk(KERN_INFO "test_chrdev_open\n");rGPJ0CON = 0x11111111;rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮return 0; }static int test_chrdev_release(struct inode *inode, struct file *file) {printk(KERN_INFO "test_chrdev_release\n");rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭return 0; }ssize_t test_chrdev_read(struct file *file, char __user *ubuf, \size_t count, loff_t *ppos) {int ret = -1;printk(KERN_INFO "test_chrdev_read\n");ret = copy_to_user(ubuf, kbuf, count);if (ret){printk(KERN_ERR "copy_to_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_to_user success..\n");return 0; }//写函数的本质就是将应用层传递过来的数据先复制到内核中, //然后将之以正确的方式写入硬件完成操作。 static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,\size_t count, loff_t *ppos) {int ret = -1;printk(KERN_INFO "test_chrdev_write\n");// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中//memcpy(kbuf, ubuf); // 不行,因为2个不在同一个地址空间中memset(kbuf, 0, sizeof(kbuf));ret = copy_from_user(kbuf, ubuf, count);if (ret){printk(KERN_ERR "copy_from_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_from_user success..\n");if (kbuf[0] == '1'){rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));}else if (kbuf[0] == '0'){rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));}return 0; }// 自定义一个file_operations结构体变量,并且去填充 static const struct file_operations test_fops = {.owner = THIS_MODULE, // 惯例,直接写即可 .open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的.release = test_chrdev_release, // 就是这个.open对应的函数.write = test_chrdev_write,.read = test_chrdev_read, };// 模块安装函数 static int __init chrdev_init(void) { printk(KERN_INFO "chrdev_init helloworld init\n");// 在module_init宏调用的函数中去注册字符设备驱动// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号// 内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数mymajor = register_chrdev(0, MYNAME, &test_fops);if (mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);/*rGPJ0CON = 0x11111111;rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮*/return 0; }// 模块卸载载函数 static void __exit chrdev_exit(void) {printk(KERN_INFO "chrdev_exit helloworld exit\n"); // 在module_exit宏调用的函数中去注销字符设备驱动unregister_chrdev(mymajor, MYNAME);// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭 }module_init(chrdev_init); module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息 MODULE_LICENSE("GPL"); // 描述模块的许可证 MODULE_AUTHOR("aston"); // 描述模块的作者 MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息 MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
三、动态映射操作LED
1、建立动态映射
首先使用request_mem_region()函数向内核申请需要映射的内存资源。
然后使用ioremap()函数来实现映射,传给它一个物理地址,返回一个虚拟地址。
也就是说,要先申请(不一定申请成功),然后再映射,然后再使用。
2、销毁动态映射
首先使用iounmap()函数解除映射,然后使用release_mem_region()函数释放申请。
也就是说,使用完要解除映射时,先解除映射,然后再释放申请。
3、代码实践
#include <linux/module.h> // module_init module_exit #include <linux/init.h> // __init __exit #include <linux/fs.h> #include <asm/uaccess.h> #include <mach/regs-gpio.h> #include <mach/gpio-bank.h> // arch/arm/mach-s5pv210/include/mach/gpio-bank.h #include <linux/string.h> #include <linux/io.h> #include <linux/ioport.h>#define MYNAME "testchar"#define GPJ0CON_PA 0xe0200240 //物理地址 #define GPJ0DAT_PA 0xe0200244unsigned int *pGPJ0CON; unsigned int *pGPJ0DAT;int mymajor; char kbuf[100]; // 内核空间的buf//************省略部分代码*****************// 模块安装函数 static int __init chrdev_init(void) { printk(KERN_INFO "chrdev_init helloworld init\n");mymajor = register_chrdev(0, MYNAME, &test_fops);if (mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);// 使用动态映射的方式来操作寄存器if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))/*参数含义:起始地址(或者说寄存器的原始地址)寄存器的长度(一般32bit,因此4字节)这段空间的名字*/return -EINVAL;if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))return -EINVAL;pGPJ0CON = ioremap(GPJ0CON_PA, 4);//参数含义和上一样,返回一个指针pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);*pGPJ0CON = 0x11111111;*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮return 0; }// 模块卸载函数 static void __exit chrdev_exit(void) {printk(KERN_INFO "chrdev_exit helloworld exit\n");//想要熄灯,必要要在解除映射的前面*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //熄灯// 解除映射iounmap(pGPJ0CON);iounmap(pGPJ0DAT);release_mem_region(GPJ0CON_PA, 4);release_mem_region(GPJ0DAT_PA, 4);// 在module_exit宏调用的函数中去注销字符设备驱动unregister_chrdev(mymajor, MYNAME); }module_init(chrdev_init); module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息 MODULE_LICENSE("GPL"); // 描述模块的许可证 MODULE_AUTHOR("aston"); // 描述模块的作者 MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息 MODULE_ALIAS("alias xxx"); // 描述模块的别名信息