• Linux驱动开发-字符设备驱动开发


    linux 驱动开发

    1. 驱动程序的类型

    在 Linux 中,驱动程序主要有以下几种类型:

    字符设备驱动:处理字节流的设备,如串口、键盘等。它们通过字符设备接口(如 /dev/tty)与用户空间进行交互。

    块设备驱动:处理块存储设备,如硬盘、SSD 等。它们支持随机访问,提供高效的数据传输。

    网络设备驱动:用于网络接口卡,处理网络数据包的发送和接收。

    USB 驱动:支持 USB 设备的连接和管理。

    2. 驱动开发流程

    以下是开发 Linux 驱动的一般流程:

    2.1 环境准备
    确保你有一个支持的 Linux 发行版。
    安装开发工具,如 gcc 和 make。
    下载和编译 Linux 内核源代码(与你的内核版本一致)。
    
    2.2 编写驱动代码
    根据你要开发的设备类型,选择合适的接口。
    创建相应的初始化(init)和退出(exit)函数。
    根据设备实现文件操作结构体 file_operations。
    
    2.3 编写 Makefile
    创建 Makefile,用于编译你的模块。
    
    2.4 编译模块
    在终端中通过 make 命令编译模块。
    
    2.5 加载和测试模块
    使用 insmod 加载模块,使用 rmmod 卸载模块。
    通过 dmesg 查看内核日志,调试和验证模块功能。
    
    

    字符设备驱动

    字符设备驱动是Linux内核中的一种设备驱动类型,
    主要用于处理字符设备(如键盘、鼠标、串口等)。
    字符设备以字节流的形式进行数据传输,与块设备(如硬盘、SSD等)不同,
    块设备以固定大小的块进行数据传输。

    下面是一个简单的字符设备驱动入门指南:

    1. 基本概念

    字符设备(Character Device):以字节流的形式进行数据传输的设备。

    设备文件:在Linux中,字符设备通过设备文件(通常位于/dev目录下)与用户空间进行交互。

    主设备号和次设备号:设备文件由主设备号和次设备号标识。主设备号用于标识设备驱动,次设备号用于标识特定的设备实例。

    2. 字符设备驱动的基本结构

    字符设备驱动通常包括以下几个部分:

    设备注册:向内核注册字符设备。

    文件操作接口:定义设备文件的操作接口(如open, read, write, close等)。

    设备操作函数:实现具体的设备操作逻辑。

    架构

    字符设备驱动程序在 Linux 中的架构通常包括以下几个关键组件:
    
    设备注册: 使用 register_chrdev 注册设备,并提供操作函数。驱动程序需要处理打开、读取、写入和关闭操作,通常通过定义 file_operations 结构体来实现。
    
    打开、读写、关闭接口: 驱动程序需要实现用户空间程序可以调用的接口。读取和写入操作使用 copy_to_user 和 copy_from_user 进行数据传输。
    
    内存管理: 使用 kmalloc 和 kfree 进行动态内存分配和释放,以管理驱动所需的数据结构。
    
    进程同步: 使用等待队列(基于 wait_queue_head_t)来管理进程的等待与唤醒,实现异步操作。
    
    错误处理: 驱动程序需要实现适当的错误处理机制,以确保在发生错误时能够提供适当的反馈。
    

    字符设备驱动开发中常用的 API

    /*
     * 1. register_chrdev
     * 功能:注册字符设备。
     * 参数:
     *    int nr: 主设备号。如果设置为 0,内核会自动分配一个主设备号。
     *    const char *name: 设备的名字,将显示在 /proc/devices 中。
     *    struct file_operations *fops: 指向文件操作结构体的指针,定义设备的操作函数。
     * 返回值:
     *    返回主设备号,如果发生错误,则返回负值。
     */
    int register_chrdev(unsigned int nr, const char *name, struct file_operations *fops);
    
    /*
     * 2. unregister_chrdev
     * 功能:注销字符设备。
     * 参数:
     *    int nr: 要注销的主设备号。
     *    const char *name: 设备的名字。
     * 返回值:
     *    无。
     */
    void unregister_chrdev(unsigned int nr, const char *name);
    
    /*
     * 3. copy_to_user
     * 功能:将数据从内核空间复制到用户空间。
     * 参数:
     *    void __user *to: 用户空间的目标地址。
     *    const void *from: 内核空间的源地址。
     *    unsigned long count: 要复制的字节数。
     * 返回值:
     *    返回未成功复制的字节数。如果为 0,表示成功。
     */
    long copy_to_user(void __user *to, const void *from, unsigned long count);
    
    /*
     * 4. copy_from_user
     * 功能:将数据从用户空间复制到内核空间。
     * 参数:
     *    void *to: 内核空间的目标地址。
     *    const void __user *from: 用户空间的源地址。
     *    unsigned long count: 要复制的字节数。
     * 返回值:
     *    返回未成功复制的字节数。如果为 0,表示成功。
     */
    long copy_from_user(void *to, const void __user *from, unsigned long count);
    
    /*
     * 5. kmalloc
     * 功能:分配内核内存。
     * 参数:
     *    size_t size: 要分配的内存字节数。
     *    gfp_t flags: 分配内存的标志,如 GFP_KERNEL。
     * 返回值:
     *    返回指向分配内存区域的指针。如果分配失败,返回 NULL。
     */
    void *kmalloc(size_t size, gfp_t flags);
    
    /*
     * 6. kfree
     * 功能:释放内核内存。
     * 参数:
     *    void *ptr: 指向要释放内存的指针。
     * 返回值:
     *    无。
     */
    void kfree(void *ptr);
    
    /*
     * 7. init_waitqueue_head
     * 功能:初始化等待队列头。
     * 参数:
     *    wait_queue_head_t *q: 等待队列头指针。
     * 返回值:
     *    无。
     */
    void init_waitqueue_head(wait_queue_head_t *q);
    
    /*
     * 8. wait_event
     * 功能:将当前进程放入等待队列,直到条件满足为止。
     * 参数:
     *    wait_queue_head_t *q: 等待队列头指针。
     *    int condition: 条件表达式,当其评估为真时,返回进程。
     * 返回值:
     *    无。
     */
    #define wait_event(q, condition) wait_event_interruptible(q, condition)
    
    /*
     * 9. wake_up
     * 功能:唤醒等待队列中的进程。
     * 参数:
     *    wait_queue_head_t *q: 等待队列头指针。
     * 返回值:
     *    无。
     */
    void wake_up(wait_queue_head_t *q);
    
    /*
     * 10. printk
     * 功能:在内核中打印日志信息。
     * 参数:
     *    const char *fmt: 格式化字符串。
     * 返回值:
     *    返回打印的字符数。
     */
    int printk(const char *fmt, ...);
    
    

    示例

    #include    // 包含模块相关的宏和函数
    #include      // 包含模块初始化和退出相关的宏
    #include        // 包含文件系统相关的函数和结构体
    #include      // 包含字符设备相关的函数和结构体
    #include   // 包含用户空间和内核空间数据传输的函数
    
    // 设备号
    static dev_t dev_num;
    
    // 字符设备结构体
    static struct cdev chr_dev;
    
    // 设备缓冲区
    static char buffer[1024];
    
    // 缓冲区大小
    static int buffer_size = 0;
    
    // 模块许可证
    MODULE_LICENSE("GPL");
    
    // 模块作者
    MODULE_AUTHOR("gopher");
    
    // 模块描述
    MODULE_DESCRIPTION("A simple character device driver");
    
    // 打开设备文件时的回调函数
    // 打开设备文件时,内核会创建设备文件结构体和相关资源
    // 该函数在内核空间中执行,因此不能有进程上下文的操作
    // 该函数的返回值是成功与否的标志
    //@param struct inode *inode 设备文件节点结构体
    // @param struct file *file 设备文件结构体
    static int chr_dev_open(struct inode *inode, struct file *file) {
        printk(KERN_INFO "chr_dev: Device opened\n");
        //打印设备号
        
        return 0;
    }
    
    // 关闭设备文件时的回调函数
    // 关闭设备文件时,内核会释放设备文件结构体和相关资源
    // 该函数在内核空间中执行,因此不能有进程上下文的操作
    // 该函数的返回值是成功与否的标志
    //@param struct inode *inode 设备文件节点结构体
    // @param struct file *file 设备文件结构体
    static int chr_dev_release(struct inode *inode, struct file *file) {
        printk(KERN_INFO "chr_dev: Device closed\n");
        return 0;
    }
    
    // 从设备读取数据时的回调函数
    // 从设备读取数据时,内核会将数据从内核缓冲区复制到用户空间缓冲区
    // 并更新读取位置
    // 从设备读取数据时,内核会返回实际读取的字节数
    // 如果读取位置超出缓冲区大小,则返回0
    // 如果发生其他错误,则返回-EFAULT
    // 成功读取数据时,返回实际读取的字节数
    // 注意:从设备读取数据时,内核不会阻塞,而是立即返回
    //@param struct file *file 设备文件结构体
    // @param char __user *user_buf 用户空间缓冲区
    // @param size_t count 要读取的字节数
    // @param loff_t *ppos 读取位置指针
    static ssize_t chr_dev_read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) {
        int ret;
    
        // 检查读取位置是否超出缓冲区大小
        if (*ppos >= buffer_size)
            return 0;
    
        // 将数据从内核缓冲区复制到用户空间缓冲区
        ret = copy_to_user(user_buf, buffer + *ppos, count);
        if (ret)
            return -EFAULT;  // 复制失败
    
        printk(KERN_INFO "chr_dev: Read %d bytes from device\n", count);
        // 更新读取位置
        *ppos += count;
        return count;
    }
    
    // 向设备写入数据时的回调函数
    // 向设备写入数据时,内核会将数据从用户空间缓冲区复制到内核缓冲区
    // 并更新缓冲区大小
    // 向设备写入数据时,内核会返回实际写入的字节数
    // 如果缓冲区已满,则返回-ENOSPC
    // 如果发生其他错误,则返回-EFAULT
    // 成功写入数据时,返回实际写入的字节数
    // 注意:向设备写入数据时,内核不会阻塞,而是立即返回
    //@param struct file *file 设备文件结构体
    // @param const char __user *user_buf 用户空间缓冲区
    // @param size_t count 要写入的字节数
    // @param loff_t *ppos 读取位置指针
    static ssize_t chr_dev_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) {
        int ret;
    
        // 检查缓冲区是否有足够的空间
        if (count > sizeof(buffer) - buffer_size)
            return -ENOSPC;  // 空间不足
    
        // 将数据从用户空间缓冲区复制到内核缓冲区
        //@param void *to 目的地址
        // @param const void *from 源地址
        // @param size_t n 字节数
        ret = copy_from_user(buffer + buffer_size, user_buf, count);
        if (ret)
            return -EFAULT;  // 复制失败
            // 打印调试信息
        printk(KERN_INFO "chr_dev: Wrote %d bytes to device\n", count);
        // 更新缓冲区大小
        buffer_size += count;
        return count;
    }
    
    // 文件操作结构体,定义了设备文件的操作接口
    // 包含了文件操作的回调函数和一些其他属性
    // 这些回调函数在设备文件被打开、关闭、读、写时被调用
    static struct file_operations chr_dev_fops = {
        .owner = THIS_MODULE,  // 模块所有者
        .open = chr_dev_open,  // 打开设备文件的回调函数
        .release = chr_dev_release,  // 关闭设备文件的回调函数
        .read = chr_dev_read,  // 从设备读取数据的回调函数
        .write = chr_dev_write,  // 向设备写入数据的回调函数
    };
    
    // 模块初始化函数
    static int __init chr_dev_init(void) {
        int ret;
    
        // 分配设备号
        //@param dev_t dev_num 设备号
        // @param unsigned int major 主设备号
        // @param unsigned int minor 次设备号
        // @param const char *name 设备名
        ret = alloc_chrdev_region(&dev_num, 0, 1, "chr_dev");
        if (ret < 0) {
            printk(KERN_ERR "chr_dev: Failed to allocate device number\n");
            return ret;
        }
    
        // 初始化字符设备
        //@param struct cdev *cdev 字符设备结构体
        // @param struct file_operations *fops 字符设备操作结构体
        cdev_init(&chr_dev, &chr_dev_fops);
        chr_dev.owner = THIS_MODULE;
    
        // 添加字符设备
        //@param struct cdev *cdev 字符设备结构体
        // @param dev_t dev 设备号
        // @param unsigned int count 设备号计数
        ret = cdev_add(&chr_dev, dev_num, 1);
        if (ret < 0) {
            printk(KERN_ERR "chr_dev: Failed to add character device\n");
            unregister_chrdev_region(dev_num, 1);
            return ret;
        }
    
        printk(KERN_INFO "chr_dev: Device initialized\n");
        //打印设备号
            printk(KERN_INFO "chr_dev: Major number: %d\n", MAJOR(dev_num));
        printk(KERN_INFO "chr_dev: Minor number: %d\n", MINOR(dev_num));
        return 0;
    }
    
    // 模块退出函数
    static void __exit chr_dev_exit(void) {
        // 删除字符设备
        //@param dev_t dev 设备号/
        cdev_del(&chr_dev);
    
        // 释放设备号
        //@param dev_t dev 设备号 
        // @param unsigned int count 设备号计数
        unregister_chrdev_region(dev_num, 1);
    
        (KERN_INFO "chr_dev: Device removed\n");
    }
    
    // 注册模块初始化函数
    // 内核在加载模块时,调用模块的初始化函数
    // 该函数在内核空间中执行,因此不能有进程上下文的操作
    // 该函数的返回值是模块初始化成功与否的标志
    module_init(chr_dev_init);
    
    // 注册模块退出函数
    // 内核在卸载模块时,调用模块的退出函数
    // 该函数在内核空间中执行,因此不能有进程上下文的操作
    module_exit(chr_dev_exit);
    

    以下代码加入了设备类和设备实例的创建

    
    /*以下代码加入了设备类和设备实例的创建*/
    #include    // 包含模块相关的宏和函数
    #include      // 包含模块初始化和退出相关的宏
    #include        // 包含文件系统相关的函数和结构体
    #include      // 包含字符设备相关的函数和结构体
    #include   // 包含用户空间和内核空间数据传输的函数
    #include    // 包含设备类和设备实例相关的函数
    
    // 设备号
    static dev_t dev_num;
    
    // 字符设备结构体
    static struct cdev chr_dev;
    
    // 设备缓冲区
    static char buffer[1024];
    
    // 缓冲区大小
    static int buffer_size = 0;
    
    // 设备类
    static struct class *chr_dev_class;
    
    // 设备实例
    static struct device *chr_dev_device;
    
    // 模块许可证
    MODULE_LICENSE("GPL");
    
    // 模块作者
    MODULE_AUTHOR("gopher");
    
    // 模块描述
    MODULE_DESCRIPTION("A simple character device driver");
    
    // 打开设备文件时的回调函数
    static int chr_dev_open(struct inode *inode, struct file *file) {
        printk(KERN_INFO "chr_dev: Device opened\n");
        return 0;
    }
    
    // 关闭设备文件时的回调函数
    static int chr_dev_release(struct inode *inode, struct file *file) {
        printk(KERN_INFO "chr_dev: Device closed\n");
        return 0;
    }
    
    // 从设备读取数据时的回调函数
    static ssize_t chr_dev_read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) {
        int ret;
    
        if (*ppos >= buffer_size)
            return 0;
    
        ret = copy_to_user(user_buf, buffer + *ppos, count);
        if (ret)
            return -EFAULT;
    
        printk(KERN_INFO "chr_dev: Read %zu bytes from device\n", count);
        *ppos += count;
        return count;
    }
    
    // 向设备写入数据时的回调函数
    static ssize_t chr_dev_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) {
        int ret;
    
        if (count > sizeof(buffer) - buffer_size)
            return -ENOSPC;
    
        ret = copy_from_user(buffer + buffer_size, user_buf, count);
        if (ret)
            return -EFAULT;
    
        printk(KERN_INFO "chr_dev: Wrote %zu bytes to device\n", count);
        buffer_size += count;
        return count;
    }
    
    // 文件操作结构体
    static struct file_operations chr_dev_fops = {
        .owner = THIS_MODULE,
        .open = chr_dev_open,
        .release = chr_dev_release,
        .read = chr_dev_read,
        .write = chr_dev_write,
    };
    
    // 模块初始化函数
    static int __init chr_dev_init(void) {
        int ret;
    
        // 分配设备号
        ret = alloc_chrdev_region(&dev_num, 0, 1, "chr_dev");
        if (ret < 0) {
            printk(KERN_ERR "chr_dev: Failed to allocate device number\n");
            return ret;
        }
    
        // 初始化字符设备
        cdev_init(&chr_dev, &chr_dev_fops);
        chr_dev.owner = THIS_MODULE;
    
        // 添加字符设备
        ret = cdev_add(&chr_dev, dev_num, 1);
        if (ret < 0) {
            printk(KERN_ERR "chr_dev: Failed to add character device\n");
            unregister_chrdev_region(dev_num, 1);
            return ret;
        }
    
        // 创建设备类
        // 用于管理设备实例
        // @param struct module *owner 模块所有者
        // @param const char *name 设备类名
        // @return struct class * 设备类结构体
        chr_dev_class = class_create(THIS_MODULE, "chr_dev_class");
        if (IS_ERR(chr_dev_class)) {// 出错
            printk(KERN_ERR "chr_dev: Failed to create device class\n");
            cdev_del(&chr_dev);// 删除字符设备
            unregister_chrdev_region(dev_num, 1);// 释放设备号
            return PTR_ERR(chr_dev_class);// 返回错误码
        }
    
        // 创建设备实例
        // 用于管理设备文件
        // @param struct class *class 设备类结构体
        // @param struct device *parent 父设备实例
        // @param dev_t dev 设备号
        // @param void *drvdata 设备数据
        // @param const char *fmt 设备名格式
        // @return struct device * 设备实例结构体
        chr_dev_device = device_create(chr_dev_class, NULL, dev_num, NULL, "chr_dev");
        if (IS_ERR(chr_dev_device)) {
            printk(KERN_ERR "chr_dev: Failed to create device\n");
            class_destroy(chr_dev_class);// 删除设备类
            cdev_del(&chr_dev);// 删除字符设备
            unregister_chrdev_region(dev_num, 1);// 释放设备号
            return PTR_ERR(chr_dev_device);// 返回错误码
        }
    
        printk(KERN_INFO "chr_dev: Device initialized\n");// 打印调试信息
        printk(KERN_INFO "chr_dev: Major number: %d\n", MAJOR(dev_num));// 打印设备号
        printk(KERN_INFO "chr_dev: Minor number: %d\n", MINOR(dev_num));// 打印设备号
        return 0;
    }
    
    // 模块退出函数
    static void __exit chr_dev_exit(void) {
        // 删除设备实例
        device_destroy(chr_dev_class, dev_num);
    
        // 删除设备类
        class_destroy(chr_dev_class);
    
        // 删除字符设备
        cdev_del(&chr_dev);
    
        // 释放设备号
        unregister_chrdev_region(dev_num, 1);
    
        printk(KERN_INFO "chr_dev: Device removed\n");
    }
    
    // 注册模块初始化函数
    module_init(chr_dev_init);
    
    // 注册模块退出函数
    module_exit(chr_dev_exit);
    
  • 相关阅读:
    C++中的继承
    Kubernetes(k8s)的Pod资源清单常用属性介绍
    蚂蚁金服Java研发岗二面:redis 常见数据结构以及使用场景分析
    Verilog 大小端以及 +:使用
    Rust嵌入式编程---#![no_std]和中断处理
    《软件方法(下)》第8章2023版连载(05)关于实体类
    4、SySeVR复现——Generating slices
    搞清信息化是什么,让企业转型升级走上正确的道路
    c#中在datagridview的表格动态增加一个按钮方法
    模块及模块管理(原理篇)
  • 原文地址:https://blog.csdn.net/gopher9511/article/details/142265233