linux - 字符设备驱动简介
一、设备号
1.1 设备号概念
设备号在内核中唯一确定某个设备,包括主设备号和次设备号,主设备号唯一,次设备号不唯一
1.2 设备号说明
可通过cat /proc/devices命令查看当前系统挂载的设备的主设备号及对应的名称。
dev_t类型:
定义在/include/linux/types.h如下。
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
由上面两行代码得知dev_t实际为u32类型数据,默认高12位为主设备号,低20位为次设备号在文件include\linux\kdev_t.h文件中有定义。
同样include\linux\kdev_t.h提供了关于dev_t常用的几个宏定义包括,根据主次设备号组成dev_t,根据dev_t分别获得主次设备号。子设备号掩码等。
1.3 设备号分配及注销
1. 静态分配一个设备号
register_chrdev_region(dev_t, unsigned, const char *);
需要明确知道我们系统里面哪些设备号没用。
参数:
第一个:设备号的起始值,类型为dev_t。
第二个:次设备号的个数。
第三个:设备的名称。
返回值:成功返回0,失败返回其它。
2. 动态分配设备号
int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
参数:
第一个:声明的设备号的地址,保存生成的设备号。
第二个:请求的第一个次设备号,通常是0。
第三个:连续申请的次设备号个数。
第四个:设备名称。
返回值:成功返回0,失败返回其它。
注意:使用动态分配设备号优先分配234-255的主设备号。
3. 另外根据主设备号分配(通常不用)
__register_chrdev(unsigned int major, unsigned int baseminor, unsigned int count, const char *name, const struct file_operations *fops);
上述函数通常会将同一主设备号的所有次设备号都注册掉,造成资源浪费,通常不用。
4. 注销设备号
void unregister_chrdev_region(dev_t, unsigned);
第一个:声明的设备号的起始地址dev_t。
第二个:设备的数量。
1.4示例代码及说明
#include<linux/init.h> static int a,b; module_param(a, int, S_IRUSR);//加载驱动时传递参数,若无传参,默认为0 dev_t chr_dev; static int chr_init(void) if(a==0){ static void chr_exit(void) module_init(chr_init); |
---|
上诉代码编译:
- 通过传递参数加载驱动可以静态获取设备号如下:
2. 执行insmod chr.ko可以通过动态获取到设备号,在内核中可以获得以下log。
执行cat /proc/devices可以查找到对应的设备。
二、字符设备注册
2.1 说明
cdev结构体,描述一个字符设备:
struct cdev {
struct kobject kobj;
struct module *owner;//所有者
const struct file_operations *ops;//操作集合
struct list_head list;//字符设备链表
dev_t dev;//设备号
unsigned int count;//次设备号数量
};//其中写程序需要关注、owner、ops、dev、count
2.2步骤
1) 定义一个cdev结构体。
2)初始化cdev结构体成员变量:
void cdev_init(struct cdev *, const struct file_operations *);//作用是填充cdev结构体及字符设备链表
第一个参数:字符设备结构体指针。
第二个参数:文件操作集合。
3)注册进内核
int cdev_add(struct cdev *, dev_t, unsigned);
第一个参数:字符设备结构体指针。
第二个参数:设备号。
第三个参数:次设备号的数量
4)生成设备节点
第一种方法,使用mknod命令,格式:mknode 设备名 类型 主设备号 次设备号,例如: mknode /dev/test c 238 0
第二种方法,驱动自动创建设备节点。后续详细介绍
5)注销
cdev_del(struct cdev *);
参数:字符设备结构体指针。
2.3 自动创建设备节点
使用udev实现设备节点创建,嵌入式设备通常使用mdev,mdev是udev的简化版
udev概念:根据系统中设备的状态更新设备文件,保证系统目录下都是存在的设备例如/dev下的设备节点。
创建设备节点注意包括两个步骤:
1)创建一个class:
class_create(owner, name)//4.4内核定义在include/linux/device.h。5.10内核定义在include/linux/device/class.h,class结构体同样定义在这两个文件
参数:
第一个参数,:所有者。
第二个参数:类的名称
返回值:类的指针
2)创建设备节点
device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); 定义在include/linux/device/class.h
参数:
第一个参数:cls就是刚刚创建的。
第二个参数:父设备,通常为NULL。
第三个参数:设备号。
第四个参数:设备可能用到的数据,通常为NULL。
第五个参数:fmt为设备名字,即创建后的/dev目录下的名字
3)注销
class_destroy(struct class *cls);//注销类
device_destroy(struct class *cls, dev_t devt);//注销设备
2.4 示例程序
#include<linux/init.h> dev_t chr_dev; static struct cdev chr_dev_test; struct class *chr_dev_class; int chr_dev_open(struct inode *inode, struct file *file) struct file_operations chr_dev_ops = { static int chr_init(void) ret = alloc_chrdev_region(&chr_dev, 0, 1, "chr_test1"); chr_dev_test.owner = THIS_MODULE; static void chr_exit(void) module_init(chr_init); |
---|
程序编译成ko加载之后可以在/sys/class目录和/dev目录得到对应的节点如下。
三、应用层和内核层的数据传输
在第二章中我们可以创建设备节点,Linux有一切皆文件的概念。因此我们对dev目录下的设备节点进行打开读写等操作时,会对应设备驱动里面的相应程序。
3.1 file_operations结构体
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 (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*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 *); unsigned long mmap_supported_flags; 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 (*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 ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); } |
---|
该结构体包含了应用层对设备节点的各种操作对应的函数。常用的如下
当应用层打开设备时,调用函数
(*open) (struct inode *, struct file *);
当应用层关闭设备时,调用函数
(*release) (struct inode *, struct file *);
当应用层对设备节点执行读操作时,调用函数
(*read) (struct file *, char __user *, size_t, loff_t *);
当应用层对设备节点执行写操作时,调用函数
(*write) (struct file *, const char __user *, size_t, loff_t *);
当应用层对设备节点执行poll/select操作时,调用函数
(*poll) (struct file *, struct poll_table_struct *);
当应用层对设备节点执行ioctl操作时,调用函数
(*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
3.2 应用层和内核层数据传输
1)内核向应用传输copy_to_user(void __user * to, const void * from, unsigned long n)
参数:
第一个参数to标识数据要传输的地址。
第二个参数from是要传输的数据指针。
第三个参数是传输数据的长度。
2)应用向内核传输copy_from_user(void * to, const void __user * from, unsigned long n)
第一个参数to标识数据要传输的地址。
第二个参数from是要传输的数据地址。
第三个参数是传输数据的长度。
3.3 ioctl 方式
- unlocked_ioctl接口(linux3.0之前为ioctrl接口)。
ioctl是带锁的因此造成了内核实时性能变弱,为了提高实时性能去掉了锁变为unlocked_ctl,功能和对应的系统调用均没有改变。包含在在文件操作集合里面。
2. unlocked_ioctl与read/write的相同点和不同点。
都可以实现内核和应用层通信。
不同点,ioctl同时实现读写功能,但是读取大数据的时候效率不如read/write。ioctl适合设置设备参数
3. ioctl接口命令规则。
参数:总共32位,
第一分区:0-7,命令的编号,范围是0-255。
第二分区:8-15,命令的幻数(魔数)。与第一分区一起主要作用是来区分命令的,防止出现同样命令。
第三分区:16-29,标识传递的数据大小。
第四分区:30-31,标识读写方向。
00:表示用户和驱动没有数据交换,
10:表示用户在驱动里面读数据,
01:表示用户向驱动里面写数据,
11:表示用户先向驱动写数据,再从驱动读数据。
4. 命令的合成宏和分解宏
合成宏
_IO(type, nr) : 用来定义没有数据读写的命令,type表示幻数对应8-15, nr是编号,对应0-7
_IOR(type, nr, size) : 用来定义从驱动中读数据,type表示幻数, nr是编号,size是传输数据大小
_IOW(type, nr, size) : 用来定义向驱动中写数据,type表示幻数, nr是编号,size是传输数据大小
_IOWR(type, nr, size) : 用来定义先向驱动中写数据再从驱动中读数据,type表示幻数, nr是编号,size是传输数据大小
分解宏
_IOC_DIR(nr) : 获取输入或输出方向,nr为命令。
_IOC_TYPE(nr) : 获取命令幻数,nr为命令。
_IOC_NR(nr) : 获取命令编号,nr为命令。
_IOC_SIZE(nr) : 获取数据大小,nr为命令。
3.4 示例程序
在上一章程序的基础上扩充file_operations 结构体,添加以下代码
#include<linux/init.h> #define TEST_IO_READ _IOR('A',1,int)//定义读命令宏
static struct cdev chr_dev_test; struct class *chr_dev_class;
ssize_t chr_dev_read(struct file *file, char __user *ubuf, size_t size, loff_t *loff) ssize_t chr_dev_write(struct file *file,const char __user *ubuf, size_t size, loff_t *loff) int chr_dev_close(struct inode *inode, struct file *file) long chr_dev_ioctl(struct file *file, unsigned int cmd, unsigned long val) struct file_operations chr_dev_ops = { static int chr_init(void) ret = alloc_chrdev_region(&chr_dev, 0, 1, "chr_test1"); chr_dev_test.owner = THIS_MODULE; return 0; static void chr_exit(void) module_init(chr_init); |
---|
编写应用层示例程序
#include <stdio.h>
fd = open("/dev/chr_test",O_RDWR); int val; return 0; |
---|
程序执行结果:
上层应用在代码层读到的数据如下:
内核从上层得到的数据如下:
四、物理地址到虚拟地址的映射
单片机上往往都是直接对寄存器地址读写实现硬件控制,在Linux系统是不行的,需要先把物理地址映射为虚拟地址。Linux系统使用了MMU,内核通过对虚拟地址的操作实现对设备寄存器读写。
4.1 有了MMU的好处:
首先可以实现虚拟内存,使每个进程可以获取到连续的内存,使虚拟地址连续物理地址不连续成为可能。减内存碎片,方便程序管理及提高代码可移植性。
上层应用不能直接通过物理地址来操作硬件,隔绝了上层对物理设备的修改。保证了系统安全。
4.2 如何将物理地址映射到虚拟地址
1)ioremap(phys_addr_t offset, size_t size);//映射函数
第一个参数:要映射的物理地址的起始地址。
第二个参数:要映射物理地址的大小。
返回值:成功执行返回虚拟地址的首地址。失败返回NULL。
2) iounmap(void __iomem *addr)//释放
参数,要释放的虚拟地址
注意:物理地址只能映射一次,多次映射会失败。
可以通过cat /proc/iomem查看已经映射的物理地址。
五、总结
最终驱动在init的时候转换一下虚拟地址,在用户读写的时候根据虚拟地址对设备的寄存器进行读写实现简单字符设备的读写。