【学习笔记】
(1)设备号的不同:杂项设备的主设备号是固定的,固定为10,而字符类设备需要我们自己或者系统来给我们分配。
(2)设备节点的生成方式不同:杂项设备可以自动生成设备节点,而字符设备需要我们自己生成设备节点。
需要明确知道系统里面哪些设备号没有被使用,然后手动分配。
函数定义在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 失败:返回非零
这个函数同样也定义在linux-4.9.268/include/linux/fs.h中 extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *); 参数: 第一个:保存生成的设备号 第二个:我们请求的第一个设备号,通常是0 第三个:连续申请的设备号的个数 第四个:设备名称 返回值: 成功:返回0 失败:返回负数 使用动态分配会优先使用255~234设备号
这个函数同样也定义在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");
在实际开发中,建议使用动态申请设备号的方式,多人开发时,使用静态申请很容易造成设备号冲突。
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;//次设备号的数量 };
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结构体指针 第二个:文件操作集
int cdev_add(struct cdev *, dev_t, unsigned); 参数: 第一个:cdev的结构体指针 第二个:设备号 第三个:次设备号的数量
void cdev_del(struct cdev *);
直接在上面申请设备号的代码修改,前面已有的代码注释,下面不在写,便于区别修改位置
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目录下自动创建相应的设备文件
在嵌入式Linux中使用mdev来实现设备节点文件的自动创建和删除
mdev是udev的简化版本,是busybox中所带的程序,最适合用在嵌入式系统
udev是一种工具,它跟狗根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev目录下。使用udev后,在/dev目录下就只包含系统中真正存在的设备,udev一般用在PC上的linux中,相对于mdev来说复制些。
自动创建设备节点分为两个步骤:
(1)使用class_create函数创建一个class的类
(2)使用device_create函数在我们创建的类下面创建一个设备
在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就是要删除的类。
当使用上节点的函数创建完成一个类后,使用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驱动篇