• Linux驱动开发:字符设备驱动开发实战


    Linux驱动开发:字符设备驱动开发实战

    一、工程创建

    VSCode 创建工程,设置 C/C++ 配置,导入 linux kernel 源码目录,方便 vscode 写代码自动补全,vscode 配置

    {
        "configurations": [
            {
                "name": "Linux",
                "includePath": [
                    "/home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                    "/home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                    "/home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated"
                ],
                "defines": [],
                "compilerPath": "/usr/bin/gcc",
                "cStandard": "c11", 
                "cppStandard": "c++17",
                "intelliSenseMode": "linux-gcc-x64"
            },
            {
                "name": "linux_drive",
                "includePath": [
                    "${workspaceFolder}/**"
                ],
                "defines": [],
                "compilerPath": "/usr/bin/gcc",
                "cStandard": "c11",
                "cppStandard": "c++17",
                "intelliSenseMode": "linux-gcc-x64"
            }
        ],
        "version": 4
    }
    

    二、驱动程序编写

    新建一个文件 char_drive.c 文件:

    驱动程序主要包含:驱动的注册和卸载,以及基本驱动操作,比如打开、关闭、读取、写入等操作,这里使用原子的例程分析:

    2.1 驱动操作接口

    打开字符设备,因为驱动在 linux 下就是一个文件,我们传入文件的 inode 和文件结构体用于关联设备。

    /*
     * @description		: 打开设备
     * @param - inode 	: 传递给驱动的inode
     * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
     * 					  一般在open的时候将private_data指向设备结构体。
     * @return 			: 0 成功;其他 失败
     */
    static int chrdevbase_open(struct inode *inode, struct file *filp)
    {
        // printk("chrdevbase open!\r\n");
        return 0;
    }
    

    关闭字符设备,关闭设备就是打开设备的逆操作,断开设备结构体和文件结构体的关联。

    /*
     * @description		: 关闭/释放设备
     * @param - filp 	: 要关闭的设备文件(文件描述符)
     * @return 			: 0 成功;其他 失败
     */
    static int chrdevbase_release(struct inode *inode, struct file *filp)
    {
        // printk("chrdevbase release!\r\n");
        return 0;
    }
    

    读字符设备,传入驱动设备文件,这里我们并没有使用文件指针,模拟进行操作,使用 copy_to_user 内核函数,将数据打印到用户数据空间。

    /*
     * @description		: 从设备读取数据
     * @param - filp 	: 要打开的设备文件(文件描述符)
     * @param - buf 	: 返回给用户空间的数据缓冲区
     * @param - cnt 	: 要读取的数据长度
     * @param - offt 	: 相对于文件首地址的偏移
     * @return 			: 读取的字节数,如果为负值,表示读取失败
     */
    static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
    {
        int retvalue = 0;
    
        /* 向用户空间发送数据 */
        memcpy(readbuf, kerneldata, sizeof(kerneldata));
        retvalue = copy_to_user(buf, readbuf, cnt);
        if (retvalue == 0)
        {
            printk("kernel senddata ok!\r\n");
        }
        else
        {
            printk("kernel senddata failed!\r\n");
        }
    
        // printk("chrdevbase read!\r\n");
        return 0;
    }
    

    写字符设备,和读字符串设备相同,只不过数据读写方向相反。

    /*
     * @description		: 向设备写数据
     * @param - filp 	: 设备文件,表示打开的文件描述符
     * @param - buf 	: 要写给设备写入的数据
     * @param - cnt 	: 要写入的数据长度
     * @param - offt 	: 相对于文件首地址的偏移
     * @return 			: 写入的字节数,如果为负值,表示写入失败
     */
    static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
    {
        int retvalue = 0;
        /* 接收用户空间传递给内核的数据并且打印出来 */
        retvalue = copy_from_user(writebuf, buf, cnt);
        if (retvalue == 0)
        {
            printk("kernel recevdata:%s\r\n", writebuf);
        }
        else
        {
            printk("kernel recevdata failed!\r\n");
        }
        // printk("chrdevbase write!\r\n");
        return 0;
    }
    

    2.2 注册、注销接口

    驱动基本操作程序写话后,编写驱动注册和注销接口:

    驱动注册本质上就是将操作函数作为指针传递进去,挂载到调用结构体上,注册时要传入设备号和设备名称

    /*
     * @description	: 驱动入口函数
     * @param 		: 无
     * @return 		: 0 成功;其他 失败
     */
    static int __init chrdevbase_init(void)
    {
        int retvalue = 0;
    
        /* 注册字符设备驱动 */
        retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
        if (retvalue < 0)
        {
            printk("chrdevbase driver register failed\r\n");
        }
        printk("chrdevbase init!\r\n");
        return 0;
    }
    

    设备注销就只需要传入设备号和名称就行,注销函数会根据设备号清除相关结构体

    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static void __exit chrdevbase_exit(void)
    {
        /* 注销字符设备驱动 */
        unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
        printk("chrdevbase exit!\r\n");
    }
    

    注册和注销函数还需要在注册一次,关联到系统模块加载和卸载相关的结构体上

    module_init(chrdevbase_init);
    module_exit(chrdevbase_exit);
    

    除此之外还要作者信息,此处不能缺少

    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("zuozhongkai");
    

    三、应用程序编写

    编写 main 函数,根据传入参数进行驱动代码的测试,main 函数的参数也叫做命令参数,在 linux 下敲命令的时候就有用了,argv 指的是 char * argc[] 的大小 char * argc[]是一个指针数组,里面存放的是各个传入字符串的首地址,测试代码如下:

    int main(int argc, char *argv[])
    {
        int fd, retvalue;
        char *filename;
        char readbuf[100], writebuf[100];
    
        if (argc != 3)
        {
            printf("Error Usage!\r\n");
            return -1;
        }
    
        filename = argv[1];
    
        /* 打开驱动文件 */
        fd = open(filename, O_RDWR);
        if (fd < 0)
        {
            printf("Can't open file %s\r\n", filename);
            return -1;
        }
    
        if (atoi(argv[2]) == 1)
        { /* 从驱动文件读取数据 */
            retvalue = read(fd, readbuf, 50);
            if (retvalue < 0)
            {
                printf("read file %s failed!\r\n", filename);
            }
            else
            {
                /*  读取成功,打印出读取成功的数据 */
                printf("read data:%s\r\n", readbuf);
            }
        }
    
        if (atoi(argv[2]) == 2)
        {
            /* 向设备驱动写数据 */
            memcpy(writebuf, usrdata, sizeof(usrdata));
            retvalue = write(fd, writebuf, 50);
            if (retvalue < 0)
            {
                printf("write file %s failed!\r\n", filename);
            }
        }
    
        /* 关闭设备 */
        retvalue = close(fd);
        if (retvalue < 0)
        {
            printf("Can't close file %s\r\n", filename);
            return -1;
        }
    
        return 0;
    }
    
    

    这段应用程序就是简单的传入设备文件名称,然后再根据后续参数进行读或者写的操作。

    四、程序编译

    驱动程序使用 makefile 用交叉工具链进行编译:

    KERNELDIR := /home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga
    CURRENT_PATH := $(shell pwd)
    obj-m := char_drive.o
    
    build: kernel_modules
    
    kernel_modules:
    	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
    clean:
    	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
    

    KERNELDIR 为 Linux 内核目录,编译指令:

    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
    

    编译完成,得到 ko 文件可用于内核加载:

    image-20220923231353917

    然后直接在 linux 用交叉工具链将测试程序编译链接成可执行文件:

    arm-linux-gnueabihf-gcc char_device_app.c -o test_app
    

    image-20220923231605368

    模块编译成功后,创建内核模块目录:/lib/modules/4.1.15,其中 4.1.15 是内核版本,可以通过 uname -r 查询。

    复制 ko 文件和测试程序到文件系统目录下面,加载 ko 模块:

    image-20220925152054083

    加载指令:

    insmod char_drive.ko
    

    然后使用 test_app 进行测试就行

  • 相关阅读:
    节点CODE相同会导致数据重复
    Scala入门到精通(尚硅谷学习笔记)章节二——语法格式
    如何提高外贸独立站流量?
    【前置句与倒装句练习题】特殊语序
    基于二维切片图序列的三维立体建模MATLAB仿真
    10 个你必须要知道的重要JavaScript 数组方法
    OpenMLDB + Jupyter Notebook:快速搭建机器学习应用
    Swagger 的介绍以及使用
    领英如何批量添加搜索的人脉,批量加领英推荐人脉,批量加精灵推荐人脉,批量加Groups成员,通过链接批量加人
    Doris通过ODBC驱动导入外部表数据
  • 原文地址:https://blog.csdn.net/qq_45396672/article/details/127043975