4. 字符设备驱动

您所在的位置:网站首页 imx6u嵌入式linux驱动开发指南15 4. 字符设备驱动

4. 字符设备驱动

2024-06-17 16:26| 来源: 网络整理| 查看: 265

4. 字符设备驱动¶

上一章节我们了解到什么是内核模块,模块的加载卸载详细过程以及内核模块的使用等内容。 本章,我们将学习驱动相关的概念,理解字符设备驱动程序的基本框架,并从源码上分析字符设备驱动实现和管理。 主要内容有如下五点:

Linux设备分类;

字符设备的抽象,字符设备设计思路;

字符设备相关的概念以及数据结构,了解设备号等基本概念以及file_operations、file、inode相关数据结构;

字符字符设备驱动程序框架,例如内核是如何管理设备号的;系统关联、调用file_operation接口,open函数所涉及的知识等等。

设备驱动程序实验。

4.1. Linux设备分类¶

linux是文件型系统,所有硬件都会在对应的目录(/dev)下面用相应的文件表示。 在windows系统中,设备大家很好理解,像硬盘,磁盘指的是实实在在硬件。 而在文件系统的linux下面,都有对于文件与这些设备关联的,访问这些文件就可以访问实际硬件。 像访问文件那样去操作硬件设备,一切都会简单很多,不需要再调用以前com,prt等接口了。 直接读文件,写文件就可以向设备发送、接收数据。 按照读写存储数据方式,我们可以把设备分为以下几种:字符设备、块设备和网络设备。

字符设备:指应用程序按字节/字符来读写数据的设备。 这些设备节点通常为传真、虚拟终端和串口调制解调器、键盘之类设备提供流通信服务, 它通常不支持随机存取数据。字符设备在实现时,大多不使用缓存器。系统直接从设备读取/写入每一个字符。 例如,键盘这种设备提供的就是一个数据流,当你敲入“cnblogs”这个字 符串时, 键盘驱动程序会按照和输入完全相同的顺序返回这个由七个字符组成的数据流。它们是顺序的,先返回c,最后是s。

块设备:通常支持随机存取和寻址,并使用缓存器。 操作系统为输入输出分配了缓存以存储一块数据。当程序向设备发送了读取或者写入数据的请求时, 系统把数据中的每一个字符存储在适当的缓存中。当缓存被填满时,会采取适当的操作(把数据传走), 而后系统清空缓存。它与字符设备不同之处就是,是否支持随机存储。字符型是流形式,逐一存储。 典型的块设备有硬盘、SD卡、闪存等,应用程序可以寻址磁盘上的任何位置,并由此读取数据。 此外,数据的读写只能以块的倍数进行。

网络设备:是一种特殊设备,它并不存在于/dev下面,主要用于网络数据的收发。

Linux内核中处处体现面向对象的设计思想,为了统一形形色色的设备,Linux系统将设备分别抽象为struct cdev, struct block_device,struct net_devce三个对象,具体的设备都可以包含着三种对象从而继承和三种对象属性和操作, 并通过各自的对象添加到相应的驱动模型中,从而进行统一的管理和操作

字符设备驱动程序适合于大多数简单的硬件设备,而且比起块设备或网络驱动更加容易理解, 因此我们选择从字符设备开始,从最初的模仿,到慢慢熟悉,最终成长为驱动界的高手。

4.2. 字符设备抽象¶

Linux内核中将字符设备抽象成一个具体的数据结构(struct cdev),我们可以理解为字符设备对象, cdev记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations), 在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一个文件(设备节点)绑定对象的cdev, 当我们对这个文件进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。

C语言中没有面向对象语言的继承的语法,但是我们可以通过结构体的包含来实现继承,这种抽象提取了设备的共性, 为上层提供了统一接口,使得管理和操作设备变得很容易。

在硬件层,我们可以通过查看硬件的原理图、芯片的数据手册,确定底层需要配置的寄存器,这类似于裸机开发。 将对底层寄存器的配置,读写操作放在文件操作接口里面,也就是实现file_operations结构体。

其次在驱动层,我们将文件操作接口注册到内核,内核通过内部散列表来登记记录主次设备号。

在文件系统层,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件的文件操作接口来设置底层寄存器

实际上,在Linux上写驱动程序,都是做一些“填空题”。因为Linux给我们提供了一个基本的框架, 我们只需要按照这个框架来写驱动,内核就能很好的接收并且按我们所要求的那样工作。有句成语工欲善其事,必先利其器, 在理解这个框架之前我们得花点时间来学习字符设备驱动相关概念及数据结构。

4.3. 相关概念及数据结构¶

在linux中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。 cdev结构体被内核用来记录设备号,而在使用设备时,我们通常会打开设备节点,通过设备节点的inode结构体、 file结构体最终找到file_operations结构体,并从file_operations结构体中得到操作设备的具体方法。

4.3.1. 设备号¶

对于字符的访问是通过文件系统的名称进行的,这些名称被称为特殊文件、设备文件,或者简单称为文件系统树的节点, Linux根目录下有/dev这个文件夹,专门用来存放设备中的驱动程序,我们可以使用ls -l 以列表的形式列出系统中的所有设备。 其中,每一行表示一个设备,每一行的第一个字符表示设备的类型。

如下图:’c’用来标识字符设备,’b’用来标识块设备。如 autofs 是一个字符设备c, 它的主设备号是10,次设备号是235; loop0 是一个块设备,它的主设备号是7,次所备案为0,同时可以看到loop0-loop3共用一个主设备号,次设备号由0开始递增。

一般来说,主设备号指向设备的驱动程序,次设备号指向某个具体的设备。如上图,I2C-0,I2C-1属于不同设备但是共用一套驱动程序

4.3.1.1. 内核中设备编号的含义¶

在内核中,dev_t用来表示设备编号,dev_t是一个32位的数,其中,高12位表示主设备号,低20位表示次设备号。 也就是理论上主设备号取值范围:0-2^12,次设备号0-2^20。 实际上在内核源码中__register_chrdev_region(…)函数中,major被限定在0-CHRDEV_MAJOR_MAX,CHRDEV_MAJOR_MAX是一个宏,值是512。 在kdev_t中,设备编号通过移位操作最终得到主/次设备号码,同样主/次设备号也可以通过位运算变成dev_t类型的设备编号, 具体实现参看上面代码MAJOR(dev)、MINOR(dev)和MKDEV(ma,mi)。

dev_t定义 (内核源码/include/linux/types.h)¶ 1 2 3typedef u32 __kernel_dev_t; typedef __kernel_dev_t dev_t; 设备号相关宏 (内核源码/include/linux/kdev_t.h)¶ 1 2 3 4 5 6#define MINORBITS 20 #define MINORMASK ((1U > MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) i_op = &shmem_special_inode_operations; init_special_inode(inode, mode, dev); break; ...... } } else shmem_free_inode(sb); return inode; }

第10行:mknod命令最终执行init_special_inode函数

init_special_inode函数(内核源码/fs/inode.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) { inode->i_mode = mode; if (S_ISCHR(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = rdev; } else if (S_ISBLK(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = rdev; } else if (S_ISFIFO(mode)) inode->i_fop = &pipefifo_fops; else if (S_ISSOCK(mode)) ; /* leave it no_open_fops */ else printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for" " inode %s:%lu\n", mode, inode->i_sb->s_id, inode->i_ino); }

第4-17行:判断文件的inode类型,如果是字符设备类型,则把def_chr_fops作为该文件的操作接口,并把设备号记录在inode->i_rdev。

inode上的file_operation并不是自己构造的file_operation,而是字符设备通用的def_chr_fops, 那么自己构建的file_operation等在应用程序调用open函数之后,才会绑定在文件上。接下来我们再看open函数到底做了什么。

4.5. open函数到底做了什么¶

使用设备之前我们通常都需要调用open函数,这个函数一般用于设备专有数据的初始化,申请相关资源及进行设备的初始化等工作, 对于简单的设备而言,open函数可以不做具体的工作,你在应用层通过系统调用open打开设备时, 如果打开正常,就会得到该设备的文件描述符,之后,我们就可以通过该描述符对设备进行read和write等操作; open函数到底做了些什么工作?下图中列出了open函数执行的大致过程。

户空间使用open()系统调用函数打开一个字符设备时(int fd = open(“dev/xxx”, O_RDWR))大致有以下过程:

在虚拟文件系统VFS中的查找对应与字符设备对应 struct inode节点

遍历散列表cdev_map,根据inod节点中的 cdev_t设备号找到cdev对象

创建struct file对象(系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件秒速符作为数组下标标识了一个设备对象)

初始化struct file对象,将 struct file对象中的 file_operations成员指向 struct cdev对象中的 file_operations成员(file->fops = cdev->fops)

回调file->fops->open函数

我们使用的open函数在内核中对应的是sys_open函数,sys_open函数又会调用do_sys_open函数。在do_sys_open函数中, 首先调用函数get_unused_fd_flags来获取一个未被使用的文件描述符fd,该文件描述符就是我们最终通过open函数得到的值。 紧接着,又调用了do_filp_open函数,该函数通过调用函数get_empty_filp得到一个新的file结构体,之后的代码做了许多复杂的工作, 如解析文件路径,查找该文件的文件节点inode等,直接来到了函数do_dentry_open函数,如下所示。

do_dentry_open函数(内核源码/fs/open.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14static int do_dentry_open(struct file *f,struct inode *inode,int (*open)(struct inode *, struct file *),const struct cred *cred) { …… f->f_op = fops_get(inode->i_fop); …… if (!open) open = f->f_op->open; if (open) { error = open(inode, f); if (error) goto cleanup_all; } …… }

第4行:使用fops_get函数来获取该文件节点inode的成员变量i_fop,在上图中我们使用mknod创建字符设备文件时,将def_chr_fops结构体赋值给了该设备文件inode的i_fop成员。

第7行:到了这里,我们新建的file结构体的成员f_op就指向了def_chr_fops。

def_chr_fops结构体(内核源码/fs/char_dev.c)¶ 1 2 3 4const struct file_operations def_chr_fops = { .open = chrdev_open, .llseek = noop_llseek, };

最终,会执行def_chr_fops中的open函数,也就是chrdev_open函数,可以理解为一个字符设备的通用初始化函数,根据字符设备的设备号, 找到相应的字符设备,从而得到操作该设备的方法,代码实现如下。

chrdev_open函数(内核源码/fs/char_dev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50static int chrdev_open(struct inode *inode, struct file *filp) { const struct file_operations *fops; struct cdev *p; struct cdev *new = NULL; int ret = 0; spin_lock(&cdev_lock); p = inode->i_cdev; if (!p) { struct kobject *kobj; int idx; spin_unlock(&cdev_lock); kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); if (!kobj) return -ENXIO; new = container_of(kobj, struct cdev, kobj); spin_lock(&cdev_lock); /* Check i_cdev again in case somebody beat us to it whilewe dropped the lock.*/ p = inode->i_cdev; if (!p) { inode->i_cdev = p = new; list_add(&inode->i_devices, &p->list); new = NULL; } else if (!cdev_get(p)) ret = -ENXIO; } else if (!cdev_get(p)) ret = -ENXIO; spin_unlock(&cdev_lock); cdev_put(new); if (ret) return ret; ret = -ENXIO; fops = fops_get(p->ops); if (!fops) goto out_cdev_put; replace_fops(filp, fops); if (filp->f_op->open) { ret = filp->f_op->open(inode, filp); if (ret) goto out_cdev_put; } return 0; out_cdev_put: cdev_put(p); return ret; }

在Linux内核中,使用结构体cdev来描述一个字符设备。

第8行:inode->i_rdev中保存了字符设备的设备编号,

第13行:通过函数kobj_lookup函数便可以找到该设备文件cdev结构体的kobj成员,

第16行:再通过函数container_of便可以得到该字符设备对应的结构体cdev。函数container_of的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址。同时,将cdev结构体记录到文件节点inode中的i_cdev,便于下次打开该文件。

第38-43行:函数chrdev_open最终将该文件结构体file的成员f_op替换成了cdev对应的ops成员,并执行ops结构体中的open函数。

最后,调用上图的fd_install函数,完成文件描述符和文件结构体file的关联,之后我们使用对该文件描述符fd调用read、write函数, 最终都会调用file结构体对应的函数,实际上也就是调用cdev结构体中ops结构体内的相关函数。

总结一下整个过程,当我们使用open函数,打开设备文件时,会根据该设备的文件的设备号找到相应的设备结构体, 从而得到了操作该设备的方法。也就是说如果我们要添加一个新设备的话,我们需要提供一个设备号, 一个设备结构体以及操作该设备的方法(file_operations结构体)。

4.6. 字符设备驱动程序实验¶ 4.6.1. 硬件介绍¶

本节实验使用到 EBF6ULL-PRO 开发板。

4.6.2. 实验代码讲解¶

本章的示例代码目录为:linux_driver/EmbedCharDev/CharDev/

结合前面所有的知识点,首先,字符设备驱动程序是以内核模块的形式存在的,、 因此,使用内核模块的程序框架是毫无疑问的。 紧接着,我们要向系统注册一个新的字符设备,需要这几样东西:字符设备结构体cdev,设备编号devno, 以及最最最重要的操作方式结构体file_operations。

下面,我们开始编写我们自己的字符设备驱动程序。

4.6.2.1. 内核模块框架¶

既然我们的设备程序是以内核模块的方式存在的,那么就需要先写出一个基本的内核框架,见如下所示。

内核模块加载函数(位于../linux_driver/EmbedCharDev/CharDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39#define DEV_NAME "EmbedCharDev" #define DEV_CNT (1) #define BUFF_SIZE 128 //定义字符设备的设备号 static dev_t devno; //定义字符设备结构体chr_dev static struct cdev chr_dev; static int __init chrdev_init(void) { int ret = 0; printk("chrdev init\n"); //第一步 //采用动态分配的方式,获取设备编号,次设备号为0, //设备名称为EmbedCharDev,可通过命令cat /proc/devices查看 //DEV_CNT为1,当前只申请一个设备编号 ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME); if (ret BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_from_user(vbuf, buf, tmp); *ppos += tmp; return tmp; }

当我们的应用程序调用write函数,最终就调用我们的chr_dev_write函数。

第3行:变量p记录了当前文件的读写位置,

第6-9行:如果超过了数据缓冲区的大小(128字节)的话,直接返回0。并且如果要读写的数据个数超过了数据缓冲区剩余的内容的话,则只读取剩余的内容。

第10-11行:使用copy_from_user从用户空间拷贝tmp个字节的数据到数据缓冲区中,同时让文件的读写位置偏移同样的字节数。

chr_dev_read函数(位于../linux_driver/EmbedCharDev/CharDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos) { unsigned long p = *ppos; int ret; int tmp = count ; if (p >= BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_to_user(buf, vbuf+p, tmp); *ppos +=tmp; return tmp; }

同样的,当我们应用程序调用read函数,则会执行chr_dev_read函数的内容。 该函数的实现与chr_dev_write函数类似,区别在于,使用copy_to_user从数据缓冲区拷贝tmp个字节的数据到用户空间中。

4.6.2.3. 简单测试程序¶

下面,我们开始编写应用程序,来读写我们的字符设备,如下所示。

main.c函数(位于../linux_driver/EmbedCharDev/CharDev/main.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25#include #include #include #include char *wbuf = "Hello World\n"; char rbuf[128]; int main(void) { printf("EmbedCharDev test\n"); //打开文件 int fd = open("/dev/chrdev", O_RDWR); //写入数据 write(fd, wbuf, strlen(wbuf)); //写入完毕,关闭文件 close(fd); //打开文件 fd = open("/dev/chrdev", O_RDWR); //读取文件内容 read(fd, rbuf, 128); //打印读取的内容 printf("The content : %s", rbuf); //读取完毕,关闭文件 close(fd); return 0; }

第11行:以可读可写的方式打开我们创建的字符设备驱动

第12-15行:写入数据然后关闭

第17-21行:再次打开设备将数据读取出来

4.6.3. 实验准备¶

获取内核模块源码,将配套代码 linux_driver/EmbedCharDev/charDev 解压到内核代码同级目录。

4.6.3.1. makefile修改说明¶ makefile(位于../linux_driver/EmbedCharDev/CharDev/Makefile)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17KERNEL_DIR=../../ebf_linux_kernel/build_image/build ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- export ARCH CROSS_COMPILE obj-m := chrdev.o out = chrdev_test all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules $(CROSS_COMPILE)gcc -o $(out) main.c .PHONY:clean clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean rm $(out)

Makefile与此前相比,增加了编译测试程序部分。

第1行:该Makefile定义了变量KERNEL_DIR,来保存内核源码的目录。

第3-5行: 指定了工具链并导出环境变量

第7行:变量obj-m保存着需要编译成模块的目标文件名。

第8行:变量out保存着需要编译成测试程序的目标文件名。

第11行:’$(MAKE)modules’实际上是执行Linux顶层Makefile的伪目标modules。通过选项’-C’,可以让make工具跳转到源码目录下读取顶层Makefile。’M=$(CURDIR)’表明返回到当前目录,读取并执行当前目录的Makefile,开始编译内核模块。CURDIR是make的内嵌变量,自动设置为当前目录。

第12行:交叉编译工具链编译测试程序。

4.6.3.2. 编译命令说明¶ make

编译成功后,实验目录下会生成两个名为”chrdev.ko”驱动模块文件和” chrdev_test”测试程序。

4.6.4. 程序运行结果¶

编写Makefile,执行make,生成的chrdev.ko文件和驱动测试程序chrdev_test, 通过nfs网络文件系统或者scp,将文件拷贝到开发板。执行以下命令:

sudo insmod chrdev.ko cat /proc/devices

我们从/proc/devices文件中,可以看到我们注册的字符设备EmbedCharDev的主设备号为244。 注意此设备号下面会用到,大家开发板根据实际情况调整

mknod /dev/chrdev c 244 0

以root权限使用mknod命令来创建一个新的设备chrdev,见下图。

以root权限运行chrdev_test,测试程序,效果见下图。

实际上,我们也可以通过echo或者cat命令,来测试我们的设备驱动程序。

echo "EmbedCharDev test" > /dev/chrdev

如果没有获取su的权限 也可以这样使用

sudo sh -c "echo 'EmbedCharDev test' > /dev/chrdev"

然后,执行 cat /dev/chrdev 可以看到 echo 的内容

当我们不需要该内核模块的时候,我们可以执行以下命令:

sudo rmmod chrdev.ko sudo rm /dev/chrdev

使用命令rmmod,卸载内核模块,并且删除相应的设备文件。

4.7. 一个驱动支持多个设备¶

在Linux内核中,主设备号用于标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备服务。但是, 次设备号表示了同类设备的各个设备。每个设备的功能都是不一样的。如何能够用一个驱动程序去控制各种设备呢? 很明显,首先,我们可以根据次设备号,来区分各种设备;其次,就是前文提到过的file结构体的私有数据成员private_data。 我们可以通过该成员来做文章,不难想到为什么只有open函数和close函数的形参才有file结构体, 因为驱动程序第一个执行的是操作就是open,通过open函数就可以控制我们想要驱动的底层硬件。

4.7.1. 硬件介绍¶

本节实验使用到 EBF6ULL-PRO 开发板上

4.7.2. 实验代码讲解¶ 4.7.2.1. 实现方式一 管理各种的数据缓冲区¶

下面介绍第一种实现方式,将我们的上一节程序改善一下,生成了两个设备,各自管理各自的数据缓冲区。

本章的示例代码目录为:linux_driver/EmbedCharDev/1_SupportMoreDev/

chrdev.c修改部分(位于../linux_driver/EmbedCharDev/1_SupportMoreDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10#define DEV_NAME "EmbedCharDev" #define DEV_CNT (2) (1) #define BUFF_SIZE 128 //定义字符设备的设备号 static dev_t devno; //定义字符设备结构体chr_dev static struct cdev chr_dev; //数据缓冲区 static char vbuf1[BUFF_SIZE]; (2) static char vbuf2[BUFF_SIZE]; (3)

第2行:修改了宏定义DEV_CNT,将原本的个数1改为2,这样的话,我们的驱动程序便可以管理两个设备。

第9-10行:处修改为两个数据缓冲区。

chr_dev_open函数修改(位于../linux_driver/EmbedCharDev/1_SupportMoreDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15static int chr_dev_open(struct inode *inode, struct file *filp) { printk("\nopen\n "); switch (MINOR(inode->i_rdev)) { case 0 : { filp->private_data = vbuf1; break; } case 1 : { filp->private_data = vbuf2; break; } } return 0; }

我们知道inode结构体中,对于设备文件的设备号会被保存到其成员i_rdev中。

第4行:在chr_dev_open函数中,我们使用宏定义MINOR来获取该设备文件的次设备号,使用private_data指向各自的数据缓冲区。

第5-12行:对于次设备号为0的设备,负责管理vbuf1的数据,对于次设备号为1的设备,则用于管理vbuf2的数据,这样就实现了同一个设备驱动,管理多个设备了。

接下来,我们的驱动只需要对private_data进行读写即可。

chr_dev_write函数(位于../linux_driver/EmbedCharDev/1_SupportMoreDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos) { unsigned long p = *ppos; int ret; char *vbuf = filp->private_data; int tmp = count ; if (p > BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_from_user(vbuf, buf, tmp); *ppos += tmp; return tmp; }

可以看到,我们的chr_dev_write函数改动很小,只是增加了第5行的代码,将原先vbuf数据指向了private_data,这样的话, 当我们往次设备号为0的设备写数据时,就会往vbuf1中写入数据。次设备号为1的设备写数据,也是同样的道理。

chr_dev_read函数(位于../linux_driver/EmbedCharDev/1_SupportMoreDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos) { unsigned long p = *ppos; int ret; int tmp = count ; char *vbuf = filp->private_data; if (p >= BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_to_user(buf, vbuf+p, tmp); *ppos +=tmp; return tmp; }

同样的,chr_dev_read函数也只是增加了第6行的代码,将原先的vbuf指向了private_data成员。

4.7.2.2. 实现方式二 i_cdev变量¶

我们回忆一下,我们前面讲到的文件节点inode中的成员i_cdev,为了方便访问设备文件,在打开文件过程中, 将对应的字符设备结构体cdev保存到该变量中,那么我们也可以通过该变量来做文章。

本章的示例代码目录为:linux_driver/EmbedCharDev/2_SupportMoreDev/

定义设备(位于../linux_driver/EmbedCharDev/2_SupportMoreDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9/*虚拟字符设备*/ struct chr_dev { struct cdev dev; char vbuf[BUFF_SIZE]; }; //字符设备1 static struct chr_dev vcdev1; //字符设备2 static struct chr_dev vcdev2;

以上代码中定义了一个新的结构体struct chr_dev,它有两个结构体成员:字符设备结构体dev以及设备对应的数据缓冲区。 使用新的结构体类型struct chr_dev定义两个虚拟设备vcdev1以及vcdev2。

chrdev_init函数(位于../linux_driver/EmbedCharDev/2_SupportMoreDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29static int __init chrdev_init(void) { int ret; printk("4 chrdev init\n"); ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME); if (ret private_data = container_of(inode->i_cdev, struct chr_dev, dev); return 0; } static int chr_dev_release(struct inode *inode, struct file *filp) { printk("release\n"); return 0; }

我们知道inode中的i_cdev成员保存了对应字符设备结构体的地址,但是我们的虚拟设备是把cdev封装起来的一个结构体, 我们要如何能够得到虚拟设备的数据缓冲区呢?为此,Linux提供了一个宏定义container_of,该宏可以根据结构体的某个成员的地址, 来得到该结构体的地址。该宏需要三个参数,分别是代表结构体成员的真实地址,结构体的类型以及结构体成员的名字。 在chr_dev_open函数中,我们需要通过inode的i_cdev成员,来得到对应的虚拟设备结构体,并保存到文件指针filp的私有数据成员中。 假如,我们打开虚拟设备1,那么inode->i_cdev便指向了vcdev1的成员dev,利用container_of宏, 我们就可以得到vcdev1结构体的地址,也就可以操作对应的数据缓冲区了。

chr_dev_write函数(位于../linux_driver/EmbedCharDev/2_SupportMoreDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos) { unsigned long p = *ppos; int ret; //获取文件的私有数据 struct chr_dev *dev = filp->private_data; char *vbuf = dev->vbuf; int tmp = count ; if (p > BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_from_user(vbuf, buf, tmp); *ppos += tmp; return tmp; }

对比第一种方法,实际上只是新增了第6行代码,通过文件指针filp的成员private_data得到相应的虚拟设备。 修改第7行的代码,定义了char类型的指针变量,指向对应设备的数据缓冲区。

chr_dev_read函数(位于../linux_driver/EmbedCharDev/2_SupportMoreDev/chrdev.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos) { unsigned long p = *ppos; int ret; int tmp = count ; //获取文件的私有数据 struct chr_dev *dev = filp->private_data; char *vbuf = dev->vbuf; if (p >= BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_to_user(buf, vbuf+p, tmp); *ppos +=tmp; return tmp; }

读函数,与写函数的改动部分基本一致,这里就只贴出代码,不进行讲解。

4.7.3. 实验准备¶

分别获取两个种方式的内核模块源码,将配套代码 linux_driver/CharDev下 1_SupportMoreDev和2_SupportMoreDev 解压到内核代码同级目录。

4.7.3.1. makefile说明¶

至于Makefile文件,与上一小节的相同,这里便不再罗列出来了。

4.7.3.2. 编译命令说明¶

在实验目录下输入如下命令来编译驱动模块:

make

编译成功后,实验目录下会分别生成驱动模块文件

4.7.4. 程序运行结果¶

通过NFS或者SCP将编译好的驱动模块拷贝到开发板中

下面我们 使用cat以及echo命令,对我们的驱动程序进行测试。

insmod chrdev.ko mknod /dev/chrdev1 c 244 0 mknod /dev/chrdev2 c 244 1

通过以上命令,加载了新的内核模块,同时创建了两个新的字符设备,分 别是/dev/chrdev1和/dev/chrdev2,开始进行读写测试:

echo "hello world" > /dev/chrdev1 或者 sudo sh -c "echo 'hello world' > /dev/chrdev1" echo "123456" > /dev/chrdev2 或者 sudo sh -c "echo '123456' > /dev/chrdev2" cat /dev/chrdev1 cat /dev/chrdev2

可以看到设备chrdev1中保存了字符串“hello world”,而设备chrdev2中保存了字符串“123456”。 只需要几行代码,就可以实现一个驱动程序,控制多个设备。

总结一下,一个驱动支持多个设备的具体实现方式的重点在于如何运用file的私有数据成员。 第一种方法是通过将各自的数据缓冲区放到该成员中,在读写函数的时候,直接就可以对相应的数据缓冲区进行操作; 第二种方法则是通过将我们的数据缓冲区和字符设备结构体封装到一起,由于文件结构体inode的成员i_cdev保存了对应字符设备结构体, 使用container_of宏便可以获得封装后的结构体的地址,进而得到相应的数据缓冲区。

到这里,字符设备驱动就已经讲解完毕了。如果你发现自己有好多不理解的地方,学完本章之后,建议重新梳理一下整个过程, 有助于加深对整个字符设备驱动框架的理解。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3