字符设备驱动开发
记录第一次写博客
这玩意儿真的太难学了,前段时间对着正点原子的pdf学习了Ubuntu系统入门篇和系统移植篇,也跟着步骤敲了代码,其中乐趣自然有,但能坚持下去的动力还是差了点,所以决定从第三篇驱动开发开始记录自己学习嵌入式Linux的历程,希望能和大家一起走进Linux,发现嵌入式开发之美,成为一名合格的embedded engineer。
字符设备驱动简介
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节
流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,
LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
在详细的学习字符设备驱动架构之前,我们先来简单的了解一下 Linux 下的应用程序是如
何调用驱动程序的,Linux 应用程序对驱动程序的调用如下图所示:
Linux 应用程序对驱动程序的调用流程
](https://img-blog.csdnimg.cn/b9bb8d05e78d4e6c95dfbc5a1dc5a9b6.png#pic_center)
在 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 函数的
时候流程如下图所示:
其中关于 C 库以及如何通过系统调用“陷入”到内核空间这个我们不用去管,我们重点关
注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,
比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一
个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中
有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所
示:
示例代码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 等都是需要实现的,当然了,具体需要实现哪些函数还是要
看具体的驱动要求。
字符设备驱动开发步骤
我们在学习裸机或者 STM32 的时候关于驱动的开发就是初始化相应的外设寄存器,在 Linux 驱
动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在 Linux 驱动开发中
我们需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架。
驱动模块的加载和卸载
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 命令。
字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模
块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
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 中进行。字符设备的注册和注销,内容如下所示:
加入字符设备注册和注销
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 < 0){
12 /* 字符设备注册失败,自行处理 */
13 }
14 return 0;
15 }
16
17 /* 驱动出口函数 */
18 static void __exit xxx_exit(void)
19 {
20 /* 注销字符设备驱动 */
21 unregister_chrdev(200, "chrtest");
22 }
23
24 /* 将上面两个函数指定为驱动的入口和出口函数 */
25 module_init(xxx_init);
26 module_exit(xxx_exit);
第 1 行,定义了一个 file_operations 结构体变量 test_fops,test_fops 就是设备的操作函数集
合,只是此时我们还没有初始化 test_fops 中的 open、release 等这些成员变量,所以这个操作函
数集合还是空的。
第 10 行,调用函数 register_chrdev 注册字符设备,主设备号为 200,设备名字为“chrtest”,
设备操作函数集合就是第 1 行定义的 test_fops。要注意的一点就是,选择没有被使用的主设备
号,输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号,
在图 中可以列出当前系统中所有的字符设备和块设备,其中第 1 列就是设备对应
的主设备号。200 这个主设备号在我的开发板中并没有被使用,所以我这里就用了 200 这个主
设备号。
第 21 行,调用函数 unregister_chrdev 注销主设备号为 200 的这个设备。
实现设备的具体操作函数
file_operations 结构体就是设备的具体操作函数,在示例代码 20.2.2.1 中我们定义了
file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、
release、read 和 write 等具体的设备操作函数。本节我们就完成变量 test_fops 的初始化,设置好
针对 chrtest 设备的操作函数。在初始化 test_fops 之前我们要分析一下需求,也就是要对 chrtest
这个设备进行哪些操作,只有确定了需求以后才知道我们应该实现哪些操作函数。假设对 chrtest
这个设备有如下两个要求:
1、能够对 chrtest 进行打开和关闭操作
设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们
需要实现 file_operations 中的 open 和 release 这两个函数。
2、对 chrtest 进行读写操作
假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函
数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函
数。
需求很清晰了,修改示例代码 20.2.2.1,在其中加入 test_fops 这个结构体变量的初始化操
作,完成以后的内容如下所示:
示例代码 20.2.3.1 加入设备操作函数
1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
7
8 /* 从设备读取 */
9 static ssize_t chrtest_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
10 {
11 /* 用户实现具体功能 */
12 return 0;
13 }
14
15 /* 向设备写数据 */
16 static ssize_t chrtest_write(struct file *filp,
const char __user *buf,
size_t cnt, loff_t *offt)
17 {
18 /* 用户实现具体功能 */
19 return 0;
20 }
21
22 /* 关闭/释放设备 */
23 static int chrtest_release(struct inode *inode, struct file *filp)
24 {
25 /* 用户实现具体功能 */
26 return 0;
27 }
28
29 static struct file_operations test_fops = {
30 .owner = THIS_MODULE,
31 .open = chrtest_open,
32 .read = chrtest_read,
33 .write = chrtest_write,
34 .release = chrtest_release,
35 };
36
37 /* 驱动入口函数 */
38 static int __init xxx_init(void)
39 {
40 /* 入口函数具体内容 */
41 int retvalue = 0;
42
43 /* 注册字符设备驱动 */
44 retvalue = register_chrdev(200, "chrtest", &test_fops);
45 if(retvalue < 0){
46 /* 字符设备注册失败,自行处理 */
47 }
48 return 0;
49 }
50
51 /* 驱动出口函数 */
52 static void __exit xxx_exit(void)
53 {
54 /* 注销字符设备驱动 */
55 unregister_chrdev(200, "chrtest");
56 }
57
58 /* 将上面两个函数指定为驱动的入口和出口函数 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
在示例代码 20.2.3.1 中我们一开始编写了四个函数:chrtest_open、chrtest_read、
chrtest_write 和 chrtest_release。这四个函数就是 chrtest 设备的 open、read、write 和 release 操
作函数。第 29 行~35 行初始化 test_fops 的 open、read、write 和 release 这四个成员变量。
添加 LICENSE 和作者信息
最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否
则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用如下两个函数:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
最后给示例代码 20.2.3.1 加入 LICENSE 和作者信息,完成以后的内容如下:
示例代码 20.2.4.1 字符设备驱动最终的模板
1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
......
57
58 /* 将上面两个函数指定为驱动的入口和出口函数 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
61
62 MODULE_LICENSE("GPL");
63 MODULE_AUTHOR("zuozhongkai");
第 62 行,LICENSE 采用 GPL 协议。
第 63 行,添加作者名字。
至此,字符设备驱动开发的完整步骤就讲解完了,而且也编写好了一个完整的字符设备驱
动模板,以后字符设备驱动开发都可以在此模板上进行。
Linux 设备号
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分
组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了
一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:
示例代码 20.3.1.1 设备号 dev_t
13 typedef __u32 __kernel_dev_t;
......
16 typedef __kernel_dev_t dev_t;
可以看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h 里面,定义如下:
示例代码 20.3.1.2 __u32 类型
27 typedef unsigned int __u32;
综上所述,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构
成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux
系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文
件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:
示例代码 20.3.1.3 设备号操作函数
7 #define MINORBITS 20
8 #define MINORMASK ((1U << MINORBITS) - 1)
9
10 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
11 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
12 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
第 7 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 8 行,宏 MINORMASK 表示次设备号掩码。
第 10 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 11 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 12 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
撤销:
设备号的分配
1、静态分配设备号
本小节讲的设备号分配主要是主设备号的分配。前面讲解字符设备驱动的时候说过了,注
册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态指定的一个
设备号,比如 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,
具体分配的内容可以查看文档 Documentation/devices.txt。并不是说内核开发者已经分配掉的主
设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设
备号,使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。
2、动态分配设备号
静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用
的。而且静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字
符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。
卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:
dev:保存申请到的设备号。
baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这
些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递
增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count:要申请的设备号数量。
name:设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
此函数有两个参数:
from:要释放的设备号。
count:表示从 from 开始,要释放的设备号数量。
chrdevbase 字符设备驱动开发实验
字符设备驱动开发的基本步骤我们已经了解了,本节我们就以 chrdevbase 这个虚拟设备为
例,完整的编写一个字符设备驱动模块。chrdevbase 不是实际存在的一个设备,是笔者为了方
便讲解字符设备的开发而引入的一个虚拟设备。chrdevbase 设备有两个缓冲区,一个读缓冲区,
一个写缓冲区,这两个缓冲区的大小都为 100 字节。在应用程序中可以向 chrdevbase 设备的写
缓冲区中写入数据,从读缓冲区中读取数据。chrdevbase 这个虚拟设备的功能很简单,但是它
包含了字符设备的最基本功能。
实验程序编写
本实验对应的例程路径为:开发板光盘-> 1、Linux 驱动例程-> 1_chrdevbase。
应用程序调用 open 函数打开 chrdevbase 这个设备,打开以后可以使用 write 函数向
chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),也可以使用 read 函数读取读
缓冲区 readbuf 中的数据操作,操作完成以后应用程序使用 close 函数关闭 chrdevbase 设备。
1、创建 VSCode 工程
在 Ubuntu 中创建一个目录用来存放 Linux 驱动程序,比如我创建了一个名为 Linux_Drivers
的目录来存放所有的 Linux 驱动。在 Linux_Drivers 目录下新建一个名为 1_chrdevbase 的子目录
来存放本实验所有文件,如图所示
在 1_chrdevbase 目录中新建 VSCode 工程,并且新建 chrdevbase.c 文件,完成以后
1_chrdevbase 目录中的文件如图所示
2、添加头文件路径
因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux
源码中的头文件路径。打开 VSCode,按下“Crtl+Shift+P”打开 VSCode 的控制台,然后输入
“C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件,如图所示
打开以后会自动在.vscode 目录下生成一个名为 c_cpp_properties.json 的文件,此文件默认
内容如下所示:
示例代码 20.4.1.1 c_cpp_properties.json 文件原内容
1 {
2 "configurations": [
3 {
4 "name": "Linux",
5 "includePath": [
6 "${workspaceFolder}/**",
7 ],
8 "defines": [],
9 "compilerPath": "/usr/bin/clang",
10 "cStandard": "c11",
11 "cppStandard": "c++17",
12 "intelliSenseMode": "clang-x64"
13 }
14 ],
15 "version": 4
16 }
第 5 行的 includePath 表示头文件路径,需要将 Linux 源码里面的头文件路径添加进来,也
就是我们前面移植的 Linux 源码中的头文件路径。添加头文件路径以后的 c_cpp_properties.json
的文件内容如下所示:
示例代码 20.4.1.2 添加头文件路径后的 c_cpp_properties.json
1 {
2 "configurations": [
3 {
4 "name": "Linux",
5 "includePath": [
6 "${workspaceFolder}/**",
7 "/home/zuozhongkai/linux/atk-mp1/linux/my_linux/
linux-5.4.31/arch/arm/include",
8 "/home/zuozhongkai/linux/atk-mp1/linux/my_linux/
linux-5.4.31/include",
9 "/home/zuozhongkai/linux/atk-mp1/linux/my_linux/
linux-5.4.31/arch/arm/include/generated"
10 ],
11 "defines": [],
12 "compilerPath": "/usr/bin/gcc",
13 "cStandard": "gnu11",
14 "cppStandard": "gnu++14",
15 "intelliSenseMode": "gcc-x64"
16 }
17 ],
18 "version": 4
19 }
第 7~9 行就是添加好的 Linux 头文件路径。分别是开发板所使用的 Linux 源码下的 include、
arch/arm/include 和 arch/arm/include/generated 这三个目录的路径,注意,这里使用了绝对路径。
3、编写实验程序
工程建立好以后就可以开始编写驱动程序了,新建 chrdevbase.c,然后在里面输入如下内容:
示例代码 20.4.1.3 chrdevbase.c 文件
1 #include <linux/types.h>
2 #include <linux/kernel.h>
3 #include <linux/delay.h>
4 #include <linux/ide.h>
5 #include <linux/init.h>
6 #include <linux/module.h>
7 /***************************************************************
8 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
9 文件名 : chrdevbase.c
10 作者 : 正点原子
11 版本 : V1.0
12 描述 : chrdevbase 驱动文件。
13 其他 : 无
14 论坛 : www.openedv.com
15 日志 : 初版 V1.0 2020/12/26 正点原子团队创建
16 ***************************************************************/
17
18 #define CHRDEVBASE_MAJOR 200 /* 主设备号 */
19 #define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
20
21 static char readbuf[100]; /* 读缓冲区 */
22 static char writebuf[100]; /* 写缓冲区 */
23 static char kerneldata[] = {"kernel data!"};
24
25 /*
26 * @description : 打开设备
27 * @param – inode : 传递给驱动的 inode
28 * @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
29 * 一般在 open 的时候将 private_data 指向设备结构体。
30 * @return : 0 成功;其他 失败
31 */
32 static int chrdevbase_open(struct inode *inode, struct file *filp)
33 {
34 //printk("chrdevbase open!\r\n");
35 return 0;
36 }
37
38 /*
39 * @description : 从设备读取数据
40 * @param - filp : 要打开的设备文件(文件描述符)
41 * @param - buf : 返回给用户空间的数据缓冲区
42 * @param - cnt : 要读取的数据长度
43 * @param - offt : 相对于文件首地址的偏移
44 * @return : 读取的字节数,如果为负值,表示读取失败
45 */
46 static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
47 {
48 int retvalue = 0;
49
50 /* 向用户空间发送数据 */
51 memcpy(readbuf, kerneldata, sizeof(kerneldata));
52 retvalue = copy_to_user(buf, readbuf, cnt);
53 if(retvalue == 0){
54 printk("kernel senddata ok!\r\n");
55 }else{
56 printk("kernel senddata failed!\r\n");
57 }
58
59 //printk("chrdevbase read!\r\n");
60 return 0;
61 }
62
63 /*
64 * @description : 向设备写数据
65 * @param - filp : 设备文件,表示打开的文件描述符
66 * @param - buf : 要写给设备写入的数据
67 * @param - cnt : 要写入的数据长度
68 * @param - offt : 相对于文件首地址的偏移
69 * @return : 写入的字节数,如果为负值,表示写入失败
70 */
71 static ssize_t chrdevbase_write(struct file *filp,
const char __user *buf,
size_t cnt, loff_t *offt)
72 {
73 int retvalue = 0;
74 /* 接收用户空间传递给内核的数据并且打印出来 */
75 retvalue = copy_from_user(writebuf, buf, cnt);
76 if(retvalue == 0){
77 printk("kernel recevdata:%s\r\n", writebuf);
78 }else{
79 printk("kernel recevdata failed!\r\n");
80 }
81
82 //printk("chrdevbase write!\r\n");
83 return 0;
84 }
85
86 /*
87 * @description : 关闭/释放设备
88 * @param - filp : 要关闭的设备文件(文件描述符)
89 * @return : 0 成功;其他 失败
90 */
91 static int chrdevbase_release(struct inode *inode,
struct file *filp)
92 {
93 //printk("chrdevbase release!\r\n");
94 return 0;
95 }
96
97 /*
98 * 设备操作函数结构体
99 */
100 static struct file_operations chrdevbase_fops = {
101 .owner = THIS_MODULE,
102 .open = chrdevbase_open,
103 .read = chrdevbase_read,
104 .write = chrdevbase_write,
105 .release = chrdevbase_release,
106 };
107
108 /*
109 * @description : 驱动入口函数
110 * @param : 无
111 * @return : 0 成功;其他 失败
112 */
113 static int __init chrdevbase_init(void)
114 {
115 int retvalue = 0;
116
117 /* 注册字符设备驱动 */
118 retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
&chrdevbase_fops);
119 if(retvalue < 0){
120 printk("chrdevbase driver register failed\r\n");
121 }
122 printk("chrdevbase_init()\r\n");
123 return 0;
124 }
125
126 /*
127 * @description : 驱动出口函数
128 * @param : 无
129 * @return : 无
130 */
131 static void __exit chrdevbase_exit(void)
132 {
133 /* 注销字符设备驱动 */
134 unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
135 printk("chrdevbase_exit()\r\n");
136 }
137
138 /*
139 * 将上面两个函数指定为驱动的入口和出口函数
140 */
141 module_init(chrdevbase_init);
142 module_exit(chrdevbase_exit);
143
144 /*
145 * LICENSE 和作者信息
146 */
147 MODULE_LICENSE("GPL");
148 MODULE_AUTHOR("ALIENTEK");
149 MODULE_INFO(intree, "Y");
第 32~36 行,chrdevbase_open 函数,当应用程序调用 open 函数的时候此函数就会调用。
本例程中我们没有做任何工作,只是输出一串字符,用于调试。这里使用了 printk 来输出信息,
而不是 printf!因为在 Linux 内核中没有 printf 这个函数。printk 相当于 printf 的孪生兄妹,printf
运行在用户态,printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用
printk 这个函数。不同之处在于,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级
别,这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面,定义如下:
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用
KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
一共定义了 8 个级别,其中 0 的优先级最高,7 的优先级最低。如果要设置消息级别,参
考如下示例:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
上述代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。在
具体的消息前面加上 KERN_EMERG 就可以将这条消息的级别设置为 KERN_EMERG。如果使
用 printk 的 时 候 不 显 式 的 设 置 消 息 级 别 , 那 么 printk 将 会 采 用 默 认 级 别
MESSAGE_LOGLEVEL_DEFAULT。
在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT
MESSAGE_LOGLEVEL_DEFAULT 和 CONFIG_CONSOLE_LOGLEVEL_DEFAULT 是通
过内核图形化界面配置的,配置路径如下:
-> Kernel hacking
-> 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 的值。默认如图 所示:
图可以看出,默认为 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.”这个警告
编写测试 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 函数的详细内容,如图所示:
②、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 <1>|<2>
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 < 0){
45 printf("Can't open file %s\r\n", filename);
46 return -1;
47 }
48
49 if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
50 retvalue = read(fd, readbuf, 50);
51 if(retvalue < 0){
52 printf("read file %s failed!\r\n", filename);
53 }else{
54 /* 读取成功,打印出读取成功的数据 */
55 printf("read data:%s\r\n",readbuf);
56 }
57 }
58
59 if(atoi(argv[2]) == 2){
60 /* 向设备驱动写数据 */
61 memcpy(writebuf, usrdata, sizeof(usrdata));
62 retvalue = write(fd, writebuf, 50);
63 if(retvalue < 0){
64 printf("write file %s failed!\r\n", filename);
65 }
66 }
67
68 /* 关闭设备 */
69 retvalue = close(fd);
70 if(retvalue < 0){
71 printf("Can't close file %s\r\n", filename);
72 return -1;
73 }
74
75 return 0;
76 }
第 21 行,数组 usrdata 是测试 APP 要向 chrdevbase 设备写入的数据。
第 35 行,判断运行测试 APP 的时候输入的参数是不是为 3 个,main 函数的 argc 参数表示
参数数量,argv[]保存着具体的参数,如果参数不为 3 个的话就表示测试 APP 用法错误。比如,
现在要从 chrdevbase 设备中读取数据,需要输入如下命令:
./chrdevbaseApp /dev/chrdevbase 1
上述命令一共有三个参数“./chrdevbaseApp”、“/dev/chrdevbase”和“1”,这三个参数分别
对应 argv[0]、argv[1]和 argv[2]。第一个参数表示运行 chrdevbaseAPP 这个软件,第二个参数表
示测试APP要打开/dev/chrdevbase这个设备。第三个参数就是要执行的操作,1表示从chrdevbase
中读取数据,2 表示向 chrdevbase 写数据。
第 40 行,获取要打开的设备文件名字,argv[1]保存着设备名字。
第 43 行,调用 C 库中的 open 函数打开设备文件:/dev/chrdevbase。
第 49 行,判断 argv[2]参数的值是 1 还是 2,因为输入命令的时候其参数都是字符串格式
的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字。
第 50 行,当 argv[2]为 1 的时候表示要从 chrdevbase 设备中读取数据,一共读取 50 字节的
数据,读取到的数据保存在 readbuf 中,读取成功以后就在终端上打印出读取到的数据。
第 59 行,当 argv[2]为 2 的时候表示要向 chrdevbase 设备写数据。
第 69 行,对 chrdevbase 设备操作完成以后就关闭设备。
chrdevbaseApp.c 内容还是很简单的,就是最普通的文件打开、关闭和读写操作。
编译驱动程序和测试 APP
1、编译驱动程序
首先编译驱动程序,也就是 chrdevbase.c 这个文件,我们需要将其编译为.ko 模块,创建
Makefile 文件,然后在其中输入如下内容:
1 KERNELDIR := /home/zuozhongkai/linux/my_linux/linux-5.4.31
2 CURRENT_PATH := $(shell pwd)
3 obj-m := chrdevbase.o
4
5 build: kernel_modules
6
7 kernel_modules:
8 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
9 clean:
10 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第 1 行,KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径,大家根
据自己的实际情况填写即可。
第 2 行,CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路
径。
第 3 行,obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块。
第 8 行,具体的编译命令,后面的 modules 表示编译模块,-C 表示将当前的工作目录切
换到指定目录中,也就是 KERNERLDIR 目录。M 表示模块源码目录,“make modules”命令
中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
Makefile 编写好以后输入“make”命令编译驱动模块,编译过程如图所示:
编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动
模块。至此,chrdevbase 设备的驱动就编译成功
2、编译测试 APP
测试 APP 比较简单,只有一个文件,因此就不需要编写 Makefile 了,直接输入命令编译。
因为测试 APP 是要在 ARM 开发板上运行的,所以需要使用 arm-linux-gnueabihf-gcc 来编译,
输入如下命令:
arm-none-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
编译完成以后会生成一个叫做 chrdevbaseApp 的可执行程序,输入如下命令查看
chrdevbaseAPP 这个程序的文件信息:
file chrdevbaseApp
结果如图所示:
从图可以看出,chrdevbaseAPP 这个可执行文件是 32 位 LSB 格式,ARM 版本的,
因此 chrdevbaseAPP 只能在 ARM 芯片下运行。
运行测试
1、加载驱动模块
驱动模块 chrdevbase.ko 和测试软件 chrdevbaseAPP 都已经准备好了,接下来就是运行测试。
为了方便测试,Linux 系统选择通过 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统。
确保 uboot 中 bootcmd 环境变量的值为:
tftp c2000000 uImage;tftp c4000000 stm32mp157d-atk.dtb;bootm c2000000 - c4000000
bootargs 环境变量的值为:
console=ttySTM0,115200 root=/dev/nfs nfsroot=192.168.1.249:/home/zuozhongkai/linux/nfs/r
ootfs,proto=tcp rw ip=192.168.1.250:192.168.1.249:192.168.1.1:255.255.255.0::eth0:off
设置好以后启动 Linux 系统,检查开发板根文件系统中有没有“/lib/modules/5.4.31”这个目
录,如果没有的话自行创建。因为是通过 NFS 将 Ubuntu 中的 rootfs(第十八章制作好的 buildroot
根文件系统)目录挂载为根文件系统,所以可以很方便的将 chrdevbase.ko 和 chrdevbaseAPP 复制
到 rootfs/lib/modules/5.4.31 目录中,命令如下:
sudo cp chrdevbase.ko chrdevbaseApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/ -f
拷 贝 完 成 以 后 就 会 在 开 发 板 的 /lib/modules/5.4.31 目 录 下 存 在 chrdevbase.ko 和
chrdevbaseAPP 这两个文件,如图所示:
输入如下命令加载 chrdevbase.ko 驱动文件:
modprobe chrdevbase.ko
或
insmod chrdevbase.ko
如果使用 modprobe 加载驱动的话,可能会出现如图 20.4.4.2 或图 20.4.4.3 所示的提示:
图 20.4.4.2 和图 20.4.4.3 分别提示找不到“moudules.dep”和“chrdevbase.ko”,产生这两个
错误的原因都是一样的。modprobe 命令会在“/lib/modules/5.4.31”目录下解析 modules.dep 文
件,modules.dep 文件里面保存了要加载的.ko 模块,我们不用手动创建 modules.dep 这个文件,
直接输入 depmod 命令即可自动生成 modules.dep,有些根文件系统可能没有 depmod 这个命令,
如果没有这个命令就只能重新配置 busybox,使能此命令,然后重新编译 busybox。输入“depmod”
命令以后会自动生成 modules.alias、modules.symbols 和 modules.dep 等等一些 modprobe 所需的
文件,如图所示:
重新使用 modprobe 加载 chrdevbase.ko,结果如图所示:
从图 20.4.4.5 可以看到“chrdevbase init!”这一行,这一行正是 chrdevbase.c 中模块入口函
数 chrdevbase_init 输出的信息,说明模块加载成功!
注意:用 modprobe 这个命令加载模块不用加‘.ko’,加了的话可能报会图 20.4.4.3 所示错误,
所以建议大家在使用 modprobe 命令加载驱动模块的时候不用加模块名字后面的“.ko”后缀。
输入“lsmod”命令即可查看当前系统中存在的模块,结果如图所示:
从图 20.4.4.6 可以看出,当前系统只有“chrdevbase”这一个模块。输入如下命令查看当前
系统中有没有 chrdevbase 这个设备:
cat /proc/devices
结果如图 所示:
从图可以看出,当前系统存在 chrdevbase 这个设备,主设备号为 200,跟我们设置的主设备号一致
2、创建设备节点文件
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操
作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节
点文件:
mknod /dev/chrdevbase c 200 0
其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个
字符设备,“200”是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在
/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看,结果如图所示:
如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。
相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。前面一直说 Linux 下
一切皆文件,包括设备也是文件,现在大家应该是有这个概念了吧?
3、chrdevbase 设备操作测试
一切准备就绪,接下来就是“大考”的时刻了。使用 chrdevbaseApp 软件操作 chrdevbase 这
个设备,看看读写是否正常,首先进行读操作,输入如下命令:
./chrdevbaseApp /dev/chrdevbase 1
结果如图所示:
从图可以看出,首先输出“kernel senddata ok!”这一行信息,这是驱动程序中
chrdevbase_read 函数输出的信息,因为 chrdevbaseAPP 使用 read 函数从 chrdevbase 设备读取数
据,因此 chrdevbase_read 函数就会执行。chrdevbase_read 函数向 chrdevbaseAPP 发送“kernel
data!”数据,chrdevbaseAPP 接收到以后就打印出来,“read data:kernel data!”就是 chrdevbaseAPP
打印出来的接收到的数据。说明对 chrdevbase 的读操作正常,接下来测试对 chrdevbase 设备的
写操作,输入如下命令:
./chrdevbaseApp /dev/chrdevbase 2
结果如图所示:
只有一行“kernel recevdata:usr data!”,这个是驱动程序中的 chrdevbase_write 函数输出的。
chrdevbaseAPP 使用 write 函数向 chrdevbase 设备写入数据“usr data!”。chrdevbase_write 函数接
收到以后将其打印出来。说明对 chrdevbase 的写操作正常,既然读写都没问题,说明我们编写
的 chrdevbase 驱动是没有问题的。
4、卸载驱动模块
如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉 chrdevbase 这
个设备:
rmmod chrdevbase
卸载以后使用 lsmod 命令查看 chrdevbase 这个模块还存不存在,结果如图所示:
从图可以看出,此时系统已经没有任何模块了,chrdevbase 这个模块也不存在了,
说明模块卸载成功。
至此,chrdevbase 这个设备的整个驱动就验证完成了,驱动工作正常。本章我们详细的讲
解了字符设备驱动的开发步骤,并且以一个虚拟的 chrdevbase 设备为例,带领大家完成了第一
个字符设备驱动的开发,掌握了字符设备驱动的开发框架以及测试方法,以后的字符设备驱动
实验基本都以此为蓝本。