您当前的位置: 首页 >  嵌入式

正点原子

暂无认证

  • 0浏览

    0关注

    382博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【正点原子MP157连载】第二十章 字符设备驱动开发-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7

正点原子 发布时间:2022-02-11 10:55:54 ,浏览量:0

1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html 4)正点原子官方B站:https://space.bilibili.com/394620890 5)正点原子STM32MP157技术交流群:691905614 在这里插入图片描述

第二十章 字符设备驱动开发

本章我们从Linux驱动开发中最基础的字符设备驱动开始,重点学习Linux下字符设备驱动开发框架。本章会以一个虚拟的设备为例,讲解如何进行字符设备驱动开发,以及如何编写测试APP来测试驱动工作是否正常,为以后的学习打下坚实的基础。

20.1 字符设备驱动简介 字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD等等都是字符设备,这些设备的驱动就叫做字符设备驱动。 在详细的学习字符设备驱动架构之前,我们先来简单的了解一下Linux下的应用程序是如何调用驱动程序的,Linux应用程序对驱动程序的调用如图20.1.1所示: 在这里插入图片描述

图20.1.1 Linux应用程序对驱动程序的调用流程 在Linux中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫做/dev/led的驱动文件,此文件是led灯的驱动文件。应用程序使用open函数来打开文件/dev/led,使用完成以后使用close函数关闭/dev/led这个文件。open和close就是打开和关闭led驱动的函数,如果要点亮或关闭led,那么就使用write函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开led的控制参数。如果要获取led灯的状态,就用read函数从驱动中读取相应的状态。 应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open函数打开/dev/led这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write和read等这些函数是由C库提供的,在Linux系统中,系统调用作为C库的一部分。当我们调用open函数的时候流程如图20.1.2所示: 在这里插入图片描述

图20.1.2 open函数调用流程 其中关于C库以及如何通过系统调用“陷入”到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了open这个函数,那么在驱动程序中也得有一个名为open的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在Linux内核文件 include/linux/fs.h中有个叫做file_operations的结构体,此结构体就是Linux内核驱动操作函数集合,内容如下所示:

示例代码20.1.1 file_operations结构体
1822    struct file_operations {
1823        struct module *owner;
1824        loff_t (*llseek) (struct file *, loff_t, int);
1825        ssize_t (*read) (struct file *, char __user *, size_t, 
loff_t *);
1826        ssize_t (*write) (struct file *, const char __user *, 
size_t, loff_t *);
1827        ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1828        ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1829        int (*iopoll)(struct kiocb *kiocb, bool spin);
1830        int (*iterate) (struct file *, struct dir_context *);
1831        int (*iterate_shared) (struct file *, struct dir_context *);
1832        __poll_t (*poll) (struct file *, struct poll_table_struct *);
1833        long (*unlocked_ioctl) (struct file *, unsigned int, 
unsigned long);
1834        long (*compat_ioctl) (struct file *, unsigned int, 
unsigned long);
1835        int (*mmap) (struct file *, struct vm_area_struct *);
1836        unsigned long mmap_supported_flags;
1837        int (*open) (struct inode *, struct file *);
1838        int (*flush) (struct file *, fl_owner_t id);
1839        int (*release) (struct inode *, struct file *);
1840        int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1841        int (*fasync) (int, struct file *, int);
1842        int (*lock) (struct file *, int, struct file_lock *);
1843        ssize_t (*sendpage) (struct file *, struct page *, 
int, size_t, loff_t *, int);
1844        unsigned long (*get_unmapped_area)(struct file *, 
unsigned long, unsigned long, unsigned long, unsigned long);
1845        int (*check_flags)(int);
1846        int (*flock) (struct file *, int, struct file_lock *);
1847        ssize_t (*splice_write)(struct pipe_inode_info *, 
struct file *, loff_t *, size_t, unsigned int);
1848        ssize_t (*splice_read)(struct file *, loff_t *, 
struct pipe_inode_info *, size_t, unsigned int);
1849        int (*setlease)(struct file *, long, struct file_lock **, 
void **);
1850        long (*fallocate)(struct file *file, int mode, loff_t offset,
1851                  loff_t len);
1852        void (*show_fdinfo)(struct seq_file *m, struct file *f);
1853    #ifndef CONFIG_MMU
1854        unsigned (*mmap_capabilities)(struct file *);
1855    #endif
1856        ssize_t (*copy_file_range)(struct file *, loff_t, 
1857                struct file *, loff_t, size_t, unsigned int);
1858        loff_t (*remap_file_range)(struct file *file_in, 
loff_t pos_in,
1859                       struct file *file_out, loff_t pos_out,
1860                       loff_t len, unsigned int remap_flags);
1861        int (*fadvise)(struct file *, loff_t, loff_t, int);
1862    } __randomize_layout;

简单介绍一下file_operation结构体中比较重要的、常用的函数: 第1823行,owner拥有该结构体的模块的指针,一般设置为THIS_MODULE。 第1824行,llseek函数用于修改文件当前的读写位置。 第1825行,read函数用于读取设备文件。 第1826行,write函数用于向设备文件写入(发送)数据。 第1832行,poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写。 第1833行,unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的ioctl函数对应。 第1834行,compat_ioctl函数与unlocked_ioctl函数功能一样,区别在于在64位系统上,32位的应用程序调用将会使用此函数。在32位的系统上运行32位的应用程序调用的是unlocked_ioctl。 第1835行,mmap函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如LCD驱动的显存,将帧缓冲(LCD显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。 第1837行,open函数用于打开设备文件。 第1839行,release函数用于释放(关闭)设备文件,与应用程序中的close函数对应。 第1841行,fasync函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。 在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像open、release、write、read等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求。 20.2 字符设备驱动开发步骤 上一小节我们简单的介绍了一下字符设备驱动,那么字符设备驱动开发都有哪些步骤呢?我们在学习裸机或者STM32的时候关于驱动的开发就是初始化相应的外设寄存器,在Linux驱动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在Linux驱动开发中我们需要按照其规定的框架来编写驱动,所以说学Linux驱动开发重点是学习其驱动框架。 20.2.1 驱动模块的加载和卸载 Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用“modprobe”或者“insmod”命令加载驱动模块,本教程我们统一使用“modprobe”命令。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个Linux代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux内核中,当然也可以不编译进Linux内核中,具体看自己的需求。 模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下: module_init(xxx_init); //注册模块加载函数 module_exit(xxx_exit); //注册模块卸载函数 module_init函数用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用“modprobe”命令加载驱动的时候,xxx_init这个函数就会被调用。module_exit函数用来向Linux内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候xxx_exit函数就会被调用。字符设备驱动模块加载和卸载模板如下所示:

示例代码20.2.1.1 字符设备驱动模块加载和卸载函数模板
1  /* 驱动入口函数 */
2  static int __init xxx_init(void)
3  {
4   	/* 入口函数具体内容 */
5   	return 0;
6  }
7  
8  /* 驱动出口函数 */
9  static void __exit xxx_exit(void)
10 {
11  	/* 出口函数具体内容 */
12 }
13 
14 /* 将上面两个函数指定为驱动的入口和出口函数 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);
第2行,定义了个名为xxx_init的驱动入口函数,并且使用了“__init”来修饰。
第9行,定义了个名为xxx_exit的驱动出口函数,并且使用了“__exit”来修饰。
第15行,调用函数module_init来声明xxx_init为驱动入口函数,当加载驱动的时候xxx_init函数就会被调用。
第16行,调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用。
驱动编译完成以后扩展名为.ko,前面说了,有两种命令可以加载驱动模块:insmod和modprobe,insmod是最简单的模块加载命令,此命令用于加载指定的.ko模块,比如加载drv.ko这个驱动模块,命令如下:

insmod drv.ko insmod命令不能解决模块的依赖关系,比如drv.ko依赖first.ko这个模块,就必须先使用insmod命令加载first.ko这个模块,然后再加载drv.ko这个模块。但是modprobe就不会存在这个问题,modprobe会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe命令相比insmod要智能一些。modprobe命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用modprobe命令来加载驱动。modprobe命令默认会去/lib/modules/目录中查找模块,比如本书使用的Linux kernel的版本号为5.4.31,因此modprobe命令默认会到/lib/modules/5.4.31这个目录中查找相应的驱动模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建,这个我们在讲解buildroot构建根文件系统的时候已经说过了。 驱动模块的卸载使用命令“rmmod”即可,比如要卸载drv.ko,使用如下命令即可: rmmod drv.ko 也可以使用“modprobe -r”命令卸载驱动,比如要卸载drv.ko,命令如下: modprobe -r drv 使用modprobe命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用modprobe来卸载驱动模块。所以对于模块的卸载,还是推荐使用rmmod命令。 20.2.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)
register_chrdev函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major:主设备号,Linux下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体file_operations类型指针,指向设备的操作函数集合变量。

unregister_chrdev函数用户注销字符设备,此函数有两个参数,这两个参数含义如下: major:要注销的设备对应的主设备号。 name:要注销的设备对应的设备名。 一般字符设备的注册在驱动模块的入口函数xxx_init中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行。在示例代码20.2.1.1中字符设备的注册和注销,内容如下所示:

示例代码20.2.2.1 加入字符设备注册和注销
1  static struct file_operations test_fops;
2 
3  /* 驱动入口函数 */
4  static int __init xxx_init(void)
5  {
6   	/* 入口函数具体内容 */
7   	int retvalue = 0;
8 
9   	/* 注册字符设备驱动 */
10  	retvalue = register_chrdev(200, "chrtest", &test_fops);
11  	if(retvalue  printk and dmesg options
		-> (7) Default console loglevel (1-15)  	 //设置默认终端消息级别
		-> (4) Default message log level (1-7)  	 //设置默认消息级别

“Default console loglevel”就是用来设置CONSOLE_LOGLEVEL_DEFAULT的值,“Default message log level”设置CONFIG_MESSAGE_LOGLEVEL_DEFAULT的值。默认如图20.4.1.1所示: 在这里插入图片描述

图20.4.1.1 内核消息配置 图20.4.1.1可以看出,默认为CONSOLE_LOGLEVEL_DEFAULT默认为7。所以红CONSOLE_LOGLEVEL_DEFAULT的值默认也为7,MESSAGE_LOGLEVEL_DEFAULT默认值为4。CONSOLE_LOGLEVEL_DEFAULT控制着哪些级别的消息可以显示在控制台上,此宏默认为7,意味着只有优先级高于7的消息才能显示在控制台上。 这个就是printk和printf的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为4,4的级别比7高,所示直接使用printk输出的信息是可以显示在控制台上的。 参数filp有个叫做private_data的成员变量,private_data是个void指针,一般在驱动中将private_data指向设备结构体,设备结构体会存放设备的一些属性。 第46~61行,chrdevbase_read函数,应用程序调用read函数从设备中读取数据的时候此函数会执行。参数buf是用户空间的内存,读取到的数据存储在buf中,参数cnt是要读取的字节数,参数offt是相对于文件首地址的偏移。kerneldata里面保存着用户空间要读取的数据,第51行先将kerneldata数组中的数据拷贝到读缓冲区readbuf中,第52行通过函数copy_to_user将readbuf中的数据复制到参数buf中。因为内核空间不能直接操作用户空间的内存,因此需要借助copy_to_user函数来完成内核空间的数据到用户空间的复制。copy_to_user函数原型如下: static inline long copy_to_user(void __user *to, const void *from, unsigned long n) 参数to表示目的,参数from表示源,参数n表示要复制的数据长度。如果复制成功,返回值为0,如果复制失败则返回负数。 第71~84行,chrdevbase_write函数,应用程序调用write函数向设备写数据的时候此函数就会执行。参数buf就是应用程序要写入设备的数据,也是用户空间的内存,参数cnt是要写入的数据长度,参数offt是相对文件首地址的偏移。第75行通过函数copy_from_user将buf中的数据复制到写缓冲区writebuf中,因为用户空间内存不能直接访问内核空间的内存,所以需要借助函数copy_from_user将用户空间的数据复制到writebuf这个内核空间中。 第91~95行,chrdevbase_release函数,应用程序调用close关闭设备文件的时候此函数会执行,一般会在此函数里面执行一些释放操作。如果在open函数中设置了filp的private_data成员变量指向设备结构体,那么在release函数最终就要释放掉。 第100~106行,新建chrdevbase的设备文件操作结构体chrdevbase_fops,初始化chrdevbase_fops。 第113~124行,驱动入口函数chrdevbase_init,第118行调用函数register_chrdev来注册字符设备。 第131~136行,驱动出口函数chrdevbase_exit,第134行调用函数unregister_chrdev来注销字符设备。 第141~142行,通过module_init和module_exit这两个函数来指定驱动的入口和出口函数。 第147~148行,添加LICENSE和作者信息。 第149行是为了欺骗内核,给本驱动添加intree标记,如果不加就会有“loading out-of-tree module taints kernel.”这个警告 20.4.2 编写测试APP 1、C库文件操作基本函数 编写测试APP就是编写Linux应用,需要用到C库里面和文件操作有关的一些函数,比如open、read、write和close这四个函数。 ①、open函数 open函数原型如下: int open(const char *pathname, int flags) open函数参数含义如下: pathname:要打开的设备或者文件名。 flags:文件打开模式,以下三种模式必选其一: O_RDONLY 只读模式 O_WRONLY 只写模式 O_RDWR   读写模式 因为我们要对chrdevbase这个设备进行读写操作,所以选择O_RDWR。除了上述三种模式以外还有其他的可选模式,通过逻辑或来选择多种模式: O_APPEND   每次写操作都写入文件的末尾 O_CREAT   如果指定文件不存在,则创建这个文件 O_EXCL   如果要创建的文件已存在,则返回 -1,并且修改 errno 的值 O_TRUNC   如果文件存在,并且以只写/读写方式打开,则清空文件全部内容 O_NOCTTY   如果路径名指向终端设备,不要把这个设备用作控制终端。 O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞 O_DSYNC   等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提 下,不等待文件属性更新。 O_RSYNC   read 等待所有写入同一区域的写操作完成后再进行。 O_SYNC   等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。 返回值:如果文件打开成功的话返回文件的文件描述符。 在Ubuntu中输入“man 2 open”即可查看open函数的详细内容,如图20.4.2.1所示: 在这里插入图片描述

图20.4.2.1 open函数帮助信息 ②、read函数 read函数原型如下: ssize_t read(int fd, void *buf, size_t count) read函数参数含义如下: fd:要读取的文件描述符,读取文件之前要先用open函数打开文件,open函数打开文件成功以后会得到文件描述符。 buf:数据读取到此buf中。 count:要读取的数据长度,也就是字节数。 返回值:读取成功的话返回读取到的字节数;如果返回0表示读取到了文件末尾;如果返回负值,表示读取失败。在Ubuntu中输入“man 2 read”命令即可查看read函数的详细内容。 ③、write函数 write函数原型如下: ssize_t write(int fd, const void *buf, size_t count); write函数参数含义如下: fd:要进行写操作的文件描述符,写文件之前要先用open函数打开文件,open函数打开文件成功以后会得到文件描述符。 buf:要写入的数据。 count:要写入的数据长度,也就是字节数。 返回值:写入成功的话返回写入的字节数;如果返回0表示没有写入任何数据;如果返回负值,表示写入失败。在Ubuntu中输入“man 2 write”命令即可查看write函数的详细内容。 ④、close函数 close函数原型如下: int close(int fd); close函数参数含义如下: fd:要关闭的文件描述符。 返回值:0表示关闭成功,负值表示关闭失败。在Ubuntu中输入“man 2 close”命令即可查看close函数的详细内容。 2、编写测试APP程序 驱动编写好以后是需要测试的,一般编写一个简单的测试APP,测试APP运行在用户空间。测试APP很简单通过输入相应的指令来对chrdevbase设备执行读或者写操作。在1_chrdevbase目录中新建chrdevbaseApp.c文件,在此文件中输入如下内容:

示例代码20.4.2.1 chrdevbaseApp.c文件
1  #include "stdio.h"
2  #include "unistd.h"
3  #include "sys/types.h"
4  #include "sys/stat.h"
5  #include "fcntl.h"
6  #include "stdlib.h"
7  #include "string.h"
8  /***************************************************************
9  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
10 文件名     : chrdevbaseApp.c
11 作者       : 正点原子
12 版本       : V1.0
13 描述       : chrdevbase驱测试APP。
14 其他       : 使用方法:./chrdevbaseApp /dev/chrdevbase |
15              argv[2] 1:读文件
16              argv[2] 2:写文件       
17 论坛       : www.openedv.com
18 日志       : 初版V1.0 2019/1/30 正点原子团队创建
19 ***************************************************************/
20 
21 static char usrdata[] = {"usr data!"};
22 
23 /*
24  * @description  	: main主程序
25  * @param - argc  	: argv数组元素个数
26  * @param - argv  	: 具体参数
27  * @return        	: 0 成功;其他 失败
28  */
29 int main(int argc, char *argv[])
30 {
31  	int fd, retvalue;
32  	char *filename;
33  	char readbuf[100], writebuf[100];
34 
35  	if(argc != 3){
36      	printf("Error Usage!\r\n");
37      	return -1;
38  	}
39 
40  	filename = argv[1];
41 
42  	/* 打开驱动文件 */
43  	fd  = open(filename, O_RDWR);
44  	if(fd             
关注
打赏
1665308814
查看更多评论
0.0432s