最全Linux驱动开发八股文(六)

你好,我是拉依达。

这是我的Linux驱动开发八股文详细解析系列

本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。

全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料

现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。

6.2 新字符设备基本驱动框架

上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。

在 Linux 下通过 udev(用户空间程序)实现设备文件的创建与删除,但是在嵌入式 Linux 中使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。

1.设备文件系统

设备文件系统有devfs,mdev,udev这三种

  1. devfs, 一个基于内核的动态设备文件系统
  • devfs缺点(过时原因)
    • 不确定的设备映射
    • 没有足够的主/辅设备号
    • /dev目录下文件太多
    • 内核内存使用
  1. udev,采用用户空间(user-space)工具来管理/dev/目录树,udev和文件系统分开
  • udev和devfs的区别
    • 采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动
    • udev的Linux应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候
    • 系统中所有的设备都应该产生热拔插事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。
  1. mdev,是udev的简化版本,是busybox中所带的程序,适合用在嵌入式系统

2.申请设备号

上述设备号为开发者挑选一个未使用的进行注册。Linux驱动开发推荐使用动态分配设备号

  • 动态申请设备号

    int alloc_chrdev_region(dev_t *dev, 
    						unsigned baseminor, 
    						unsigned count, 
    						const char *name)
    

    dev:保存申请到的设备号。 baseminor: 次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0 count: 要申请的设备号数量。 name:设备名字。

  • 静态申请设备号

    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    

    from - 要申请的起始设备号
    count - 设备号个数
    name - 设备号在内核中的名称 返回0申请成功,否则失败

  • 释放设备号

    void unregister_chrdev_region(dev_t from, unsigned count)
    

    from:要释放的设备号。 count: 表示从 from 开始,要释放的设备号数量。

  • 申请设备号模板

    //创建设备号 
    if (newchrled.major)   //定义了设备号就静态申请
    {		
    	newchrled.devid = MKDEV(newchrled.major, 0);
    	register_chrdev_region(newchrled.devid, 
    							NEWCHRLED_CNT, 
    							NEWCHRLED_NAME);
    } 
    else   //没有定义设备号就动态申请
    {		
     
    	alloc_chrdev_region(&newchrled.devid, 
    						0, 
    						NEWCHRLED_CNT, 
    						NEWCHRLED_NAME);//申请设备号 
    	newchrled.major = MAJOR(newchrled.devid);	//获取分配号的主设备号
    	newchrled.minor = MINOR(newchrled.devid);	// 获取分配号的次设备号
    }
    

3.注册字符设备

在 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;
};

在 cdev 中有两个重要的成员变量:ops 和 dev,字符设备文件操作函数集合file_operations 以及设备号 dev_t

  • 初始化cdev结构体变量

    void cdev_init(struct cdev *cdev, 
    				const struct file_operations *fops);
    

    struct cdev testcdev;
    //设备操作函数
    static struct file_operations test_fops = {
        .owner = THIS_MODULE,
        //其他具体的初始项 
    };
    testcdev.owner = THIS_MODULE;
    //初始化 cdev 结构体变量
    cdev_init(&testcdev, &test_fops); 
    
  • 将设备添加到内核

    cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备

    将cdev添加到内核同时绑定设备号。 其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev函数完成者两步操作

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    

    p - 要添加的cdev结构
    dev - 绑定的起始设备号
    count - 设备号个数

    cdev_add(&testcdev, devid, 1); //添加字符设备
    
  • 将设备从内核注销 卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备

    void cdev_del(struct cdev *p);
    

    p - 要添加的cdev结构

    cdev_del(&testcdev); //删除 cdev
    

4.自动创建设备节点

上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。

在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。

  • 创建一个class类

    struct class *class_create(struct module *owner, const char *name);
    
    • class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
    • 设备类名对应 /sys/class 目录的子目录名。
    • 返回值是个指向结构体 class 的指针,也就是创建的类。
  • 删除一个class类

    void class_destroy(struct class *cls); // cls要删除的类
    
  • 创建设备 还需要在类下创建一个设备,使用 device_create 函数在类下面创建设备。 成功会在 /dev 目录下生成设备文件。

    struct device *device_create(struct class *class,
        						struct device *parent,
        						dev_t devt,
        						void *drvdata,
    							const char *fmt, ...)
    

    *class——设备类指针, *parent——父设备指针, devt——设备号, *drvdata——额外数据, *fmt——设备文件名

  • 删除设备 卸载驱动的时候需要删除掉创建的设备

    void device_destroy(struct class *class, dev_t devt);
    

    class——设备所处的类 devt——设备号

5.文件私有数据

  • 每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device),
  • 一个设备的所有属性信息将其做成一个结构体,
  • 编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中。
  • 在 write、 read、 close 函数中直接读取 private_data即可得到设备结构体
/* newchrled设备结构体 */
struct newchrled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};
struct newchrled_dev newchrled;	/* led设备 */
 
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}
 
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    struct newchrled_dev *dev = (struct newchrled_dev *)filp->private_data;
	return 0;
}

6.新字符设备驱动程序框架

#define NEWCHR_CNT 1
#define NEWCHR_NAME "NEWCHR"
//内核缓存区
static char readbuf[100];						//读数据缓存
static char writebuf[100];						//写数据缓存
static char kerneldata[] = {"kernel data!"};	//测试数据
//硬件寄存器
#define GPIO_TEST_BASE (0x01234567) 	//宏定义寄存器映射地址
static void __iomem *GPIO_TEST;			// __iomem 类型的指针,指向映射后的虚拟空间首地址

/* newchr设备结构体 */
struct newchr_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};
 
struct newchrled_dev newchr;	/* newchr设备 */

//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp) 
{
	filp->private_data = &newchr; /* 设置私有数据 */
	return 0;
}
// 从设备读取数据 
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt) 
{
	int retvalue = 0;
	unsigned char databuf[1];
	//读取私有数据
	struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
// 读取硬件寄存器
#if 0  
	//读取寄存器状态
	databuf[0] = readl(GPIO_TEST);
	retvalue = copy_to_user(buf , databuf, cnt);
//读取内核内存
#else	
	//测试数据拷贝到读数据缓存中
    memcpy(readbuf , kerneldata , sizeof(kerneldata));  
    //内核中数据(读缓存)拷贝到用户空间
    retvalue = copy_to_user(buf , readbuf , cnt);
#endif

    if(retvalue == 0) printk("kernel senddate ok!\n");   
  	else printk("kernel senddate failed!\n");
    return 0;
}
//向设备写数据 
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt) 
{
	int retvalue = 0;
	//读取私有数据
	struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
//写硬件寄存器
#if 0
	writel(buf[0],GPIO_TEST);
//写内核缓存
#else
	//用户数据拷贝到内核空间(写缓存)
    retvalue = copy_from_user(writebuf , buf ,cnt);
#endif
    if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);
  	else printk("kernel recevdate failed!");
    return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp) 
{
	return 0;
}
//设备操作函数
static struct file_operations chrdevbase_fops = {
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
	int retvalue = 0;
	//寄存器物理映射,物理地址映射到虚拟地址指针
	GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);
	
	//申请设备号
    if(newchr.major)		//静态申请
    {
        newchr.devid = MKDEV(newchr.major , 0);
        register_chrdev_region(newchr.devid, NEWCHR_CNT,NEWCHR_NAME);
    }else					//动态申请
    {
        alloc_chrdev_region(&newchr.devid , 0 , NEWCHR_CNT , NEWCHR_NAME);
        newchr.major = MAJOR(newchr.devid);
        newchr.minor = MINOR(newchr.devid);
    }   
    printk("newche major=%d,minor=%d\r\n",newchr.major , newchr.minor);

	//字符串设备初始化、注册添加到内核
	newchr.cdev.owner = THIS_MODULE;
    cdev_init(&newchr.cdev , &newchr_fops);
    cdev_add(&newchr.cdev , newchr.devid ,NEWCHR_LED_CNT);
	//创建设备类
    newchr.class = class_create(THIS_MODULE , NEWCHR_NAME);
    if(IS_ERR(newchr.class))
    {
        return PTR_ERR(newchr.class);
    }
	//创建类的实例化设备 ,dev下面创建文件
    newchr.device = device_create(newchr.class , NULL , newchr.devid ,NULL ,NEWCHR_NAME);
    if(IS_ERR(newchr.device))
    {
        return PTR_ERR(newchr.device);
    }
    return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
	//解除寄存器映射
	iounmap(GPIO_TEST);
	//删除cdev字符串设备
	cdev_del(&newchr.cdev);
	//释放设备号
    unregister_chrdev_region(newchr.devid , NEWCHR_CNT);
	//具体设备注销
    device_destroy(newchr.class, newchr.devid);
    //类注销
    class_destroy(newchr.class);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_LICENSE("GPI");//GPL模块许可证
MODULE_AUTHOR("songwei");//作者信息
#嵌入式##秋招##八股文##Linux##linux驱动#
最全Linux驱动开发八股文 文章被收录于专栏

你好,我是拉依达。 这是我的Linux驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务