linux驱动框架

对于初学者,直接去查看一些成熟的驱动代码例如TP驱动的,可能一开始会比较迷糊,为什么这里要用这个API,为什么要传这个参数,这个结构体是什么,个人觉得如果能够从0开始去了解一下驱动框架,后续去看一些其他的驱动代码就不会太晕。

话不多说,下面会以4例驱动代码进行讲述,并且在代码中进行注释讲解。

一、手动创建设备节点加载字符驱动

代码:

#include <linux/module.h>
//字符设备头文件
#include <linux/cdev.h>
#include <linux/fs.h>
//通过预先cat proc/devices,查看系统当前的设备号占用情况定义一个当前驱动程序的宏
#define HELLO_MAJOT_NUM 100

//创建一个字符设备对象
static struct cdev hello_cdev;

//创建一个dev_t对象,在内核中,设备编号信息都是存储在dev_t结构体内
static dev_t hello_dev;

static int hello_cdev_open(struct inode *inode,struct file *file)
{
    pr_info("hello_cdev_open\n");
    return 0;
}

//fops,用于提供各种API给上层应用程序进行调用以及声明内核模块
const struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_cdev_open,
};

static int __init hello_init(void)
{
    int ret;
    //MKDEV(int major,int minor),MKDEV是将主设备号和次设备号转换成dev_t类型的一个内核函数。
    dev_t dev = MKDEV(HELLO_MAJOT_NUM,0);
    pr_info("hello_init mkdev success\n");

    /*int register_chrdev_region(dev_t first, unsigned int count, char *name);注册并且获取此设备号,驱动程序需要判断这个设备是否存在才可进行下一步操作*/
    ret = register_chrdev_region(dev,1,"hello");
    if ( ret < 0 ){
        pr_info("unable malloc mayor number %s \n",MY_MAJOR_NUM);
        return ret;
    }

    //void cdev_init(struct cdev *cdev, struct file_operations *fops),初始化字符设备,并且添加至内核中
    cdev_init(&hello_cdev, &hello_fops);
    //int cdev_add(struct cdev *p, dev_t dev, unsigned count)  添加至cdev结构内
    ret = cdev_add(&hello_cdev, hello_dev, 1);
    if ( ret < 0){
        pr_info("cdev_add failed\n");
        return ret;
    }

    return 0;
}

static void __exit hello_exit(void)
{
    //void cdev_del(struct cdev *) ,删除一个字符设备
    cdev_del(&hello_cdev);
    //释放在hello_init内申请的设备号
    unregister_chrdev_region(hello_dev, 1);
    return;
}

//驱动程序的入口
module_init(hello_init);
//驱动函数的出口
module_exit(hello_exit);
//必须使用MODULE_LICENSE进行声明,否则内核会报错被污染
MODULE_LICENSE("GPL");

看完上面简单的代码,可以稍微总结一下手动创建设备节点来加载字符驱动的步骤:

  1. 通过cat proc/devices选择一个未被占用的主设备号

  1. 调用MKDEV,使用一个dev_t类型对象来存储主设备号、次设备号

  1. 调用register_chrdev_region,将该设备注册到内核中

  1. 由于我们希望创建一个字符设备,因此调用cdev_init进行字符设备的初始化以及cdev_add将其放置到字符设备结构内

  1. 通过手动mknod一个设备后,再执行insmod挂载该驱动程序

看完上面的总结,会有个问题:

  1. 如果其他的驱动采用的是随机的方式进行主设备号的分配,但是我的驱动将我的设备 主设备号给写死了,当其他设备号随机创建的主设备号与我分配的一致,是否会发生冲突?

  1. 每次启动都要mknod创建一个设备,能否将这一步也放到驱动程序内去完成?

那么基于上面的问题,第二种方式:自动创建设备框架就呼之欲出了

二、自动创建设备加载字符驱动

代码:

#include <linux/module.h> 
#include <linux/cdev.h> 
#include <linux/fs.h>
//class和device依赖 的头文件
#include <linux/device.h> 

#define CLASS_NAME "hello"

static dev_t dev_num;
static struct cdev cdev_hello;
static struct class *class_dev;

static int hello_open(struct inode *inode, struct file *file)
{
    pr_info("alloc_chrdev hello_open\n");
    return 0;
}

const static struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
};

static int __init hello_init(void)
{
    int ret;
    int minor,major;
    dev_t dev;
    struct device *device_hello;
    //int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name),自动分配一个设备号给dev,设备名称为hello_dev
    ret = alloc_chrdev_region(&dev, 0, 1, "hello_dev");
    if ( ret < 0 ){
        pr_info("alloc_chrdev_region failed \n");
        return ret;
    }
    
    cdev_init(&cdev_hello,&hello_fops);
    ret = cdev_add(&cdev_hello, dev_num,1);
    if ( ret < 0 ){
        pr_info("cdev_add failed \n");
        unregister_chrdev_region(dev_num,1);
        return ret;
    }

    //class_create()用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进Linux内核系统中。class_create(owner, name)
    class_dev = class_create(THIS_MODULE,CLASS_NAME);
    if ( IS_ERR(class_dev)){
        pr_info("class create failed \n");
        cdev_del(&cdev_hello);
        unregister_chrdev_region(dev_num,1);
        return PTR_ERR(class_dev);
    }

    //创建设备节点struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt
    device_hello = device_create(class_dev, NULL, dev_num, NULL, CLASS_NAME);
    if ( IS_ERR(device_hello)){
        pr_info("device create failed \n");
        class_destroy(class_dev);
        cdev_del(&cdev_hello);
        unregister_chrdev_region(dev_num,1);
        return PTR_ERR(device_hello);
    }

    pr_info("dev/%s success\n", CLASS_NAME);
    return 0;
}


static void __exit hello_exit(void)
{
    //void device_destroy(struct class *cls, dev_t devt);
    device_destroy(class_dev, dev_num);
    // void class_destroy(struct class *cls);
    class_destroy(class_dev);
    cdev_del(&cdev_hello);
    unregister_chrdev_region(dev_num,1);
    pr_info("hello_exit \n");
    return ;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

对比于第一种手动创建设备的方式,第二种就将我们提出的两个问题给完美解决了。代码量看起来较第一种不减反增的原因是需要考虑失败的情况下,将程序内申请的内存都给释放掉,否则会导致内核崩溃。

那么第二种自动创建设备挂载字符驱动程序的步骤为:

  1. 通过alloc_chrdev_region自动分配主设备号并且创建一个设备

  1. 调用cdev_init,cdev_add对上面创建的设备进行初始化为字符设备

  1. 通过class_create创建一个逻辑类

  1. 通过device_create以及3中创建的逻辑类,在/dev下创建一个设备节点

再次通过自问的方式提出问题:

  1. 既然主设备号、次设备号都可以通过linux驱动框架中的API完成,那么2、3、4能否也通过某个api进行封装?否则那么多的驱动都要编写同样的代码,代码冗余非常严重

  1. 我明明只要创建一个设备节点来挂载,为什么还要在sys/class下生成一个类?

那么就引申出了第三种和第四种驱动框架:杂项(misc)驱动框架以及平台(platform)驱动框架

三、杂项(misc)驱动框架

代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h> 

static int hello_misc_open(struct inode* inode,struct file* file)
{
    pr_info("hello_misc_open \n");
    return 0;
}

static const struct file_operations misc_fops = {
    .owner = THIS_MODULE,
    .open = hello_misc_open,
};

static struct miscdevice misc_dev = {
    //设置次设备号,直接调用MISC_DYNAMIC_MINOR宏,让misc驱动框架自行分配即可
    .minor = MISC_DYNAMIC_MINOR,
    //创建的设备节点名称
    .name = "hello",
    .fops = &misc_fops,
};

static int __init hello_init(void)
{
    int ret;
    //int misc_register(struct miscdevice *misc),调用misc_register函数,注册一个misc驱动
    ret = misc_register(&misc_dev);
    if ( ret < 0 ){
        pr_err("misc_register failed \n");
        return ret;
    }
    pr_info("misc_register success, minor = %s \n",misc_dev.minor);
    return 0;
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

现在可以看到我们的代码量锐减到39行了,我们1、2中的各种分配主设备号、创建设备节点等函数都不需要再调用了,因为misc_register已经将他们给封装起来了。misc框架内所有的设备都共用一个主设备号,因此我们只需要misc框架分配一个次设备号即可。

当我们misc_register时他会将misc_dev对象内的各个属性进行处理。这里可以稍微看一下misc_register的实现,以便我们更进一步了解misc驱动框架与1、2的联系

int misc_register(struct miscdevice *misc)
{
        dev_t dev;
        int err = 0;
        //判断申请的次设备号是否使用MISC_DYNAMIC_MINOR宏,由misc框架自行分配次设备号
        bool is_dynamic = (misc->minor == MISC_DYNAMIC_MINOR);
        INIT_LIST_HEAD(&misc->list);
        mutex_lock(&misc_mtx);
        if (is_dynamic) {
                //寻找第一个bit为0的设备号,第一个bit为0代表该次设备号尚未被占用
                int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS);
                if (i >= DYNAMIC_MINORS) {
                        err = -EBUSY;
                        goto out;
                }
                misc->minor = DYNAMIC_MINORS - i - 1;
                //DYNAMIC_MINORS - i - 1这个次设备号已经被当前设备占用了,因此要设置一下。
                set_bit(i, misc_minors);
        } else {
                struct miscdevice *c;
                list_for_each_entry(c, &misc_list, list) {
                        if (c->minor == misc->minor) {
                                err = -EBUSY;
                                goto out;
                        }
                }
        }
        //1中的分配设备主次设备号又出现了,被封装在misc_register内实现了
        dev = MKDEV(MISC_MAJOR, misc->minor);
        misc->this_device =
                device_create_with_groups(misc_class, misc->parent, dev,
                                          misc, misc->groups, "%s", misc->name);
        if (IS_ERR(misc->this_device)) {
                if (is_dynamic) {
                        int i = DYNAMIC_MINORS - misc->minor - 1;
                        if (i < DYNAMIC_MINORS && i >= 0)
                                clear_bit(i, misc_minors);
                        misc->minor = MISC_DYNAMIC_MINOR;
                }
                err = PTR_ERR(misc->this_device);
                goto out;
        }
        //添加到misc框架链表内。
        list_add(&misc->list, &misc_list);
 out:
        mutex_unlock(&misc_mtx);
        return err;
}

所以,实际上的misc框架就是将我们1、2内的一些步骤结合他本身的特性(misc框架内的所有设备都共用一个主设备号10),因此显得异常简洁。在开发过程中有一些驱动我们确实不知道他是属于哪个类别的话可以使用misc框架。

问题:

我们上面的1、2、3都是针对字符驱动进行讲解的,那如果我想编写一个非字符驱动程序,有什么其他比较简洁的框架嘛?

当然有!除了例如spi、i2c等总线框架以外,运用较为广泛的就是platform平台驱动框架。

四、platform平台驱动框架

代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h> 

static int hello_plat_misc_open(struct inode* inode,struct file* file)
{
    pr_info("hello_misc_open \n");
    return 0;
}

static const struct file_operations hello_plat_misc = {
    .owner = THIS_MODULE,
    .open = hello_plat_misc_open,
};

static struct miscdevice hello_misc_device = {
    .minor = MISC_DYNAMIC_MINOR,
    //创建节点名称
    .name = "hello_plat_misc",
    .fops = &hello_plat_misc,
};

static int hello_probe(struct platform_device *pdev)
{
    int ret;
    pr_info("hello_probe start \n");

    ret = misc_register(&hello_misc_device);
    if ( ret < 0 ){
        pr_err("hello_misc_register failed \n");
        return ret;
    }

    pr_info("hello_probe success\n");
    return 0;
}

static int hello_remove(struct platform_device *pdev)
{
    pr_info("hello_plat_dev exit\n");

    misc_deregister(&hello_misc_device);
    return 0;
}

static const struct of_device_id hello_match_table[] = {
    {.compatible = "hello,devicetree"},
    {},
};

//必须使用MODULE_DEVICE_TABLE进行转换,将该设备添加到外设列表中
MODULE_DEVICE_TABLE(of, hello_match_table);

static struct platform_driver hello_platform_driver = {
    .probe = hello_probe,
    .remove = hello_remove,
    .driver = {
        .name = "hello_platform",
        //of_match_table用于与设备树内的节点进行匹配
        .of_match_table = hello_match_table,
        .owner = THIS_MODULE,
    }
};
//注册平台总线驱动
module_platform_driver(hello_platform_driver);
MODULE_LICENSE("GPL");

设备树

        hello {
                compatible = "hello,devicetree";
        };

平台总线驱动与1、2、3中的又有不同,引入了设备树。这是因为当我们调试一个平台性或者说经常调试参数的外设而言,如果将各种参数例如寄存器地址、值、gpio状态以及值都写死在驱动程序内的话,那么调试起来会十分麻烦,这也不符合驱动框架的最初愿景,因此直接将设备节点与驱动程序进行分离,驱动程序只负责对各种参数进行处理,设备节点则负责存储这些对应的值,由于本文的初衷只是简单讲解一下各个驱动框架的不同,因此就不涉及具体驱动的开发讲解。

总结

对于上述四种驱动框架,实际上我们只需要了解3、4即可。编写1、2的初衷是希望一开始学习驱动框架的同学能够了解为什么3、4里面要调用这些框架API,他们的底层原理是什么,而不是死记硬背框架API。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值