字符设备是linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的,比如我们最常见的点灯、按键、IIC、SPI、LCD等等都是字符设备,这些设备的驱动叫做字符设备驱动。
我记得除了字节流之外好像还有一个叫字符流的
没错百度了一下真的有,科普一下,顺便给自己做个笔记
字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流在操作时使用了缓冲区,通过缓冲区再操作文件
在学习字符设备驱动架构之前,先简单了解一下linux下的应用程序是如何调用驱动程序的,linux应用程序对驱动程序的调用如下:
在linux中一切都是文件,驱动加载成功之后会在/dev目录下生成一个相应的文件,比如有一个/dev/led的驱动文件,此文件是led灯的驱动文件。应用程序使用open函数来打开文件/dev/led的驱动文件。使用完成之后使用close函数关闭/dev/led这个文件。如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。
==用户空间不能对内核进行操作,所以要使用系统调用的方法来实现从用户空间陷入到系统空间。==当我们调用open函数的时候,流程图如下:
每一个系统调用,在驱动中都有与之相对应的一个驱动函数,在Linux内核文件include/linux/fs.h中,有一个叫做file_operations的结构体,就是linux内核驱动操作函数集合。如下:
1589行:owner拥有该结构体的模块的指针,一般设置为THIS_MODULE
。
第 1590 行, llseek 函数用于修改文件当前的读写位置。
第 1591 行, read 函数用于读取设备文件。
第 1592 行, write 函数用于向设备文件写入(发送)数据。
第 1596 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 1597 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 1598 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
第 1599 行, mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 1601 行, open 函数用于打开设备文件。
第 1603 行, release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 1604 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 1605 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像 open、 release、 write、 read 等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求
linux驱动有两种运行方式,第一种就是将驱动编译进linux内核中,这样当linux内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(linux下模块拓展名为.ko),在linux内核启动以后使用insmod命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
当使用“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。
当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
字符设备驱动模块加载和卸载模板如下:
/*驱动入口函数*/
static int __init xxx_init(void) //定义了一个名为xxx_init的驱动入口函数,用_init来修饰
{
/*入口函数具体内容*/
return 0;
}
/*驱动出口函数*/
static void __exit xxx_exit(void) //定义了个名为 xxx_exit 的驱动出口函数,并且使用了“__exit”来修饰。
{
/*出口函数具体内容*/
}
/*将上面两个函数指定为驱动的入口和出口函数*/
module_init(xxx_init); //调用函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init函数就会被调用。
module_exit(xxx_exit); //调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用。
驱动编译完成以后拓展名为.ko
有两种命令可以加载驱动模块:insmod
modprobe
insmod
用来加载指定的.ko模块,比如加载drv.ko这个驱动模块,命令为insmod drv.ko
但是insmod不能解决模块的依赖关系,modprobe可以。比如说drv.ko 依赖 first.ko 这个模块,使用insmod的时候就要先执行insmod first.ko
再执行insmod drv.ko
也有两种命令卸载驱动模块,比如要卸载drv.ko可以使用:
rmmod drv.ko
也可以使用
modprobe -r drv.ko
使用 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 中进行。示例如下:
static struct file_operations test_fops;
/*驱动入口函数*/
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);
可能会有疑问,为什么知道上面函数register_chrdev中的设备号为200,其实为其他的也是可以,只要没被使用,怎样看呢?在secureCRT中输入cat /proc/devices
后面还有一些,只不过没有列出来,然后发现设备号200没有被使用,所以就用这个。
file_operations 结构体就是设备的具体操作函数,在Linux内核文件include/linux/fs.h中,上面有说过哦。在上一个代码片中,定义了file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、release、read和write等jvti的设备操作函数。在初始化test_fops之前我们要分析一下需求,也就是要对chrtest这个设备进行哪些操作。
1、能够对 chrtest 进行打开和关闭操作
设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open 和 release 这两个函数。
2、对 chrtest 进行读写操作
假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函数。
需求清晰之后,整个流程变成了下面这个样子
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
}
/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
/* 用户实现具体功能 */
return 0;
}
/* 向设备写数据 */
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 *fil
{
/* 用户实现具体功能 */
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);
其实我发现就是把file_operations结构体里面的操作函数具体化
最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下两个函数:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
这个添加到上面代码片的最后。
为了方面管理,linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。linux提供了一个名为dev_t的数据类型表示设备号,dev_t定义在文件include/linux/types.h中,定义如下:
可以看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h
typedef unsigned int __u32;
所以 dev_t就是unsigned int类型,是一个32位的数据类型。这32位数据构成了主设备号和次设备号两部分,其中高12位为主设备号,低20位为次设备号。因此Linux系统中主设备号范围为 0~4095
在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
宏 MINORBITS 表示次设备号位数,一共是 20 位。
宏 MINORMASK 表示次设备号掩码。
宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
宏 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 开始,要释放的设备号数量