字符设备驱动开发流程

在嵌入式linux开发中,主要包括字符设备开发、块设备开发以及网络设备开发三种。字符设备开发是最常见的驱动开发,包括我们常见的gpio、led、SPI、I2C等,都是字符设备开发的一种。

一、常用概念

1、Linux设备号

为了方便系统管理驱动以及设备,linux中每个设备都有设备号,分为主设备号、次设备号两部分。其中,主设备号代表某个具体的驱动,次设备号表示使用这个驱动的某个具体的设备
linux提供了一个名为dev_t的数据结构表示设备号,其是一个32位的数据类型,其中高12位表示主设备号、低20位代表次设备号

  • 设备号的分配:
    设备号可以采用静态分配或者动态分配两种,其中前者是我们手动进行分配,后者则是系统内核自动给我们分配,一般推荐动态分配。
    用于动态设备设备号的函数如下:
int alloc_chrdev_region(dev_t* dev,unsigned baseminor,unsigned count, const char* name)
//dev:用于保存我们申请的设备号
//baseminor:次设备号的起始地址,该函数可以申请很多主设备号一样、次设备号不同的设备号
//count:设备号申请的数量
//name:设备名称

//如果已经有设备号了,使用该函数来注册设备号即可
int register_chrdev_region(dev_t from,unsigned count,const char* name);
//销毁字符设备后要释放设备号,释放函数如下:
void unregister_chrdev_region(dev_t from,unsigned count);

二、字符设备驱动开发框架

linux驱动开发并不像裸机开发那样,驱动程序可以按照我们的意愿各种变换,其拥有一套规定的框架,我们只需要按照这套框架来编写我们的驱动程序即可。

2.1 字符驱动的加载与卸载

驱动的加载有两种形式:

  • 一种为在我们内核编译的时候,就直接将驱动编译进入内核,这样驱动就会一直存在于内核中,比如我们想要加载V4L2驱动,一般在内核图形化配置的时候,就会直接选中加载入内核中;
  • 另一种为驱动模块化(linux下扩展名为.ko),这样我们使用“modprobe”或者“insmod”命令,就可以将驱动模块加载进内核。在调试过程中,一般我们采用模块化驱动,这样就可以只编译驱动即可,而不需要编译整个内核了。

驱动的加载与卸载操作需要对用两种操作函数,模块加载和卸载注册函数,如下:

module_init(xxx_init); //加载
module_exit(xxx_exit);//卸载

2.2 字符设备结构cdev

在linux下使用cdev这个结构体来表示一个字符设备,定义如下:

struct cdev {
    struct kobject kobj;
    struct module *owner; //指向该设备使用的字符驱动模块
    const struct file_operations *ops; //操作函数
    dev_t dev; //设备号
    unsigned int count;
};

struct cdev test_cdev;//在编写驱动前,先定义一个字符设备结构体

定义好后,对其进行初始化,如下:

void cdev_init(struct cdev* cdev,const struct file_operation *fops);

然后向linux系统中添加字符设备,使用函数如下:

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

卸载驱动的时候要删除字符设备

cdev_del(struct cdev *p);

2.3 自动创建设备节点

自动创建设备节点的工作是在驱动程序初始化中完成的,一般在cdev_add函数后面添加自动创建设备节点的代码。
首先要创建一个类,每一个设备都应该属于一个类下,然后在这个类下创建具体的设备,比如创建一个类LED,这个类下面包括led0、led1等具体的设备。

struct class *class_create(struct module *owner,const char *name);
//owner表示驱动程序
//name:类名称

类创建好之后,就是创建我们的设备节点,函数如下:

struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt,...)
//class表示设备要创建在那个类下
//parent:设备的父设备,一般为NULL
//devt:设备号
//drvdata:设备可能使用的一些数据,一般为NULL
//fmt:设备名称,如果fmt=xxx,则会在/dev/class/xxx
//最后返回的就是这个具体的设备

2.4 完整流程:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

static int major;
static struct cdev my_cdev;
static struct class *my_class;

static int my_open(struct inode *inode, struct file *file) {
    // 初始化设备资源
    return 0;
}

static int my_release(struct inode *inode, struct file *file) {
    // 释放设备资源
    return 0;
}

static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
    // 从设备读取数据到用户空间
    return count;
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    // 将用户空间数据写入设备
    return count;
}

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
};

static int __init my_driver_init(void) {
    dev_t dev;
    int ret;

    // 动态分配主设备号
    ret = alloc_chrdev_region(&dev, 0, 1, "my_device");
    if (ret < 0) {
        printk(KERN_ERR "Failed to allocate chrdev region\n");
        return ret;
    }

    major = MAJOR(dev);

    // 初始化字符设备结构体
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    // 添加字符设备到内核
    ret = cdev_add(&my_cdev, dev, 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add cdev\n");
        unregister_chrdev_region(dev, 1);
        return ret;
    }

    // 创建设备类
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        printk(KERN_ERR "Failed to create class\n");
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(my_class);
    }

    // 创建设备节点(/dev/my_device)
    device_create(my_class, NULL, MKDEV(major, 0), NULL, "my_device");

    printk(KERN_INFO "Character device registered with major %d\n", major);
    return 0;
}

static void __exit my_driver_exit(void) {
    dev_t dev = MKDEV(major, 0);

    device_destroy(my_class, dev);
    class_unregister(my_class);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev, 1);

    printk(KERN_INFO "Character device unregistered\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");

总之,编写字符驱动的过程如下:

  1. 注册一个cdev结构体
  2. 编写fop函数
  3. 申请设备号dev_t
  4. cdev初始化
  5. 向内核添加cdev
  6. 创建设备类
  7. 创建字符设备节点

更多推荐