一、注册字符设备驱动新接口 1
1、新接口与老接口
- (1)新接口:register_chrdev_region/alloc_chrdev_region+cdev_add
- (2)老接口:register_chrdev
2、cdev介绍
- (1)cdev:是一个设备结构体,用于字符设备注册,但注册前需用 cdev_init 初始化(cdev、 file_operations 为参数)。
- (2)相关函数:cdev_alloc、cdev_init、cdev_add、cdev_del。
3、设备号
- (1)包括主设备号和次设备号。
- (2)dev_t 类型:unsigned int 类型,32 位,用于在驱动程序中定义设备编号,高 12 位为主设备号,低 20 位为次设备号。
- (3)MKDEV、MAJOR、MINOR 三个宏:MKDEV 用于由主设备号、次设备号获得设备号, MAJOR 用于由设备号获取主设备号,MINOR 用于 由设备号获取次设备号。
4、编程实践
- (1)使用 register_chrdev_region + cdev_init、cdev_add 进行字符设备驱动注册。
#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>//======= #include <linux/cdev.h> #define GPJ0CON S5PV210_GPJ0CON #define GPJ0DAT S5PV210_GPJ0DAT #define GPJ0CON_PA 0xe0200240 //物理地址 #define GPJ0DAT_PA 0xe0200244 unsigned int *pGPJ0CON; //虚拟地址 unsigned int *pGPJ0DAT; char kbuf[100];//内核空间的buf //自定义一个file_operations结构体变量,并且去填充 static const struct file_operations test_fops={ .owner = THIS_MODULE, .open = test_chrdev_open, .release = test_chrdev_release, .write = test_chrdev_write, .read = test_chrdev_read, }; //模块安装函数==入口 static int __init chrdev_init(void) { int retval; 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); //========================================================正餐开始, //==========第一步,注册主次设备号 mydev=MKDEV(MYMAJOR,0); retval=register_chrdev_region(mydev,MYCNT,MYNAME); if(retval){ printk(KERN_ERR "Unable to register minors for %s\n", MYNAME); return -EINVAL; } printk(KERN_INFO "register_chrdev_region success\n"); //=========第二步,注册字符设备驱动 cdev_init(&test_cdev,&test_fops); retval=cdev_add(&test_cdev,mydev,MYCNT); if(retval){ printk(KERN_ERR "Unable to cdev_add\n");return -EINVAL; } printk(KERN_INFO "cdev_add success\n"); // 使用动态映射的方式来操作寄存器 if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON")) 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); // 第一步真正注销字符设备驱动用 cdev_del cdev_del(&test_cdev); // 第二步去注销申请的主次设备号 unregister_chrdev_region(mydev, MYCNT); }
二、注册字符设备驱动新接口 2
1、使用alloc_chrdev_region自动分配设备号
- (1)register_chrdev_region:需要事先知道要使用的主次设备号,要先用 cat /proc/devices 去查看设备号使用情况。
- (2)alloc_chrdev_region:让内核自动分配一个主设备号,更加智能。原型为:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
-(3)自动分配的设备号,我们必须知道它的主次设备号,才能 mknod 创建该设备驱动对应 的设备文件。
2、得到分配的主设备号和次设备号
- (1)使用 MAJOR 宏和 MINOR 宏从 dev_t 变量得到主设备号和次设备号。
- (2)反过来可使用 MKDEV 宏从主设备号和次设备号得到设备号。
3、中途出错的倒影式错误处理方法
- (1)内核中很多函数中包含来了很多个操作,这些操作每一个都有可能出错,而且一旦出 错后面的步骤就没有进行下去的必要了,且该操作前面的成功操作还得进行相应的处理以免浪费系统资源或造成 bug
#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> #include <linux/cdev.h> #define MYCNT 1 #define MYNAME "testchar" #define GPJ0CON S5PV210_GPJ0CON #define GPJ0DAT S5PV210_GPJ0DAT #define rGPJ0CON *((volatile unsigned int *)GPJ0CON) #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT) #define GPJ0CON_PA 0xe0200240 #define GPJ0DAT_PA 0xe0200244 unsigned int *pGPJ0CON; unsigned int *pGPJ0DAT; //int mymajor; static dev_t mydev; static struct cdev test_cdev; char kbuf[100]; // 内核空间的 buf,而用户空间是kbuf是有很大区别不是一个空间 //==============省略了具体操作的函数接口 //自定义一个file_operations结构体变量,并且去填充 staic const struct file_operations test_fops={ .owner =THIS_MODULE, .open = test_chrdev_open, .release = test_chrdev_release, .write = test_chrdev_write, .read = test_chrdev_read, } //模块安装函数==入口 static int __init chrdev_init(void) { int retval; printk(KERN_INFO "chrdev_init helloworld init\n"); //第一步:分配主次设备号 retval=alloc_chrdev_region(&mydev,12,MYCNT,MYNAME); if (retval < 0){ printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME); goto flag1; } printk(KERN_INFO "alloc_chrdev_region success\n"); printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev)); //第二步:注册字符设备驱动 cdev_init(&test_cdev,&test_fops); retval=cdev_add(&test_cdev,mydev,MYCNT); if(retval){ printk(KERN_ERR "Unable to cdev_add\n"); goto flag2; } printk(KERN_INFO "cdev_add success\n"); //使用动态映射的方式来操作寄存器 if(!request_mem_region(GPJ0CON_PA,4,"GPJ0CON")) goto flag3; if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT")) goto flag3; pGPJ0CON=ioremap(GPJ0CON_PA,4); pGPJ0DAT=ioremap(GPJ0DAT_PA,4); if(ERROR) goto flag4; return 0; flag4: release_mem_region(GPJ0CON_PA, 4); release_mem_region(GPJ0DAT_PA, 4); flag3: cdev_del(&test_cdev); flag2: // 在这里把第 1 步做成功的东西给注销掉 unregister_chrdev_region(mydev, MYCNT); flag1: return -EINVAL; } //函数卸载函数==出口 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(pGPJ0CON_PA,4); release_mme_region(pGPJ0DAT_PA,4); //第一步真正注销字符设备驱动用 cdev_del cdev_del(&test_cdev); //第二步去注销申请的主次设备号 unregister_chrdev_region(mydev,MYCNT); }
三、注册字符设备驱动新接口 3
1、使用cdev_alloc
- (1)cdev_alloc 的编程实践。
//全局变量: static struct cdev* pcdev; //=============================模块安装函数: //第二步,注册字符设备驱动 pcdev=cdev_alloc();// 给 pcdev 分配内存,指针实例化 cdev_init(pcdev,&test_fops); retval=cdev_add(pcdev,mydev,MYCNT); //=============================模块卸载函数: cdev_del(pcdev);
- (2)从内存角度体会 cdev_alloc 用与不用的差别:不用的 cdev_alloc 时在程序开始时直接 创建一个 cdev 结构体,在加载程序时就占用了 cdev 结构体大小的内存,直到程序运 行结束时释放;用 cdev_alloc 时程序开始时创建一个 cdev 结构体指针,在加载程序时 只占用了指针大小(在 32 位系统中占 4 个字节)的内存,直到运行时调用 cdev_alloc 才申请 cdev 结构体大小的内存,最后在调用 cdev_del 时释放。
- (3)这就是非面向对象的语言和面向对象的代码。
2、cdev_init 的替代
- (1)cdev_init 源码分析。
- (2)不使用 cdev_init 时的编程。
//模块安装函数: //dev_init(pcdev,&test_fops); pcdev->owner=THIS_MODULE; pcdev->ops=&test_fops;
四、字符设备驱动注册代码分析 1
1、老接口分析
register_chrdev
__register_chrdev
__register_chrdev_region
cdev_alloc
cdev_add
2、新接口分析
register_chrdev_region
__register_chrdev_region
alloc_chrdev_region
__register_chrdev_region
五、自动创建设备文件(字符设备驱动)
1、问题描述
- (1)在之前的实践中,安装模块后,还需要使用 mknod 来创建设备文件,以便应用程序打 开,进而调用文件操作结构体绑定的 open、read、write、release 等函数。
2、解决方案:udev(嵌入式中用的是 mdev)
- (1)udev 是应用层的一个应用程序,可以创建设备文件
- (2)内核驱动和应用层 udev 之间有一套信息传输机制(netlink 协议)。
- (3)应用层启用 udev,内核驱动中使用相应接口。
- (4)驱动注册和注销的信息都会被传输给 udev,由 udev 中应用层进行设备文件的创建和 删除。
3、内核驱动设备类相关函数
- (1)class_create。
- (2)device_create。
//============模块安装函数: // 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息 // 给 udev,让 udev 自动创建和删除设备文件 test_class=class_create(THIS_MODULE,"aston_class"); if(IS_ERR(test_class)) return -EINVAL; // 最后 1 个参数字符串,就是我们将来要在/dev 目录下创建的设备文件的名字 // 所以我们这里要的文件名是/dev/test device_create(test_class, NULL, mydev, NULL, "test111"); //========模块卸载函数: // 设备驱动注销之前,需要删除设备文件和类文件夹 device_destroy(test_class, mydev); class_destroy(test_class);
六、静态映射表建立过程分析
1、建立静态映射表的三个关键部分
- (1)映射表具体物理地址和虚拟地址的值的相关宏定义
- (2)映射表建立函数:由(1)中的宏定义来建立 Linux 内核的页表映射关系。
- (3)开机时调用映射表建立函数。
七、动态映射结构体方式操作寄存器
1、问题描述
- (1)仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射,来替代多次 映射。
2、编程实践
//============结构体及指针:
typedef struct GPJ0REG
{
volatile unsigned int gpj0con;
volatile unsigned int gpj0dat;
}gpj0_reg_t;
gpj0_reg_t* pGPJ0REG;
//============宏定义:
#define GPJ0_REGBASE 0xe0200240
//============模块安装函数:
//动态映射部分
if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
return -EINVAL;
pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
// 映射之后用指向结构体的指针来进行操作
pGPJ0REG -> gpj0con = 0x11111111;
pGPJ0REG -> gpj0dat = ((0<<3) | (0<<4) | (0<<5)); // 亮
//============模块卸载函数:
pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5)); // 灭
// 解除映射
iounmap(pGPJ0REG);
release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));
八、内核提供的寄存器读写接口
1、前面使用的对指针解引用的方式访问寄存器
- (1)可行,但移植性较差,如果要从 ARM 体系移植到 x86 则需要修改很多代码。
2、内核提供的寄存器读写接口
- (1)writel 和 readl:写和读一个 32 位的寄存器。
- (2)iowrite32 和 ioread32:与(1)中的函数作用相同。
3、编程实践
// 测试 1:用 2 次 ioremap 得到的动态映射虚拟地址来操作,测试成功
writel(0x11111111, pGPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), pGPJ0DAT); // 亮
//测试 2:用静态映射的虚拟地址来操作,测试成功
writel(0x11111111, GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT); // 亮
// 测试 3:用 1 次 ioremap 映射多个寄存器得到虚拟地址,测试成功
//宏定义和全局变量:
#define GPJ0CON_PA 0xe0200240
#define S5P_GPJ0REG(x) (x)
#define S5P_GPJ0CON S5P_GPJ0REG(0)
#define S5P_GPJ0DAT S5P_GPJ0REG(4)
static void __iomem *baseaddr;//寄存器的虚拟地址的基地址
//驱动入口函数
if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))
return -EINVAL;
baseaddr = ioremap(GPJ0CON_PA, 8);
writel(0x11111111, baseaddr + S5P_GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT); //亮