专栏首页嵌入式iotlinux字符设备驱动基本框架

linux字符设备驱动基本框架

对于Linux的驱动程序,需要遵循一定的框架结构。嵌入式Linux的学习其实并不难,只要深入理解Linux的框架,写起来也可以得心应手。

1.linux函数调用过程

1.1 系统函数调用的意义

在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()串口打印调用的过程。

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 <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函数。

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 <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看到自己创建的设备节点。相关源代码可参考附录。

4. 总结

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");

本文分享自微信公众号 - 嵌入式IoT(Embeded_IoT),作者:bigmagic

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-04-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 全志V3S 编译运行xboot笔记

    现在手上有一块V3S的板子,想将其用起来。现在来记录一下这个过程。记录一下荔枝派zero的使用流程。

    bigmagic
  • 摄像头图像处理YUV转RGB效率分析

    1.文章简述2. YUV转RGB的代码优化问题2.1 浮点转换2.2 浮点转整形2.3 浮点运算和整数运算在PC上模拟的效果3. x1000上进行对比测试3.1...

    bigmagic
  • 如何在树莓派2上移植rt-thread操作系统

    2.Ubuntu上需要装scons,因为rt-thread操作系统是通过scons组织的。

    bigmagic
  • JavaScript对象编程-第3章

    时间:UTC全球标准时间,也被称为格林威治标准时间,UTC+800指比格林威治标准时间提前8个小时,也是北京时间。

    达达前端
  • Spark Streaming如何使用checkpoint容错

    我是攻城师
  • 【从零学习OpenCV 4】均值滤波

    经过几个月的努力,小白终于完成了市面上第一本OpenCV 4入门书籍《从零学习OpenCV 4》。为了更让小伙伴更早的了解最新版的OpenCV 4,小白与出版社...

    小白学视觉
  • WCF技术剖析之十七:消息(Message)详解(下篇)

    《WCF技术剖析(卷1)》自出版近20天以来,得到了园子里的朋友和广大WCF爱好者的一致好评,并被卓越网计算机书店作为首页推荐,在这里对大家的支持表示感谢。同时...

    蒋金楠
  • 物联网名词解释

    公众号php_pachong
  • Date对象和Math对象

        1.  var  date=new Date()    //无参数的情况下返回值为当前时间

    用户3159471
  • 微信Kbone-API正式上线,解决Web端和小程序的兼容问题

    微信开发者有福了,昨天,微信官方宣布,为了让 Kbone 开发者可以更加顺畅的使用,微信推出了 Kbone-API 来帮开发者解决兼容问题。

    德顺

扫码关注云+社区

领取腾讯云代金券