【Linux驱动】字符设备驱动框架(一)—— 模块初始化(设备号注册、设备创建)

驱动运行有两种方式:

  1. 将驱动编译进 Linux 内核,内核启动时自动运行驱动程序
  2. 将驱动编译成模块(.ko 文件),内核启动以后,使用 insmod 或 modprobe 加载驱动模块

这里我们采用第二种方式,方便调试,只需加载驱动,无需编译内核,等确定没问题以后再根据需要编译到内核。


目录

一、驱动模块的加载 / 卸载

二、注册字符设备

1、注册设备号 

2、创建并加载字符设备

3、注销设备号、卸载字符设备

三、自动创建字符设备节点

1、创建类、设备节点

2、类目录、设备节点的移除

四、整理:module_init、module_exit 函数模板


一、驱动模块的加载 / 卸载

驱动模块被加载或者卸载,可能需要有一些初始化或者收尾,这就需要内核提供的宏

  • module_init:当前模块被加载到内核时,会自动调用 module_init 注册的函数
  • module_exit:当前模块从内核中卸载时,会自动调用 module_exit 注册的函数
// 使用 __init 修饰
static int __init chrdevbase_init(void)
{
    /* 驱动入口实现 */
	return 0;
}

// 使用 __exit 修饰
static void __exit chrdevbase_exit(void)
{
    /* 驱动出口实现 */
}

module_init(xxx_init);     //注册 “模块入口函数”
module_exit(xxx_exit);     //注册 “模块出口函数”

二、注册字符设备

注册字符设备相当于告诉内核,希望他帮你管理这个字符设备,应用程序读写字符设备文件时,就会自动调用该字符设备对应的 read / write 函数。

除了设备号外,后续还有字符设备注册、设备节点的创建,为了方便管理,我们把这些统一放到一个结构体中。

struct chrdev_led_t{
    struct class* class;	/* 设备节点所属类 */
	struct device* device;	/* 设备节点 */

    struct cdev dev;		/* 字符设备 */
	dev_t devid;			/* 设备号 */
	int major;				/* 主设备号 */
	int minor;				/* 次设备号 */
};	
static struct chrdev_led_t chrdev_led;

1、注册设备号 

注册设备号主要是判断之前是否已经注册过设备号

  • 如果不是第一次注册,因为已经有了主设备号,所以直接静态分配即可
  • 如果是第一次注册,动态分配设备号,此时 major、minor、devid 都会获取到值
if (chrdev_led.major)
{
    chrdev_led.devid = MKDEV(chrdev_led.major, 0);
    ret = register_chrdev_region(chrdev_led.devid, 1, CHRDEVBASE_NAME);
}
else
{
    ret = alloc_chrdev_region(&chrdev_led.devid, 0, 1, CHRDEVBASE_NAME);
    chrdev_led.major = MAJOR(chrdev_led.devid);
    chrdev_led.minor = MINOR(chrdev_led.devid);
}

注意:在 module_init 中注册设备号,同时需要在 module_exit 中释放设备号

2、创建并加载字符设备

我们需要将字符设备以及设备号一起加载到内核,字符设备才算注册完成,在加载到内核之前,我们需要先对字符设备做一个初始化。

  • 创建一个字符设备:struct cdev dev  
  • 初始化字符设备:cdev_init
  • 加载字符设备到内核:cdev_add
/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE, 
	.open = chrdevbase_open,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
	.release = chrdevbase_release,
};


/* 2. 初始化字符设备 */
chrdev_led.dev.owner = THIS_MODULE;
cdev_init(&chrdev_led.dev, &chrdevbase_fops);			// 初始化字符设备
/* 3. 将字符设备添加到内核 */
cdev_add(&chrdev_led.dev, chrdev_led.devid, 1);			// 将字符设备添加到内核

注意:在 module_init 中加载字符设备,同时需要在 module_exit 中卸载字符设备

3、注销设备号、卸载字符设备

当驱动被卸载时,我们当然也需要做一些收尾工作

/* 注销字符设备 */
unregister_chrdev_region(chrdev_led.devid, 1);		// 注销设备号
cdev_del(&chrdev_led.dev);							// 卸载字符设备

三、自动创建字符设备节点

该部分涉及到的函数可参考:自动创建 / 删除设备节点

1、创建类、设备节点

将设备驱动加载到内核以后,并不会在 /dev 目录下生成设备文件,所以需要自己实现设备文件创建的逻辑。创建设备文件包含两部分:

  • 创建设备所属类。会在 /sys/class 目录下生成对应的类目录
  • 创建设备文件。会在 /dev 下生成设备文件,/sys/class/类目录/ 下生成设备目录
/* 自动创建设备节点 */
// 设备节点所属类
chrdev_led.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
if (IS_ERR(chrdev_led.class))
{
    goto node_create_err;
}
// 创建设备节点
chrdev_led.device = device_create(chrdev_led.class, NULL, chrdev_led.devid, NULL, CHRDEVBASE_NAME);
if (IS_ERR(chrdev_led.device))
{
    goto node_create_err;
}

2、类目录、设备节点的移除

卸载驱动时,要将设备目录和设备节点一并移除,删除的顺序和创建顺序相反:

  • 先删除设备文件
  • 再移除类目录
device_destroy(chrdev_led.class, chrdev_led.devid);	// 删除节点
class_destroy(chrdev_led.class);					// 删除类

四、整理:module_init、module_exit 函数模板

#define CHRDEVBASE_NAME "chrdevbase" 	/* 设备名 */

struct chrdev_led_t{
	struct class* class;	/* 设备节点所属类 */
	struct device* device;	/* 设备节点 */

	struct cdev dev;		/* 字符设备 */
	dev_t devid;			/* 设备号 */
	int major;				/* 主设备号 */
	int minor;				/* 次设备号 */
};	
static struct chrdev_led_t chrdev_led;


/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE, 
	.open = chrdevbase_open,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
	.release = chrdevbase_release,
};

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

	/* 1. 注册设备号 */
	if (chrdev_led.major)
	{
		chrdev_led.devid = MKDEV(chrdev_led.major, 0);
		ret = register_chrdev_region(chrdev_led.devid, 1, CHRDEVBASE_NAME);
	}
	else
	{
		ret = alloc_chrdev_region(&chrdev_led.devid, 0, 1, CHRDEVBASE_NAME);
		chrdev_led.major = MAJOR(chrdev_led.devid);
		chrdev_led.minor = MINOR(chrdev_led.devid);
	}

	/* 2. 初始化字符设备 */
	chrdev_led.dev.owner = THIS_MODULE;
	cdev_init(&chrdev_led.dev, &chrdevbase_fops);					// 初始化字符设备
	/* 3. 将字符设备添加到内核 */
	cdev_add(&chrdev_led.dev, chrdev_led.devid, 1);			// 将字符设备添加到内核

	/* 自动创建设备节点 */
	// 设备节点所属类
 	chrdev_led.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
	if (IS_ERR(chrdev_led.class))
	{
		return PTR_ERR(chrdev_led.class);
	}
	// 创建设备节点
	chrdev_led.device = device_create(chrdev_led.class, NULL, chrdev_led.devid, NULL, CHRDEVBASE_NAME);
	if (IS_ERR(chrdev_led.device))
	{
		return PTR_ERR(chrdev_led.device);
	}
	
	printk("chrdevbase init!\n");
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{	
	/* 注销字符设备 */
	unregister_chrdev_region(chrdev_led.devid, 1);		// 注销设备号
	cdev_del(&chrdev_led.dev);							// 卸载字符设备
	
	device_destroy(chrdev_led.class, chrdev_led.devid);	// 删除节点
	class_destroy(chrdev_led.class);					// 删除类
	printk("chrdevbase exit!\n");
}

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

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

猜你喜欢

转载自blog.csdn.net/challenglistic/article/details/131879587
今日推荐