赞
踩
在Linux系统中通常把驱动分成三类:
字符设备驱动、块设备驱动、网络设备驱动,关于三种设备设备驱动的相关定义如下:
字符设备:只能一个字节一个字节的读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序进行。字符设备是面向流的设备,常见的字符设备如鼠标、键盘、串口、控制台、LED等。
块设备:是指可以从设备的任意位置读取一定长度的数据设备。块设备如硬盘、磁盘、U盘和SD卡等存储设备。
网络设备:网络设备比较特殊,不在是对文件进行操作,而是由专门的网络接口来实现。应用程序不能直接访问网络设备驱动程序。在/dev目录下也没有文件来表示网络设备。
本篇博文主要阐述字符设备驱动模型,字符设备驱动结构:
1.确定主设备号;
2.构造file_operation结构体。
3.注册设备register_chrdev
4.入口函数
5.出口函数
首先字符设备驱动模型的总体代码框架如下:
#include <linux/module.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/miscdevice.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/mutex.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/stat.h> #include <linux/init.h> #include <linux/device.h> #include <linux/tty.h> #include <linux/kmod.h> #include <linux/gfp.h> static int major = 0; static int kernel_buf = 77; static struct class *hello_class; static int hello_open (struct inode *node, struct file *file) { printk("this is open\n"); return 0; } static ssize_t hello_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { printk("this is write\n"); copy_from_user(&kernel_buf,buf,4); printk("from user %d\n",kernel_buf); return size; } static ssize_t hello_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { printk("this is read\n"); copy_to_user(buf,&kernel_buf,4); printk("to user %d",kernel_buf); return size; } static struct file_operations hello_op = { .owner = THIS_MODULE, .open = hello_open, .read = hello_read, .write = hello_write, }; static int hello_init(void) { printk("this is init\n"); major = register_chrdev(0,"hello1",&hello_op); hello_class = class_create(THIS_MODULE,"hello_class"); device_create(hello_class,NULL,MKDEV(major,0),NULL,"hello1"); return 0; } static void hello_exit(void) { printk("this is exit\n"); device_destroy(hello_class,MKDEV(major,0)); class_destroy(hello_class); unregister_chrdev(major,"hello"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
接下来对字符设备驱动框架进行详细描述:
对于字符设备驱动来说首先需要找到其入口函数,在Linux中将module_init()定义为宏,用于设置某个函数作为真正入口函数
对于上述代码框架而言hello_init是真正的驱动入口函数。也可以不使用module_init去定义驱动入口函数,修改static void hello_init(void)函数改为static void init_module(void).既然有入口函数就有出口函数,与定义驱动入口函数一样定义驱动出口函数使用的是module_exit()这个宏,也可以直接将驱动真正出口函数改为static void exit_module(void)。MODULE_LICENSE(“GPL”)作为协议通常默认添加。
驱动入口函数如下:
static int hello_init(void)
{
printk("this is init\n");
major = register_chrdev(0,"hello1",&hello_op);
hello_class = class_create(THIS_MODULE,"hello_class");
device_create(hello_class,NULL,MKDEV(major,0),NULL,"hello1");
return 0;
}
在驱动入口函数中主设备号可以利用register_chrdrvde函数的返回值进行赋值,register_chrdrv是字符驱动注册函数,第一个参数为主设备号,使用0就可以让其自动生成主设备号,第二个参数为驱动名,第三个函数为结构体file_operation。
file_operation结构体
file_operation定义如下:
在file_operation中定义了应用层有可能使用到的所有的系统调用函数,在应用层通过调用系统调用函数会触发一个异常(swi异常),会进入到驱动层。通常在驱动中定义open 、read和write是三个系统调用,以open为例描述应用层到底层再到硬件过程,在应用层使用open函数以相应的打开方式去打开对应的底层驱动文件,这是就触发了swi异常进入到内核态,在内核态调用sys_open函数,通过判断是普通文件还是设备文件,如果是普通文件则可以使用可读、可写或者可读可写的方式打开。如果是设备文件就根据主设备号找到对应的驱动调动驱动文件中file_operation中定义的结构体指针指向的open函数,如果使用到了硬件通常是在open函数中对硬件寄存器进行初始化而不是在入口函数中进行,因为如果放在入口函数中,只要insmod装载驱动就会对硬件进行操作,这样就会导致应用层程序还没运行寄存器就已经被初始化。对硬件寄存器的操作其实是对物理地址使用ioremap映射成虚拟地址,在Linux系统中是对虚拟地址进行操作的。通常驱动程序中file_operation结构体如下:
static struct file_operations hello_op = {
.owner = THIS_MODULE,
.open = hello_open,
.read = hello_read,
.write = hello_write,
};
在结构体外对这些结构体中的函数指针进行初始化:
static int hello_open (struct inode *node, struct file *file) { printk("this is open\n"); return 0; } static ssize_t hello_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { printk("this is write\n"); copy_from_user(&kernel_buf,buf,4); printk("from user %d\n",kernel_buf); return size; } static ssize_t hello_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { printk("this is read\n"); copy_to_user(buf,&kernel_buf,4); printk("to user %d",kernel_buf); return size; }
调用完register_chrdev之后系统就完成对驱动程序的注册,不需要class_create和device_create就可以完成驱动注册,对程序进行编译并insmod装载驱动之后可以在/proc/dev中查看驱动程序和主设备号。
前面的数字代表主设备号,此时系统中已经注册了该驱动。
创建类和设备节点:
创建设备节点可以通过手动设置和在驱动程序中自动设置;
手动设置:当程序编译完成之后装载驱动通过cat /proc/devices查看驱动的主设备号,根据主设备号使用mknod创建设备节点
自动创建:class_create和device_create
调用class_create去创建该设备的类,可以进入到/sys/class中查看是否生成对应的类。
之后再调用device_create创建对应的设备节点,查看设备节点有多种方式:
1.同样通过/sys/dev/char查看到主设备号和次设备号,以此判断是否创建成功。
2.通过/dev/char查看设备名
驱动出口函数
完成了驱动入口函数完成之后,需要对出口函数进行初始化:
驱动出口函数的操作顺序顺序和入口操作顺序相反,首先是销毁设备节点,然后再销毁类,最后卸载驱动程序。
static void hello_exit(void)
{
printk("this is exit\n");
device_destroy(hello_class,MKDEV(major,0));
class_destroy(hello_class);
unregister_chrdev(major,"hello");
}
如果要实现应用层和驱动层的信息交互,需要借用API:copy_from_user和copy_to_user去获取和传递信息。
copy_from_user(void *to, const void __user *from, unsigned long n)
copy_to_user(void __user *to, const void *from, unsigned long n)
用法可见下面带有硬件的驱动程序。
在操纵硬件的驱动程序中,和传统的单片机配置寄存器不同点在于,在入口函数中需要对寄存器的物理地址进行映射,使用ioremap将寄存器的物理地址映射为虚拟地址,因为Linux系统程序运行是在虚拟地址上运行,所以需要将物理地址映射成虚拟地址。另外对于寄存器的初始化通常在file_operation中的open中进行。在write和read中根据情况使用copy_from_user和copy_to_user,获得相关信息之后配置寄存器实现硬件的相关操作。
带有硬件操作的驱动程序:
#include <linux/fs.h> //file_operations声明 #include <linux/module.h> //module_init module_exit声明 #include <linux/init.h> //__init __exit 宏定义声明 #include <linux/device.h> //class devise声明 #include <linux/uaccess.h> //copy_from_user 的头文件 #include <linuxpes.h> //设备号 dev_t 类型声明 #include <asm/io.h> //ioremap iounmap的头文件 static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno; //设备号 static int major =231; //主设备号 static int minor =0; //次设备号 static char *module_name="pin4"; //模块名 volatile int *GPFSEL0 = NULL; volatile int *GPSET0 = NULL; volatile int *GPCLR0 = NULL; //driver_open函数 static int driver_open(struct inode *inode,struct file *file) { printk("driver_open\n"); //内核的打印函数和printf类似 *GPFSEL0 &= ~(0x6 << 12); *GPFSEL0 |= (0x1 << 12); return 0; } //driver_write函数 static ssize_t driver_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { int userCmd; printk("driver_write\n"); copy_from_user(&userCmd,buf,count); printk("get value\n"); if(userCmd == 0){ printk("set 0\n"); *GPSET0 |= 0x1<<4; }else if(userCmd == 1){ printk("set 1\n"); *GPCLR0|= 0x1<<4; }else{ printk("undo\n"); } return 0; } static struct file_operations driver_fops = { .owner = THIS_MODULE, .open = pin4_open, .write = pin4_write, }; int __init driver_init(void) { int ret; devno = MKDEV(major,minor); //创建设备号 ret = register_chrdev(major, module_name,&driver_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中 driver_class=class_create(THIS_MODULE,"driver"); driver_class_dev =device_create(driver_class,NULL,devno,NULL,module_name); //创建设备文件 GPFSEL0 = (volatile int *)0x3f200000; GPSET0 = (volatile int *)0x3f20001C; GPCLR0 = (volatile int *)0x3f200028; return 0; } void __exit driver_exit(void) { iounmap(GPFSEL0); iounmap(GPSET0); iounmap(GPCLR0); device_destroy(driver_class,devno); class_destroy(driver_class); unregister_chrdev(major, module_name); //卸载驱动 } module_init(driver_init); //入口 module_exit(driver_exit); MODULE_LICENSE("GPL");
注意在应用层的程序调试方式可以使用printf打印相关信息,但是在驱动程序中是使用printk打印信息,并且命令窗口中不会直接显示驱动程序中printk打印的相关信息,需要再命令窗口使用dmesg来查看驱动程序的prink打印的相关信息。
learned from:韦东山
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。