使用alloc_chrdev_region/register_chrdev_region/cdev注册字符设备和使用class在驱动加载时自动创建设备节点

发布时间 2023-03-27 18:07:45作者: 跌落星球

前言

在旧版本中使用register_chrdev函数注册字符设备,该函数只需给定主设备号即可。一旦确认了主设备号,该主设备号下的所有次设备号全部都没有用,并且在驱动加载成功后还需手动使用mknod命令创建设备节点。

而新版本则使用register_chrdev_region函数通过给定主设备号和次设备号来注册字符设备,避免了旧版本因使用了一个主设备号,而浪费了所有次设备号的问题。


概要

注册字符设备和自动创建设备节点步骤共分4步:
1、申请设备号(设备号=主设备号+次设备号)
2、注册设备号
3、注册设备
4、创建类,实现输入modprobe命令加载驱动后自动在/dev目录下创建设备文件


申请设备号

通过alloc_chrdev_region函数,向kernel申请设备号,函数原型如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数:
dev:输出参数,表示分配到的设备号,可使用MAJOR和MINOR宏,将主设备号和次设备号分别提取出来。
baseminor:表示次设备号从第几开始分配
count:次设备号个数
name:驱动名字

可通过MAJOR和MINOR宏提取主设备号和次设备号,示例如下:

testchrdev.major = MAJOR(testchrdev.dev);
testchrdev.minor = MINOR(testchrdev.dev);

注册设备号

通过register_chrdev_region函数注册设备号,函数原型如下:

int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数:
from:申请的起始设备号,即给定的设备号
count:次设备号的个数
name:驱动名字

注册设备号分2种情况:
1、已经给定设备的主设备号和次设备号,直接使用register_chardev_region函数注册设备号即可。
2、未给定设备的主设备号和次设备号,需要使用alloc_chrdev_region函数申请设备号后,再通过register_chardev_region函数注册设备号。

//示例
int major;	//主设备号
int minor;	//次设备号
det_t devid;	//设备号(包括主设备号和次设备号)

if (major){	//定义了主设备号
	devid = MKDEV(major, 0);	//主设备号为major,次设备号为0
	register_chrdev_region(devid, 1, "test");
} else {	//没有定义主设备号
	alloc_chrdev_region(devid, 0, 1, "test");	//申请设备号
	major = MAJOR(devid);	//提取主设备号
	minor = MINOR(devid);	//提取次设备号
}

在注销字符设备后,需要使用unregister_chrdev_region函数释放设备号,原型如下:

void unregister_chrdev_region(dev_t from, unsigned count)
原型:
from:要释放的起始设备号
count:需要释放设备号的个数
//示例
unregister_chrdev_region(devid, 1);	//释放的起始设备号为devid,个数为1个

注册设备

1、在linux中,使用cdev结构体表示字符设备,定义如下:

struct cdev{
	struct kobject kobj;
	struct module *owner;	//值为THIS_MODULE,表示模块
	const struct file_operations *ops;	//注册驱动的关键,要填充成这个结构体变量
	struct list_head list;
	dev_t dev;	//设备号(包括主设备号和次设备号)
	unsigned int count;	//次设备号个数
};

//在编写字符驱动之前,需要先定义一个cdev结构体变量,示例如下:
struct cdev test_cdev;

2、在定义cdev结构体变量后,需要使用cdev_init进行初始化,函数原型如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数:
cdev:cdev结构体变量
fops:字符设备文件操作函数集合
//示例:
/* 定义cdev结构体变量 */
struct cdev test_cdev;

/* 字符设备文件操作函数集合 */
static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	/* 其他具体的初始项 */
};

/* cdev结构体变量的owner要为THIS_MODULE,表示模块 */
test_cdev.owner = THIS_MODULE;

/* 初始化cdev结构体变量 */
cdev_init(&test_cdev, &test_fops);

3、初始化cdev结构体变量后,使用cdev_add函数向linux系统添加字符设备,函数原型如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数:
p:指向要添加的字符设备
dev:字符设备所使用的设备号
count:要添加的设备数量
//示例:
/* 定义cdev结构体变量 */
struct cdev test_cdev;

/* 字符设备文件操作函数集合 */
static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	/* 其他具体的初始项 */
};

/* cdev结构体变量的owner要为THIS_MODULE,表示模块 */
test_cdev.owner = THIS_MODULE;

/* 初始化cdev结构体变量 */
cdev_init(&test_cdev, &test_fops);

/* 添加字符设备 */
cdev_add(&test_cdev, devid, 1);

4、在卸载驱动时,需要使用cdev_del函数从kernel中删除对应的字符设备,原型如下:

void cdev_dev(struct cdev *p)
参数:
p:指向要删除的字符设备
//示例如下:
static void __exit test_exit(void)
{
	/* 注销字符设备驱动 */
	cdev_del(&testchrdev.cdev);	/* 删除cdev */
	unregister_chrdev_region(testchrdev.devid, 1);
}

自动创建设备节点

作用:自动创建节点功能实现后,使用modprode加载驱动模块成功后,会自动在/dev目录下创建对应的设备文件。

1、使用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)

参数:
owner:一般为THIS_MODULE
name:表示类的名称

返回值:创建的类

2、若卸载驱动,需要使用class_destroy函数删掉类,原型如下:

void class_destroy(struct class *cls)
参数:
cls:表示要删掉的类

3、在类创建完成后,使用device_create函数(可变参数)在类的下面创建设备,原型如下:

struct device *device_create(struct class *class,
				struct device *parent,
				dev_t devt,
				void *drvdata,
				const char *fmt,
				...)
参数:
class:表示设备要创建在哪个类下面
parent:父设备,一般为NULL
devt:设备号
drvdata:表示设备可能会使用的一些数据,默认为NULL
fmt:设备名字,若fmt=xxx,会生成/dev/xxx这个设备文件

4、若卸载驱动,需要使用device_destroy函数删掉创建的设备,原型如下:

void device_destory(struct class *class, dev_t devt)
参数:
class:要删除的设备所处的类
devt:要删除的设备号
//示例
struct class *class;	//类
struct device *devce;	//设备
dev_t devid;	//设备号

//驱动入口函数
static int __init test_init(void)
{
	class = class_create(THIS_MODULE, "test");	//创建类
	device = device_create(class, NULL, devid, NULL, "test");	//创建设备
	return 0;
}

//驱动出口函数
static void __exit test_exit(void)
{
	device_destory(class, devid);	//删除设备
	class_destory(class);	//删除类
}

设置文件私有数据

1、考虑到每个硬件设备都有自己的属性,一般情况下,会把设备的所有属性信息封装成结构体,如下:

//设备结构体
struct test_dev{
	dev_t decid;	//设备号
	struct cdev cdev;	//cdev
	struct class *class;	//类
	struct device *device;	//设备
	int major;	//主设备号
	int minor;	//次设备号
}
struct test_dev testdev;

2、将设备属性封装成结构体后,在编写open函数时,将该结构体作为私有数据添加到设备文件中,如下:

//open函数
static int test_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &testdev;	//设置私有数据
	return 0;
}
//在私有数据设置好后,在write、read、close等函数中直接读取privata_data就可以访问设备结构体

实例

以正点原子IMX6ULL阿尔法为平台。
1、编写LED灯的字符设备驱动程序如下:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>

#include <linux/cdev.h>
#include <linux/device.h>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define	NEWCHRLED_CNT	1	/* 设备号个数 */
#define NEWCHRLED_NAME	"newchrled"	/* 名字 */

#define	LED_OFF	0	/* 关灯 */
#define	LED_ON	1	/* 开灯 */


/* 寄存器物理地址 */
#define	CCM_CCGR1_BASE	(0X020C406C)
#define	SW_MUX_GPIO1_IO03_BASE	(0X020E0068)
#define	SW_PAD_GPIO1_IO03_BASE	(0X020E02F4)
#define	GPIO1_DR_BASE	(0X0209C000)
#define	GPIO1_GDIR_BASE	(0X0209C004)

/* 映射后寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* newchrled 设备结构体 */
struct newchrled_dev{
	dev_t devid;	/* 设备号 */
	struct cdev cdev;	/* cdev */
	struct class *class;	/* 类 */
	struct device *device;	/* 设备 */
	int major;	/* 主设备号 */
	int minor;	/* 次设备号 */

};
struct newchrled_dev newchrled;	/* led设备 */

/*
 * @description:切换LED状态
 * @param - sta:LED_ON 打开LED;LED_OFF 关闭LED
 * @return:无
 */
void led_switch(unsigned char sta)
{
	u32 val = 0;
	if (sta == LED_ON){
		val = readl(GPIO1_DR);
		val &= ~(1<<3);
		writel(val, GPIO1_DR);
	}else if (sta == LED_OFF){
		val = readl(GPIO1_DR);
		val |= (1<<3);
		writel(val, GPIO1_DR);
	}
}

/*
 * @description : 打开设备
 * @param - inode : 传递给驱动的inode
 * @param - filp : 设备文件,在open时将file结构体的private_data成员变量指向设备结构体
 * @return : 0:成功;其他:失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled;	/* 设置私有数据 */
	return 0;
}

/*
 * @description : 从设备读取数据
 * @param - filp : 要打开的设备文件(文件描述符)
 * @param - buf : 返回给用户空间的数据缓冲区
 * @param - cnt : 要读取的数据数据长度
 * @param - offt : 相对于文件首地址的偏移
 * @return : 读取的字节数,若为负,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf,
				size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description : 向设备写数据
 * @param - filp : 设备文件,表示打开的文件描述符
 * @param - buf : 要给设备写入的数据
 * @param - cnt : 要写入的数据长度
 * @param - offt : 写入的字节数,若为负,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf,
				size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if (retvalue < 0){
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];	/* 获取状态值 */

	if (ledstat == LED_ON){
		led_switch(LED_ON);	/* 打开LED */
	} else if (ledstat == LED_OFF){
		led_switch(LED_OFF);	/* 关闭LED */
	}

	return 0;
}

/*
 * @description : 关闭或释放设备
 * @param - inode : 设备的inode
 * @param - filp : 要关闭的设备文件(文件描述符)
 * @return : 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
 * 设备操作函数结构体
 */
static struct file_operations newchrled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

/*
 * @description : 驱动入口函数
 * @param : 无
 * @return : 0 成功;其他 失败
 */
static int __init led_init(void)
{
	unsigned int val = 0;

	/* 1、寄存器地址映射 */
	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR =  ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3<<26);	/* 清除旧值 */
	val |= (3<<26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为GPIO1_IO03并设置IO属性*/
	writel(5, SW_MUX_GPIO1_IO03);
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1<<3);
	val |= (1<<3);
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1<<3);
	writel(val, GPIO1_DR);

	/* 6、注册字符设备驱动 */
	/* 创建设备号 */
	if (newchrled.major){	/* 定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
	} else {	/* 没有定义设备号 */
		alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */
		newchrled.major = MAJOR(newchrled.devid);	/* 获取主设备号 */
		newchrled.minor = MINOR(newchrled.devid);	/* 获取此设备号 */
	}
	printk("newchrled major = %d, minor = %d\r\n", newchrled.major, newchrled.minor);

	/* 初始化cdev */
	newchrled.cdev.owner = THIS_MODULE;
	cdev_init(&newchrled.cdev, &newchrled_fops);

	/* 添加一个cdev */
	cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

	/* 创建类 */
	newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.class)){
		return PTR_ERR(newchrled.class);
	}

	/* 创建设备 */
	newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.device)){
		return PTR_ERR(newchrled.device);
	}

	return 0;
}

/*
 * @description : 驱动出口函数
 * @param : 无
 * @return : 无
 */
static void __exit led_exit(void)
{
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);	/* 删除cdev */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);

	device_destroy(newchrled.class, newchrled.devid);
	class_destroy(newchrled.class);

}

/*
 * 将上面两个函数指定为驱动的入口和出口函数
 */
module_init(led_init);
module_exit(led_exit);

/*
 * LICENSE和作者信息
 */
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("xxx");

2、修改Makfile文件
3、使用make -j32命令编译驱动模块
4、使用depmod(首次加载驱动需要用到)和modprobe xxx.ko命令加载模块
image

5、使用ls命令查看/dev目录下是否有对应的设备文件
image