字符设备驱动-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这个设备文件
返回值就是创建号的设备