LV06-03-chrdev-08-1个驱动兼容多个设备
可不可以通过一个驱动控制多个设备?怎么实现?若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04 | |
linux内核 | linux-4.19.71(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
nxp-imx/linux-imx/releases/tag/v4.19.71 | NXP linux内核仓库tags中的v4.19.71 | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 | NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0 | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
Source Code | https://elixir.bootlin.com/linux/latest/source | linux kernel源码 |
kernel/git/stable/linux.git - Linux kernel stable tree | linux kernel源码(官网,tag 4.19.71) | |
https://elixir.bootlin.com/u-boot/latest/source | uboot源码 |
一、概述
1. 背景
在Linux内核中,主设备号用于标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备服务。但是, 次设备号表示了同类设备的各个设备。每个设备的功能都是不一样的。如何能够用一个驱动程序去控制各种设备呢?
很明显,首先,我们可以根据次设备号,来区分各种设备;其次,就是提到过的file结构体的私有数据成员private_data。 我们可以通过该成员来指向不同的设备,不难想到为什么只有open函数和close函数的形参才有file结构体, 因为驱动程序第一个执行的是操作就是open,通过open函数就可以控制我们想要驱动的底层硬件。
2. 可以有哪些方式
有两种方式:
- (1)一个cdev对象管理多个设备;
- (2)多个cdev对象,每个cdev对象管理一个设备。
3. inode结构体
前面学习这个inode结构体的时候有这么两个成员:fs.h - include/linux/fs.h - struct inode
1 | struct inode { |
- dev_t i_rdev: 表示设备文件的结点,这个域实际上包含了设备号。
- struct cdev *i_cdev: struct cdev是内核的一个内部结构,它是用来表示字符设备的,当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。
一个含有设备号,一个指向字符设备,我们可以利用这两个成员来区分不同设备的。后面会说明这两个成员怎么用。
二、1个cdev设备
1. 怎么实现?
1个cdev的话,我们只需要一个cdev对象,一个类,但是设备号需要创建多个,并且根据不同的次设备号创建多个设备节点,用来控制不同的缓冲区。
不同的设备节点对应不同的次设备号,我们可以在open的时候根据设备节点,找到设备号,根据次设备号来对应不同的缓冲区。那另一个问题来了,怎么在open的时候获取次设备号?
inode结构体中,对于设备文件的设备号会被保存到其成员i_rdev中,所以:
1 | static int sdev_open(struct inode *inode, struct file *file) |
为什么?这个我没有去深究inode->i_rdev中怎么吧设备号保存下来的,我查到在使用mknod命令创建节点的时候会调用这个函数inode.c - fs/inode.c - init_special_inode:
1
2
3
4
5
6
7
8
9
10 void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
}
//......
}
EXPORT_SYMBOL(init_special_inode);会发现这里是有赋值的,大概应该是这里,不过自动创建节点的函数中怎么赋值的我追了一下代码,没找到,这里先不管了,知道它保存的是设备号就是了,后面有需要了再详细去了解。
2. demo实现
直接看我的gitee仓库吧:04_chrdev_basic/09_chrdev_more_dev_1c · 苏木/imx6ull-driver-demo - 码云 - 开源中国
直接执行make即可进行编译,编译完成后会得到对应的驱动程序和应用测试程序。
点击查看详情
1 |
|
3. 开发板测试
- (1)加载驱动
1 | insmod chrdev_more_dev_1c_demo.ko |
- (2)查看设备节点
1 | ls /dev/sdevice* -alh |
会看到有如下打印信息:
1 | crw-rw---- 1 0 root 246, 0 Jan 1 00:00 /dev/sdevice0 |
在创建设备节点的时候命名就是/dev/sdeviceN,所以这里是0和1两个设备节点,他们会对应不同的缓冲区。
- (3)向设备节点写入数据
1 | echo 123456789 > /dev/sdevice0 |
- (4)从设备节点读取数据
1 | cat /dev/sdevice0 |
三、多个cdev设备
1. 怎么实现?
我们现在有多个设备,他们的主设备号一样,次设备号不同,他们也可以使用不同的cdev设备对象,都需要进行注册。但是他们都属于同一个类,所以我们只需要创建一个类就可以了。
- 1、设备号申请的时候要直接连续申请多个;
- 2、向内核注册新字符设备的时候,每个设备都要注册;
- 3、创建类的时候只需要创建一个;
- 4、创建设备的时候需要创建多个设备;
- 5、删除设备的时候需要删除多个;
- 6、删除字符设备对象的时候需要删除多个。
1.1 设备号申请与注销
设备号身申请的时候不需要申请多次,只需要申请一次,并且指定要申请的设备数量即可,以 alloc_chrdev_region 为例:char_dev.c - fs/char_dev.c - alloc_chrdev_region:
1 | alloc_chrdev_region(&parentDevno, 0, dev_cnt, NEWCHAR_DEV_NAME); /* 申请设备号 */ |
这样我们就可以得到一个“父”设备号devno,这个设备号包含主设备号和次设备号:
1 | major = MAJOR(parentDevno); /* 获取分配号的主设备号 */ |
我们可以从这个设备号中获取到dev_cnt个“子”设备号,”子“设备号怎么获取呢?这样来获取:
1 | for(int i = 0; i < dev_cnt; i++) |
注销设备号的时候,直接注销多个设备即可:char_dev.c - fs/char_dev.c - unregister_chrdev_region
1 | /* 注销设备号(将会删除 /proc/devices 中的设备号和对应的名称记录) */ |
1.2 设备注册与注销
设备注册主要是cdev对象的初始化和注册,这个是每个”子”设备都需要进行的:
1 | struct cdev dev[dev_cnt]; /* 定义一个字符设备对象设备 */ |
注销时候就是一个一个删除就行了:
1 | /* 3.删除字符设备对象 cdev */ |
1.3 类的创建与销毁
这些多个设备属于同一个类,我们只需要创建一个类就可以了:
1 | /* 创建类(要注意无论是一个设备还是多个设备,类只有一个) */ |
删除时候也只需要删除一个类:
1 | /* 删除类(只有一个类,删除一次即可) */ |
1.4 设备创建与销毁
因为我们是多个设备,所以每个设备都需要进行创建
1 | /* 创建设备(有几个设备,就创建几个) */ |
销毁的时候也要逐个销毁:
1 | /* 删除设备(多个设备,逐个进行删除) */ |
1.5 怎么区分不同的设备?
还是一样的问题,之前我们是通过inode→i_rdev,也就是设备号来获取子设备号,然后找到对应的设备的缓冲区,理论上来说,多个cdev设备的时候,也可以通过这种方式,但是我没试。这次我们来使用inode的i_cdev成员。
inode中的i_cdev成员保存了对应字符设备结构体的地址,但是我们的虚拟设备是把cdev封装起来的一个结构体,例如:
1 | /* 新字符设备的属性 */ |
那我们要如何能够得到虚拟设备的数据缓冲区呢?为此,Linux提供了一个宏定义 container_of(),该宏可以根据结构体的某个成员的地址, 来得到该结构体的地址。
1 | /** |
该宏需要三个参数,分别是代表结构体成员的真实地址,结构体的类型以及结构体成员的名字。这个在这里就详细再去分析原理了,可以看后面参考资料的【1】和【2】。在sdev_open()函数中,我们需要通过inode的i_cdev成员,来得到对应的虚拟设备结构体,并保存到文件指针file的私有数据成员中。 假如,我们打开虚拟设备1,那么inode->i_cdev便指向了sdevice0的成员mydev,利用container_of宏, 我们就可以得到g_my_dev结构体的地址,也就可以操作对应的数据缓冲区了。
2. demo实现
直接看gitee仓库吧:04_chrdev_basic/10_chrdev_more_dev_mc · 苏木/imx6ull-driver-demo - 码云 - 开源中国
点击查看详情
1 |
|
3. 开发板测试
- (1)加载驱动
1 | insmod chrdev_more_dev_mc_demo.ko |
- (2)查看设备节点
1 | ls /dev/sdevice* -alh |
会看到有如下打印信息:
1 | crw-rw---- 1 0 root 246, 0 Jan 1 00:00 /dev/sdevice0 |
在创建设备节点的时候命名就是/dev/sdeviceN,所以这里是0和1两个设备节点,他们会对应不同的缓冲区。
- (3)向设备节点写入数据
1 | echo 123456789 > /dev/sdevice0 |
- (4)从设备节点读取数据
1 | cat /dev/sdevice0 |
参考资料
【1】Linux内核宏Container_Of的详细解释_Linux_脚本之家
【2】【Linux API 揭秘】container_of函数详解 - 董哥聊技术 - 博客园