00. 目录
文章目录
01. Linux设备分类
Linux是文件型系统,所有硬件都会在对应的目录(/dev)下面用相应的文件表示。 在windows系统中,设备大家很好理解,像硬盘,磁盘指的是实实在在硬件。 而在文件系统的linux下面,都有对于文件与这些设备关联的,访问这些文件就可以访问实际硬件。 像访问文件那样去操作硬件设备,一切都会简单很多,不需要再调用以前com,prt等接口了。 直接读文件,写文件就可以向设备发送、接收数据。 按照读写存储数据方式,我们可以把设备分为以下几种:字符设备、块设备和网络设备。
字符设备:指应用程序按字节/字符来读写数据的设备。 这些设备节点通常为传真、虚拟终端和串口调制解调器、键盘之类设备提供流通信服务, 它通常不支持随机存取数据。字符设备在实现时,大多不使用缓存器。系统直接从设备读取/写入每一个字符。 例如,键盘这种设备提供的就是一个数据流,当你敲入“cnblogs”这个字 符串时, 键盘驱动程序会按照和输入完全相同的顺序返回这个由七个字符组成的数据流。它们是顺序的,先返回c,最后是s。
块设备:通常支持随机存取和寻址,并使用缓存器。 操作系统为输入输出分配了缓存以存储一块数据。当程序向设备发送了读取或者写入数据的请求时, 系统把数据中的每一个字符存储在适当的缓存中。当缓存被填满时,会采取适当的操作(把数据传走), 而后系统清空缓存。它与字符设备不同之处就是,是否支持随机存储。字符型是流形式,逐一存储。 典型的块设备有硬盘、SD卡、闪存等,应用程序可以寻址磁盘上的任何位置,并由此读取数据。 此外,数据的读写只能以块的倍数进行。
网络设备:是一种特殊设备,它并不存在于/dev下面,主要用于网络数据的收发。
Linux内核中处处体现面向对象的设计思想,为了统一形形色色的设备,Linux系统将设备分别抽象为struct cdev, struct block_device,struct net_devce三个对象,具体的设备都可以包含着三种对象从而继承和三种对象属性和操作, 并通过各自的对象添加到相应的驱动模型中,从而进行统一的管理和操作
字符设备驱动程序适合于大多数简单的硬件设备,而且比起块设备或网络驱动更加容易理解, 因此我们选择从字符设备开始,从最初的模仿,到慢慢熟悉,最终成长为驱动界的高手。
02. 字符设备抽象
Linux内核中将字符设备抽象成一个具体的数据结构(struct cdev),我们可以理解为字符设备对象, cdev记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations), 在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一个文件(设备节点)绑定对象的cdev, 当我们对这个文件进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。
C语言中没有面向对象语言的继承的语法,但是我们可以通过结构体的包含来实现继承,这种抽象提取了设备的共性, 为上层提供了统一接口,使得管理和操作设备变得很容易。
在硬件层,我们可以通过查看硬件的原理图、芯片的数据手册,确定底层需要配置的寄存器,这类似于裸机开发。 将对底层寄存器的配置,读写操作放在文件操作接口里面,也就是实现file_operations结构体。
其次在驱动层,我们将文件操作接口注册到内核,内核通过内部散列表来登记记录主次设备号。
在文件系统层,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件的文件操作接口来设置底层寄存器
实际上,在Linux上写驱动程序,都是做一些“填空题”。因为Linux给我们提供了一个基本的框架, 我们只需要按照这个框架来写驱动,内核就能很好的接收并且按我们所要求的那样工作。有句成语工欲善其事,必先利其器, 在理解这个框架之前我们得花点时间来学习字符设备驱动相关概念及数据结构。
03. Linux杂项设备驱动
杂项设备驱动是属于我们 linux 三大设备驱动的哪一项呢?由于linux 驱动倾向于分层设计,所以每个具体的设备都可以找到它归属的类型,从而可以套到它相应的架构里面去,我们只需要实现它最底层的那部分。但是也有部分字符设备,确实不知道它属于哪种类型,我们一般推荐大家采用 miscdevice 的框架结构。misc 的意思是混合的杂项的,所以 misc 设备驱动也叫做杂项设备驱动,当我们板子上的某个设备没有办法分类时,就可以用 misc 设备驱动。它的注册跟使用比较的简单,所以比较适用于功能简单的设备。正因为简单,所以它通常嵌套在 platform 总线驱动中,配合总线驱动达到更复杂,多功能的效果。杂项设备是字符设备的一种,杂项设备可以自动生成设备节点。
在学习 misc 设备驱动之前,先来了解几个基础概念。
概念1 设备节点
我们可以启动我们的开发板,进入到 dev 目录下,dev 目录下全部都是生成的设备节点,如下图所示:
deng@local:~/a72/x3399/kernel$ ls /dev/
autofs disk hwrng loop3 mqueue rtc0 stdin tty17 tty28 tty39 tty5 tty60 ttyS12 ttyS23 ttyS6 vcs2 vcsu vmci
block dma_heap initctl loop4 net sda stdout tty18 tty29 tty4 tty50 tty61 ttyS13 ttyS24 ttyS7 vcs3 vcsu1 vsock
bsg dmmidi input loop5 null sda1 tty tty19 tty3 tty40 tty51 tty62 ttyS14 ttyS25 ttyS8 vcs4 vcsu2 zero
btrfs-control dri kmsg loop6 nvram sda2 tty0 tty2 tty30 tty41 tty52 tty63 ttyS15 ttyS26 ttyS9 vcs5 vcsu3 zfs
bus ecryptfs log loop7 port sda3 tty1 tty20 tty31 tty42 tty53 tty7 ttyS16 ttyS27 udmabuf vcs6 vcsu4
cdrom fb0 loop0 loop8 ppp sg0 tty10 tty21 tty32 tty43 tty54 tty8 ttyS17 ttyS28 uhid vcsa vcsu5
char fd loop1 loop9 psaux sg1 tty11 tty22 tty33 tty44 tty55 tty9 ttyS18 ttyS29 uinput vcsa1 vcsu6
console full loop10 loop-control ptmx shm tty12 tty23 tty34 tty45 tty56 ttyprintk ttyS19 ttyS3 urandom vcsa2 vfio
core fuse loop11 mapper pts snapshot tty13 tty24 tty35 tty46 tty57 ttyS0 ttyS2 ttyS30 userfaultfd vcsa3 vga_arbiter
cpu hidraw0 loop12 mcelog random snd tty14 tty25 tty36 tty47 tty58 ttyS1 ttyS20 ttyS31 userio vcsa4 vhci
cpu_dma_latency hpet loop13 mem rfkill sr0 tty15 tty26 tty37 tty48 tty59 ttyS10 ttyS21 ttyS4 vcs vcsa5 vhost-net
cuse hugepages loop2 midi rtc stderr tty16 tty27 tty38 tty49 tty6 ttyS11 ttyS22 ttyS5 vcs1 vcsa6 vhost-vsock
deng@local:~/a72/x3399/kernel$
我们的系统里面有很多杂项设备。我们可以输入以下命令来查看,如下图所示:
deng@local:~/a72/x3399/kernel$ cat /proc/misc
121 vsock
122 vmci
235 autofs
123 cpu_dma_latency
227 mcelog
236 device-mapper
223 uinput
1 psaux
200 tun
124 udmabuf
237 loop-control
228 hpet
229 fuse
125 ecryptfs
126 userfaultfd
231 snapshot
183 hw_random
127 vga_arbiter
242 rfkill
deng@local:~/a72/x3399/kernel$
概念2 杂项设备的优点
杂项设备除了比字符设备代码简单,还有别的区别吗?所有的 misc 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号。主设设备号相同就可以节省内核的资源,在内核中大概可以找到 200 多处使用 miscdevice 框架结构的驱动。
概念3 主设备号和次设备号的概念
设备号包含主设备号和次设备号,设备号是计算机识别设备的一种方式,主设备号相同的就被视为同一类设备,主设备号在 Linux 系统里面是唯一的,次设备号不一定唯一。主设备号可以比做成电话号码的区号。比如北京的区号是 010,次设备号可以比作成电话号码。
主设备号可以通过以下命令来查看,前面的数字就是主设备号,如下图所示:
deng@local:~/a72/x3399/kernel$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
6 lp
7 vcs
10 misc
13 input
14 sound/midi
14 sound/dmmidi
21 sg
29 fb
89 i2c
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
202 cpu/msr
204 ttyMAX
226 drm
241 hidraw
242 ttyDBC
243 bsg
244 watchdog
245 remoteproc
246 ptp
247 pps
248 rtc
249 dma_heap
250 dax
251 dimmctl
252 ndctl
253 tpm
254 gpiochip
261 accel
Block devices:
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
252 device-mapper
253 virtblk
254 mdp
259 blkext
deng@local:~/a72/x3399/kernel$
04. Linux杂项设备相关类型和API
4.1 miscdevice 结构体
misc 设备用 miscdevice 结构体表示,miscdevice 结构体的定义在内核源码具体定义在include/linux/miscdevice.h 中,内容如下:
struct miscdevice {
int minor; //次设备号
const char *name; //设备节点的名字
const struct file_operations *fops; //文件操作集
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
当我们创建一个 misc 设备的 miscdevice 结构体时,需要我们指定 minor、name 和 fops 这三个成员变量。minor 表示次设备号,需要用户设置,在 Linux 内核中有一些预定义的 misc 设备的次设备号,定义在 include/linux/miscdevice.h 文件中,如下所示:
/*
* These allocations are managed by device@lanana.org. If you use an
* entry that is not in assigned your entry may well be moved and
* reassigned, or set dynamic if a fixed value is not justified.
*/
#define PSMOUSE_MINOR 1
#define MS_BUSMOUSE_MINOR 2 /* unused */
#define ATIXL_BUSMOUSE_MINOR 3 /* unused */
/*#define AMIGAMOUSE_MINOR 4 FIXME OBSOLETE */
#define ATARIMOUSE_MINOR 5 /* unused */
#define SUN_MOUSE_MINOR 6 /* unused */
#define APOLLO_MOUSE_MINOR 7 /* unused */
#define PC110PAD_MINOR 9 /* unused */
/*#define ADB_MOUSE_MINOR 10 FIXME OBSOLETE */
#define WATCHDOG_MINOR 130 /* Watchdog timer */
#define TEMP_MINOR 131 /* Temperature Sensor */
#define RTC_MINOR 135
#define EFI_RTC_MINOR 136 /* EFI Time services */
#define VHCI_MINOR 137
#define SUN_OPENPROM_MINOR 139
#define DMAPI_MINOR 140 /* unused */
#define NVRAM_MINOR 144
#define SGI_MMTIMER 153
#define STORE_QUEUE_MINOR 155 /* unused */
#define I2O_MINOR 166
#define MICROCODE_MINOR 184
#define VFIO_MINOR 196
#define TUN_MINOR 200
#define CUSE_MINOR 203
#define MWAVE_MINOR 219 /* ACP/Mwave Modem */
#define MPT_MINOR 220
#define MPT2SAS_MINOR 221
#define MPT3SAS_MINOR 222
#define UINPUT_MINOR 223
#define MISC_MCELOG_MINOR 227
#define HPET_MINOR 228
#define FUSE_MINOR 229
#define KVM_MINOR 232
#define BTRFS_MINOR 234
#define AUTOFS_MINOR 235
#define MAPPER_CTRL_MINOR 236
#define LOOP_CTRL_MINOR 237
#define VHOST_NET_MINOR 238
#define UHID_MINOR 239
#define USERIO_MINOR 240
#define MISC_DYNAMIC_MINOR 255
我们设置子设备号时要注意不要重复使用其他设备的子设备号。可以从这些预定义的子设备号中选择一个,也可以自定义。
name 就是这个 misc 设备的名字,当设备注册成功后,会在/dev 目录下自动生成一个名为 name 的设备文件。fops 就是这个 misc 设备的操作集合。
4.2 misc_register
/**
* misc_register - register a miscellaneous device
* @misc: device structure
*
* Register a miscellaneous device with the kernel. If the minor
* number is set to %MISC_DYNAMIC_MINOR a minor number is assigned
* and placed in the minor field of the structure. For other cases
* the minor number requested is used.
*
* The structure passed is linked into the kernel and may not be
* destroyed until it has been unregistered. By default, an open()
* syscall to the device sets file->private_data to point to the
* structure. Drivers don't need open in fops for this.
*
* A zero is returned on success and a negative errno code for
* failure.
*/
int misc_register(struct miscdevice * misc)
功能:
注册misc设备
参数:
misc 之前创建好的miscdevice结构体
返回值:
成功返回0
失败返回负数
4.3 misc_deregister
/**
* misc_deregister - unregister a miscellaneous device
* @misc: device to unregister
*
* Unregister a miscellaneous device that was previously
* successfully registered with misc_register().
*/
void misc_deregister(struct miscdevice *misc)
功能:
注销misc设备
参数:
misc 之前创建好的miscdevice结构体
返回值:
无
4.4 file_operations
在 miscdevice 结构体的第四行,它指向了一个 file_operation 的结构体。file_operations 文件操作集在定义在 include/linux/fs.h 下面,如下图所示。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
- llseek: 用于修改文件的当前读写位置,并返回偏移后的位置。参数file传入了对应的文件指针,我们可以看到以上代码中所有的函数都有该形参,通常用于读取文件的信息,如文件类型、读写权限;参数loff_t指定偏移量的大小;参数int是用于指定新位置指定成从文件的某个位置进行偏移,SEEK_SET表示从文件起始处开始偏移;SEEK_CUR表示从当前位置开始偏移;SEEK_END表示从文件结尾开始偏移。
- read: 用于读取设备中的数据,并返回成功读取的字节数。该函数指针被设置为NULL时,会导致系统调用read函数报错,提示“非法参数”。该函数有三个参数:file类型指针变量,charuser类型的数据缓冲区,user用于修饰变量,表明该变量所在的地址空间是用户空间的。内核模块不能直接使用该数据,需要使用copy_to_user函数来进行操作。size_t类型变量指定读取的数据大小。
- write: 用于向设备写入数据,并返回成功写入的字节数,write函数的参数用法与read函数类似,不过在访问__user修饰的数据缓冲区,需要使用copy_from_user函数。
- unlocked_ioctl: 提供设备执行相关控制命令的实现方法,它对应于应用程序的fcntl函数以及ioctl函数。在 kernel 3.0 中已经完全删除了 struct file_operations 中的 ioctl 函数指针。
- open: 设备驱动第一个被执行的函数,一般用于硬件的初始化。如果该成员被设置为NULL,则表示这个设备的打开操作永远成功。
- release: 当file结构体被释放时,将会调用该函数。与open函数相反,该函数可以用于释放
05. Linux杂项设备驱动示例
注册杂项设备有一个通用的思路和方法,这里给大家总结为三个步骤:
填充 miscdevice 这个结构体
填充 file_operations 这个结构体
注册杂项设备并生生成设备节点。
test.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
static struct file_operations misc_fops = {
.owner = THIS_MODULE,
};
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "misc_dev",
.fops = &misc_fops,
};
static int __init test_init(void)
{
int ret = 0;
printk("test_init.....\n");
//注册杂项设备
ret = misc_register(&misc_dev);
if (0 != ret)
{
printk("misc_register failed.....\n");
return 1;
}
printk("misc_register OK....\n");
return 0;
}
static void __exit test_exit(void)
{
//注销杂项设备
misc_deregister(&misc_dev);
printk("test_exit......\n");
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jin.Deng");
MODULE_INFO(intree, "Y");
运行结果
[root@rk3399:/mnt/kernel/5th/1misc]# insmod test.ko
[10301.761151] test_init.....
[10301.764711] misc_register OK....
[root@rk3399:/mnt/kernel/5th/1misc]# ls /dev/misc_dev -l
crw------- 1 root root 10, 53 Apr 28 06:37 /dev/misc_dev
[root@rk3399:/mnt/kernel/5th/1misc]#
[root@rk3399:/mnt/kernel/5th/1misc]# rmmod test
[10308.881889] test_exit......
[root@rk3399:/mnt/kernel/5th/1misc]#
[root@rk3399:/mnt/kernel/5th/1misc]# ls -l /dev/misc_dev
ls: cannot access '/dev/misc_dev': No such file or directory
[root@rk3399:/mnt/kernel/5th/1misc]#