Linux教程

Linux驱动开发三.驱动框架重构

本文主要是介绍Linux驱动开发三.驱动框架重构,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

通过前面两章内容我们先后做了个虚拟设备驱动,还成功共过驱动文件操作GPIO的点亮了LED,但是那个驱动的架构是有些问题的:

  • 需要自己设定主次设备号,并且要在去驱动中定义好设备号。移植性差,在A机子开发的驱动放在B设备上可能设备号被占用,需要重新i修改驱动,并且要手动查询哪些设备号可以被使用。
  • 每次模块加载完成后还要手动创建设备节点,操作复杂
  • 在注册模块时候用到函数为register_chrdev,这个注册函数使用的时候只传了主设备号,由于设备号是32位的,其中高12位为主设备号,低20位为次设备号。问题就是我们注册了设备号以后这个主设备号下面所有次设备号都被占用了,比较浪费资源

针对上面几点,我们需要对前面的驱动进行修改。

指定设备号

Linux为我们提供了专门的函数用来指定设备号。相当于每当我们有个新的设备需要驱动,就向内核申请一个组设备号,内核根据当前状态给出一个合理的设备号供我们使用。等到需要卸载设备时将其释放掉即可,整个过程是动态的,不需要开发人员认为干预。

这里就要用到下面的函数

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
这个函数里的参数源代码里没有给说明,这个是从网上查到的。这个函数的几个参数,第一个是传递一个新设备(结构体,我们稍后会讲)指针,第二个次设备号的起始地址,第三个是设备数量,最后是设备名。设备号通过函数内指针操作传给第一个指针变量。 使用方法如下
struct new_device
{
    dev_t dev_id;       //设备号
    int major;          //主设备号
    int minor;          //次设备号
};


struct new_device led;

static int main(void){
    int ret = 0;
    
    ret = alloc_chrdev_region(&led.dev_id,0,1,DEV_NAME);
    return 0;
}

我们先定义一个结构体,里面内容为设备号、主设备号和次设备号。然后声明一个该结构体的变量(led),在主函数中我们使用alloc_chrdev_region函数获取设备号,由于函数第一个参数是指针类型,所以我们传参的时候用了取址符。调用函数的时候回将设备号赋值给dev_id,需要设备号时就可以使用这个dev_id。Linux还为我们提供了新的函数换算设备号

主要是major和minor的用法,下面是内核里对几个宏的说明

#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)

#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

主设备号就是将设备号右移20位(取高12位),次设备号也是是通过相应计算直接拿到,可以通过下面的代码调试

ret = alloc_chrdev_region(&led.dev_id,0,1,DEV_NAME);

led.major = MAJOR(led.dev_id);
led.minor = MINOR(led.dev_id);

printk("dev_t = %d,major = %d,minor = %d\r\n",led.dev_id,led.major,led.minor);

主次设备号就可以通过上面的方法拿到,写到这可以make成ko文件放在rootfs里加载一下

设备号为261095424,换算后高12位就是249,低20位都是0,没问题!

设备号释放

前面说过,Linux内大部分功能函数都是成对出现的,有设备号申请,肯定有个释放和其配对使用

void unregister_chrdev_region(dev_t from, unsigned count)

用上面的函数就可以释放掉我们申请的设备号。当然,和前面的用法一样,设备号的释放要放在模块卸载对应的函数中。

unregister_chrdev_region(led.dev_id,1);

这样就行了!

字符设备cdev结构体

我们在前面的驱动框架中,我们在调用设备注册函数时

ret = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops);

给了一个参数led_fops,也就是文件操作结构体,在这个结构体中我们关联了驱动文件打开、关闭以及读写时对应的函数。但是这个新的驱动架构中我们没有使用这个函数,那么怎么告知文件打开、关闭及读写时对应的函数呢?这里就要用到一个新的结构体——cdev。cdev主要用来描述一个字符设备,主要包含了设备号(dev_t)和文件操作VFS接口函数(file_operations)。

struct cdev {
    struct kobject kobj;                    //内嵌的内核对象
    struct module *owner;                   //该字符设备所在内核模块的对象指针
    const struct file_operations *ops;      //驱动文件操作接口
    struct list_head list;                  //用于将已经注册的字符设备形成链表
    dev_t dev;                              //设备号
    unsigned int count;                     //隶属于该主设备号下次设备号的个数
};

上面是内核中定义的cdev数据类型,可以看出来里面包含了file_operations这个元素,所以我们需要使用cdev来关联文件操作结构体。

本章一开始第二个程序段里我们定义了一个包含设备号信息的新设备结构体,下面我们要把cdev声明到这个结构体中

struct new_device
{
    struct cdev cdev;   //字符设备
    dev_t dev_id;       //设备号
    int major;          //主设备号
    int minor;          //次设备号
};

static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
    .release = led_release
};


struct new_device led;

我们声明的变量名称和数据类型一样,这个是没问题的, 使用的时候可以直接调用,后面声明的led_fops变量对应的文件VFS接口函数,。

cdev和ops之间关联

虽然我们声明了cdev和led_fops,但是这两个之间还没有直接关联,二者的关联需要一个函数执行:

void cdev_init(struct cdev *, const struct file_operations *);

函数很简单,我们只需要将cdev的地址和led_fops传过去就行了。

cdev_init(&led.cdev, &led_fops);

注意参数都是指针变量,要加取址符

cdev添加和删除

关联好文件操作接口的cdev还需要用一个函数将其添加到Linux系统中

int cdev_add(struct cdev *, dev_t, unsigned count);

参数为cdev,设备号及要增加设备的数量。

ret = cdev_add(&led.cdev,led.dev_id, 1);

由于我们要添加到设备只有1个,所以直接给了个1,就可以了。

有添加就肯定有卸载,卸载的函数要简单些

void cdev_del(struct cdev *);

只需要把cdev传给他就可以了。

static int __exit led_exit(void){
    cdev_del(&led.cdev);
    //注销设备号
    unregister_chrdev_region(led.dev_id,1);
    return 0;
}

要注意应该先删除设备,再释放设备号。

整个框架代码如下:

/**
 * @file led.c
 * @author your name (you@domain.com)
 * @brief led点亮程序模块测试
 * @version 0.1
 * @date 2022-04-04
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/types.h>
#include <linux/cdev.h>

#define DEV_NAME    "LED"   //设备名称

struct new_device
{
    struct cdev cdev;   //字符设备
    dev_t dev_id;       //设备号
    int major;          //主设备号
    int minor;          //次设备号
};



struct new_device led;

static int led_open(struct inode *inode, struct file *filp)
{
    printk("dev open!\r\n");
    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    printk("dev release!\r\n");
    return 0;
}


static ssize_t led_write(struct file *file, 
                        const char __user *buf, 
                        size_t count, 
                        loff_t *ppos)
{   
    return 0;
}   

static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
    .release = led_release
};

static int __init led_init(void){
    int ret = 0;

    /*程序中未经指定设备号,直接注册设备*/
    if(led.major){                      
        led.dev_id = MKDEV(led.major,0);
        ret = register_chrdev_region(led.dev_id,1,DEV_NAME);
    }

    /*程序中未指定设备号,申请设备号*/
    else{
        ret = alloc_chrdev_region(&led.dev_id,0,1,DEV_NAME);
        led.major = MAJOR(led.dev_id);
        led.minor = MINOR(led.dev_id);
    }
    if(ret<0){
        printk("new device region err!\r\n");
        return -1;
    }
    printk("dev_t = %d,major = %d,minor = %d\r\n",led.dev_id,led.major,led.minor);

    led.cdev.owner = THIS_MODULE;

    cdev_init(&led.cdev, &led_fops);

    ret = cdev_add(&led.cdev,led.dev_id, 1);

    return 0;
}


static int __exit led_exit(void){
    cdev_del(&led.cdev);
    //注销设备号
    unregister_chrdev_region(led.dev_id,1);
    return 0;
}


module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZeqiZ");

我们要使用的话直接修改初始化和文件操作函数就可以了! 

这篇关于Linux驱动开发三.驱动框架重构的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!