linux字符设备驱动基本框架
扫描二维码
随时随地手机看文章
对于Linux的驱动程序,需要遵循一定的框架结构。嵌入式Linux的学习其实并不难,只要深入理解Linux的框架,写起来也可以得心应手。
1.linux函数调用过程
1.1 系统函数调用的意义
在Linux的中,有一个思想比较重要:一切皆文件。
也就是说,在应用程序中,可以通过open,write,read等函数来操作底层的驱动。
比如操作led,函数如下
//点灯 fd1 = open("/dev/led",O_RDWR);
write(fd1,&val,4); //写文本文件 fd2 = open("hello.txt",O_RDWR)
write(fd2,&val,4);
一般的,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些(这就是为什么它被称作"保护模式")。为了和用户空间上执行的进程进行交互,内核提供了一组接口。透过该接口,应用程序能够访问问硬件设备和其它操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求。而内核负责满足这些请求(或者让应用程序临时搁置)。
实际上提供这组接口主要是为了保证系统稳定可靠。避免应用程序肆意妄行,惹出大麻烦。
下面是printf()串口打印调用的过程。

1.2 系统函数的调用过程
当应用程序调用open,read,write等函数时,最终会调用驱动中的fopen,fwrite,fread等函数。其过程如下
1.当应用程序调用open,read,ioctl等函数(C库)时,会触发一个系统异常SWI。
2.当触发异常时,会进入到内核系统调用接口(system call interface),会调用sys_open,sys_read,sys_write。
3.然后会进入虚拟文件系统(VFS)virtual filesystem。
4.最后进入到驱动函数的open,read,write函数,read函数的本质就是copy_to_user,而write函数就是copy_from_user。

1.3 用户空间与内核空间
Linux的操作系统分为内核态和用户态,内核态完成与硬件的交互,比如读写内存,硬件操作等。用户态运行上层的程序,比如Qt等。分成这两种状态的原因是即使应用程序出现异常,也不会使操作系统崩溃。
值得注意的是,用户态和内核态是可以互相转换的。每当应用程序执行系统调用或者被硬件中断挂起时,Linux操作系统都会从用户态切换到内核态;当系统调用完成或者中断处理完成后,操作系统会从内核态返回到用户态,继续执行应用程序。
2.驱动程序的框架
在理解设备框架之前,首先要知道驱动程序主要做了以下几件事
1.将此内核驱动模块加载到内核中
2.从内核中将驱动模块卸载
3.声明遵循的开源协议
2.1 Linux下的设备
Linux下分成三大类设备:
字符设备:字符设备是能够像字节流一样被访问的设备。一般来说对硬件的IO操作可归结为字符设备。常见的字符设备有led,蜂鸣器,串口,键盘等等。包括lcd与摄像头驱动都属于字符设备驱动。
块设备:块设备是通过内存缓存区访问,可以随机存取的设备,一般理解就是存储介质类的设备,常见的字符设备有U盘,TF卡,eMMC,电脑硬盘,光盘等等
网络设备:可以和其他主机交换数据的设备,主要有以太网设备,wifi,蓝牙等。
字符设备与块设备驱动程序的区别与联系
1.字符设备的最小访问单元是字节,块设备是块字节512或者512字节为单位
2.访问顺序上面,字符设备是顺序访问的,而块设备是随机访问的
3.在linux中,字符设备和块设备访问字节没有本质区别
网络设备驱动程序的本质
提供了协议与设备驱动通信的通用接口。
简单的说,对于字符设备驱动就是可以按照先后顺序访问,不能随机访问,比如LCD,camera,UART等等,这些是字符设备的代表。对于I2C也划分为字符设备驱动程序,也可以细分为总线设备驱动程序。块设备驱动程序就是可以随机访问的缓冲区。
2.2 驱动程序框架的一个例子
对于一个驱动程序,如果想让内核知道,就准守一定的框架,下面来看一下一个最简单的驱动程序的框架
#include #include //驱动程序入口函数 static int test_init(void) {
printk("---Add---\n"); return 0;
} //驱动函数出口函数 static void test_exit(void) {
printk("---Remove---\n");
} //告诉内核,入口函数 module_init(test_init); //告诉内核,出口函数 module_exit(test_exit);
MODULE_LICENSE("GPL"); //GPL GNU General Public License MODULE_AUTHOR("ZFJ"); //作者
如果要将上面的源码编译成驱动程序,还需要写Makefile程序
obj-m:=test.o KDIR:=/lib/modules/$(shell uname -r)/build PWD:=$(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions *.order *symvers *Module.markers
其中需要解释一下的是
$(MAKE) -C $(KDIR) M=$(PWD) modules
该命令是make modules命令的扩展,-C选项的作用是指将当前的工作目录转移到指定目录,即(KDIR)目录,程序到(shell pwd)当前目录查找模块源码,将其编译,生成.ko文件。
生成的.ko文件就是驱动程序,如果要将当前的驱动程序插入到内核中,可以在控制台输入
sudo insmod test.ko
该命令会执行test_init函数。如果要查看内核打印信息,可输入dmesg。用lsmod可查看目前挂载的驱动程序。
如果要移除当前的驱动程序,可调用
sudo rmmod test
该函数会执行test_exit函数。
3.字符设备驱动程序解析
字符设备在Linux驱动中起到十分关键的作用。包括我们要实现的LCD驱动以及CAM驱动都属于字符设备驱动。所以现在主要分析一下字符设备驱动程序的框架。
3.1 基本概念
对于了解字符设备驱动程序,需要知道的问题
(1)应用程序、库、内核、驱动程序的关系
应用程序调用函数库,通过文件的操作完成一系列的功能。作为Linux特有的抽象方式,将所有的硬件抽象成文件的读写。
(2)设备类型
字符设备、块设备、网络设备
(3)设备文件、主设备号、从设备号
有了设备类型的划分,还需要进行进一步明确。所以驱动设备会生成字符节点,以文件的方式存放在/dev目录下,操作时可抽象成文件操作即可。每个设备节点有主设备号和次设备号,用一个32位来表示,前12位表示主设备号,后20位表示次设备号。例如"/dev/fb0","/dev/fb1"或者"/dev/tty1","/dev/tty2"等等。
3.2 创建流程
第一步:写出驱动程序的框架
前面在创建驱动程序的框架时,只是测试了安装与卸载驱动,并且找到驱动程序的入口与出口。并没有一个字符设备操作的接口。作为一个字符设备驱动程序,其open,read,write等函数是必要的。但是最开始还是要实现一个驱动程序的入口与出口函数。
#include #include static int __init dev_fifo_init() { return 0;
} static void __exit dev_fifo_exit() {
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);
MODULE_LICENSE("Dual DSB/GPL");
MODULE_AUTHOR("ZHAO");
第二步:在驱动入口函数中申请设备号
一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。
//设备号 : 主设备号(12bit) | 次设备号(20bit) dev_num = MKDEV(MAJOR_NUM, 0); //静态注册设备号 ret = register_chrdev_region(dev_num,1,"dev_fifo"); if(ret < 0)
{ //静态注册失败,进行动态注册设备号 ret = alloc_chrdev_region(&dev_num,0,1,"dev_fifo"); if(ret < 0)
{
printk("Fail to register_chrdev_region\n"); goto err_register_chrdev_region;
}
}
静态分设备号的函数原型
register_chrdev_region(dev_t first,unsigned int count,char *name)
1:第一个参数:要分配的设备编号范围的初始值, 这组连续设备号的起始设备号, 相当于register_chrdev()中主设备号
2:第二个参数:连续编号范围. 是这组设备号的大小(也是次设备号的个数)
3:第三个参数:编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称
其中动态分配的函数原型
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);
1:这个函数的第一个参数,是输出型参数,获得一个分配到的设备号。可以用MAJOR宏和MINOR宏,将主设备号和次设备号,提取打印出来,看是自动分配的是多少,方便我们在mknod创建设备文件时用到主设备号和次设备号。 mknod /dev/xxx c 主设备号 次设备号
2:第二个参数:次设备号的基准,从第几个次设备号开始分配。
3:第三个参数:次设备号的个数。
4:第四个参数:驱动的名字
由于每个设备只有一个主设备号,所以如果用静态分配设备号时,有可能会导致分配不成功,所以采用动态分配的方式。
注意,在入口函数中注册,那么一定要记得在驱动出口函数中释放
//释放申请的设备号 unregister_chrdev_region(dev_num, 1);
第三步:创建设备类
这一步会在/sys/class/dev_fifo下创建接口
sysfs 文件系统总是被挂载在 /sys 挂载点上。虽然在较早期的2.6内核系统上并没有规定 sysfs 的标准挂载位置,可以把 sysfs 挂载在任何位置,但较近的2.6内核修正了这一规则,要求 sysfs 总是挂载在 /sys 目录上。
//创建设备类 cls = class_create(THIS_MODULE, "dev_fifo"); if(IS_ERR(cls))
{
ret = PTR_ERR(cls); goto err_class_create;
}
第四步:初始化字符设备
在这一步中,会初始化一个重要的结构体,file_operations。
//初始化字符设备 cdev_init(&gcd->cdev,&fifo_operations);
该函数的原型为
cdev_init(struct cdev *cdev, const struct file_operations *fops)
第一个参数时字符设备结构体,第二个参数为操作函数
Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。
用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。
通常来说,字符设备驱动程序经常用到的5种操作
struct file_operations { ssize_t (*read)(struct file *,char *, size_t, loff_t *);//从设备同步读取数据 ssize_t (*write)(struct file *,const char *, size_t, loff_t *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);//执行设备IO控制命令 int (*open) (struct inode *, struct file *);//打开 int (*release)(struct inode *, struct file *);//关闭 };
第五步:添加设备到用户操作系统
//添加设备到操作系统 ret = cdev_add(&gcd->cdev,dev_num,1); if (ret < 0)
{ goto err_cdev_add;
}
函数原型为
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
第一个参数为cdev 结构的指针
第二个参数为设备起始编号
第三个参数为设备编号范围
这一步的含义在于将字符设备驱动加入到操作系统的驱动数组中。当应用程序调用open函数时,会首先找到该设备的设备号,然后根据这个设备号找到相应file_operations。调用其中的open以及读写函数。
第六步:导出设备信息到用户空间
//导出设备信息到用户空间(/sys/class/类名/设备名) device = device_create(cls,NULL,dev_num,NULL,"dev_fifo%d",0); if(IS_ERR(device)){
ret = PTR_ERR(device);
printk("Fail to device_create\n"); goto err_device_create;
}
函数原型
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
第一个参数:struct class 指针,必须在本函数调用之前先被创建
第二个参数:该设备的parent指针。
第三个参数:字符设备的设备号,如果dev_t不是0,0的话,1个”dev”文件将被创建。
第四个参数:被添加到该设备回调的数据。
第五个参数:设备名字。
之前写的字符类设备驱动,没有自动创建设备节点,因为只使用了register_chrdev()函数,只是注册了这个设备。然后在系统启动后,就要自己创建设备节点mknod,这样虽然是可行的,但是比较麻烦。于是想在init函数里面,自动创建设备节点。
创建设备节点使用了两个函数 class_create()和class_device_create(),当然在exit()函数里,要使用class_destory()和class_device_desotry()注销创建的设备节点!。
需要注意的是要使用该函数自动生成节点,内核版本至少在Linux2.6.32 。
到这里,一个字符设备驱动程序的基本流程就完成了。编译好驱动程序,然后安装到Linux中,用insmod加载模块。可以在/dev/dev_fifo0看到自己创建的设备节点。相关源代码可参考附录。
4. 总结
Linux将所有的设备都抽象成文件,这样的操作接口比较的统一,也给开发带来很大的方便。通过将写好的驱动程序装载到内核可见的区域,使得内核感知到模块的存在,然后用户空间才能通过系统调用联系到驱动,从而完成它的任务。
写驱动程序需要按照一定的步骤,首先申明驱动的入口和出口,然后注册设备号。接着填充file_operations结构体。引用程序通过调用open,read,或者write函数,最终调用到file_operations的open,read或者write函数,从而实现了从应用层到内核层的调用。
附录:程序代码
#include #include #include #include #include #include #include //指定的主设备号 #define MAJOR_NUM 250 //自己的字符设备 struct mycdev { int len; unsigned char buffer[50]; struct cdev cdev; };
MODULE_LICENSE("GPL"); //设备号 static dev_t dev_num = {0}; //全局gcd struct mycdev *gcd; //设备类 struct class *cls; //打开设备 static int dev_fifo_open(struct inode *inode, struct file *file) {
printk("dev_fifo_open success!\n"); return 0;
} //读设备 static ssize_t dev_fifo_read(struct file *file, char __user *ubuf, size_t size, loff_t *ppos) { int n; int ret; char *kbuf;
printk("read *ppos : %lld\n",*ppos); if(*ppos == gcd->len) return 0; //请求大大小 > buffer剩余的字节数 :读取实际记得字节数 if(size > gcd->len - *ppos)
n = gcd->len - *ppos; else n = size;
printk("n = %d\n",n); //从上一次文件位置指针的位置开始读取数据 kbuf = gcd->buffer + *ppos; //拷贝数据到用户空间 ret = copy_to_user(ubuf,kbuf, n); if(ret != 0) return -EFAULT; //更新文件位置指针的值 *ppos += n;
printk("dev_fifo_read success!\n"); return n;
} //写设备 static ssize_t dev_fifo_write(struct file *file, const char __user *ubuf, size_t size, loff_t *ppos) { int n; int ret; char *kbuf;
printk("write *ppos : %lld\n",*ppos); //已经到达buffer尾部了 if(*ppos == sizeof(gcd->buffer)) return -1; //请求大大小 > buffer剩余的字节数(有多少空间就写多少数据) if(size > sizeof(gcd->buffer) - *ppos)
n = sizeof(gcd->buffer) - *ppos; else n = size; //从上一次文件位置指针的位置开始写入数据 kbuf = gcd->buffer + *ppos; //拷贝数据到内核空间 ret = copy_from_user(kbuf, ubuf, n); if(ret != 0) return -EFAULT; //更新文件位置指针的值 *ppos += n; //更新dev_fifo.len gcd->len += n;
printk("dev_fifo_write success!\n"); return n;
} //设备操作函数接口 static const struct file_operations fifo_operations = { .owner = THIS_MODULE,
.open = dev_fifo_open,
.read = dev_fifo_read,
.write = dev_fifo_write,
}; //模块入口 int __init dev_fifo_init(void) { int ret; struct device *device; //动态申请内存 gcd = kzalloc(sizeof(struct mycdev), GFP_KERNEL); if(!gcd){ return -ENOMEM;
} //设备号 : 主设备号(12bit) | 次设备号(20bit) dev_num = MKDEV(MAJOR_NUM, 0); //静态注册设备号 ret = register_chrdev_region(dev_num,1,"dev_fifo"); if(ret < 0){ //静态注册失败,进行动态注册设备号 ret = alloc_chrdev_region(&dev_num,0,1,"dev_fifo"); if(ret < 0){
printk("Fail to register_chrdev_region\n"); goto err_register_chrdev_region;
}
} //创建设备类 cls = class_create(THIS_MODULE, "dev_fifo"); if(IS_ERR(cls)){
ret = PTR_ERR(cls); goto err_class_create;
} //初始化字符设备 cdev_init(&gcd->cdev,&fifo_operations); //添加设备到操作系统 ret = cdev_add(&gcd->cdev,dev_num,1); if (ret < 0)
{ goto err_cdev_add;
} //导出设备信息到用户空间(/sys/class/类名/设备名) device = device_create(cls,NULL,dev_num,NULL,"dev_fifo%d",0); if(IS_ERR(device)){
ret = PTR_ERR(device);
printk("Fail to device_create\n"); goto err_device_create;
}
printk("Register dev_fito to system,ok!\n"); return 0;
err_device_create:
cdev_del(&gcd->cdev);
err_cdev_add:
class_destroy(cls);
err_class_create:
unregister_chrdev_region(dev_num, 1);
err_register_chrdev_region: return ret;
} void __exit dev_fifo_exit(void) { //删除sysfs文件系统中的设备 device_destroy(cls,dev_num ); //删除系统中的设备类 class_destroy(cls); //从系统中删除添加的字符设备 cdev_del(&gcd->cdev); //释放申请的设备号 unregister_chrdev_region(dev_num, 1); return;
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);
MODULE_LICENSE("Dual DSB/GPL");
MODULE_AUTHOR("ZHAO");