参考博客:
字符设备驱动-使用alloc_chrdev_region+cdev注册设备驱动_明故宫的记忆的博客-CSDN博客_alloc_chrdev_region
Linux驱动device_create创建字符设备文件_hwx1546的博客-CSDN博客
一、分配和释放设备号
我们前面在编写chrdevbase和led的字符驱动的时候,都需要手动通过mknod来创建设备节点。并且存在下面的诸多问题:
1)需要我们事先确定好哪些主设备号没有使用
2)会将一个主设备号下的所有次设备号都使用掉,比如现在设置LED这个主设备号为200,那么0~2^20-1这个区间的次设备号就全部被LED一个设备分走了。这样十分浪费次设备号。一个LED设备肯定只能有一个主设备号,一个次设备号。
现在新的字符设备驱动已经不再使用register_chrdev和unregister_chrdev函数了,而是使用linux内核推荐的新字符设备驱动创建API函数。
二、设备号申请
解决1中出现的两个问题的解决方法是我们在使用设备号的时候向linux内核申请,需要几个就申请几个,由linux内核分配设备可以使用的设备号,没有指定设备号的话就可以使用下面的函数来进行申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);
参数说明:
dev :alloc_chrdev_region函数向内核申请下来的设备号
baseminor :次设备号的起始
count: 申请次设备号的个数
name :执行 cat /proc/devices显示的名称
如果给定了设备的主设备号和次设备号就使用下面的函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数说明:
from:申请的起始设备号,也就是给定的设备号
count:申请的数量,一般为1
name:名字
一般是给定主设备号,然后使用MKDEV构建完整的dev_t,一般次设备号选择0
卸载的时候使用unregister_chrdev_region函数。函数原型如下:
void unregister_chrdev_region(dev_t from, unsigned count);
参数说明:
from:设备号
count:设备号个数
示例代码如下:
int major;
int minor;
dev_t devid;
if (major) {
devid = MKDEV(major, 0); // 如果major有效的话就使用MKDEV来构建设备号,次设备选择0
register_chrdev_region(devid, 1, "test"); // 注册设备号
} else {
alloc_chrdev_region(&dev, 0, 1, "test"); // 申请设备号
major = MAJOR(devid); // 获取分配设备号的主设备号
minor = MINOR(devid); // 获取分配设备号的次设备号
}
三、cdev结构体及相关函数
cdev结构体将设备操作函数集合和设备号进行绑定。
cdev表示字符设备,cdev结构体如下:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; // 设备操作函数集合
struct list_head list;
dev_t dev; // 设备号
unsigned int count;
} __randomize_layout;
cdev_init函数
void cdev_init(struct cdev *, const struct file_operations *);
参数说明
第一个参数是cdev的结构体变量,第二个参数就是fops的字符设备文件操作函数集合
定义好cdev结构体之后需要使用cdev_init函数对其进行初始化。即:将字符设备和字符设备的操作函数集合进行绑定。
cdev_init初始化完成cdev以后,使用cdev_add添加到linux内核
int cdev_add(struct cdev *, dev_t, unsigned);
参数说明:
第一个参数是cdev的指针,第二个参数是设备号,第三个参数是要添加的设备个数
cdev_del函数:卸载驱动时一定要使用cdev_del函数从linux内核删除相应的字符设备,函数原型如下:
cdev_del(&testdev);
cdev_del和unregister_chrdev_region这两个函数合起来的功能相当于unregister_chrdev函数。
四、自动创建设备节点
在使用busybox构建根文件系统的时候,busybox会创建一个udev的简化版本--mdev(两者的作用一致),所以在嵌入式linux中我们使用mdev来实现设备节点文件的自动创建和删除。linux系统中的热插拔事件也由mdev管理。
它主要的功能是管理/dev目录底下的设备节点。它同时也用来接替devfs及热插拔的功能,这意味着它要在添加/删除硬件时处理/dev目录以及所有用户空间的行为。
在2.6内核之后引入了udev机制替换了devfs。udev机制提供热插拔管理,可以在加载驱动的时候自动创建/dev/xxx设备文件。
下面分析如果通过mdev来实现文件节点的自动创建与删除:
1、创建和删除设备类
系统中的设备所属的类有结构体struct class对象来描述,用于表示某一类设备(抽象成一种设备的集合),它是一组具有共同属性和功能的设备的抽象体,类似于面向对象中的类的概念;
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个class类,class是个结构体,定义在include/linux/device.h里,class_create是类创建函数,class_create是个宏定义,内容如下:
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)
宏class_create展开以后内容如下:
struct class *__class_create(struct module *owner, const char *name)
参数说明:
owner:THIS_MODULE
name:类名字
返回值:
指向结构体class的指针,也就是创建的类
删除类的函数class_destroy,函数原型如下:
void class_destroy(struct class *cls);
参数说明:
cls就是要删除的类
struct class
{
const char* name; //类名
struct module* owner; //所属模块
struct subsystem subsys; //所属子系统subsystem
struct list_head children; //属于该class类型的所有子类组成的链表;
struct list_head devices; //属于该class类型的所有设备class_device组成的链表;
struct list_head interfaces; //类接口class_interface链表
struct semaphore sem; /* locks both the children and interfaces lists */
struct class_attribute* class_attrs; //类属性
struct class_device_attribute* class_dev_attrs; //类设备属性
int (*uevent)(struct class_device* dev, char** envp, int num_envp, char* buffer, int buffer_size);
void (*release)(struct class_device* dev);
void (*class_release)(struct class* class);
};
int class_register(struct class *cls):类注册;
void class_unregister(struct class *cls):类注销;
2、创建和删除设备
创建好设备类之后还不能实现自动创建节点,我们还需要在这个类下创建一个设备。使用device_create函数在类下面创建设备,device_create函数原型为:
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
参数说明:
class:在哪一个设备类下创建,该设备依附的类
parent:父设备,没有即为NULL,一般为NULL
devt:设备号
drvdata:可能会使用的一些数据,一般为NULL
fmt:设备名字,fmt = xxx,则会生成/dev/xxx这个设备文件
返回值:
创建好的设备
device_create能自动创建设备文件是依赖于udev这个应用程序。udev是一种工具,它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev目录下。使用udev后,在/dev目录下就只包含系统中真正存在的设备。
void device_destroy(struct class *class, dev_t devt)
参数说明:
class:设备依附的设备类
devt:设备号
五、测试demo
【仅限测试】
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched/signal.h>
#include <linux/device.h>
#include <linux/ioctl.h>
#include <linux/parport.h>
#include <linux/ctype.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/major.h>
#include <linux/ppdev.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/compat.h>
#define NEWCHRDEVBASE_NAME "newchrdevbase"
struct newchrdevbase_dev {
struct cdev cdev; // 字符设备
dev_t devid; // 设备号
struct class *class; // 类
struct device *device; // 设备
int major; // 主设备号
int minor; // 次设备号
};
static struct newchrdevbase_dev newchrdevbase;
static int newchrdevbase_open(struct inode *inode, struct file *file)
{
printk("newchrdevbase open \n");
}
static int chrdevbase_release(struct inode *inode, struct file *file)
{
printk("newchrdevbase close \n");
}
static struct file_operations newchrdevbase_fops = {
.owner = THIS_MODULE;
.open = newchrdevbase_open,
.release = newchrdevbase_release,
};
static int __init newchrdevbase_init(void)
{
int ret = 0;
printk("newchrdevbase__init \n");
if (newchrdevbase.major) { // 给定主设备号
newchrdevbase.devid = MKDEV(newchrdevbase.major, 0);
ret = register_chrdev_region(newchrdevbase.devid, 1, NEWCHRDEVBASE_NAME);
} else { // 没有给定主设备号
ret = alloc_chrdev_region(&newchrdevbase.devid, 0, 1, NEWCHRDEVBASE_NAME);
newchrdevbase.major = MAJOR(newchrdevbase.devid);
newchrdevbase.minor = MINOR(newchrdevbase.devid);
}
if (ret < 0) {
printk("newchrdevbase register failed \n");
return -1;
}
newchrdevbase.cdev.owner = THIS_MODULE;
cdev_init(&newchrdevbase.cdev , &newchrdevbase_fops);
cdev_add(&newchrdevbase.cdev , newchrdevbase.devid, 1);
newchrdevbase.class = class_create(THIS_MODULE, "newchrdevbase");
if(IS_ERR(newchrdevbase.class)) { // IS_ERR判断指针是否合法,合法返回0
return PTR_ERR(newchrdevbase.class); // PTR_ERR() 将一个错误指针强转为一个错误码,并作为函数的返回值使用, ERR_PTR() 将一个错误码强转为一个错误指针,并作为函数的返回值使用
}
newchrdevbase.device = device_create(newchrdevbase.class, NULL, newchrdevbase.devid, NULL, "newchrdevbase");
if (IS_ERR(newchrdevbase.device)) {
return PTR_ERR(newchrdevbase.device);
}
return 0;
}
static void __exit newchrdevbase_exit(void)
{
// 删除字符设备
cdev_del(&newchrdevbase.cdev);
// 注销设备号
unregister_chrdev_region(newchrdevbase.devid, 1);
// 删除设备
device_destroy(newchrdevbase.class, newchrdevbase.devid);
// 删除设备类
class_destroy(newchrdevbase.class);
printk("newchrdevbase__exit end\n");
}
module_init(chrdevbase__init);
module_exit(chrdevbase__exit);
上述测试demo模块加载进内核之后,我们就不用手动通过mknod命令设置 /dev/xxx 设备节点。
并且
在/sys/class目录下可以搜索到newchrdevbase,即 /sys/class/newchrdevbase。
在/dev目录下可以搜索到newchrdevbase,即 /dev/newchrdevbase。