前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >linux字符设备驱动基本框架

linux字符设备驱动基本框架

作者头像
bigmagic
发布2020-03-17 14:43:45
5.9K0
发布2020-03-17 14:43:45
举报
文章被收录于专栏:嵌入式iot

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

1.linux函数调用过程

1.1 系统函数调用的意义

在Linux的中,有一个思想比较重要:一切皆文件。

也就是说,在应用程序中,可以通过open,write,read等函数来操作底层的驱动。

比如操作led,函数如下

代码语言:javascript
复制
//点灯
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 驱动程序框架的一个例子

对于一个驱动程序,如果想让内核知道,就准守一定的框架,下面来看一下一个最简单的驱动程序的框架

代码语言:javascript
复制
#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程序

代码语言:javascript
复制
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

其中需要解释一下的是

代码语言:javascript
复制
 $(MAKE) -C $(KDIR) M=$(PWD) modules

该命令是make modules命令的扩展,-C选项的作用是指将当前的工作目录转移到指定目录,即(KDIR)目录,程序到(shell pwd)当前目录查找模块源码,将其编译,生成.ko文件。

生成的.ko文件就是驱动程序,如果要将当前的驱动程序插入到内核中,可以在控制台输入

代码语言:javascript
复制
sudo insmod test.ko

该命令会执行test_init函数。如果要查看内核打印信息,可输入dmesg。用lsmod可查看目前挂载的驱动程序。

如果要移除当前的驱动程序,可调用

代码语言:javascript
复制
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等函数是必要的。但是最开始还是要实现一个驱动程序的入口与出口函数。

代码语言:javascript
复制
#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");

第二步:在驱动入口函数中申请设备号

一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。

代码语言:javascript
复制
//设备号 : 主设备号(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;
    }
}

静态分设备号的函数原型

代码语言:javascript
复制
register_chrdev_region(dev_t first,unsigned int count,char *name)

1:第一个参数:要分配的设备编号范围的初始值, 这组连续设备号的起始设备号, 相当于register_chrdev()中主设备号

2:第二个参数:连续编号范围. 是这组设备号的大小(也是次设备号的个数)

3:第三个参数:编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称

其中动态分配的函数原型

代码语言:javascript
复制
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:第四个参数:驱动的名字

由于每个设备只有一个主设备号,所以如果用静态分配设备号时,有可能会导致分配不成功,所以采用动态分配的方式。

注意,在入口函数中注册,那么一定要记得在驱动出口函数中释放

代码语言:javascript
复制
//释放申请的设备号
unregister_chrdev_region(dev_num, );

第三步:创建设备类

这一步会在/sys/class/dev_fifo下创建接口

sysfs 文件系统总是被挂载在 /sys 挂载点上。虽然在较早期的2.6内核系统上并没有规定 sysfs 的标准挂载位置,可以把 sysfs 挂载在任何位置,但较近的2.6内核修正了这一规则,要求 sysfs 总是挂载在 /sys 目录上。

代码语言:javascript
复制
//创建设备类
cls = class_create(THIS_MODULE, "dev_fifo");
if(IS_ERR(cls))
{
    ret = PTR_ERR(cls);
    goto err_class_create;
}

第四步:初始化字符设备

在这一步中,会初始化一个重要的结构体,file_operations。

代码语言:javascript
复制
//初始化字符设备
cdev_init(&gcd->cdev,&fifo_operations);

该函数的原型为

代码语言:javascript
复制
 cdev_init(struct cdev *cdev, const struct file_operations *fops)

第一个参数时字符设备结构体,第二个参数为操作函数

Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。

用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。

通常来说,字符设备驱动程序经常用到的5种操作

代码语言:javascript
复制
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 *);//关闭
};

第五步:添加设备到用户操作系统

代码语言:javascript
复制
//添加设备到操作系统
ret = cdev_add(&gcd->cdev,dev_num,);
if (ret < )
{
    goto  err_cdev_add;
}

函数原型为

代码语言:javascript
复制
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

第一个参数为cdev 结构的指针

第二个参数为设备起始编号

第三个参数为设备编号范围

这一步的含义在于将字符设备驱动加入到操作系统的驱动数组中。当应用程序调用open函数时,会首先找到该设备的设备号,然后根据这个设备号找到相应file_operations。调用其中的open以及读写函数。

第六步:导出设备信息到用户空间

代码语言:javascript
复制
//导出设备信息到用户空间(/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; 
}

函数原型

代码语言:javascript
复制
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函数,从而实现了从应用层到内核层的调用。

附录:程序代码

代码语言:javascript
复制
#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");
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-04-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式IoT 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.linux函数调用过程
    • 1.1 系统函数调用的意义
      • 1.2 系统函数的调用过程
        • 1.3 用户空间与内核空间
        • 2.驱动程序的框架
          • 2.1 Linux下的设备
            • 2.2 驱动程序框架的一个例子
            • 3.字符设备驱动程序解析
              • 3.1 基本概念
                • 3.2 创建流程
                • 4. 总结
                • 附录:程序代码
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档