• 字符设备驱动


    简介

    字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备。应用可以通过调用open、close、wirte、read等函数对已经编译好的驱动文件进行读写操作,驱动文件中就需要实现open、close、wirte、read等函数的具体功能。

    Linux下的字符设备驱动的框架

    在这里插入图片描述

    Linux中一切皆为文件,编写好的所有驱动都会在/dev目录下以文件的形式存放。当需要控制外设时,就需要通过读写驱动文件的方式来控制外设的状态。
    应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行在内核空间。当用户空间要实现对内核的操作,比如调用open函数打开/dev/SPI驱动,由于用户不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。
    应用程序使用的函数在驱动中都有与之对应的函数,比如应用程序调用了open这个函数,那么在驱动程序中也得有一个名为open的函数。在Linux内核文件include/linux/fs.h中有个叫做file_operations的结构体,此结构体就是Linux内核驱动操作函数集合。下面为在此结构体中常用的函数:

    函数名描述
    owner拥有file_operations结构体模块的指针,一般设置为THIS_MODULE
    llseek用于修改文件当前的读写位置
    read用于读取设备文件
    write用于设备文件写入(发送)数据
    poll轮询函数,用于查询设备是否可以进行非阻塞的读写
    unlocked_ioctl提供对于设备的控制功能,与应用程序中的ioctl函数对应
    compat_ioctl与unlock_ioctl函数功能一样,区别在于该函数用于64位系统,而32位系统用unlock_ioctl函数
    mmap用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应
    open用于打开设备文件
    release用于释放(关闭)设备文件,与
    fasync用于刷新待处理的数据,用于将缓存区中的数据刷新到磁盘中
    aio_fasync与fasync函数的功能类似,只是此函数用于异步刷新等待处理的数据

    Linux驱动架构

    在Linux驱动开发中肯定也是要初始化相应的外设寄存器,只是在Linux驱动开中需要按照规定的架构来编写驱动。

    Linux驱动运行的两种方式

    Linux驱动运行有两种方式:
    1)将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。
    2)将驱动编译成模块(Linux下模块扩展名位.ko),在Linux内核启动以后使用“insmod”命令加载驱动模块。
    模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:

    module_init(XXX_Init)	//注册模块加载函数
    module_exit(XXX_exit)	//注册模块卸载函数
    
    • 1
    • 2

    举例:

    /* 驱动入口函数 */
     static int __init xxx_init(void) 
    {  
        /* 入口函数具体内容 */
    	return 0; 
    }
    /* 驱动出口函数 */
     static void __exit xxx_exit(void)
     {
     	/* 出口函数具体内容 */
     }
    /* 将上面两个函数指定为驱动的入口和出口函数 */
    module_init(xxx_init);
    module_exit(xxx_exit);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块;insmod和modprobe,insmod是最简单的模块加载命令,此命令用于加载指定的.ko模块,比如加载drv.ko这个驱动模块,命令如下:

    insmod drv.ko
    
    • 1

    insmod加载模块不能解决依赖关系,而modprobe可以分析依赖关系,然后将所有的依赖模块都加载到内核中。
    驱动模块的卸载命令:

    rmmod drv.ko
    modprobe -r drv.ko //会卸载驱动所依赖的其他模块,建议卸载模块不要使用该命令
    
    • 1
    • 2

    字符设备注册与注销

    对于字符设备驱动以模块的形式加载成功以后就需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:

    static inline int register_chrdev(unsigned int major, const char *name,
    									const struct file_operations *fops)
    static inline void unregister_chrdev(unsigned int major, const char *name)
    
    • 1
    • 2
    • 3

    register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
    major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两 部分,关于设备号后面会详细讲解。
    name:设备名字,指向一串字符串。
    fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
    unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
    major:要注销的设备对应的主设备号。
    name:要注销的设备对应的设备名。

    实现设备具体操作函数

    驱动的主要目的就是实现file_operations结构体中的成员变量,也就是初始化open、release、read、write。

    添加LICENSE和作者信息

    LICENSE是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE和作者信息的添加使用如下两个函数:

    MODULE_LICENSE() //添加模块 LICENSE 信息
    MODULE_AUTHOR() //添加模块作者信息
    
    • 1
    • 2

    字符驱动开发框架

    创建创建chrdevbase.c文件

    static struct file_operations test_fops;
    /*打开设备*/
     static int chrtest_open(struct inode *inode, struct file *filp){
         /* 用户实现具体功能 */
         return 0;
     }
    /*
    * @description : 从设备读取数据
    * @param - filp : 要打开的设备文件(文件描述符)
    * @param - buf : 返回给用户空间的数据缓冲区
    * @param - cnt : 要读取的数据长度
    * @param - offt : 相对于文件首地址的偏移
    * @return : 读取的字节数,如果为负值,表示读取失败
    */
    static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
        /*用户实现具体功能*/
        return 0;
    }
    /*
    * @description : 向设备写数据
    * @param - filp : 设备文件,表示打开的文件描述符
    * @param - buf : 要写给设备写入的数据
    * @param - cnt : 要写入的数据长度
    * @param - offt : 相对于文件首地址的偏移
    * @return : 写入的字节数,如果为负值,表示写入失败
    */
    static ssize_t chrtest_write(struct file *filp,const char __user *buf,size_t cnt, loff_t *offt)
    {
        /* 用户实现具体功能 */
        return 0;
    }
    /* 关闭/释放设备 */
     static int chrtest_release(struct inode *inode, struct file *filp)
     {
         /* 用户实现具体功能 */
    		return 0;
     }
    static struct file_operations test_fops = {
     .owner = THIS_MODULE, 
     .open = chrtest_open,
     .read = chrtest_read,
     .write = chrtest_write,
     .release = chrtest_release,
     };
    
    /* 驱动入口函数 */
    static int __init xxx_init(void)
    {
        /* 入口函数具体内容 */
    	int retvalue = 0;
    	/* 注册字符设备驱动 */
        retvalue = register_chrdev(200, "chrtest", &test_fops);
    	if(retvalue < 0){
        	/* 字符设备注册失败,自行处理 */
        }
        return 0;
    }
    /* 驱动出口函数 */
    static void __exit xxx_exit(void)
    {
        /* 注销字符设备驱动 */
        unregister_chrdev(200, "chrtest");
    }
    /* 将上面两个函数指定为驱动的入口和出口函数 */
    
    module_init(xxx_init);
    module_exit(xxx_exit);
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("heihei");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    Linux设备号

    设备号的组成

    Linux中每个设备都有一个设备号,设备号有主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备 。设备号是由主设备号和次设备号两部分,其中高12位为主设备号(主设备号范围为0-4095),低20位为次设备号。
    设备号分配方法:
    1)静态分配号
    静态分配就是在注册字符设备的时候需要给设备指定一个设备号,比如200这个主设备号。
    2)动态分配设备号
    静态分配设备号会导致设备号冲突,Linux社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,设备号申请函数如下:

    /*申请设备号函数*/
    /*
    *@param dev:保存申请到的设备号。
    *		baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这 些设备号的主设备号一样,但是次设*		备号不同,次设备号以 baseminor 为起始地址地址开始递 增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
    *		count:要申请的设备号数量。
    *		name:设备名字。
    *
    */
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    /*注销设备号函数*/
    /*
    *@param from:要释放的设备号。
    *		count:表示从 from 开始,要释放的设备号数量。
    *
    */
    void unregister_chrdev_region(dev_t from, unsigned count)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    编写测试APP

    C库文件操作基本函数

    编写测试APP就是编写Linux应用,需要用C库里面和文件操作有关的一些函数,比如open、read、write、close这四个函数。

    open函数

    函数原型如下

    /*
    *@param-pathname:要打开的设备或者文件名
    *@param-flags:文件打开模式,以下三种模式必选其一:
    			O_RDONLY	只读模式
    			O_WRONLY	只写模式
    			O_RDWR		读写模式
    			O_APPEND 	每次写操作都写入文件的末尾
    			O_CREAT 	如果指定文件不存在,则创建这个文件
    			O_EXCL 		如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
    			O_TRUNC 	如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
    			O_NOCTTY 	如果路径名指向终端设备,不要把这个设备用作控制终端
    			O_NONBLOCK 	如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞DSYNC 等待物理 I/O 结束								后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新
    			O_RSYNC 	read 等待所有写入同一区域的写操作完成后再进行
    			O_SYNC 		等待物理 I/O 结束后再 write,包括更新文件属性的 I/O
    *@return 如果文件打开成功的话返回文件的文件描述符
    */
    int open(const char *pathname, int flags)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    read函数

    函数原型如下

    /*
    *@param-fd:要打开的设备或者文件名
    *@param-buf:数据读取到此buf中
    *@param-count:要读取的数据长度,也就是字节数
    *@return 读取成功的话返回读取到的字节数;如果返回0表示读取到了文件末尾;如果返回负值,表示读取失败。
    */
    ssize_t read(int fd, void *buf, size_t count)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    write 函数

    函数原型如下

    /*
    *fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文件成功以后会得到文件描述符
    *buf:要写入的数据
    *count:要写入的数据长度,也就是字节数
    */
    ssize_t write(int fd, const void *buf, size_t count);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    close函数
    /*
    *fd:要关闭的文件描述符
    */
    int close(int fd);
    
    • 1
    • 2
    • 3
    • 4

    编写驱动测试APP

    创建chrdevbaseApp.c文件

     #include "stdio.h"
     #include "unistd.h"
     #include "sys/types.h"
     #include "sys/stat.h"
     #include "fcntl.h"
     #include "stdlib.h"
     #include "string.h"
     static char usrdata[] = {"usr data!"}; //app向设备中写入的数据
    
     /*
     * @description : main 主程序
     * @param - argc : argv 数组元素个数
     * @param - argv : 具体参数
     * @return : 0 成功;其他 失败
     */
     int main(int argc, char *argv[])
     {
     	int fd, retvalue;
     	char *filename;
     	char readbuf[100], writebuf[100];
    
     	if(argc != 3){//参数如果不为3的话表示测试APP用法错误 (详细看注释①)
     		printf("Error Usage!\r\n");
     		return -1;
     	}
    
     	filename = argv[1]; //获取要打开的设备文件名字,argv[1]保存着文件名字。
    
     	/* 打开驱动文件 */
     	fd = open(filename, O_RDWR); //调用 C 库中的 open 函数打开设备文件:/dev/chrdevbase
     	if(fd < 0){
     		printf("Can't open file %s\r\n", filename);
     		return -1;
     	}
    	/*判断 argv[2]参数的值是 1 还是 2,因为输入命令的时候其参数都是字符串格式的,因此需要借助 atoi 函数将字符串格式的数字转换		为真实的数字。*/
     	if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
     	retvalue = read(fd, readbuf, 50);/*当 argv[2]为 1 的时候表示要从 chrdevbase 设备中读取数据,一共读取 50 字节的数										据,读取到的数据保存在 readbuf 中,读取成功以后就在终端上打印出读取到的数据。*/
    
     	if(retvalue < 0){
     		printf("read file %s failed!\r\n", filename);
     	}else{
     		/* 读取成功,打印出读取成功的数据 */
     		printf("read data:%s\r\n",readbuf);
     	}
     }
    	/*当 argv[2]为 2 的时候表示要向 chrdevbase 设备写数据*/
    	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);/*对 chrdevbase 设备操作完成以后就关闭设备*/
    if(retvalue < 0){
     	printf("Can't close file %s\r\n", filename);
     	return -1;
     }
    
     return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    main 函数的 argc 参数表示 参数数量,argv[]保存着具体的参数。
    注释
    ①比如, 现在要从 chrdevbase 设备中读取数据,需要输入如下命令:

    ./chrdevbaseApp /dev/chrdevbase 1
    
    • 1

    上述命令一共有三个参数“./chrdevbaseApp”、“/dev/chrdevbase”和“1”,这三个参数分别 对应 argv[0]、argv[1]和 argv[2]。第一个参数表示运行 chrdevbaseAPP 这个软件,第二个参数表 示测试APP要打开/dev/chrdevbase这个设备。第三个参数就是要执行的操作,1表示从设备中读取数据,2 表示向 设备中写数据。

    编译驱动和APP文件

    编译驱动文件

    创建Makefile文件,并填入以下内容:

    KERNELDIR := /home/heihei/MX6U/kenerl/mx6ull_kenerl
    CURRENT_PATH := $(shell pwd)
    obj-m := chrdevbase.o
    
    build: kernel_modules
    
    kernel_modules:
    	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
    clean:
    	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径,大家根 据自己的实际情况填写即可。

    CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。

    obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块

    编译APP文件

    arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
    
    • 1

    编译完成后可以用 file chrdevbaseApp 查看程序文件的信息

    测试

    将编译完成的chrdevbase.ko和chrdevbaseApp 拷贝到nfs/rootfs/lib/mouble/4.1.15/

    加载驱动
    depmod //自动生成动生成 modules.alias、modules.symbols 和 modules.dep
    insmod chrdevbase.ko
    modprobe chrdevbase.ko
    
    • 1
    • 2
    • 3
    查看已经挂载的驱动
    lsmod 				//查看当前系统中存在的模块
    cat /proc/devices 	//查看当前系统中的所有设备
    
    • 1
    • 2
    创建设备节点文件

    驱动加载成功需要在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。

    mknod /dev/chrdevbase c 200 0
    
    • 1

    mknod是创建节点命令,“/dev/chrdevbase”是创建的节点文件,c 表示这是个字符设备,“200”是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在“/dev/chrdevbase”这个文件是chrdevbase设备在用户空间中的实现。

    操作设备
    ./chrdevbaseApp /dev/chrdevbase 1 //
    ./chrdevbaseApp /dev/chrdevbase 2 //
    
    • 1
    • 2
    卸载驱动模块
    rmmod chrdevbase.ko
    
    • 1

    总结

    1)字符驱动设备其实就是对file_operations结构体成员变量的具体功能的实现,编写完驱动后还需要编写对应的测试APP。
    2)编写字符驱动要对芯片对应外设的寄存器很熟悉,了解如何驱动该外设的方法。

    参考

    《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.6》

  • 相关阅读:
    Python的命令行参数实例详解
    【VisualStudio】error C2760: 语法错误: 意外的令牌“标识符”,预期的令牌为“类型说明符”
    接口项目实战
    Ajax——AJAX跨域问题
    随想录一期 day4 [24. 两两交换链表中的节点|19. 删除链表的倒数第 N 个结点|面试题 02.07. 链表相交|142. 环形链表 II]
    【注解学习】基础掌握与简单示例使用
    Python移除指定文件夹中所有文件名称中的特殊字符(数字、特殊字符、空格等)、只保留字母字符
    丢掉破解版,官方免费了!!!
    2023/8/8 下午10:42:04 objectarx
    JAVA微信小程序美食菜谱系统毕业设计 开题报告
  • 原文地址:https://blog.csdn.net/qq_43460106/article/details/127921446