Linux设备驱动程序之字符设备驱
在学习Linux设备驱动程序设计过程中,见得最多的应该是字符型设备,但是,对于字符型设备驱动程序的注册,注销,在书上看到的大多就是应用register_chrdev_region, unregister_chrdev_region这两个函数,但是,在内核源码中看到得最多的又是register_chrdev, unregister_chrdev这两个函数,在有的资料上面把后面两个函数说明为旧方法,提倡我们应用前两个函数,但是,很多时候,除了做几个简单的实验外,我们更多是应用Linux内核提供给我们的相应的驱动模块来注册或者注销我们的设备驱动程序,Linux内核中已经有很多成功的设备驱动,对于某些设备,我们只需调用Linux内核给我们提供的驱动模块就可以了,比如i2c总线驱动,不过这得在了解Linux内核中i2c总线子系统的前提下才能够很好的应用,所以,这不得不去看内核驱动源码,这要求我们对内核各驱动模块有一定的了解。
好了,我们回到字符设备驱动上来,在我没有深入了解字型设备驱动之前,也跟大家一样,跟着书上所告诉我的知识,(这里以register_chrdev_region与unregister_chrdev_region这两个函数注册字符型设备驱动程序来说明,在后面将会说明两种注册方法的不同),首先定义一个结构体,在里面嵌入一个cdev结构体,因为我们在使用cdev相关结构体的时候将会应用到,其实,不嵌入也行,不过,一般使用嵌入方法。一般结构如下:
static struct xx_cdev{
…..
struct cdev xxx_cdev;
};
接下来是定义相关的file_operations结构体,进行相应的初始化,再实现相关域的映射,最后,应用register_chrdev_region进行注册,应用unregister_chrdev_region进行注销,完成模块的初始化与注册后,这样一个字符型设备驱动程序就行了,但是,不知道大家有没有看到内核代码或者一些其它的资料,很多应用register_chrdev,unregister_chrdev来进行注册与注销的字符型设备驱动程序,更本没有提到过cdev结构体,这是什么呢,我将在下面进行说明。
首先我们从函数原型来说明,下面为这两种注册与注销方法的函数原型:
第一种:
int register_chrdev(unsigned int major, const char *name,struct file_operations *fops);
int unregister_chrdev(unsigned int major, const char *name);
第二种:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
void unregister_chrdev_region(dev_t from, unsigned count);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, \
unsigned count,const char *name);
从函数原型上可以看出,两种方法有很大的不同,其实,也没有多大的区别,只是形参不同罢了,最后都是调用
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor,int minorct, const char *name)
来实现注册,调用
static struct char_device_struct * __unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)
来完成相应的注销,不同之处在于第一种方会一次给分配256个同主设备号不同次设备号的设备驱动,应用此种方法,必须得保存此主设备号还有256个次设备号可用,否则将注册出错,而第二种方法则应用count这个参数来指定连续分配的个数,在后面的分配过程中将会看到怎么实现的。
好的,让我们来看看函数的重要点,在register_chrdev中,我们可以看到这样的一行代码:cd = __register_chrdev_region(major, 0, 256, name);这就是为每一个主设备号分配256个设备驱动,关于是怎么分配的,我们将在后面进行分析__register_chrdev_region函数。对于此种方法,我们没有明显的嵌入或者应用cdev结构体,因为在此函数中,内核帮我们完成相关的工作。如下:
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
err = cdev_add(cdev, MKDEV(cd->major, 0), 256);
这几行代码帮我们完成了,所以,我想很大初学者,跟我一样,当看到过两种方法写过的驱动程序时,会有一点不了解,现在应该了解了吧。关于注销函数在此不进行说明,想知道的朋友请自行去参考内核源代码,其实,本人觉在,搞嵌入Linux,很多时候在看内核源码,不过这是看一件艺术品,在看内核源码中,你会学到很多东西,比如,宏定义的相关技巧,指针应该得有所提高,因为内核源码中差不多每一行都有指针存在。应用预定义控制调试信息,让调试相关代码留在源码中,为将来的维护带来方便。
我们来分析第二种注册方法,此函数同样也是调用__register_chrdev_region函数来完成相应的注册工作,但是,它可以主次多个主设备号。这是通过一个for循环语言来实现的,源码如下:
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
我们应用register_chrdev_region传入的是起始设备编号form,连续主册的个数count,设备字称,在此函数的最前面有这么一句dev_t to = from + count;这是求一个范围,从上面的循环中可以看出,如果注册的设备编号在一个主设备号之间,那么只注册一次,但是如果在多个主设备号之间,需要注册多次,虽然此方法可以注册多主设备号,但是,我们提倡应该把count控制在一个主设备号的范围内,一个字符设备名称,不同主设备号,有点不太合常理。还有动态分配主设备号的方法,与此方法差不多,所以在此不再进行说明。