字符设备驱动(一)
一 linux设备概述
linux设备可分为三大类,字符设备,块设备以及网络设备,在本文我们主要讲的是字符设备,那么什么是字符设备呢,字符(char)设备是个像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。而字符设备驱动程序则至少要实现open,close,read,write这样的系统调用。字符设备是以字节为最小访问单位,而其他设备如块设备则是以块(通常是512字节)为最小传输单位的设备,常见的字符设备有鼠标,键盘,串口,控制台和LED设备等等。常见的块设备包括硬盘,磁盘,U盘和SD卡。
二 字符设备驱动程序基础
2.1 主设备号和次设备号
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。简单点来说就是对于主设备号,字符设备文件对应一个数字,字符设备驱动也对应一个数字,两者相同时就会相互对应。对于次设备号,假如一个串口驱动程序对应硬件上多个串口设备,该怎么去分配使之一一对应,这就是次设备号的作用。
Linux内核中使用dev_t类型来定义设备号,dev_t这种类型其实质为32位的unsigned int,其中高12位为主设备号,低20位为次设备号。
以下三个宏是和设备号相关的:
dev_t dev = MKDEV(int 主设备号,int 次设备号) //知道主设备号和次设备号组合成dev_t类型
主设备号= MAJOR(dev_t dev) // 从dev_t中分解出主设备号
次设备号=MINOR(dev_t dev) // 从dev_t中分解出次设备号
2.2 分配设备号
设备号的分配包括静态分配和动态分配。
静态分配
register_chrdev_region(dev_t from, unsigned count, const char *name);向内核申请使用。缺点:如果申请使用的设备号已经被内核中的其他驱动使用了,则申请失败。
动态分配
使用alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);由内核分配一个可用的主设备号。优点:因为内核知道哪些号已经被使用了,所以不会导致分配到已经被使用的号。
分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
2.3 注销设备号
不论使用何种方法分配设备号,都应该在驱动退出时,使用unregister_chrdev_region(dev_t from, unsigned count);函数释放这些设备号。
三 一些重要的数据结构
大部分基本的驱动程序操作涉及及到三个重要的内核数据结构,分别是file_operations、file和inode。
file_operations
file_operations是一个函数指针的集合,设备所能提供的功能大部分都由此结构提供。这些操作也是设备相关的系统调用的具体实现。file_operation结构在内核源码中可以找到,由于此结构设备方法较多(设备方法即为file_operation中的操作实现),这里就列出驱动程序中经常使用的五个设备方法:
struct file_operations
{
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
}
llseek是实现定位文件指针操作,read实现读操作,write实现写操作,open实现打开文件操作,release实现关闭文件操作。
struct file
struct file代表一个打开的文件描述符,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。
重要成员:
const struct file_operations *f_op; //该操作是定义文件关联的操作的。内核在执行open时对这个指针赋值
off_t f_pos; //该文件读写位置
void *private_data;//该成员是系统调用时保存状态信息非常有用的资源
struct inode
每一个存在于文件系统里面的文件都会关联一个inode 结构,该结构主要用来记录文件物理上的信息。因此, 它和代表打开文件的file结构是不同的。一个文件没有被打开时不会关联file结构,但是却会关联一个inode 结构。
重要成员:
dev_t i_rdev:设备号
四 字符设备驱动模型框架
上面说了那么多,现在我们通过一个实际的代码去理解理解字符设备驱动代码的步骤,在编写每个程序的时候,我们需要了解字符设备驱动程序的模型框架是怎么搭建的。
这里就直接把程序框架贴出来了,我们只需要根据这个框架进行设计即可。
驱动初始化:
在任何一种驱动模型中,设备都会用内核中的一种结构来描述。我们的字符设备在内核中使用struct
cdev来描述。通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。
1 分配
cdev变量的定义可以采用静态和动态两种方法
静态分配:
struct cdev mdev;
动态分配:
struct cdev *pdev = cdev_alloc();
2 初始化
初始化通常使用cdev_init函数完成
cdev_init(struct cdev *cdev, const struct file_operations *fops)
3 注册
字符设备的注册使用cdev_add函数来完成。
cdev_add(struct cdev *p, dev_t dev, unsigned count)
实现设备操作
分析file_operations(设备方法就是file_operation 里的操作),具体分析我放在下一部分,这里谈谈这些操作有什么用。
驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。
如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。
五 代码实现
1 驱动的初始化
驱动的初始化又包括分配cdev,初始化cdev,注册cdev,实现硬件操作
struct cdev mdev; //分配cdev包括静态分配和动态分配,这里采用静态分配
dev_t devno;
int memdev_init()
{
cdev_init(&mdev, &memfops);
/*初始化cdev:函数原型为void cdev_init(struct cdev *cdev, const struct file_operations *fops),由于第一个参数是分配的cdev的指针,所以是静态分配过的mdev的地址&mdev。第二个是file_operation,即为系统调用和内核的接口。 */
alloc_chrdev_region(&devno,0,2,'memdev');
/*动态分配设备号:原型是int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name) ,第一个参数是设备号的地址,第二个参数动态分配设备号起始的次设备号设置为0,第三参数是有多少个设备,这里设置为两个,第四个参数设备名称设置为memdev*/
cdev_add(&mdev,devno, 2);
/*注册cdev:原型是int cdev_add(struct cdev *p, dev_t dev, unsigned count) ,第一个参数取mdev的地址,第二个参数是设备号,需要定义一个dev的变量名devno,第三个参数是根据alloc_chrdev_region中第三个参数设置的,那里填多少,这里就填多少。 */
return 0;
}
同时定义file_operation结构体变量memfops
static const struct file_operations memfops=
{
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_close,
};
对五个我们设计的设备方法名称进行赋值。这样memfops变量就已经完成了,memfops传给cdev_init第二个参数,由于参数是指针这里传的是memfops的地址。
到这里驱动的初始化工作已经完成了。
2 实现设备操作
这里实现五个设备方法。第一个首先实现mem_open这个设备方法,
在程序开始的时候定义两个寄存器数组;
int dev1_register[5];
int dev2_register[5];
int mem_open(struct inode *node, struct file *filp)
{
int num= node->i_rdev; //提取设备号
if (num==0)
filp->private_data=dev1_register; //如果num等于0,则是第一个设备,把dev1_register的首地址赋值给filp中的private_data
else
if (num==1)
filp->private_data=dev2_register; //如果num等于1,则是第一个设备,把dev2_register的首地址赋值给filp中的private_data
else
return -ENODEV; //无效的次设备号
return 0;
}
这个设备方法有两个参数,第一个是用来获取设备号,第二个是将这个设备号保存在file这个结构体中,用来给其他设备方法使用,因为已经设置了两个设备,所以用MINOR来获取次设备号,再根据设备号将数组的基地址赋值给filp中的private_data。
注:struvct file *filp中filp>-private_data 是专门留给用户使用的
第二个是实现mem_close设备方法,
int mem_close(struct inode *node, struct file *filp)
{
return 0;
}
这个设备方法因为暂时还没用到硬件,所以这里直接返回0即可。
第三个是实现mem_read设备方法
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
int *register_addr = filp->private_data; /*获取设备的寄存器基地址*/
/*判断读位置是否有效*/
if (p >= 5*sizeof(int))
return 0;
if (count > 5*sizeof(int) - p)
count = 5*sizeof(int) - p;
/*读数据到用户空间*/
if (copy_to_user(buf, register_addr+p, count))
ret = -EFAULT;
else
{
*ppos += count;
ret = count;
}
return ret;
}
首先第一步应该是将设备的基地址从fstruct file中private_data中取出来赋值给*register_addr,这样read函数就知道该从哪个设备中读取数据了。把读取到的数据量赋值给count,把偏移量*ppos赋值给p,接下来把提取到的数据通copy_to_user返回给应用程序,原型为 copy_to_user(to, from, n),第一个参数是将数据复制到的某个位置去,这里是buf,第二个参数是数据从哪里提取,这里从register_addr中取,这里还需要加一个偏移量ppos,最后一个参数是传递多大的数据,这里就是count。
这里还需要将struct file中*ppos( 文件读写指针)加上相应的偏移,偏移就是数据的大小。最后再返回数据的大小。
第四个是实现mem_write设备方法
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
int *register_addr = filp->private_data; /*获取设备的寄存器地址*/
/*分析和获取有效的写长度*/
if (p >= 5*sizeof(int))
return 0;
if (count > 5*sizeof(int) - p)
count = 5*sizeof(int) - p;
/*从用户空间写入数据*/
if (copy_from_user(register_addr + p, buf, count))
ret = -EFAULT;
else
{
*ppos += count;
ret = count;
}
return ret;
}
这个设备方法和read的内容基本一样,这里只要将read复制过来,需要修改的是copy_from_user
原型为copy_from_user(to, from, n),参数只要和read中的参数对调就行了。
第五个 也是最后一个是修改lseek读写位置,即文件读写位置
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case SEEK_SET:
newpos = offset;
break;
case SEEK_CUR:
newpos = filp->f_pos + offset;
break;
case SEEK_END:
newpos = 5*sizeof(int)-1 + offset;
break;
default:
return -EINVAL;
}
if ((newpos<0) || (newpos>5*sizeof(int)))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
这个函数类似于系统调用里的lseek函数
读写位置是通过偏移去修改的,而这些变量需要先存放在struct file中,然后通过switch语句修改文件位置指针,switch中分别是文件开头,文件中间位置和文件末尾三种情况下的指针偏移。
3 完成注销
int memdev_exit()
{
cdev_del(&mdev);
unregister_chrdev_region(devno,2);
}
注销设备cdev_del
原型为cdev_del(struct cdev *p)
这里的参数非常简单,填入字符设备,也就是mdev的地址即可
注销设备号
unregister_chrdev_region函数释放这些设备号。
unregister_chrdev_region(dev_t from, unsigned count)
第一个参数表示要注销的设备号,第二个参数表示要注销几个设备号
到此,字符设备驱动的模型已经结束,我们大概了解到如何根据这个框架去设计一个我们需要的字符设备驱动程序。但是仅仅知道如何去设计,而不知道底层驱动和上层应用程序是如何打通的还是远远不够的,下节就详细讲一下这内核驱动和应用程序之间的是如何联系在一起的。
上一篇: 图片内的超链接
下一篇: 安卓扫码Zxing旋转90及270度