字符设备驱动-Linux驱动学习(5)

【学习笔记】

一、申请字符类设备号

1、字符设备和杂项设备的区别

(1)设备号的不同:杂项设备的主设备号是固定的,固定为10,而字符类设备需要我们自己或者系统来给我们分配。

(2)设备节点的生成方式不同:杂项设备可以自动生成设备节点,而字符设备需要我们自己生成设备节点。

2、两种方法注册字符类设备号

(1)静态分配设备号

需要明确知道系统里面哪些设备号没有被使用,然后手动分配。

函数定义在linux-4.9.268/include/linux/fs.h
extern int register_chrdev_region(dev_t, unsigned, const char *);
参数:
	第一个:设备的起始值,类型是dev_t类型
	第二个:次设备号的个数
	第三个:设备的名称

dev_t类型: dev_t是用来保存设备号的,是一个32位数
其中高12为用来保存设备号,低12为用来保存次设备号

dev_t定义在linux-4.9.268/include/linux/types.h里边
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t		dev_t;

Linux 提供了几个宏定义来操作设备号

定义在linux-4.9.268/include/linux/kdev_t.h里边

#define MINORBITS	20	//提供了次设备的位数,一共20位
#define MINORMASK	((1U << MINORBITS) - 1)//次设备的掩码

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))//在dev_t里面获取主设备号
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))//在dev_t里面获取主次设备号
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))//将主设备号和次设备号组成dev_t类型(上面提到,dev_t类型是用来保存设备号的)
其中MKDEV(ma,mi)参数:
	ma:主设备号
	mi:次设备号
返回值:
	成功:返回0
	失败:返回非零

(2)动态分配设备号

这个函数同样也定义在linux-4.9.268/include/linux/fs.h中
extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
参数:
	第一个:保存生成的设备号
	第二个:我们请求的第一个设备号,通常是0
	第三个:连续申请的设备号的个数
	第四个:设备名称
	
返回值:
	成功:返回0
	失败:返回负数
使用动态分配会优先使用255~234设备号

3、注销设备号

这个函数同样也定义在linux-4.9.268/include/linux/fs.h中
extern void unregister_chrdev_region(dev_t, unsigned);
参数:
	第一个:分配设备号的起始地址
	第二个:申请的连续设备号个数

实例操作:

chrdev.c

#include <linux/init.h>
#include <linux/module.h>
//注册设备号函数所在头文件
#include <linux/fs.h>
//处理设备号宏定义所在头文件
#include <linux/kdev_t.h>

//定义次设备号个数
#define DEVICE_NUMBER 1
//次设备号起始地址,通常为0
#define DEVICE_MINOR_NUMBER 0

//定义设备名称
#define DEVICE_SNAME "schrdev"	//静态注册
#define DEVICE_ANAME "achrdev"	//动态注册


static int major_num,minor_num;

module_param(major_num,int,S_IRUSR);
module_param(minor_num,int,S_IRUSR);


static int hello_init(void){
	
	dev_t dev_num;

	int ret;//定义保存函数返回值变量
	
	//静态申请设备号
	if(major_num){//判断主设备号有没有传递进来,如果传了参数,则使用静态注册方式,否则,使用动态注册方式。

		//打印主次设备号
		printk("major_num= %d\n",major_num);
		printk("minor_num= %d\n",minor_num);
		
		//组合主次设备号
		dev_num = MKDEV(major_num, minor_num);
	
		//注册设备号函数,并保存返回值
		ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);

		if(ret < 0){//返回值<0,注册失败
			printk("register_chrdev_region error\n");
		}
		printk("register_chrdev_region successful\n");//否则说明注册成功
	}
	else{//动态申请设备号

		ret = alloc_chrdev_region(dev_num, DEVICE_MINOR_NUMBER, DEVICE_NUMBER, DEVICE_ANAME);

		if(ret < 0){//返回值<0,注册失败
			printk("register_chrdev_region error\n");
		}
		printk("register_chrdev_region successful\n");//否则说明注册成功

		//使用宏定义获取设备号
		major_num = MAJOR(dev_num);
		minor_num = MINOR(dev_num);

		//打印主次设备号
		printk("major_num= %d\n",major_num);
		printk("minor_num= %d\n",minor_num);
	}



	return 0;
}

static void hello_exit(void){

	//注销设备号
	unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
	printk("bye bye\n");
	
}

//入口和出口
module_init(hello_init);
module_exit(hello_exit);

//声明许可证
MODULE_LICENSE("GPL");


在实际开发中,建议使用动态申请设备号的方式,多人开发时,使用静态申请很容易造成设备号冲突。

二、注册字符设备

1、重要结构说明

cdev结构体:描述字符设备的结构体

//它定义在linux-4.9.268/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;//次设备号的数量
};

2、操作步骤

(1)定义一个cdev结构体

(2)使用cdev_init函数初始化cdev结构体成员变量

void cdev_init(struct cdev *, const struct file_operations *){
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	
	cdev->ops = fops;//把文件操作集写给cdev的成员变量ops
}
参数:
	第一个:要初始化的cdev结构体指针
	第二个:文件操作集
	

(3)使用cdev_add函数注册字符设备到内核

int cdev_add(struct cdev *, dev_t, unsigned);
参数:
	第一个:cdev的结构体指针
	第二个:设备号
	第三个:次设备号的数量

(4)注销字符设备

void cdev_del(struct cdev *);

3、实际操作展示

直接在上面申请设备号的代码修改,前面已有的代码注释,下面不在写,便于区别修改位置

chrdev.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
//注册字符设备所在
#include <linux/cedv.h>


#define DEVICE_NUMBER 1
#define DEVICE_MINOR_NUMBER 0

#define DEVICE_SNAME "schrdev"
#define DEVICE_ANAME "achrdev"

static int major_num,minor_num;

//定义cdev结构体
struct cdev cdev;

module_param(major_num,int,S_IRUSR);
module_param(minor_num,int,S_IRUSR);

//应用层调用设备节点,触发的open函数
int chrdev_open(struct inode *inode, struct file *file){//(*open)函数实现
	printk("hello chrdev_open\n");
	return 0;
}

//定义文件操作集
struct file_operations chrdev_ops = {
	.owner = THIS_MODULE,
	.open = chrdev_open
};

static int hello_init(void){
	
	dev_t dev_num;
	int ret;
	
	if(major_num){

		printk("major_num= %d\n",major_num);
		printk("minor_num= %d\n",minor_num);
		
		dev_num = MKDEV(major_num, minor_num);
	
		ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);

		if(ret < 0){
			printk("register_chrdev_region error\n");
		}
		printk("register_chrdev_region successful\n");
	}
	else{
		ret = alloc_chrdev_region(dev_num, DEVICE_MINOR_NUMBER, DEVICE_NUMBER, DEVICE_ANAME);

		if(ret < 0){
			printk("register_chrdev_region error\n");
		}
		printk("register_chrdev_region successful\n");

		major_num = MAJOR(dev_num);
		minor_num = MINOR(dev_num);

		printk("major_num= %d\n",major_num);
		printk("minor_num= %d\n",minor_num);
	}


	cdev.owner = THIS_MODULE;//声明所属模块
	//初始化cdev结构体成员变量,第二个参数,要提前定义文件操作集
	cdev_init(&cdev, chrdev_ops);
	//将字符设备注册到内核
	cdev_add(&cdev, dev_num, DEVICE_NUMBER);

	return 0;
}

static void hello_exit(void){

	//注销设备号
	unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
	
	//注销字符设备(注意把它写到注册设备号的下面,一个简单的逻辑问题)
	void cdev_del(&cdev);
	printk("bye bye\n");
	
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");



app.c(只保留打开节点功能)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]){//如果打开设备节点成功,这会调用驱动里边的misc_open()函数

	int fd;
	
	char buf[64] = {0};

	fd = open("/dev/test",O_RDWR);//open the device node

	if(fd < 0){		//determine whether the opening is successful
	
		perror("open error\n");
		
		return fd;
	}

	//close(fd);//关闭节点
	
	return 0;
}


【注意】字符设备注册完后并不会自动生成设备节点,需要是哦那个mknod命令创建设备节点

命令格式:

mknod [名称] [类型] [主设备号] [次设备号]

例如:

mknod /dev/test c 247 0		//"dev/test"为app.c中定义的设备节点名称

三、自动创建设备节点

当加载模块时,在/dev目录下自动创建相应的设备文件

1、怎么自动创建一个设备节点

在嵌入式Linux中使用mdev来实现设备节点文件的自动创建和删除

2、什么是mdev

mdev是udev的简化版本,是busybox中所带的程序,最适合用在嵌入式系统

3、什么是udev

udev是一种工具,它跟狗根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev目录下。使用udev后,在/dev目录下就只包含系统中真正存在的设备,udev一般用在PC上的linux中,相对于mdev来说复制些。

4、怎么创建设备节点

自动创建设备节点分为两个步骤:

(1)使用class_create函数创建一个class的类

(2)使用device_create函数在我们创建的类下面创建一个设备

5、创建和删除类函数

在Linux驱动程序中一般通过两个函数来完成设备节点的创建和删除。首先要创建一个class类结构体。

calss结构体定义在include/linux/device.h中。class_create是类创建函数,class_create是一个宏定义,内容如下

#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

class_create一共有两个参数,参数owner一般为THIS_MODULE,参数name是类的名字。返回值是个指向结构体class的指针,也就是创建的类。

卸载驱动程序的时候需要删除掉类,类删除函数为class_destory,函数原型如下:

void class_destroy(struct class *cls);
//参数cls就是要删除的类。

6、创建设备函数

当使用上节点的函数创建完成一个类后,使用device_create函数在这个类下创建一个设备device_create

函数原型如下:

//同样定义在include/linux/device.h中
struct device *device_create_vargs(struct class *cls, struct device *parent,
				   dev_t devt, void *drvdata,
				   const char *fmt, va_list vargs);
参数说明:
device_create 是个可变参数函数
class:设备要创建在哪个类下面
parent:父设备,一般为NULL,也就是没有父设备
devt:设备号
drvdata:是设备可能会使用的一些数据,一般为NULL
fmt:是设备名字,如果设置fmt=xxx的话,就会生成/dev/xxx这个设备文件
返回值就是创建号的设备

整理自嵌入式学习之Linux驱动篇

posted @ 2021-07-14 22:50  水鸽  阅读(41)  评论(0编辑  收藏  举报