linux字符设备驱动开发

前言

从驱动框架图可以看出,在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");

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ai_Sj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值