如何编写一个简单的Linux驱动(三)——完善设备驱动
前期知识
1.如何编写一个简单的Linux驱动(一)——驱动的基本框架
2.如何编写一个简单的Linux驱动(二)——设备操作集file_operations
前言
在上一篇文章中,我们编写设备驱动遇到了不少问题:
(1) 注册设备时,设备号需要程序员给定,每次编写驱动时,程序员需要知道有哪些设备号是空闲的;
(2) 加载驱动后,需要用户使用mknod命令手动生成设备节点;
(3) 虽然用户程序调用了读写设备的函数,但是并没有数据传输。
在本篇文章中,我们会一次解决这三个问题。
要下载上一篇文章所写的全部代码,请点击这里。
1.自定义一个设备结构体
为了方便,我们自己定义一个结构体,用于描述我们的设备,存放和设备有关的属性。打开上一篇文章所写的源代码文件,加入如下代码。
1 struct shanwuyan_dev 2 { 3 struct cdev c_dev; //字符设备 4 dev_t dev_id; //设备号 5 struct class *class; //类 6 struct device *device; //设备 7 int major; //主设备号 8 int minor; //次设备号 9 }; 10 11 struct shanwuyan_dev shanwuyan; //定义一个设备结构体
我们对成员变量分别进行解析。
成员变量 | 描述 |
struct cdev c_dev | 这是一个字符设备结构体,在后文我们再介绍 |
dev_t dev_id | 这是一个32位的数据,其中高12位表示主设备号,低20位表示次设备号,高低设备号组合在一起表示一个完整的设备号 |
struct class *class | 类,主要作用后文再介绍 |
struct device *device | 设备,主要作用后文再介绍 |
int major | 主设备号 |
int minor | 次设备号 |
接下来我们要介绍三个宏函数"MAJOR"、"MINOR"、"MKDEV",它们的原型如下。
1 #define MINORBITS 20 2 #define MINORMASK ((1U << MINORBITS) - 1) 3 4 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) 5 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) 6 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
看起来很复杂,但是它们的功能很简单:"MAJOR"的作用是根据设备号获取主设备号,即设备号的高12位;"MINOR"的作用是根据设备号获取次设备号,即设备号的低20位;"MKDEV"的作用是把主设备号和次设备号合并成一个完整的设备号。
2.新的注册与注销字符设备的方法
在上一篇文章中,我们使用"register_chrdev"函数来注册设备,使用"unregister_chrdev"函数来注销设备。这一组函数的缺点是:首先,主设备号需要用户给定;其次,使用该函数的话,设备会占据整个主设备号,对应的次设备号无法使用,造成设备号的浪费。为了克服以上缺点,我们引入两组新的注册设备号的函数"register_chrdev_region"和"alloc_chrdev_region",这两个函数对应的注销设备号的函数都是"unregister_chrdev_region"。它们的函数原型如下。
1 //这些函数的声明都在linux/fs.h中 2 extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *); //第一个参数是设备号的地址,第二个参数是次设备号的起始号,第三个参数是要申请的个数,第四个参数是设备名称 3 extern int register_chrdev_region(dev_t, unsigned, const char *); //第一个参数是设备号,第二个参数是要申请的个数,第三个参数是设备名称 4 extern void unregister_chrdev_region(dev_t, unsigned); //第一个参数是设备号,第二个参数是申请的个数
如果用户给定了主设备号,可以使用"register_chrdev_region"函数来让系统分配次设备号;如果用户未给定主设备号,可以使用"alloc_chrdev_region"函数,由系统分配主设备号和次设备号。这两个函数在驱动的入口函数里调用,作初始化用。相应的,要在驱动出口函数中调用"unregister_chrdev_region"函数来注销设备号。如下方代码。
1 static int __init shanwuyan_init(void) //驱动入口函数 2 { 3 int ret = 0; 4 5 shanwuyan.major = 0; //主设备号设置为0,表示用户不给定主设备号,主次设备号都由系统分配 6 /*1.分配设备号*/ 7 if(shanwuyan.major) //如果给定了主设备号,则由系统分配次设备号 8 { 9 shanwuyan.dev_id = MKDEV(shanwuyan.major, 0); //把用户给的主设备号和0号次设备号合并成一个设备号 10 ret = register_chrdev_region(shanwuyan.dev_id, 1, SHANWUYAN_NAME); //因为我们只考虑一个设备的情况,所以只分配一个设备号,即设备号0 11 } 12 else //如果没有给定主设备号,则主次设备号全部由系统分配 13 { 14 ret = alloc_chrdev_region(&(shanwuyan.dev_id), 0, 1, SHANWUYAN_NAME); //只考虑一个设备的情况 15 shanwuyan.major = MAJOR(shanwuyan.dev_id); //获取主设备号 16 shanwuyan.minor = MINOR(shanwuyan.dev_id); //获取次设备号 17 } 18 if(ret < 0) //设备号分配失败,则打印错误信息,然后返回 19 { 20 printk(KERN_EMERG "shanwuyan chrdev_region error!\r\n"); 21 return -EINVAL; 22 } 23 else //如果设备号分配成功,则打印设备的主次设备号 24 { 25 printk(KERN_EMERG "shanwuyan.major = %d, shanwuyan.minor = %d\r\n", shanwuyan.major, shanwuyan.minor); 26 } 27 28 29 return 0; 30 } 31 32 static void __exit shanwuyan_exit(void) //驱动出口函数 33 { 34 /*1.注销设备号*/ 35 unregister_chrdev_region(shanwuyan.dev_id, 1); 36 }
以上代码的功能是:入口函数实现由系统分配主次设备号,出口函数实现注销系统分配的设备号。
听起来这两组新的注册设备号的函数好处多多,但是它们却有一个致命的缺点,那就是只能实现分配设备号的功能,却无法像"register_chrdev"函数那样还可以把设备添加到内核中。为了把设备添加到内核,我们就要引进字符设备结构体"struct cdev",这也是我们文章开头的自定义结构体的第一个成员变量。该结构体的原型如下。
1 //该结构体原型在linux/cdev.h中,记得在驱动代码中包含进去 2 struct cdev { 3 struct kobject kobj; 4 struct module *owner; 5 const struct file_operations *ops; 6 struct list_head list; 7 dev_t dev; 8 unsigned int count; 9 };
在本文中,我们只用到该结构体中的三个成员变量"struct module *owner"、"const struct file_operations *ops"、"dev_t dev",他们的描述如下。
成员变量 | 描述 |
struct module *owner
|
一般取值为THIS_MODULE |
const struct file_operations *ops
|
设备操作集file_operations的地址 |
dev_t dev
|
就是设备号 |
接下来要介绍两个与该结构体相关的函数,"cdev_init"和"cdev_add",它们的原型如下。
1 void cdev_init(struct cdev *, const struct file_operations *); //第一个参数是struct cdev结构体变量的地址,第二个参数是字符设备操作集的地址 2 int cdev_add(struct cdev *, dev_t, unsigned); //第一个参数是struct cdev结构体变量的地址,第二个参数是设备号,第三个参数是要添加的数量
这两个函数的作用分别是初始化字符设备结构体和向内核添加字符设备。
向入口函数中添加代码,将字符设备注册到内核中,添加的代码如下。
1 static int __init shanwuyan_init(void) //驱动入口函数 2 { 3 int ret = 0; 4 5 /*1.分配设备号*/ 6 ... 7 8 /*2.向内核添加字符设备*/ 9 shanwuyan.c_dev.owner = THIS_MODULE; 10 cdev_init(&(shanwuyan.c_dev), &(shanwuyan_fops)); //初始化字符设备结构体 11 cdev_add(&(shanwuyan.c_dev), shanwuyan.dev_id, 1); //添加设备到内核 12 13 return 0; 14 }
这样,设备就注册成功了。
3.自动创建设备节点
要实现自动创建设备节点,我们需要引进两个结构体,"struct class"和"struct device"。即,文章开头的自定义设备结构体中的成员变量"struct class *class"和"struct device *device"是用于实现自动生成设备节点的。这两个结构体的具体实现我们先不作深入了解,只需要了解如何在这里使用他们。我们先引进四个关于这两个结构体的函数,"class_create"、"class_destroy"、"device_create"、"device_destroy",这些函数的作用分别是创建类、摧毁类、创建设备、摧毁设备。它们的原型如下。
1 //位于"linux/device.h"中,记得在驱动代码中包含进去 2 #define class_create(owner, name) \ //第一个参数是所有者(一般为THIS_MODULE),第二个参数是设备名称 3 ({ \ 4 static struct lock_class_key __key; \ 5 __class_create(owner, name, &__key); \ 6 }) 7 8 extern void class_destroy(struct class *cls); //参数是创建的类的地址 9 10 struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); //第一个参数是类的地址,第二个参数为父设备地址(一般为NULL),第三个参数为设备号,第四个参数为可能用到的数据(一般为NULL),第五个参数为设备名称 11 extern void device_destroy(struct class *cls, dev_t devt); //第一个参数为类的地址,第二个参数为设备号
为了实现自动创建设备节点,我们要在入口函数中创建一个类,然后在类里创建一个设备。在出口函数中,也要相应地摧毁设备和类。代码如下。
1 static int __init shanwuyan_init(void) //驱动入口函数 2 { 3 int ret = 0; 4 5 /*1.分配设备号*/ 6 ... 7 8 /*2.向内核添加字符设备*/ 9 ... 10 11 /*3.自动创建设备节点*/ 12 shanwuyan.class = class_create(THIS_MODULE, SHANWUYAN_NAME); //创建类 13 shanwuyan.device = device_create(shanwuyan.class, NULL, shanwuyan.dev_id, NULL, SHANWUYAN_NAME); //创建设备,设备节点就自动生成了。正常情况下,要考虑类和设备创建失败的情况,为了简化代码,这里就不写了 14 15 return 0; 16 } 17 18 static void __exit shanwuyan_exit(void) //驱动出口函数 19 { 20 /*1.注销设备号*/ 21 ... 22 /*2.摧毁设备*/ 23 device_destroy(shanwuyan.class, shanwuyan.dev_id); 24 /*3.摧毁类*/ 25 class_destroy(shanwuyan.class); 26 }
在入口函数中,我们先创建了类,后创建了设备,即有类才能有设备,所以在出口函数中,我们要先把设备摧毁了,然后再摧毁类。
4.实现与用户程序的数据传输
上一篇文章中,file_operations的读写操作并没有发挥真正的作用。在本文中,我们改写一下驱动读写函数和用户程序代码,让设备和用户程序实现数据传输。
首先修改一下驱动程序的"shanwuyan_write"函数和"shanwuyan_read"函数,其中读函数的作用是向用户程序传输一个字符串,写函数的作用是接收用户程序发来的数据,并打印出来,代码如下。
1 /*读设备*/ 2 static ssize_t shanwuyan_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) 3 { 4 char device_data[] = "device data"; 5 copy_to_user(buf, device_data, sizeof(device_data)); //向用户程序传输设备数据 6 return 0; 7 } 8 9 /*写设备*/ 10 static ssize_t shanwuyan_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) 11 { 12 char user_data[50]; 13 copy_from_user(user_data, buf, count); //获取用户程序写到设备的数据 14 printk("device get data:%s\r\n", user_data); 15 return 0; 16 }
这里用到了两个函数,"copy_to_user"和"copy_from_user",作用分别是向用户程序传输数据和从用户程序接收数据。它们的原型如下。
1 //声明在文件linux/uaccess.h中,记得在驱动代码中包含进去 2 static __always_inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n) //第一个参数是目的地址,第二个参数是源地址,第三个参数是数据的size 3 static __always_inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) //第一个参数是目的地址,第二个参数是源地址,第三个参数是数据的size
接下来改造用户程序,全部代码如下。
1 //源代码文件名为"shanwuyanAPP.c" 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <stdio.h> 6 #include <unistd.h> 7 #include <stdlib.h> 8 #include <string.h> 9 10 /* 11 *argc:应用程序参数个数,包括应用程序本身 12 *argv[]:具体的参数内容,字符串形式 13 *./shanwuyanAPP <filename> <r:w> r表示读,w表示写 14 */ 15 int main(int argc, char *argv[]) 16 { 17 int ret = 0; 18 int fd = 0; 19 char *filename; 20 char readbuf[50]; 21 char user_data[] = "user data"; 22 23 if(argc != 3) 24 { 25 printf("Error usage!\r\n"); 26 return -1; 27 } 28 29 filename = argv[1]; //获取文件名称 30 31 fd = open(filename, O_RDWR); 32 if(fd < 0) 33 { 34 printf("cannot open file %s\r\n", filename); 35 return -1; 36 } 37 /*读操作*/ 38 if(!strcmp(argv[2], "r")) 39 { 40 read(fd, readbuf, 50); 41 printf("user get data:%s\r\n", readbuf); 42 } 43 /*写操作*/ 44 else if(!strcmp(argv[2], "w")) 45 { 46 write(fd, user_data, 50); 47 } 48 else 49 { 50 printf("ERROR usage!\r\n"); 51 } 52 53 /*关闭操作*/ 54 ret = close(fd); 55 if(ret < 0) 56 { 57 printf("close file %s failed\r\n", filename); 58 } 59 60 return 0; 61 }
5.应用
编译驱动程序,交叉编译用户程序,拷贝到开发板中。
在终端输入命令"insmod shanwuyan.ko"加载驱动,可以看到系统分配的主次设备号分别为246和0.
在终端输入命令"ls /dev/shanwuyan",可以看到已经自动创建了设备节点"/dev/shanwuyan"。
在终端输入"./shanwuyanAPP /dev/shanwuyan r",让用户程序读设备,可以看到终端打印出了设备传递给用户程序的信息。
在终端输入"./shanwuyanAPP /dev/shanwuyan w",让用户程序写设备,可以看到终端打印出了用户程序传递给设备的信息。
本文的全部代码在这里。