定义

字符设备驱动注册过程

  1. 申请设备号:使用 register_chrdev()alloc_chrdev_region() 申请主设备号。
  2. 初始化 cdev 结构:用 cdev_init() 初始化 cdev 结构,并与 file_operations 结构绑定。
  3. 注册字符设备:使用 cdev_add() 注册字符设备到内核中。
  4. 创建设备文件:通过 mknod()udev 自动创建设备文件,供用户程序访问。
  5. 提供文件操作函数:在 file_operations 结构中实现设备的读写、打开、关闭等操作。
  6. 卸载驱动:通过 cdev_del() 卸载设备,并释放资源。

cdev字符设备描述

/**
 * @brief 内核字符设备结构体
 *
 * 结构体 cdev 用于表示一个字符设备,并封装了字符设备的操作函数。
 * 这个结构体通常与设备号一起使用,用于管理字符设备驱动程序。
 *
 * @note 内核提供了 cdev_init() 和 cdev_add() 等函数来初始化和注册 cdev 设备。
 */
struct cdev
{
    struct kobject kobj;               ///< 内核对象,用于内核对象管理
    struct module *owner;              ///< 拥有此设备的模块,用于模块归属和引用计数
    const struct file_operations *ops; ///< 文件操作集合,指向 struct file_operations 结构体,定义了设备的操作函数
 
    struct list_head list; ///< 链表头,用于将设备加入到内核的设备链表中
    dev_t dev;             ///< 设备号,由主设备号和次设备号组成,用于设备的唯一标识
    unsigned int count;    ///< 计数器,用于跟踪设备使用情况
};

cdev操作函数

  • 初始化
/**
 * @brief 初始化字符设备
 *
 * 该函数用于初始化一个cdev结构体指针,设置其操作函数集合。
 *
 * @param p    指向cdev结构体的指针
 * @param fops 指向file_operations结构体的指针,包含设备操作函数
 */
void cdev_init(struct cdev *p, struct file_operations *fops);
  • 分配一个新的cdev结构体
/**
 * @brief 分配一个新的cdev结构体
 *
 * 该函数用于动态分配一个cdev结构体,并返回其指针。
 * 分配成功后,应使用cdev_init()进行初始化。
 *
 * @return 返回新分配的cdev结构体指针,如果分配失败则返回NULL
 */
struct cdev *cdev_alloc(void);
  • 释放引用
/**
 * @brief 释放cdev结构体的引用
 *
 * 当不再需要使用cdev结构体时,应调用此函数来释放引用。
 * 通常在设备卸载时调用此函数。
 *
 * @param p 指向cdev结构体的指针
 */
void cdev_put(struct cdev *p);
  • 向内核注册字符设备
/**
 * @brief 向内核注册字符设备
 *
 * 该函数用于将cdev结构体与指定的设备号相关联,并将其添加到内核中。
 * 注册成功后,其他进程可以通过设备号与该字符设备进行交互。
 *
 * @param p    指向cdev结构体的指针
 * @param dev  设备号,由主设备号和次设备号组成
 * @param count 设备号的数量,通常为1
 * @return 如果注册成功,返回0;否则返回负错误码
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
  • 注销字符设备
/**
 * @brief 从内核注销字符设备
 *
 * 该函数用于注销之前注册的字符设备,通常在设备卸载时调用。
 * 注销后,其他进程将无法通过设备号访问该字符设备。
 *
 * @param p 指向cdev结构体的指针
 */
void cdev_del(struct cdev *p);

生成设备节点

字符设备注册完以后不会自动生成设备节点。我们需要使用mknod命令创建一个设备节点

  • 格式:mknod 名称 类型 主设备号 次设备号
mknod /dev/test c 247 0

示例

编写设备驱动

/*
 * @Descripttion: 注册字符设备
 */
#include <linux/init.h>   //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/fs.h>     //包含了文件操作相关struct的定义,例如大名鼎鼎的struct file_operations
#include <linux/kdev_t.h>
#include <linux/cdev.h>        // 对字符设备结构cdev以及一系列的操作函数的定义。包含了cdev 结构及相关函数的定义。
#define DEVICE_NUMBER 1        //定义次设备号的个数
#define DEVICE_SNAME "schrdev" //定义静态注册设备的名称
#define DEVICE_ANAME "achrdev" //定义动态注册设备的名称
#define DEVICE_MINOR_NUMBER 0  //定义次设备号的起始地址
 
static int major_num, minor_num; //定义主设备号和次设备号
 
struct cdev cdev;                      //定义一个cdev结构体
module_param(major_num, int, S_IRUSR); //驱动模块传入普通参数major_num
module_param(minor_num, int, S_IRUSR); //驱动模块传入普通参数minor_num
 
int chrdev_open(struct inode *inode, struct file *file)
{
    printk("chrdev_open\n");
    return 0;
}
// file_operations chrdev_ops
struct file_operations chrdev_ops = {
    .owner = THIS_MODULE,
    .open = chrdev_open};
 
static int hello_init(void)
{
    dev_t dev_num;
    int ret; //函数返回值
 
    if (major_num)
    {
        /*静态注册设备号*/
        printk("major_num = %d\n", major_num); //打印传入进来的主设备号
        printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
 
        dev_num = MKDEV(major_num, minor_num);                              //MKDEV将主设备号和次设备号合并为一个设备号
        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); //注册设备号
        if (ret < 0)
        {
            printk("register_chrdev_region error\n");
        }
        printk("register_chrdev_region ok\n"); //静态注册设备号成功
    }
    else
    {
        /*动态注册设备号*/
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, 1, DEVICE_ANAME);
        if (ret < 0)
        {
            printk("alloc_chrdev_region error\n");
        }
        printk("alloc_chrdev_region ok\n"); //动态注册设备号成功
 
        major_num = MAJOR(dev_num);            //将主设备号取出来
        minor_num = MINOR(dev_num);            //将次设备号取出来
        printk("major_num = %d\n", major_num); //打印传入进来的主设备号
        printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
    }
 
    cdev.owner = THIS_MODULE;
    //cdev_init函数初始化cdev结构体成员变量
    cdev_init(&cdev, &chrdev_ops);
    //完成字符设备注册到内核
    cdev_add(&cdev, dev_num, DEVICE_NUMBER);
    return 0;
}
 
static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); //注销设备号
    cdev_del(&cdev);
    printk("gooodbye! \n");
}
module_init(hello_init);
module_exit(hello_exit);
 
MODULE_LICENSE("GPL");

编写应用程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    int fd;
    char buf[64] = {0};
    fd = open("/dev/test",O_RDWR);  //打开设备节点
    if(fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    //read(fd,buf,sizeof(buf)); //从文件中读取数据放入缓冲区中
    close(fd);
    return 0;
}

编译驱动

obj-m += chrdev.o
KDIR:=/home/topeet/linux/linux-imx
PWD?=$(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules ARCH=arm64
clean:
	make -C $(KDIR) M=$(PWD) clean
  • 使用insmod chrdev.ko加载驱动,打印申请到的主次设备号
  • 使用mknod /dev/test c 主设备号 次设备号创建设备节点
  • 运行./app,如果应用程序成功打开了设备节点,说明字符设备注册成功