设备驱动程序实质上是一组完成不同任务的函数的集合,通过这些函数所提供的功能可以使得从设备接受输入和将输出送到设备就象读写文件一样,因此,Linux 中的每一个设备都具有文件的外在特征,都能使用open () ,close () ,read () ,write () 等系统调用.
系统调用是操作系统内核 和应用程序之间的接口,设备驱动程序硬件之间的接口.设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件, 应用程序可以象操作普通文件一样对硬件设备进行操作.设备驱动程序是内核的一部分,它完成以下的功能:
1.对设备初始化和释放.
2.把数据从内核传送到硬件和从硬件读取数据.
3.读取应用程序传送给设备文件的数据和回送应用程序请求的数据.
4.检测和处理设备出现的错误.
在ARM平台上开发嵌入式Linux的设备驱动程序与在其他平台上开发是一样的。总的来说,实现一个嵌入式Linux设备驱动的大致流程如下:
1. 查看原理图,理解设备的工作原理
2. 定义主设备号
3. 在驱动程序中实现驱动的初始化。如果驱动程序采用模块的方式,则要实现模块初始化。
4. 设计所要实现的文件操作,定义file_operations结构。
5. 实现中断服务(中断并不是每个设备驱动所必须的)
6. 编译该驱动程序到内核中,或者用insmod命令加载
7. 测试该设备
字符设备驱动程序是linux系统最基本、最常用的驱动程序结构。可以说,只要不挂载文件系统的设备,都可以用字符设备去描述。在本节将以nvram字符设备,详细讲述linux字符驱动结构,一个完整的字符驱动构成主要是字符的初始化与卸载程序,提供给用户的文件接口程序,还有最主要的功能函数。
nvram是一种非易失性随机访问存储器。编写驱动的第一步是定义驱动将要提供给用户程序的能力,nvram驱动最终实现的功能可以使用户自由的访问存储器,并且可以随机的读写存储器里面的内容,在驱动里把nvram地址映射到用户可以访问的位置,提供给用户读写和访问位置的接口,就可以实现这个设备的基本功能。
在系统内部,I/O设备的存/取通过一组固定的入口点来进行,这组入口点是由每个设备的设备驱动程序提供的。具体到Linux系统,设备驱动程序所提供的这组入口点由一个文件操作结构来向系统进行说明。file_operations结构定义于linux/fs.h文件中,随着内核的不断升级,file_operations结构也越来越大,不同版本的内核会稍有不同。
structfile_operations{
struct module *owner;
// 指向拥有该结构的模块的指针,避免正在操作时被卸载,初始化为THIS_MODULES
loff_t (*llseek) (struct file *, loff_t, int);
// llseek用来修改文件当前的读写位置,返回新位置
// loff_t为一个"长偏移量"。当此函数指针为空,seek调用将会以不可预期的方式修改file结构中的位置计数器。
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
// 从设备中同步读取数据。读取成功返回读取的字节数。设置为NULL,调用时返回-EINVAL
ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
// 初始化一个异步的读取操作,为NULL时全部通过read处理
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
// 向设备发送数据。
ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
// 初始化一个异步的写入操作。
int (*readdir) (struct file *, void *, filldir_t);
// 仅用于读取目录,对于设备文件,该字段为 NULL
unsigned int (*poll) (struct file *, struct poll_table_struct *);
// 返回一个位掩码,用来指出非阻塞的读取或写入是否可能。
// 将pool定义为 NULL,设备会被认为即可读也可写。
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
// 提供一种执行设备特殊命令的方法。不设置入口点,返回-ENOTTY
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
// 不使用BLK的文件系统,将使用此种函数指针代替ioctl
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
// 在64位系统上,32位的ioctl调用,将使用此函数指针代替
int (*mmap) (struct file *, struct vm_area_struct *);
// 用于请求将设备内存映射到进程地址空间。如果无此方法,将访问-ENODEV。
int (*open) (struct inode *, struct file *);
// 如果为空,设备的打开操作永远成功,但系统不会通知驱动程序
// 由VFS调用,当VFS打开一个文件,即建立了一个新的"struct file",之后调用open方法分配文件结构。open属于struct inode_operations。
int (*flush) (struct file *);
// 发生在进程关闭设备文件描述符副本,执行并等待,若设置为NULL,内核将忽略用户应用程序的请求。
int (*release) (struct inode *, struct file *);
// file结构释放时,将调用此指针函数,release与open相同可设置为NULL
int (*fsync) (struct file *, struct dentry *, int datasync);
// 刷新待处理的数据,如果驱动程序没有实现,fsync调用将返回-EINVAL
int (*aio_fsync) (struct kiocb *, int datasync);
// 异步fsync
int (*fasync) (int, struct file *, int);
// 通知设备FASYNC标志发生变化,如果设备不支持异步通知,该字段可以为NULL
int (*lock) (struct file *, int, struct file_lock *);
// 实现文件锁,设备驱动常不去实现此lock
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
// readv和writev 分散/聚集型的读写操作,实现进行涉及多个内存区域的单次读或写操作。
ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
// 实现sendfile调用的读取部分,将数据从一个文件描述符移到另一个,设备驱动通常将其设置为 NULL
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
// 实现sendfile调用的另一部分,内核调用将其数据发送到对应文件,每次一个数据页,设备驱动通常将其设置为NULL
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
// 在进程地址空间找到一个合适的位置,以便将底层设备中的内存段映射到该位置。大部分驱动可将其设置为NULL
int (*check_flags)(int);
// 允许模块检查传递给fcntl(F_SETEL...)调用的标志
int (*dir_notify)(struct file *filp, unsigned long arg);
// 应用程序使用fcntl来请求目录改变通知时,调用该方法。仅对文件系统有效,驱动程序不必实现。
int (*flock) (struct file *, int, struct file_lock *);
// 实现文件锁
};
在用户自己的驱动程序中,首先要根据驱动程序的功能,完成file_operations结构中函数实现。不需要的函数接口可以直接在file_operations结构中初始化为NULL。file_operations变量会在驱动程序初始化时,注册到系统内部。当操作系统对设备进行操作时,会调用驱动程序注册的file_operations结构中的函数指针。
在nvram驱动中定义了几个常用的文件的结构,如下所示:
static struct file_operations nvram_fops = {
.owner = THIS_MODULE,
.llseek = nvram_llseek,
.read = read_nvram,
.write = write_nvram,
};
nvram_fops为包含基本函数入口点的结构体,类型为file_operations。
.owner= THIS_MODULE根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在
.llseek、.read 、.write都是驱动提供给用户的一个接口函数,它们后面所接的都是定义实现功能的子函数,llseek()为用户指定访问存储器的地址,当调用此功能时实际上调用了驱动程序中的loff_t nvram_llseek()函数:
static loff_t nvram_llseek(struct file *file, loff_t offset, int origin)
参数file 是指向这一设备的文件结构的指针,offset为指定的需要访问的偏移地址,origin为设置偏移地址在字符设备中的开始位置。
当对设备特殊文件进行read() 系统调用时,将调用驱动程序read_nvram () 函数:
static ssize_t read_nvram(struct file *file, char __user *buf, size_t count, loff_t *ppos)
file 参数定义同上,参数buf 是指向用户空间缓冲区的指针,由用户进程给出,count 为用户进程要求读取的字节数,也由用户给出.read() 函数的功能就是从硬设备或内核内存中读取或复制count 个字节到buf 指定的缓冲区中,poss为llseek()传递过来的具体的访问位置。
当设备特殊文件进行write ( ) 系统调用时,将调用驱动程序的write_nvram ( ) 函数:
static ssize_t write_nvram(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
参数说明同上。write () 的功能是将参数buf 指定的缓冲区中的count 个字节内容复制到硬件或内核内存中。
module_init(nvram_init);
module_exit(nvram_exit);
MODULE_ALIAS("nvram");
MODULE_DESCRIPTION("nvram Driver For EM104-MINI2410");
MODULE_AUTHOR("jjj.
MODULE_LICENSE("GPL");
如上所示,你的驱动程序中需要包括以上的初始化的信息。
你的模块确实应当指定它的代码使用哪个许可。 做到这一点只需包含一行
MODULE_LICENSE("GPL");
内核认识的特定许可有, "GPL"( 适用 GNU 通用公共许可的任何版本 ), "GPL v2"( 只适用 GPL 版本 2 ), "GPL and additional rights", "Dual BSD/GPL", "Dual MPL/GPL", 和 "Proprietary". 除非你的模块明确标识是在内核认识的一个自由许可下, 否则就假定它是私有的, 内核在模块加载时被"弄污浊"了。
MODULE_AUTHOR("jjj.
MODULE_DESCRIPION("nvram Driver For EM104-MINI2410") 一个人可读的关于模块做什么的声明 。
MODULE_ALIAS("nvram")定义了模块的一个别名。
module_init(nvram_init)为实现对驱动程序的初始化,其中nvram_init()为所需要调用的初始化函数,我们定义的初始化函数如下所示:
static int __init nvram_init(void)
{
unsigned int bswcon = inl((unsigned int)S3C2410_BWSCON);
bswcon = (bswcon & 0xFFFFFFCF) | 0x00000000;
outl(bswcon,(unsigned int)S3C2410_BWSCON);
nvram_remap_address = (u32)ioremap(0x08000000, 0x20000);
if(!request_region(nvram_remap_address, 0x20000, "nvram"))
{
printk("nvram request region fail. /n");
return -ENODEV;
}
printk("EM104-mini2410 non-volatile memory driver/n");
return misc_register(&nvram_dev);
}
初始化函数应当声明成静态的, 因为它们不会在特定文件之外可见; 没有硬性规定这个, 然而, 因为没有函数能输出给内核其他部分, 除非明确请求。 声明中的 __init 标志可能看起来有点怪; 它是一个给内核的暗示, 给定的函数只是在初始化使用. 模块加载者在模块加载后会丢掉这个初始化函数, 使它的内存可做其他用途。
使用 moudle_init 是强制的。 这个宏定义增加了特别的段到模块目标代码中, 表明在哪里找到模块的初始化函数。 没有这个定义,你的初始化函数不会被调用。
在初始化函数中,ioremap(0x08000000, 0x20000)主要是把nvram的物理地址映射到一段虚拟地址中,其中0x08000000为nvram存储器的物理起始地址,0x20000为nvram存储器的大小,nvram_remap_address为转换后的内存可以访问虚拟地址。
request_region()来为nvram分配ioports,当通过此设定后就可以进行inb( ),outb( )来访问所申请的端口也就是对nvram进行相应的读写。
misc_register(&nvram_dev)实现了misc_register()用主编号10调用 register_chrdev(),设备名称和函数表指针通过miscdevice数据结构获得。同样,miscdevice 数据结构还保存设备驱动程序所使用的次要号码。miscdevice 数据结构为我们定义的nvram_dev的结构,如下所示:
static struct miscdevice nvram_dev = {
nvram_MINOR,
"nvram",
&nvram_fops
};
nvram_MINOR为定义的次设备号,其他类型设备驱动程序采用次要号码区分设备。
Nvram为注册的设备名。
&nvram_fops为我们定义的接口的结构,硬件接口函数在设备驱动器内即被静态定义,当设备注册时,由内核通过传递给操作系统的文档操作函数指针获得。
当然我们也可以用register_chrdev()来直接注册字符设备,
int register_chrdev(unsigned int major,const char * name,struct file_operations *fops)
当注册字符设备时传递的参数主要是设备号,名字,和接口函数。
当然卸载硬件则是一个相反的过程了,我们的卸载函数如下:
static void __exit nvram_exit(void)
{
if(nvram_remap_address)
release_region(nvram_remap_address, 0x20000);
misc_deregister(&nvram_dev);
}
释放的过程就改为先释放申请的端口,然后注销注册的设备。
nvram为了实现可以随机访问的功能主要提供了三个可供用户间接调用的驱动程序,主要有:
读: read_nvram( )
写: write_nvram()
定位: nvram_llseek()
我们对设备的控制都是通过对这些函数的应用而完成的。在解释以上的三个函数之前我们先来看一下定义的子函数是为了更好的对驱动进行包装的。
static unsigned char read_byte_nvram(int addr)
{
return inb(nvram_remap_address + addr + 1);
}
static void write_byte_nvram(unsigned char val, int addr)
{
outb(val, nvram_remap_address + addr + 1);
}
以上两个子函数实现了对nvram进行简单的单字节的读写,这也是基于前面通过request_region()函数所申请的端口,然后就可以像简单的io端口一样的访问nvram存储器了,以上的函数仅仅是对存储器进行字节读写访问操作,在用户所用的读写函数中主要是包装了上面的子函数,进行复杂的读写操作。
对于对存储器位置的定位,使用了llseek作为函数的接口,其中使用的函数如下:
static loff_t nvram_llseek(struct file *file, loff_t offset, int origin)
{
lock_kernel();
switch (origin) {
case 1: // 当前位置向后偏移offset个字节
offset += file->f_pos;
break;
case 2: // 文件结尾向后偏移offset个字节
offset += 0x20000 - 1;
break;
}
if (offset < 0) {
unlock_kernel();
return -EINVAL;
}
file->f_pos = offset; // 刷新文件当前位置记录
unlock_kernel();
return file->f_pos;
}
当用户调用此功能后,会返回一个存储器地址的值,这样在接下来的读写控制中就可以根据此处制定的地址进行访问控制。
对于对存储器位置的读操作,使用了read() 作为函数的接口,其中使用的函数如下:
static ssize_t read_nvram(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
unsigned int i;
char __user *p = buf;
if (!access_ok(VERIFY_WRITE, buf, count))
return -EFAULT;
if (*ppos >= 0x20000 - 1) //检查偏移值是否超出范围
return 0;
for (i = *ppos; count > 0 && i < 0x20000 - 1; ++i, ++p, --count)
if (__put_user(read_byte_nvram(i), p))
return -EFAULT;
*ppos = i;
return p - buf;
}
access_ok()的目的主要是判断地址有没有超出许可的范围,也就是说进行权限的判断,所以在__put_user的开头总是要调用access_ok的。
__put_user()的作用主要是把通过read_byte_nvram(i)从nvram读出的数据传给用户区。
对于对存储器位置的写操作,使用了write() 作为函数的接口,其中使用的函数如下:
static ssize_t write_nvram(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
unsigned int i;
const char __user *p = buf;
char c;
if (!access_ok(VERIFY_READ, buf, count))
return -EFAULT;
if (*ppos >= 0x20000 - 1)
return 0;
for (i = *ppos; count > 0 && i < 0x20000 - 1; ++i, ++p, --count) {
if (__get_user(c, p))
return -EFAULT;
write_byte_nvram(c, i);
}
过程和读操作相反,不过具体调用函数的含义如上所述。