目录
说明:此博客为我自己对正点原子提供的资料学习的记录,内容大部分来源于正点原子资料,大家也可以去下载正点原子的官方资料学习,内容丰富。侵权删。
1.新字符设备驱动原理
上一篇文章说了字符设备驱的开发步骤,但是在安装驱动时,还得自己mknod创建设备节点。但是在新字符设备驱动中,提供了新字符设备驱动API函数。
1.1分配和释放设备号
使用register_chrdev函数注册字符设备的时候只需要顶一个主设备号即可,但是这样会带来两个问题:
- 需要我们事先确定好哪些主设备号没有被使用。
- 会将一个主设备号下的所有次设备号都用掉,比如现在设置 LED 这个主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
解决这两个问题最好的方法就是要使用设备号的时候向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。
//如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
//参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一般都是一个;参数 name 是设备名字。
//注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过 alloc_chrdev_region 函 数 还 是register_chrdev_region 函数申请的设备号,统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
为了兼容静态与动态分配设备号,可以这样做:
int major; /* 主设备号 */
int minor; /* 次设备号 */
dev_t devid; /* 设备号 */
if (major) { /* 定义了主设备号 */
devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0 */
register_chrdev_region(devid, 1, "test");
} else { /* 没有定义设备号 */
alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
major = MAJOR(devid); /* 获取分配号的主设备号 */
minor = MINOR(devid); /* 获取分配号的次设备号 */
}
1.2新的字符设备注册方法
1.2.1字符设备结构
在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体在 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;
};
在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。
1.2.2 cdev_init函数
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_init 函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。使用 cdev_init 函数初始化 cdev 变量的示例代码如下:
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
1.2.3cdev_add函数
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。完善示例代码 42.1.2.2,加入 cdev_add 函数,内容如下所示:
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
cdev_add(&testcdev, devid, 1); /* 添加字符设备 */
如果在加上示例代码1.1 中分配设备号的程序,那么就它们一起实现的就是函数 register_chrdev 的功能。
1.2.4cdev_del函数
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del函数原型如下:
void cdev_del(struct cdev *p)
2.自动创建设备节点
2.1mdev机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用insmod命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。使用 busybox 构建根文件系统的时候,busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理。
2.2创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。首先要创建一个 class 类,class 是个结构体,定义在文件include/linux/device.h 里面。class_create 是类创建函数,class_create 是个宏定义。
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
2.3创建设备
上一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备,device_create 函数原型如下:
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:
void device_destroy(struct class *class, dev_t devt)
参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。
3.设置文件私有数据
每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,如下所示:
struct test_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct test_dev testdev;
/* open 函数 */
static int test_open(struct inode *inode, struct file *filp)
{
filp->private_data = &testdev; /* 设置私有数据 */
return 0;
}
在 open 函数里面设置好私有数据以后,在 write、read、close 等函数中直接读取 private_data即可得到设备结构体。
4.实例代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/device.h>
/*test file*/
#define CHRDEVBASE_NAME "chrdevbase" /*Secondary equipment num*/
static char readbuf[100]; /*read buffer*/
static char writebuf[100]; /*write buffer*/
static char kerneldata[] = {"kernel data!"};
struct test_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct test_dev testdev;
static int chrdevbase_open(struct inode *inode,struct file *filp)
{
filp->private_data = &testdev; /* 设置私有数据 */
return 0;
}
static ssize_t chrdevbase_read (struct file *filp,char __user *buf,
size_t cnt,loff_t *offt)
{
int rv=0;
memcpy(readbuf,kerneldata,sizeof(kerneldata));
rv = copy_to_user(buf,readbuf,cnt);
if(0==rv)
{
printk("kernel senddata ok !\r\n");
}
else
{
printk("kernel senddata failure!\r\n");
}
return 0;
}
static ssize_t chrdevbase_write(struct file *filp,
const char __user *buf,size_t cnt,loff_t *offt)
{
int rv=0;
rv = copy_from_user(writebuf,buf,cnt);
if(0==rv)
{
printk("kernel recevdata:%s\r\n",writebuf);
}
else
{
printk("kernel recevdata failure\r\n");
}
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 rv=0;
if (testdev.major)
{ /* 定义了主设备号 */
testdev.devid = MKDEV(testdev.major, 0); /* 大部分驱动次设备号都选择 0 */
rv=register_chrdev_region(testdev.devid, 1, CHRDEVBASE_NAME);
if(rv<0)
{
printk("chrdevbase driver register failure\r\n");
goto Cleanup;
}
printk("chrdevbase_init()\r\n");
} else
{ /* 没有定义设备号 */
rv=alloc_chrdev_region(&testdev.devid, 0, 1, CHRDEVBASE_NAME); /* 申请设备号 */
if(rv<0)
{
printk("chrdevbase driver register failure\r\n");
goto Cleanup;
}
printk("chrdevbase_init()\r\n");
testdev.major = MAJOR(testdev.devid); /* 获取分配号的主设备号 */
testdev.minor = MINOR(testdev.devid); /* 获取分配号的次设备号 */
}
printk("gpioled major = %d,minor = %d\r\n",testdev.major,testdev.minor);
testdev.cdev.owner=THIS_MODULE;
cdev_init(&testdev.cdev,&chrdevbase_fops);
rv=cdev_add(&testdev.cdev,testdev.devid,1);
if(rv<0)
{
goto del_unregister;
}
testdev.class = class_create(THIS_MODULE,CHRDEVBASE_NAME);
if(IS_ERR(testdev.class))
{
goto del_cdev;
}
testdev.device = device_create(testdev.class,NULL,testdev.devid,NULL,CHRDEVBASE_NAME);
if(IS_ERR(testdev.device))
{
goto destroy_class;
}
printk("chrdevbase insmod successfully!\r\n");
return 0;
destroy_class:
class_destroy(testdev.class);
del_cdev:
cdev_del(&testdev.cdev);
del_unregister:
unregister_chrdev_region(testdev.devid,1);
Cleanup:
return -EIO;
}
static void __exit chrdevbase_exit(void)
{
cdev_del(&testdev.cdev);
unregister_chrdev_region(testdev.devid,1);
device_destroy(testdev.class,testdev.devid);
class_destroy(testdev.class);
printk("chrdevbase exit()\r\n");
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("dairongan");
Makefile和测试程序和上一篇内容一致,可以去查看字符设备开发步骤
测试结果:
与上一节的设备开发流程多了cdev结构体的创建以及class、device等的创建,实现自动添加节点,如上图所示,insmod加载驱动之后,不用mknod节点了。