最全Linux驱动开发八股文(六)
你好,我是拉依达。
这是我的Linux驱动开发八股文详细解析系列。
本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。
全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。
现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。
【下面是拉依达推荐学习相关专栏:】
一、Linux驱动学习专栏:拉依达的Linux驱动八股文 - 牛客网
二、Linux应用学习专栏:拉依达的Linux应用八股文 - 牛客网
【我的嵌入式学习和校招经验】 拉依达的嵌入式学习和秋招经验-CSDN博客
嵌入式学习规划/就业经验指导,可私信咨询
———————————————————————————————————————————————————
6.2 新字符设备基本驱动框架
上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。
在 Linux 下通过 udev(用户空间程序) 来实现设备文件的创建与删除,但是在嵌入式 Linux 中使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。
1.设备文件系统
设备文件系统有devfs,mdev,udev这三种
- devfs, 一个基于内核的动态设备文件系统
- devfs缺点(过时原因)
- 不确定的设备映射
- 没有足够的主/辅设备号
- /dev目录下文件太多
- 内核内存使用
- udev,采用用户空间(user-space)工具来管理/dev/目录树,udev和文件系统分开
- udev和devfs的区别
- 采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动
- udev的Linux应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候
- 系统中所有的设备都应该产生热拔插事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。
- 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驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。