在上一小节中已经对设备号的相关知识进行了讲解,并成功申请到了设备号,那在Linux系统中,设备号是怎样与字符设备进行关联的呢?字符设备又是怎样注册的呢?带着疑问,让我们开始本章节的学习吧。
10.1 注册字符设备
注册字符设备可以分为两个步骤:
1.字符设备初始化
2.字符设备的添加
在本小节将对上述两个步骤所用到的函数和结构体进行讲解。
10.1.1 字符设备初始化
字符设备初始化所用到的函数为cdev_init(…),在对该函数讲解之前,首先对cdev结构体进行介绍。
Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作file_operations结构体(设备的打开、读写、关闭等操作接口)等信息,struct cdev 结构体定义在“内核源码/include/linux/cdev.h”文件中(在编写驱动程序的时候要加入该文件的引用),如下(图10-1)所示:
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
图 10-1
关于该结构体参数的注释在上图已经添加,设备初始化所用到的函数为cdev_init(),该函数同样在“内核源码/include/linux/cdev.h”文件中所引用如下(图10-2)所示:
void cdev_init(struct cdev *, const struct file_operations *);
图10-2
该函数的详细内容在“内核源码/include/fs/char_dev.c”文件中定义,如下(图10-3)所示:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);//将整个结构体清零;
INIT_LIST_HEAD(&cdev->list);//初始化list成员使其指向自身;
kobject_init(&cdev->kobj, &ktype_cdev_default);//初始化kobj成员;
cdev->ops = fops;//初始化ops成员,建立cdev 和 file_operations之间的连接
}
图 10-3
函数作用:
初始化传入的cdev 类型的结构体,并与自定义的file_operations * 类型的结构体进行链接。
参数含义:
cdev: 要传入的cdev类型结构体,为要初始化的字符设备。
fops:要传入的file_operations * 类型结构体,关于file_operations结构体的相关的知识会在下一章节进行讲解。
**函数返回值:**无返回值。
10.1.2 字符设备的注册
字符设备的注册:
字符设备添加所用到的函数为cdev_add(),该函数在“内核源码/include/linux/cdev.h”文件中所引用,如下(图10-4)所示:
int cdev_add(struct cdev *, dev_t, unsigned);
图 10-4
函数原型:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
函数作用:
该函数向内核注册一个struct cdev结构体
参数含义:
(1)第一个参数为要添加的struct cdev 类型的结构体
(2)第二个参数为申请的字符设备号
(3)第三个参数为和该设备关联的设备编号的数量。
这两个参数直接赋值给struct cdev 的dev成员和count成员。
**函数返回值:**添加成功返回0,添加失败返回负数。
字符设备的注销:
字符设备删除所用到的函数为cdev_del(),该函数同样在“内核源码/include/linux/cdev.h”文件中所引用,如下(图10-5)所示:
void cdev_del(struct cdev *);
图 10-5
函数原型:
void cdev_del(struct cdev *p)
函数作用:
该函数会向内核删除一个struct cdev 类型结构体
参数含义:
该函数只有一个参数,为要删除的struct cdev 类型的结构体
**函数返回值:**无返回值
至此,关于注册字符设备实验所用到的函数就讲解完成了,在下一小节中将编写注册字符设备代码。
10.2 实验程序的编写
本实验对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\05。
本实验采用动态申请设备号的方式进行设备号的申请,然后对设备进行注册,并将申请到的主设备号和次设备号以及设备注册情况打印到终端上。
编写完成的cdev.c代码如下(图10-6)所示
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
struct cdev cdev_test;//定义cdev结构体类型的变量cdev_test
struct file_operations cdev_test_ops{
.owner=THIS_MODULE//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
};//定义file_operations结构体类型的变量cdev_test_ops
static int __init module_cdev_init(void)//驱动入口函数
{
int ret;//定义int类型变量ret,进行函数返回值判断
int major,minor;//定义int类型的主设备号major和次设备号minor
ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名chrdev_name
if (ret < 0){
printk("alloc_chrdev_region is error\n");
}
printk("alloc_register_region is ok\n");
major = MAJOR(dev_num);//使用MAJOR()函数获取主设备号
minor = MINOR(dev_num);//使用MINOR()函数获取次设备号
printk("major is %d\n",major);
printk("minor is %d\n",minor);
cdev_init(&cdev_test,&cdev_test_ops);//使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体
cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
ret = cdev_add(&cdev_test,dev_num,1);//使用cdev_add()函数进行字符设备的添加
if(ret < 0 ){
printk("cdev_add is error\n");
}
printk("cdev_add is ok\n");
return 0;
}
static void __exit module_cdev_exit(void)//驱动出口函数
{
cdev_del(&cdev_test);//使用cdev_del()函数进行字符设备的删除
unregister_chrdev_region(dev_num,1);//释放字符驱动设备号
printk("module exit \n");
}
module_init(module_cdev_init);//注册入口函数
module_exit(module_cdev_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet"); //作者信息
编写完成的cdev.c代码如下(图10-6)所示
图 10-6
相较于上一章节实验,本章节的代码去掉了静态申请设备号部分代码,并在申请设备号完成之后注册了相应的字符设备,并在驱动出口函数中添加了相应的字符设备删除代码(相关代码已加粗)。
需要注意的是,字符设备的注册要放在申请字符设备号之后,字符设备的删除要放在释放字符驱动设备号之前。
10.3 运行测试
10.3.1 编译驱动程序
在上一小节中的cdev.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下(图10-7)所示:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += cdev.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
图 10-7
对于Makefile的内容注释已在上图添加,保存退出之后,来到存放parameter.c和Makefile文件目录下,如下图(图10-8)所示:
图 10-8
然后使用命令“make”进行驱动的编译,编译完成如下图(图10-9)所示:
图 10-9
编译完会生成 cdev.ko目标文件,如下图(图10-10)所示:
图 10-10
至此我们的驱动模块就编译成功了,下面进行驱动的运行测试。
10.3.2 运行测试
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图10-11)所示:
insmod cdev.ko
图 10-11
可以看到动态申请设备号成功了,主设备号为236,次设备号为0,然后使用以下命令进行注册设备号的查看,如下图(图10-12)所示:
cat /proc/devices
图 10-12
可以看到主设备号236的设备名为chrdev_name,和驱动程序中设置的设备名称相同,证明字符设备注册成功了,最后可以使用以下命令对驱动进行卸载,卸载完成如下图(10-13)所示:
rmmod cdev.ko
图 10-13
【最新驱动资料(文档+例程)】
链接 https://pan.baidu.com/s/1M4smUG2vw_hnn0Hye-tkog
提取码:hbh6
【B 站配套视频】
https://b23.tv/XqYa6Hm
【RK3568 购买链接】
https://item.taobao.com/item.htm?spm=a1z10.5-c-s.w4002-2245
ev.ko
[外链图片转存中…(img-zvsdnSuT-1694140423531)]
图 10-13
【最新驱动资料(文档+例程)】
链接 https://pan.baidu.com/s/1M4smUG2vw_hnn0Hye-tkog
提取码:hbh6
【B 站配套视频】
https://b23.tv/XqYa6Hm
【RK3568 购买链接】
https://item.taobao.com/item.htm?spm=a1z10.5-c-s.w4002-2245
2452613.11.2fec74a6elWNeA&id=669939423234