对于Linux的驱动程序,需要遵循一定的框架结构。嵌入式Linux的学习其实并不难,只要深入理解Linux的框架,写起来也可以得心应手。
在Linux的中,有一个思想比较重要:一切皆文件。
也就是说,在应用程序中,可以通过open,write,read等函数来操作底层的驱动。
比如操作led,函数如下
//点灯
fd1 = open("/dev/led",O_RDWR);
write(fd1,&val,);
//写文本文件
fd2 = open("hello.txt",O_RDWR)
write(fd2,&val,);
一般的,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些(这就是为什么它被称作"保护模式")。为了和用户空间上执行的进程进行交互,内核提供了一组接口。透过该接口,应用程序能够访问问硬件设备和其它操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求。而内核负责满足这些请求(或者让应用程序临时搁置)。
实际上提供这组接口主要是为了保证系统稳定可靠。避免应用程序肆意妄行,惹出大麻烦。
下面是printf()串口打印调用的过程。
当应用程序调用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。
Linux的操作系统分为内核态和用户态,内核态完成与硬件的交互,比如读写内存,硬件操作等。用户态运行上层的程序,比如Qt等。分成这两种状态的原因是即使应用程序出现异常,也不会使操作系统崩溃。
值得注意的是,用户态和内核态是可以互相转换的。每当应用程序执行系统调用或者被硬件中断挂起时,Linux操作系统都会从用户态切换到内核态;当系统调用完成或者中断处理完成后,操作系统会从内核态返回到用户态,继续执行应用程序。
在理解设备框架之前,首先要知道驱动程序主要做了以下几件事
1.将此内核驱动模块加载到内核中
2.从内核中将驱动模块卸载
3.声明遵循的开源协议
Linux下分成三大类设备:
字符设备:字符设备是能够像字节流一样被访问的设备。一般来说对硬件的IO操作可归结为字符设备。常见的字符设备有led,蜂鸣器,串口,键盘等等。包括lcd与摄像头驱动都属于字符设备驱动。
块设备:块设备是通过内存缓存区访问,可以随机存取的设备,一般理解就是存储介质类的设备,常见的字符设备有U盘,TF卡,eMMC,电脑硬盘,光盘等等
网络设备:可以和其他主机交换数据的设备,主要有以太网设备,wifi,蓝牙等。
字符设备与块设备驱动程序的区别与联系
1.字符设备的最小访问单元是字节,块设备是块字节512或者512字节为单位
2.访问顺序上面,字符设备是顺序访问的,而块设备是随机访问的
3.在linux中,字符设备和块设备访问字节没有本质区别
网络设备驱动程序的本质
提供了协议与设备驱动通信的通用接口。
简单的说,对于字符设备驱动就是可以按照先后顺序访问,不能随机访问,比如LCD,camera,UART等等,这些是字符设备的代表。对于I2C也划分为字符设备驱动程序,也可以细分为总线设备驱动程序。块设备驱动程序就是可以随机访问的缓冲区。
对于一个驱动程序,如果想让内核知道,就准守一定的框架,下面来看一下一个最简单的驱动程序的框架
#include <linux/init.h>
#include <linux/module.h>
//驱动程序入口函数
static int test_init(void)
{
printk("---Add---\n");
return ;
}
//驱动函数出口函数
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函数。
字符设备在Linux驱动中起到十分关键的作用。包括我们要实现的LCD驱动以及CAM驱动都属于字符设备驱动。所以现在主要分析一下字符设备驱动程序的框架。
对于了解字符设备驱动程序,需要知道的问题
(1)应用程序、库、内核、驱动程序的关系
应用程序调用函数库,通过文件的操作完成一系列的功能。作为Linux特有的抽象方式,将所有的硬件抽象成文件的读写。
(2)设备类型
字符设备、块设备、网络设备
(3)设备文件、主设备号、从设备号
有了设备类型的划分,还需要进行进一步明确。所以驱动设备会生成字符节点,以文件的方式存放在/dev目录下,操作时可抽象成文件操作即可。每个设备节点有主设备号和次设备号,用一个32位来表示,前12位表示主设备号,后20位表示次设备号。例如"/dev/fb0","/dev/fb1"或者"/dev/tty1","/dev/tty2"等等。
第一步:写出驱动程序的框架
前面在创建驱动程序的框架时,只是测试了安装与卸载驱动,并且找到驱动程序的入口与出口。并没有一个字符设备操作的接口。作为一个字符设备驱动程序,其open,read,write等函数是必要的。但是最开始还是要实现一个驱动程序的入口与出口函数。
#include <linux/init.h>
#include <linux/module.h>
static int __init dev_fifo_init()
{
return ;
}
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, );
//静态注册设备号
ret = register_chrdev_region(dev_num,,"dev_fifo");
if(ret < )
{
//静态注册失败,进行动态注册设备号
ret = alloc_chrdev_region(&dev_num,,,"dev_fifo");
if(ret < )
{
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, );
第三步:创建设备类
这一步会在/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,);
if (ret < )
{
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",);
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看到自己创建的设备节点。相关源代码可参考附录。
Linux将所有的设备都抽象成文件,这样的操作接口比较的统一,也给开发带来很大的方便。通过将写好的驱动程序装载到内核可见的区域,使得内核感知到模块的存在,然后用户空间才能通过系统调用联系到驱动,从而完成它的任务。
写驱动程序需要按照一定的步骤,首先申明驱动的入口和出口,然后注册设备号。接着填充file_operations结构体。引用程序通过调用open,read,或者write函数,最终调用到file_operations的open,read或者write函数,从而实现了从应用层到内核层的调用。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
//指定的主设备号
#define MAJOR_NUM 250
//自己的字符设备
struct mycdev
{
int len;
unsigned char buffer[];
struct cdev cdev;
};
MODULE_LICENSE("GPL");
//设备号
static dev_t dev_num = {};
//全局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 ;
}
//读设备
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 ;
//请求大大小 > 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 != )
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 != )
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, );
//静态注册设备号
ret = register_chrdev_region(dev_num,,"dev_fifo");
if(ret < ){
//静态注册失败,进行动态注册设备号
ret = alloc_chrdev_region(&dev_num,,,"dev_fifo");
if(ret < ){
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,);
if (ret < )
{
goto err_cdev_add;
}
//导出设备信息到用户空间(/sys/class/类名/设备名)
device = device_create(cls,NULL,dev_num,NULL,"dev_fifo%d",);
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 ;
err_device_create:
cdev_del(&gcd->cdev);
err_cdev_add:
class_destroy(cls);
err_class_create:
unregister_chrdev_region(dev_num, );
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, );
return;
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);
MODULE_LICENSE("Dual DSB/GPL");
MODULE_AUTHOR("ZHAO");