首先,内核开发人员需要明白的是,我们要做的是在内核里写一些函数,这些函数是在内核中,所以它不能用到应用程序空间的所提供的一些函数,如printf()等;其次,这些设备驱动函数,目的是提供一种为底层的物理设备(如led,按键等)实现某种操作的策略,注意,是策略而不是机制,如果想要实现某种机制,请把实现这种机制的方法放到应用程序空间;同时,设备驱动函数也向上一层提供了一些系统调用接口供应用程序空间使用,例如open,read,write,ioctl等,所以,我们可以将设备驱动函数看作一个位于应用程序空间和底层硬件之间的中间层,这一中间层接收应用程序空间的请求并做相应处理后,将这些来自应用程序空间的请求具体实现到硬件设备上。事实上,在底层硬件和应用程序空间还有一些其他一些层。
对于字符设备驱动,我们需要明白在应用程序空间系统调用时,具体是如何操作到硬件的。其具体步骤如下:
1.open一个文件时,就会获取到这个文件的inode,在inode里存放在这个文件的静态信息,在这些信息里就有设备号。
2.如果是字符设备,就会遍历字符设备列表,根据inode中的设备号找到对应的cdev对象。
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
3.在cdev对象里,有一个成员为struct file_operations *ops,在内核里就有一个struct file_operations 的结构体,通过ops这个结构体指针指向了内核中的struct file_operations 结构体。在这个结构体里就指定了对于该设备提供了哪些系统调用。对于低级led,我们在struct file_operations 结构体里指定了提供这些系统调用:
static struct file_operations led_fops=
{
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.unlocked_ioctl = led_ioctl,
};
4.创建struct file对象(系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件描述符作为数组下标来标识一个设备对象)。
5.初始化struct file对象,把struct file对象中的file_operations 成员指向cdev对象中的file_operations成员。(file->fops = cdev->fops).
6.回调file->fops->open函数。
通过上面这些步骤,我们就可以理解了当我们在应用程序空间调用某个系统调用时,到底是如何找到设备驱动函数的,但是我们应该明白,在执行某个系统调用时,是操作系统完成了找到某个具体的设备驱动函数。
在内核编写低级led驱动的步骤:
1.如果设计到硬件需要初始化,先初始化相应的硬件。需要注意的是,在运行了linux操作系统的设备下编程时,因为有一个MMU(memory management unit),所以我们只能操作虚拟地址,所以要做从物理地址到虚拟地址的映射。
2.申请分配主次设备号,可动态申请,也可静态申请,但静态申请具有一个缺点,就是如果我们指定的设备号将来可能与其他的一些设备的设备号冲突,因为这些设备现在未使能,但将来可能使能。
分配设备号的代码:其中MKDEV这个宏用来将int型的主设备号和次设备号转为dev_t类型的设备号。
/*alloc device number*/
if(0 != LED_MAJOR) /*static*/
{
devno = MKDEV(dev_major,0);
result = register_chrdev_region(devno,led_count,DEV_NAME);
}
else
{
result = alloc_chrdev_region(&devno,dev_minor,led_count,DEV_NAME);/*dynamic*/
dev_major = MAJOR(devno);
}
3.申请一个cdev结构体,内核在内部使用类型 struct cdev 的结构来代表字符设备. 在内核调用你的设备操作前, 你必须分配一个这样的结构体并注册给Linux内核,在这个结构体里有对于这个设备进行操作的函数,具体定义在file_operation结构体中。该结构体定义在 include/linux/cdev.h 文件中:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
led设备驱动申请cdev结构体的函数:cdev_alloc(),需要注意的是,我们需要尽可能对我们用到的每一个函数做判断,如果失败则需返回错误信息。
if(NULL == (led_cdev = cdev_alloc()))
{
printk(KERN_ERR "s3c %s driver apply to alloc cdev struct failure!\n",DEV_NAME);
unregister_chrdev_region(devno,led_count);
return -ENOMEM;
}
4.注册cdev结构体到内核
在分配到cdev结构体后,接下来我们将初始化它,并将对该设备驱动所支持的系统调用函数存放的file_operations结构体添加进来,然后我们通过cdev_add函数将他们注册给Linux内核,这样完成整个Linux字符设备的注册过程。其中cdev_add的函数原型如下,其中dev 是 cdev 结构, num 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
下面是cdev结构体的初始化和注册过程:
static struct file_operations led_fops =
{
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.unlocked_ioctl = led_ioctl, };
struct cdev *led_cdev;
if(NULL == (led_cdev=cdev_alloc()) )
{
printk(KERN_ERR "S3C %s driver can't alloc for the cdev.\n", DEV_NAME);
unregister_chrdev_region(devno, dev_count);
return -ENOMEM;
}
led_cdev->owner = THIS_MODULE;
cdev_init(led_cdev, &led_fops);
result = cdev_add(led_cdev, devno, dev_count);
if (0 != result)
{
printk(KERN_INFO "S3C %s driver can't reigster cdev: result=%d\n", DEV_NAME, result);
goto ERROR;
}
在完成了这些步骤之后,对于led驱动的初始化就完成了,但进有这些还不够,在安装一个驱动模块时,需要有module_init()函数,卸载一个模块时,需要有module_exit()函数,并且在写一个模块时,还要有许可证,有利于linux内核在编译这个模块时可以很顺利的进行,在这里就不深入介绍了。