Linux下的虚拟串口驱动(一)
欢迎转载,转载请注明出处。
前言
最近准备在Linux下,实现虚拟串口驱动;但因为毕业后,一直从事的是裸机驱动开发,所以Linux下的设备驱动,就慢慢忘记了;为了实现这一功能,在网上也查找了很多资料,但大多只是讲解理论,或者直接贴代码;对于没接触过Linux驱动或者初学者来说,理解起来比较吃力;所以小弟我,打算将这几天整理的资料和自己的理解,结合相关代码,分享出自己的想法,希望能给有此需求的人,贡献微薄之力。
裸机、设备驱动
前面提到裸机驱动和设备驱动,在这里简单说明一下两者的区别。设备驱动,顾名思义就是“驱使设备行动”,这里的裸机驱动也应该属于设备驱动的范畴,但因为笔者的习惯,将无操作系统下的驱动,称为裸机驱动,所以这里做了区分。在没有操作系统的情况下,我们可以根据硬件设备的特点自行定义接口,如对串口定义Drv_SerialSend()、Drv_SerialRecv(),对RTC(实时时钟)定义Drv_TimeSet()、Drv_TimeGet()等。但在有操作系统时,驱动的架构就应该由操作系统来定义,我们只有按照相应的架构设计驱动,这样,驱动才能更好的整合入操作系统内核。
从图片中我们可以看到,有了操作系统后,不仅没让驱动变得简单,反而更麻烦了。为什么不能像裸机驱动那样,需要什么操作就直接调用哪个函数接口,例如将数据写入Flash时,就直接调用Drv_FlashWrite(),需要往串口发送数据时,就直接调用Drv_SerialSend(),这样不是更直接,更简单吗;听起来好像挺有道理的,比起通过系统调用接口,文件系统,间接的去访问驱动接口,直接调用驱动函数,好像更高效;但存在即是合理的,设计师们设计这样一套驱动架构,肯定是有理由的。我们先来看一张图。
我们可以看到,在drivers/serial目录下,列出了Linux2.6内核支持的串口设备,只是三星公司的s3c系列就分了好几种,区分的原因肯定是三星公司在处理器的设计结构,或者串口设备的架构,驱动电路,访问方式等做了修改。
试想一下,如果三星公司为了区分这些设备,在驱动命名上做了区分,分别以处理器类型来命名相应的驱动接口,如Drv_S3c2440SerialSend();Drv_S3c2410SerialSend();Drv_S3c6410SerialSend();这时上层应用工程师,在S2C2440这款处理器上,以直接访问驱动接口的形式,写了一个串口应用程序;某天因为业务需要,要求在S3C6410的处理器上实现相同的功能,本来以为只是个简单的移植工作,后来发现底层驱动函数名字全变了,那他还需要把所有的Drv_S3c2440SerialSend()函数,改成Drv_S3c6410SerialSend(),所有相关的初始化,发送,接收等操作都要修改。这将是多么头疼的一件事。
再举个例子,如果我们的设备搭载的多个不同的Flash存储器,这时候一个简单的Drv_FlashWrite()很明显就不够了,不同的Flash存储器,操作方式不同,为了正确的使用这些存储器,底层驱动就需要作出区分,提供出不同的函数接口,并且上层应用必须理解这些不同的驱动接口,这样才能知道分别操作的是哪一类Flash存储器。
与之相比,有操作系统时,在Linux下,上层应用只需要通过read()、write()这两个函数,就可访问任何设备,即使是不同的处理器或者不同的设备,只要通过传入的参数(文件名/设备名)就可以完成对不同设备的访问,上层应用工程师,完全不用考虑底层驱动接口是什么样的,给他们的印象就是,不管设备上搭载了多少种不同的Flash存储器,一个read()函数,就可访问所有存储器;而且即使更换了处理器,在S2C2440这款处理器上实现的串口应用程序,在S3C6410的处理器上也可以直接使用,因为操作系统和文件系统给上层提供了统一的接口,所以上层应用不用去关心,底层做了哪些修改。
其实操作系统就是通过给设备驱动制造麻烦,进而给上层应用提供便利。驱动按照操作系统提供的驱动框架和统一接口来设计,这样上层应用就可以通过操作系统提供的统一的系统调用,来访问驱动。了解过Linux的都知道,Linux下一切设备皆文件,我们可以通过read()、write()这两个函数,去访问任何设备,这就为上层应用提供了极大地便利。
Linux下的驱动架构
前面说到,有操作系统时,驱动应该按照操作系统给的驱动架构来设计。那么在Linux下的驱动架构应该是什么样的呢?首先结合我们前面说的,Linux的上层用户空间,可以通过操作文件的形式来访问设备;那这里我们应该有这样的疑问:为什么访问文件时,就可以访问设备?文件和设备是如何绑定的?上层应用同样调用read()、write()两个函数,操作系统如何知道具体是哪个驱动的读、写函数?这些问题,我都会在后面一一阐述。
在Linux下,设备可以分为三类:字符设备、块设备和网络设备
字符设备:一种能像字节流一样进行串行访问的设备,对设备的存取只能按顺序按字节存取,不能随机访问;并且字符设备没有请求缓存区,所以必须按顺序执行所有的访问请求,例如键盘,在当前输入未完成前,无法响应下一个输入。
块设备:具有请求缓冲区,可以从任意位置读取任意长度,即支持随机访问,例如硬盘,我们可以随机访问任意扇区,任意长度。
网络设备:网络设备是面向数据报文的,字符设备是面向字符流的,网络设备和字符设备一样,不支持随机访问,也没有缓冲区,交换机,路由器等都是网络设备,它们都是以数据报的形式进行数据处理。
三类不同设备,在驱动架构上,又略微有所区别,下面以字符设备为例,阐述一下Linux下的驱动架构。
图中的ssize_t (read) (struct file , char __user , size_t, loff_t );
ssize_t (write) (struct file , const char __user , size_t, loff_t )等函数就类似于前面提到的Drv_FlashRead(),Drv_FlashWrite()等函数,Linux内核把这些函数统一“映射”到file_operation这个结构体中,这样通过操作系统和文件系统给上层统一成read(),write()这些系统调用。所以在有操作系统时,底层驱动还是要实现Drv_FlashRead(),Drv_FlashWrite()这样的函数,只不过是多了一些操作。
struct cdev {
struct kobject kobj; /* 内嵌的kobject对象 */
struct module *owner; /* 所属模块 */
const struct file_operations *ops; /* 文件操作结构体 */
struct list_head list; /* 内核链表 */
dev_t dev; /* 设备号 */
unsigned int count;
};
在Linux2.6的内核中,使用cdev这样一个结构体来管理字符设备。这里我先阐述一下,dev_t dev和const struct file_oprations *ops这两个成员,其他的后面再做解释。
从图中我们可以看到,在时间信息前,有两列数字;其中靠左的一列代表主设备号,靠右的一列代表次设备号;仔细看这张图,会发现“scd0”的次设备号都是4,“fd0u……”的主设备号相同,次设备号不同,所以这里可以做个简单的解释就是,Linux内核用主设备号来判定某种设备的驱动,次设备号标识具体的设备;或者说主设备号标识某一特定的驱动程序,次设备号表示使用该驱动程序的各设备。
#if defined(DJGPP) || defined(__CYGWIN32__)
#ifdef KERNEL
typedef unsigned long u_long;
typedef unsigned int u_int;
typedef unsigned short u_short;
typedef u_long ino_t;
typedef u_long dev_t;
typedef void * caddr_t;
#ifdef DOS
typedef unsigned __int64 u_quad_t;
#else
typedef unsigned long long u_quad_t;
#endif
在Linux里,dev_t被定义成unsigned long,为32位,其中12位主设备号,20位次设备号,在我们编写驱动时,会将设备名(对应上层文件名),与设备号“绑定”,这样当上层应用对设备(文件)进行操作时,内核就可以根据设备号,知道该调用哪一驱动。
/*
* NOTE:
* read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
* can be called without the big kernel lock held in all filesystems.
*/
struct file_operations {
struct module *owner;
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 *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
从这个结构体中,可以看到llseek()、read()、write()等函数,这就与我们在用户空间经常使用到的系统调用*llseek()、read()、write(),或者fseek()、fread()、fwrite()等库函数*相对应(实际库函数最终也会通过系统调用访问设备)。
到此,我们可以了解到,Linux内核里,驱动的主要工作有两个部分,一是为驱动分配主次设备号,将设备名(文件名)与主次设备号绑定;二是填充file_operations结构体,即实现结构体中的相应函数实体。这与裸机驱动中只是实现相应接口函数,区别还是挺大的。
暂时就写到这吧!明天再通过实例,讲解驱动实现过程。
下一篇: 算法基础2:求abc的全排列