Linux 2.6 内核下字符设备(Character Device)驱动编写概述
做人要厚道,转载请注明。有人摘录我BLOG中的话当作自己说的。我认为只要能找出出处的摘录,都会注明来源,以方便阅读的人做进一步的搜索。
花了4天的时间基本整明白了怎么写一个字符设备的驱动,呵呵,我也不知道原理,紧紧是从网上找到了很多文章,加以综合,搞出一个不明原理的HOWTO,趁我的大脑还没有变成浆糊,把这些写出来。内核移植和文件系统构建部分先暂缓。
向大家推荐一个网站,,上面的版主很热心,而且之前的文章很多,可以给大家很大的帮助。
什么是字符设备?我也搞不清楚,哈哈,快要毕业了,速成的结果。目前我弄明白的是:字符设备是以字节为单位来读写的,与字符设备相对应的,块设备是以块为单位来读写的。例如我在总线上扩展的FPGA,它的控制字映射到总线上,每次读写一个字(16位)。
在Linux下和无操作系统情况下,对总线上地址的访问是不同的,Linux提供的内存虚拟内存机制使用户程序无法直接接触到物理内存——这就需要驱动程 序这个桥梁。我们先从用户的角度出发,看看怎么使用设备。做人要厚道,下面这段文字来自友善之臂的文档(这个文档可以在他们的主页上下到,http: //)
Linux操作系统将所有的设备(而不仅是存储器里的文件)全部都看成文件,都纳入文件系统的范畴,都通过文件的操作界面进行操作。这意味着:
每一个设备都至少由文件系统的一个文件代表,因而都有一个“文件名”。每个这样的“设备文件”都唯一地确定了系统中地一项设备。应用程序通过设备地文件寻找访问具体地设备,而设备则象普通文件一样受到文件系统访问权限控制机制地保护。
应用程序通常可以通过系统调用open()“打开”这个设备文件,建立起与目标设备的连接。代表着该设备的文件节点中记载着建立这种连接所需的信息。对于执行该应用程序的进程而言,建立起的连接就表现为一个已经打开的文件。
打开了代表着目标设备的文件,即建立起与设备的连接后,就可以通过read()、write()、ioctl()等常规的文件操作对目标设备进行操作。
目前我的理解是驱动程序所要做的,是将硬件设备(通常是物理地址)与文件操作相关联。当然,我觉得这句话不太全面,以后我学明白原理以后再回来改,FIXME。
驱动程序需要完成的工作有:初始化设备、管理设备、提供文件读写操作的接口、处理设备出现的错误等。
那么驱动程序怎么才能被内核使用呢?或者说怎么样才能把驱动程序加入我的系统中呢?有两种方式:一是把驱动程序直接编译进内核;二是使用模块加载的方式。 我采用的是第二种。第一种的细节问题我没有怎么研究,大致上就是写好驱动程序之后,放到内核代码的目录里,修改Makefile文件,与内核一同编译。可 以参考《Linux 字符设备驱动程序的设计》,潘俊强、刘莉,杭州应用工程技术学院学报,第12卷第4期,2000年12月。我在百度上搜到这个文章而且下载的,应该还能搜 到。
着重介绍第二种方式,因为这可以给调试带来很大的方便,我相信大家的机器不会快到编译一遍有几万个文件的内核和编译一个小文件速度差不多的程度,另外就算你有网络下载内核的开发板,600~800k和十多k还是有差距的,呵呵。
先说下我调试模块的方法,在MTD中留个USER分区,然后用VIVI将USER分区的映像用串口下载到NAND Flash,启动内核,挂载USER分区。如果把驱动程序模块和使用驱动程序模块的测试程序都放在根文件系统里,每次都下载实在比较费事。当然,如果你的 板子已经能在Linux下访问网络,那太好了,nfs也好,tftp也好,速度就更快了。
下面开始Step by Step:
配置内核,到你的内核的目录下,make menuconfig,第三项“Loadable module support”,选上“Enable loadable module support”,其他的随便,我就多选了个“Module unloading”。然后,重新编译内核。
配置Shell,我用的是Busybox 1.00,到你的Busybox目录下,make menuconfig,找到“Linux Module Utilties”,选上“insmod”、“Support version 2.6.x Linux kernels”、“rmmod”、“Support taintd module checking with new kernels”,这里很奇怪,选上“lsmod”和“modprobe”我的Busybox就不工作了,我觉得是和我用的lib有关。因为我还没有成功 编译libs,所以就拉倒,反正insmod,rmmod也够用了。解释下,insmod是挂载模块的命令,rmmod是卸载命令。然后重新编译 Busybox,重新构建文件系统映像,我用的是cramfs;哈哈,还没有移植yaffs,为了毕业先将就了。
烧写新的内核和文件系统,说下,最好备份一个可用的内核和Shell的配置文件,相信来看这篇文章的都不是高手,弄不明白那些选项和编译器、库以及各种头文件的关联关系,也许你修改一下再编译,内核或者Shell就不好使了。
编写内核驱动程序。这个部分请看我下一篇文章,我也是看了那位高手的HOWTO才会明白其中细节的。
编写使用驱动程序的测试程序。这是用来调试编写的驱动是不是真的好使,这个部分也请看我后面的文章。
在编写内核驱动程序的时候,需要注意的有这几点(建议你看完下一篇文章之后再回来看这下面的东西):
Linux 2.6和2.4内核的字符设备驱动的标准模版不同,网上能够搜到的多半是2.4的东西,需要修改。
2.4内核的驱动模版是
#define MODULE
#include
#include
static int __init init_module(void)
{ /* * code here */}
static void __exit cleanup_module(void)
{ /* * code here */}
2.6内核的驱动模版是
#include
#include
#include
MODULE_LICENSE("GPL");
static int __init name_of_initialization_routine(void)
{ /* code goes here */ return 0; }
static void __exit name_of_cleanup_routine(void)
{ /* code goes here */ }
module_init(name_of_initialization_routine);
module_exit(name_of_cleanup_routine);
(这个模版来源于ZDNET的一篇文档,但原文有些错误,可以在上搜索“Linux 2.6内核移植—硬件驱动篇”)
Linux 2.6和2.4内核的字符设备驱动的注册方法不同,而且使用了dev_t这个设备ID的类型,提供了MAJOR(kdev_t dev)和MINOR(kdev_t dev)两个宏来获得设备的ID。
2.6内核的注册方法
A)静态注册
int register_chrdev_region(dev_t from, unsigned count, char *name);
B)动态注册
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, char *name);
之后需要和文件操作结构体联系起来
包含 ,利用struct cdev和file_operations连接
struct cdev *cdev_alloc(void);
oid cdev_init(struct cdev *cdev, struct file_operations *fops);
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
分别为,申请cdev结构,和fops连接,将设备加入到系统中.
删除设备:
void cdev_del(struct cdev *cdev);
只有在cdev_add执行成功才可运行。
2.4内核的注册方法
int register_chrdev(unsigned major, char * name, struct file_operation * fops);
删除设备
int unregister_chrdev(unsigned major, char * name);
哈哈,好像2.4简单啊,为什么2.6要那么做呢?我的想法是这样的话字符设备可以不用和文件操作联系起来而用其他方法访问吧,不对的话不要笑我。但是在2.6内核中,register_chrdev仍然是可以用的。我看了下它的代码,是这样的:
int register_chrdev(unsigned int major, const char *name,
struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
char *s;
int err = -ENOMEM;
cd = __register_chrdev_region(major, 0, 256, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
*s = '!';
err = cdev_add(cdev, MKDEV(cd->major, 0), 256);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, 0, 256));
return err;
}
整个过程不就是对register_chrdev_region、cdev_add之类函数的调用么,呵呵。
内核地址和物理地址不同。也就是说仅仅知道了设备物理地址是根本无法访问设备的。这里需要在物理地址和虚拟地址之间转换。转换的方法是调用ioremap (addr, size)(原型在asm/io.h中),要问这两个参数什么类型的,我也不知道,实在没时间看那些复杂的宏。反正就是用物理地址和整形大小,能出正确的 结果。举个例子:
unsigned long tmp;
unsigned long * pREG;
// Note: pREG is a pointer which points to a 4-bytes long integer, "pREG + 4" is equal to
// add 16-bytes offset to the physical address.
// Added by Lu Xianzi 2007.5.29
pREG = ioremap(0x560000000, 0x20);
tmp = * (volatile unsigned long *) (pREG + 4);
值得一提的是,pREG在驱动程序中应该是一个全局变量,在初始化时因该将其赋值,在模块卸载时应使用iounmap(ptr),来取消地址转换。
这种地址的映射不是简单的将一个地址转化为另一个地址,在内核中有一个表来记录这种映射,超过ioremap所注册范围大小的映射是不成立的,例如,访问0x56000080就不能在pREG的基础上简单的加上偏移(我的理解,FIXME)。
内核地址和用户空间地址不同,内核虚拟出一个环境,用户程序会认为自己好像拥有了整个物理空间。实际上内核空间是从0xc0000000开始的。内核空间 和用户空间的指针所指的位置是不一样的,需要交换数据时,应使用get_user(var, ptr)和put_user(var, ptr)来进行数据的交换(原型在asm/uaccess.h中),这在下一篇文章中会提到。
内核和2.6内核模块的编译命令不同。
2.4内核是
#arm-linux-gcc -D__KERNEL__ -I[你的内核的位置]/include -DKBUILD_BASENAME=[你的模块的名字] -DMODULE -c -o [你要生成的模块文件的名字].o [驱动程序源文件的名字].c
2.6内核,必须在你的驱动程序源文件目录下建立一个Makefile,其中写上你要编译的源文件输出,如obj-m := [驱动程序源文件的名字].o,然后输入
# make -k -C [你的内核的位置] SUBDIRS=$PWD modules
2.4内核仅仅生成.o文件,2.6内核的模块扩展名是.ko。
模块的挂载与卸载,很简单
挂载模块
insmod [模块文件的名字].ko
在文件系统中建立节点
mknod /dev/[你想建立的节点名字] c [MAJOR] [MINOR]
其中,MAJOR、MINOR分别是主和子设备号,MAJOR就是你register_chrdev时的那个MAJOR。
接下来就可以用open函数打开设备,用read,write,ioctl等函数访问设备,用close函数关闭设备,这些函数的原型在stdio.h中。
用户程序注意事项
用户程序用open函数打开设备时,如果有写操作,请将open的参数设置为O_RDWR(是Operation的O,不是数字0,这个宏在 fcntl.h中),如open("/dev/mydev", O_RDWR);,否则会出现write无法进行的错误。呵呵请恕我白痴,在这个问题上困扰很久。
解释下struct file_operation,内容来自友善之臂的文档,另外《Linux 字符设备驱动程序的设计》中也有提及。
在系统内部,I/O设备的存/取通过一组固定的入口点来进行,这组入口点是由每个设备的设备驱动程序提供的。具体到Linux系统,设备驱动程序所提供的 这组入口点由一个文件操作结构来向系统进行说明。file_operations结构定义于linux/fs.h文件中,随着内核的不断升级, file_operations结构也越来越大,不同版本的内核会稍有不同。
struct file_operations
{
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, 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);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_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 *);
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);
};
file_operations结构中的成员全部是函数指针,所以实质上就是函数跳转表。每个进程对设备的操作,都会根据major、minor设备号,转换成对file_operations结构的访问。
常用的操作包括以下几种:
lseek,移动文件指针的位置,只能用于可以随机存取的设备。
read,进行读操作,参数buf为存放读取结果的缓冲区,count为所要读取的数据长度。返回值为负表示读取操作发生错误;否则,返回实际读取的字节数。对于字符型,要求读取的字节数和返回的实际读取字节数都必须是inode-I_blksize的倍数。
write,进行写操作,与read类似。
select,进行选择操作。如果驱动程序没有提供select入口,select操作将会认为已经准备好进行任何的I/O操作。
ioctl,进行读、写以外的其他操作,参数cmd为自定义的命令。
简而言之,就是你在文件操作中使用的open,read,write,ioctl,close等,都会调用你在file_operation中定义的相应的函数入口。
本文来自EE小站的blog