前言
从驱动框架图可以看出,在Linux操作系统中,用户要想使用外接硬件设备,是通过调用库函数获取设备文件,通过设备文件调用驱动,驱使硬件设备完成工作。
设备驱动框架图
字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
图片来源:https://blog.csdn.net/zqixiao_09/article/details/50839042
字符设备驱动模型
图片来源:https://blog.csdn.net/zqixiao_09/article/details/50839042
文章目录
一、Linux内核驱动接口介绍
1.1、注册、注销字符设备:
register_chrdev
register_chrdev(unsigned int major, const char *name,const struct file_operations *fops);
(1)确定一个主设备号,如果major=0,则会自动分配设备号
(2)构造一个file_operations结构体, 然后放在chrdevs数组中
(3)name:字符设备名称
说明:
- cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动),注意这里并不是驱动文件设备节点!
- 读写字符设备,根据主设备号从chrdevs数组中取出相应的结构体,并调用相应的处理函数。
- 每注册一个字符设备,还会连续注册0~255个次设备号,绑定在同一个file_operations操作方法结构体上。
- 在大多数情况下,都只用极少的次设备号,所以会浪费很多资源。
- 在 2.4 的内核使用 register_chrdev进行字符设备节点的分配,每一个主设备号只能存放一种设备,它们使用相同的 file_operation 结构体,也就是说内核最多支持 256 个字符设备驱动程序。
- 使用了register_chrdev函数,字符类设备驱动没有自动创建设备节点,只是注册了这个设备。
- 系统启动后,就要自己创建设备节点mknod,这样虽然是可行的,但是比较麻烦。于是想在__init函数里面,自动创建设备节点。
unregister_chrdev
static inline void unregister_chrdev(unsigned int major, const char *name)
1.2、静态注册字符设备: register_chrdev_region
dev_t dev_num
dev_num = MKDEV(MAJOR_NUM, 0); //设备号 : 主设备号(12bit) | 次设备号(20bit)起始号
//指定设备编号来静态注册一个字符设备
int register_chrdev_region(dev_t dev_num, unsigned count, const char *name);
- dev_num: 注册的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0
- count: 需要连续注册的次设备编号个数,比如:
起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上
- name: 字符设备名称
- 当返回值小于0,表示注册失败
1.3、动态分配字符设备: alloc_chrdev_region
/*动态分配一个字符设备,注册成功并将分配到的主次设备号放入*dev里*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
- *dev: 存放起始设备编号的指针,当注册成功, *dev就会等于分配到的起始设备编号,可以通过MAJOR()和MINNOR()函数来提取主次设备号
- baseminor:次设备号基地址,也就是起始次设备号
- count:需要连续注册的次设备编号个数,比如: 起始次设备号(baseminor)为0,baseminor=2, 表示0~1的此设备号都要绑定在同一个file_operations操作方法结构体上
- *name:字符设备名称
- 当返回值小于0,表示注册失败
1.4、注销字符设备: unregister_chrdev_region
/*注销字符设备*/
void unregister_chrdev_region(dev_t from, unsigned count);
- from: 注销的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0
- count: 需要连续注销的次设备编号个数,比如: 起始次设备号为0,baseminor=100,表示注销掉0~99的次设备号
1.5、创建设备类
<内核源码路径>/include/linux/device.h中:
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
- owner:一个struct module结构体类型的指针,指向函数__class_create()即将创建的、“拥有”这个struct class的模块。一般赋值为结构THIS_MODULE,详细定义见文件include/linux/module.h。
- name:char类型的指针,代表即将创建的struct class变量的名字,用于给struct
class的name字段赋值。通俗地说,就是指向struct class名称的字符串的指针。
- 返回值:与函数__class_create()的相同,都代表新创建的逻辑类
说明:
struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面。
- class_create - create a struct class structure
- @owner: pointer to the module that is to "own" this struct class
- @name: pointer to a string for the name of this class.
- @key: the lock_class_key for this class; used by mutex lock debugging
- - This is used to create a struct class pointer that can then be used
- in calls to device_create().
- - Returns &struct class pointer on success, or ERR_PTR() on error.
- - Note, the pointer created here is to be destroyed when finished by
- making a call to class_destroy().
*/
//导出设备信息到用户空间(/sys/class/类名/设备名)
struct class *__class_create(struct module *owner, const char *name,struct lock_class_key *key)
- owner: 指向要“拥有”此结构类的模块的指针
- name: 指向该类名称的字符串的指针
- key:该类的lock_class_key;用于互斥锁调试
1.6、自动在/dev目录下创建相应设备节点
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
- class:该设备依附的类
- parent:父设备
- devt:设备号(此处的设备号为主次设备号)
- drvdata:私有数据
- fmt:设备名。
说明:
- class_create(owner,name)创建设备类后调用device_create(…)函数,在/dev目录下创建相应的设备节点。
- 加载模块的时候,用户空间中的udev会自动响应device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。
二、Struct cdev 结构体的定义及操作
1. cdev结构体的定义如下:
<include/linux/cdev.h>
//在Linux内核中,使用cdev结构体来描述一个字符设备
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
2. 操作struct cdev结构体的接口主要有以下几个:
2.1、 初始化: void cdev_init(struct cdev *cdev, const struct file_operations *fops)
//对struct cdev结构体做初始化, 建立cdev 和 file_operations之间的连接:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev); //将整个结构体清零
INIT_LIST_HEAD(&cdev->list); //初始化list成员使其指向自身
kobject_init(&cdev->kobj, &ktype_cdev_default); //初始化kobj成员
cdev->ops = fops; // 初始化ops成员;
}
2.2、动态申请: struct cdev *cdev_alloc(void)
//分配一个struct cdev结构,动态申请一个cdev内存
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
2.3、注册: int cdev_add(struct cdev *p, dev_t dev, unsigned count);
//内核注册一个struct cdev结构体,struct cdev *p代表的字符设备就可以使用了
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
1. p:字符设备指针
2. dev:设备号,赋值给struct cdev的dev成员。
3. count:设备关联的设备编号的数量,赋值给struct cdev的count成员。
2.4、注销:void cdev_del(struct cdev *p);
//向内核注销一个struct cdev结构,struct cdev *p代表的字符设备就不可以使用了
void cdev_del(struct cdev *p);
三、驱动程序创建流程
1.创建驱动程序的入口
//设备号
static dev_t dev_num ;
//指定的主设备号
#define MAJOR_NUM 100
//自己的字符设备
struct mycdev
{
int len;
unsigned char buffer[128];
struct cdev cdev;
};
//设备类
struct class *cls;
static int __init dev_my_init()
{
//1.申请注册设备号
dev_num = MKDEV(MAJOR_NUM, 0); //设备号 : 主设备号(12bit) | 次设备号(20bit)
ret = register_chrdev_region(dev_num,10,"dev_fifo"); //静态注册设备号
if(ret <0 )
{
//静态注册失败,进行动态注册设备号
ret = alloc_chrdev_region(&dev_num,0,10,"dev_my");
if(ret < 0){
printk("Fail to register_chrdev_region\n");
goto err_register_chrdev_region;
}
}
//2.创建设备类
cls = class_create(THIS_MODULE, "dev_my");
if(IS_ERR(cls)){
ret = PTR_ERR(cls);
goto err_class_create;
}
//3.初始化字符设备
gcd = kzalloc(sizeof(struct mycdev), GFP_KERNEL); //动态申请内存
if(!gcd){
return -ENOMEM;
}
cdev_init(&gcd->cdev,&my_operations);//初始化字符设备
//4.添加设备到用户操作系统
ret = cdev_add(&gcd->cdev,dev_num,0);
if (ret < 0)
{
goto err_cdev_add;
}
//5.导出设备信息到用户空间(/sys/class/类名/设备名)
device = device_create(cls,NULL,dev_num,NULL,"dev_my%d",0);
if(IS_ERR(device)){
ret = PTR_ERR(device);
printk("Fail to device_create\n");
goto err_device_create;
}
printk("Register dev_fito to system,ok!\n");
return 0;
err_device_create:
cdev_del(&gcd->cdev);
err_cdev_add:
class_destroy(cls);
err_class_create:
unregister_chrdev_region(dev_num, 0);
err_register_chrdev_region:
return ret;
}
module_init(dev_my_init);
2.设备操作函数接口
//设备操作函数接口
static const struct file_operations my_operations = {
.owner = THIS_MODULE,
.open = dev_my_open,
.read = dev_my_read,
.write = dev_my_write,
};
//打开设备
static int dev_my_open(struct inode *inode, struct file *file)
{
printk("dev_my_open success!\n");
return 0;
}
//读设备
static ssize_t dev_my_read(struct file *file, char __user *ubuf, size_t size, loff_t *ppos)
{
int n;
int ret;
char *kbuf;
printk("read *ppos : %lld\n",*ppos);
if(*ppos == gcd->len)
return 0;
//请求大小 > buffer剩余的字节数 :读取实际记得字节数
if(size > gcd->len - *ppos)
n = gcd->len - *ppos;
else
n = size;
printk("n = %d\n",n);
//从上一次文件位置指针的位置开始读取数据
kbuf = gcd->buffer + *ppos;
//拷贝数据到用户空间
ret = copy_to_user(ubuf,kbuf, n);
if(ret != 0)
return -EFAULT;
//更新文件位置指针的值
*ppos += n;
printk("dev_my_read success!\n");
return n;
}
//写设备
static ssize_t dev_my_write(struct file *file, const char __user *ubuf, size_t size, loff_t *ppos)
{
int n;
int ret;
char *kbuf;
printk("write *ppos : %lld\n",*ppos);
//已经到达buffer尾部了
if(*ppos == sizeof(gcd->buffer))
return -1;
//请求大大小 > buffer剩余的字节数(有多少空间就写多少数据)
if(size > sizeof(gcd->buffer) - *ppos)
n = sizeof(gcd->buffer) - *ppos;
else
n = size;
//从上一次文件位置指针的位置开始写入数据
kbuf = gcd->buffer + *ppos;
//拷贝数据到内核空间
ret = copy_from_user(kbuf, ubuf, n);
if(ret != 0)
return -EFAULT;
//更新文件位置指针的值
*ppos += n;
//更新dev_fifo.len
gcd->len += n;
printk("dev_my_write success!\n");
return n;
}
3.创建驱动程序的退出
static void __exit dev_my_exit()
{
//删除sysfs文件系统中的设备
device_destroy(cls,dev_num );
//删除系统中的设备类
class_destroy(cls);
//从系统中删除添加的字符设备
cdev_del(&gcd->cdev);
//释放申请的设备号
unregister_chrdev_region(dev_num, 0);
printk("dev_my exit!\n");
return;
}
module_exit(dev_my_exit);
4. Makefile文件
obj-m:=demo2.o #demo.c->demo.o
KDIR:=/lib/modules/$(shell uname -r)/build
default:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -rf .*.cmd *.o *.mod.c *.ko *.mod .tem_versions *.order *symvers *Module.markers
5. 驱动某块的编译、安装、卸载
#编译
$ make
#安装
$ sudo insmod demo.ko
#卸载
$ sudo rmmod demo.ko
Demo
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
//#include <asm/uaccess.h>
#include <linux/uaccess.h>
//设备号
static dev_t dev_num ;
//指定的主设备号
#define MAJOR_NUM 100
//自己的字符设备
struct mycdev
{
int len;
unsigned char buffer[128];
struct cdev cdev;
};
//mygcd
struct mycdev *gcd;
//设备类
struct class *cls;
//打开设备
static int dev_my_open(struct inode *inode, struct file *file)
{
printk("dev_fifo_open success!\n");
return 0;
}
//读设备
static ssize_t dev_my_read(struct file *file, char __user *ubuf, size_t size, loff_t *ppos)
{
int n;
int ret;
char *kbuf;
printk("read *ppos : %lld\n",*ppos);
if(*ppos == gcd->len)
return 0;
//请求大大小 > buffer剩余的字节数 :读取实际记得字节数
if(size > gcd->len - *ppos)
n = gcd->len - *ppos;
else
n = size;
printk("n = %d\n",n);
//从上一次文件位置指针的位置开始读取数据
kbuf = gcd->buffer + *ppos;
//拷贝数据到用户空间
ret = copy_to_user(ubuf,kbuf, n);
if(ret != 0)
return -EFAULT;
//更新文件位置指针的值
*ppos += n;
printk("dev_fifo_read success!\n");
return n;
}
//写设备
static ssize_t dev_my_write(struct file *file, const char __user *ubuf, size_t size, loff_t *ppos)
{
int n;
int ret;
char *kbuf;
printk("write *ppos : %lld\n",*ppos);
//已经到达buffer尾部了
if(*ppos == sizeof(gcd->buffer))
return -1;
//请求大大小 > buffer剩余的字节数(有多少空间就写多少数据)
if(size > sizeof(gcd->buffer) - *ppos)
n = sizeof(gcd->buffer) - *ppos;
else
n = size;
//从上一次文件位置指针的位置开始写入数据
kbuf = gcd->buffer + *ppos;
//拷贝数据到内核空间
ret = copy_from_user(kbuf, ubuf, n);
if(ret != 0)
return -EFAULT;
//更新文件位置指针的值
*ppos += n;
//更新dev_fifo.len
gcd->len += n;
printk("dev_fifo_write success!\n");
return n;
}
//设备操作函数接口
static const struct file_operations my_operations = {
.owner = THIS_MODULE,
.open = dev_my_open,
.read = dev_my_read,
.write = dev_my_write,
};
//模块入口
int __init dev_my_init(void)
{
int ret;
struct device *device;
//1.申请注册设备号
dev_num = MKDEV(MAJOR_NUM, 0); //设备号 : 主设备号(12bit) | 次设备号(20bit)
ret = register_chrdev_region(dev_num,10,"dev_fifo"); //静态注册设备号
if(ret <0 )
{
//静态注册失败,进行动态注册设备号
ret = alloc_chrdev_region(&dev_num,0,10,"dev_fifo");
if(ret < 0){
printk("Fail to register_chrdev_region\n");
goto err_register_chrdev_region;
}
}
//2.创建设备类
cls = class_create(THIS_MODULE, "dev_fifo");
if(IS_ERR(cls)){
ret = PTR_ERR(cls);
goto err_class_create;
}
//3.初始化字符设备
gcd = kzalloc(sizeof(struct mycdev), GFP_KERNEL); //动态申请内存
if(!gcd){
return -ENOMEM;
}
cdev_init(&gcd->cdev,&my_operations);//初始化字符设备
//4.添加设备到用户操作系统
ret = cdev_add(&gcd->cdev,dev_num,0);
if (ret < 0)
{
goto err_cdev_add;
}
//5.导出设备信息到用户空间(/sys/class/类名/设备名)
device = device_create(cls,NULL,dev_num,NULL,"dev_my%d",0);
if(IS_ERR(device)){
ret = PTR_ERR(device);
printk("Fail to device_create\n");
goto err_device_create;
}
printk("Register dev_my to system,ok!\n");
return 0;
err_device_create:
cdev_del(&gcd->cdev);
err_cdev_add:
class_destroy(cls);
err_class_create:
unregister_chrdev_region(dev_num, 0);
err_register_chrdev_region:
return ret;
}
void __exit dev_my_exit(void)
{
//删除sysfs文件系统中的设备
device_destroy(cls,dev_num );
//删除系统中的设备类
class_destroy(cls);
//从系统中删除添加的字符设备
cdev_del(&gcd->cdev);
//释放申请的设备号
unregister_chrdev_region(dev_num, 0);
printk("dev_my exit!\n");
return;
}
module_init(dev_my_init);
module_exit(dev_my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author");