前言
这一篇博客来谈谈字符设备的注册、分配与释放。
嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!
目录
- 前言
- 1. 设备号
- 1.1 设备号组成
- 1.2 设备号的获取
- 2. 字符设备分配
- 2.1 静态分配
- 2.2 动态分配
- 2.3 实际代码编写
- 3. 字符设备注册
- 3.1 cdev结构体
- 3.2 cdev_init()函数
- 3.3 cdev_add()函数
- 4. 字符设备释放
- 参考资料
1. 设备号
1.1 设备号组成
Linux中,为了方便管理,每个设备都有一个设备号,由主设备号和次设备号两部分组成。主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
Linux下提供了dev_t数据类型表示设备号,其是一个unsigned int 类型,32位的数据,其中高12位为主设备号,低20位为次设备号,因此主设备号的范围不要超过0~4095。
1.2 设备号的获取
在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))
主要是最后三个,前面两个是为后三个服务的
- 宏
MAJOR
用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可 - 宏
MINOR
用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可 - 宏
MKDEV
用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号
2. 字符设备分配
设备号的分配有两种,一种是静态分配,一种是动态分配
2.1 静态分配
所谓的静态分配就是直接在注册时指定设备号,这个设备号可以是驱动开发者静态的指定一个设备号,比如200。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号。
使用cat /proc/devices
可以查看当前系统中使用了的设备号,字符设备、块设备、网络设备均可以看到。
可以使用函数register_chrdev_region来用给定的主设备号进行注册
/* * @description: 使用静态分配注册设备号* @param-from : 要分配的设备编号范围的初始值(次设备号常设为0)* @param-count: 连续编号范围* @param-name : 编号相关联的设备名称* @return : 当返回值小于0,表示注册失败*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
2.2 动态分配
静态分配还是不够灵活,需要先查看已使用的设备号,因此常用的还是动态分配,等到运行时,让系统自动分配一个没有使用的设备号,这样就避免了冲突。
/** @description : 向linux申请一个没有使用过的设备号* @param-dev : 用于保存申请到的设备号* @param-baseminor: 次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始* @param-count : 要申请的设备号数量* @param-name : 设备名字* @return : 小于0,则错误,自动分配设备号错误。否则分配得到的设备号就被第一个参数带出来*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
用alloc_chrdev_region()函数得到设备号后,需要利用MAJOR宏和MINOR宏获取一下主设备号和次设备号。例如下面:
alloc_chrdev_region(&chrdevTest.devid, 0, chrdevTest_CNT, chrdevTest_NAME); // 申请设备号chrdevTest.major = MAJOR(chrdevTest.devid); // 获取主设备号chrdevTest.minor = MINOR(chrdevTest.devid); // 获取次设备号
最后卸载驱动的时候需要释放设备号:
/** @description: 释放设备号* @param-from : 要释放的设备号* @param-count: 从from开始,要释放的设备号数量* @return : 无*/
void unregister_chrdev_region(dev_t from, unsigned count)
2.3 实际代码编写
在实际操作中,我们可以通过判断是否已经给定主设备号来选择静态分配还是动态分配,具体写法如下:
if (chrdevTest.major) {chrdevTest.devid = MKDEV(chrdevTest.major, 0);register_chrdev_region(chrdevTest.devid, chrdevTest_CNT, chrdevTest_NAME);
} else {alloc_chrdev_region(&chrdevTest.devid, 0, chrdevTest_CNT, chrdevTest_NAME); // 申请设备号chrdevTest.major = MAJOR(chrdevTest.devid); // 获取主设备号chrdevTest.minor = MINOR(chrdevTest.devid); // 获取次设备号
}
3. 字符设备注册
3.1 cdev结构体
在Linux中使用cdev结构体表示一个字符设备,cdev在include/linux/cdev.h文件中的定义:
struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
};
在 cdev 中有两个重要的成员变量:ops
和 dev
,这两个就是字符设备文件操作函数集合file_operations
以及设备号 dev_t
。编写字符设备驱动之前需要定义一个 cdev 结构体变量,一般在设备结构体中定义,如下所示:
struct chrdevTest_dev {dev_t devid;struct cdev cdev;struct class *class;struct device *device;int major;int minor;struct device_node *nd;
};
3.2 cdev_init()函数
使用cdev_init函数进行初始化:
/** @description: 初始化cdev结构体* @param-cdev : 要初始化的cdev结构体变量* @param-fops : 字符设备文件操作函数集合* @return : 无*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3.3 cdev_add()函数
cdev_add用于向Linux系统添加字符设备(cdev结构体变量)
/** @description: 向系统添加字符设备* @param-p : 指向要添加的字符设备(cdev结构体变量)* @param-dev : 设备所使用的设备号* @param-count: 要添加的设备数量* @return : 无*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
4. 字符设备释放
卸载驱动时,一定要用cdev_del函数从Linux内核中删除相应的字符设备,即在出口函数中编写cdev_del。
/* * @description: 从Linux内核中删除相应的字符设备* @param-p : 要删除的字符设备* @return : 无*/
void cdev_del(struct cdev *p)
删除字符设备后,再释放设备号,因此完整的流程是:
cdev_del(&chrdevTest.cdev);unregister_chrdev_region(chrdevTest.devid, chrdevTest_CNT);
参考资料
[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第四十章、第四十二章
[2] linux驱动2:设备号与字符设备注册与注销
[3] 字符设备驱动之register_chrdev_region()系列