使用file_operations结构体进行字符驱动设备的注册/注销:
#include //module_init() & module_exit()
#include //__init() & __exit()
#include //register_chrdev() & unregister_chrdev()
#include
#include
#include // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include //strcmp()
#include //#include
#include
#include //cdev_init()
#include //class_create() & device_create() & device_destroy() & calss_destroy()
//#define MYMAJOR 200
#define MYCNT 1
#define MYNAME "testchar"
//静态映射LED寄存器地址(虚拟地址映射)
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
//动态映射寄存器地址(物理地址映射)
#define GPJ0CON_PA 0XE0200240
#define GPJ0DAT_PA 0XE0200244
unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
//int mymajor;
static dev_t mydev;
//static struct cdev test_cdev;
static struct cdev *pcdev; //alloc_init()函数
static struct class *test_class;
struct device *dev;
char kbuf[100];
static int test_chrdev_open(struct inode *inode, struct file *file)
{
//打开该设备的硬件操作代码
rGPJ0CON = 0x11111111; // led初始化,也就是把GPJ0CON中设置为输出模式
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
printk(KERN_INFO "test_chrdev_open.\n");
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release.\n");
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭灯
return 0;
}
static ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read.\n");
ret = copy_to_user(ubuf, kbuf, count); //内核->用户
if(ret)
{
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success....\n");
return 0;
}
//写函数的本质是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write.\n");
//使用该函数将应用层中传过来的ubuf中的数据拷贝到驱动空间中的kbuf
//memcpy(kbuf, ubuf); //不行,因为两者不在同一个地址空间中
ret = copy_from_user(kbuf, ubuf, count); //用户->内核
if(ret)
{
printk(KERN_ERR "copy_from_user fail...\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success....\n");
//根据驱动中接收的kbuf中的数据进行硬件操控
if( kbuf[0] == '1')
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if(kbuf[0] == '0')
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
//自定义file_operations结构体,并且填充
static const struct file_operations test_fops = {
.owner = THIS_MODULE, //该成员根本不是一个操作,他是一个指向拥有这个结构的模块的指针,该成员用来在它的操作还在被使用时阻止模块被卸载,几乎在所有的时间中,它被简单初始化为THIS_MODULE
.open = test_chrdev_open, // 打开相关设备的函数指针
.release = test_chrdev_release, // 关闭相关设备的函数指针
.read = test_chrdev_read,
.write = test_chrdev_write,
};
//模块安装函数
static int __init chrdev_init(void)
{
int retval;
printk(KERN_INFO "chrdev_init.\n");
//使用新的接口cdev来注册设备驱动,分两步:
//第一步:注册/分配主次设备号
retval = alloc_chrdev_region(&mydev, 0, MYCNT, MYNAME); //返回所分配的设备号
if (retval < 0)
{
printk(KERN_ERR "Unable to alloc_chrdev_region minors for %s\n", MYNAME);
goto flag1;
}
printk(KERN_INFO "alloc_chrdev_region success...\n");
printk(KERN_INFO "major = %d, minor = %d\n", MAJOR(mydev), MINOR(mydev));
//第二步:注册字符设备驱动
//cdev_init(pcdev, &test_fops);
pcdev = cdev_alloc(); //给pcdev分配内存,使指针实例化
pcdev->owner = THIS_MODULE;
pcdev->ops = &test_fops;
retval = cdev_add(pcdev, mydev, MYCNT); //添加一个字符设备驱动
if (retval)
{
printk(KERN_ERR "Unable to get cdev_add \n");
goto flag2;
}
printk(KERN_INFO "cdev_add success...\n");
//注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
//给udev,让udev自动创建和删除设备文件
test_class = class_create(THIS_MODULE, "amber_class"); //为设备创建一个class
if (IS_ERR(test_class))
return -EINVAL;
//最后一个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
//所以我们这里要的文件名是/dev/test
// device_create(test_class, NULL, mydev, NULL, "test");
dev = device_create(test_class, NULL, mydev, NULL, "test111"); //创建class对应的设备
if (IS_ERR(dev))
return -EINVAL;
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON")) //request_mem_region向内核申请需要映射的内存资源
goto flag3;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
goto flag3;
pGPJ0CON = ioremap(GPJ0CON_PA, 4); //flag4
pGPJ0DAT= ioremap(GPJ0DAT_PA, 4); //真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0; // 缺少这句会报错:insmod: can't insert 'module_test.ko': File exists
//flag4:
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
flag3:
cdev_del(pcdev);
flag2:
unregister_chrdev_region(mydev, MYCNT);
flag1:
return -EINVAL;
//return 0;
}
//模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit.\n");
*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// 解除映射
iounmap(pGPJ0CON); //解除物理地址与虚拟地址的映射
iounmap(pGPJ0DAT);
release_mem_region(GPJ0CON_PA, 4); //归还向内核申请的内存资源
release_mem_region(GPJ0DAT_PA, 4);
/*
unregister_chrdev(mymajor, MYNAME);
*/
device_destroy(test_class, mydev); //设备销毁
class_destroy(test_class); //class销毁
//使用新的接口注销字符设备驱动,分两步:
//第一步:真正注销字符设备驱动
cdev_del(pcdev);
//第二步:注销申请到的主次设备号
unregister_chrdev_region(mydev, MYCNT);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("amber"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
相关问题分析: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;
}
//通过cdev_alloc的代码可以看出,它主要完成了空间的申请和简单的初始化操作;
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
//通过cdev_init的代码可以看出,主要是对空间起到一个清零作用并较之cdev_alloc多了一个ops的赋值操作。
【注意】:kzalloc后的空间是不需要再执行memset的,因为它本身就包含了这个操作。而memset一般作用在已经存在的空间上。
由此分析:
cdev_alloc函数针对于需要空间申请的操作,而cdev_init针对于不需要空间申请的操作;因此如果你定义的是一个指针,那么只需要使用cdev_alloc函数并在其后做一个ops的赋值操作就可以了;如果你定义的是一个结构体而非指针,那么只需要使用cdev_init函数就可以了。
看到有些代码在定义一个指针后使用了cdev_alloc函数,紧接着又使用了cdev_init函数,这个过程不会出现错误,但只是做了一些重复的无用工作,其实完全可以不需要的。2、注册/注销驱动必须使用倒影式
内核中很多函数中包含了很多个操作,这些操作每一步都有可能出错,而且出错后后面的步骤就没有进行下去的必要性了。
解决方案:倒影式机制,(先注册,后注销)
3、手动创建 ——>自动创建 字符设备驱动
手动创建:老接口分析
register_chrdev
__register_chrdev
__register_chrdev_region
cdev_alloc
cdev_add
手动创建过程:
装载:先insmod装载设备,再使用mknod创建设备文件【mknod /dev/test c 250 0】
卸载:先rmmod卸载设备,再删除mknod创建的设备文件
自动创建:新接口分析
register_chrdev_region
__register_chrdev_region
alloc_chrdev_region
__register_chrdev_region
自动创建过程:
装载:直接insmod,装载设备的同时创建文件
卸载:直接rmmod,卸载设备的同时删除创建的文件
udev(嵌入式中用的是mdev)
(1)什么是udev?应用层的一个应用程序
(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
(3)应用层启用udev,内核驱动中使用相应接口
(4)驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除
内核驱动设备类相关函数
(1)class_create//创建类
(2)device_create//创建设备
(2)device_destroy//销毁设备
(1)calss_destroy//销毁类
4、相关函数 & 结构体分析
注册字符设备驱动新接口:
0——驱动向内核注册自己的函数register_chrdev()
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
unsigned int major,主设备号
const char *name,设备名称
const struct file_operations *fops:
向函数内部传参(将自己写的file_operations传给register_chrdev()函数,在该函数内部完成自己编写的file_operations结构体的注册),因为file_operations是输入型参数,故前面要加const修饰
*******************************************************
常见错误定义文件目录:linux/asm-generic/errno-base.h
头文件包含:#include
cat /pro/devices 查看当前设备
**********************************************************
1——新接口与老接口
(1)老接口:register_chrdev:绑定file_operations结构体和主次设备号(可自动分配也可自己设定)
(2)新接口:register_chrdev_region/alloc_chrdev_region + cdev
register_chrdev_region:指定设备号,让内核直接分配
alloc_chrdev_region + cdev:自动分配设备号
cdev:字符驱动设备的注册
2——cdev结构体介绍:
包含于 kernel/linux/cdev.h文件中
struct cdev {
struct kobject kobj;
struct module *owner;//用来将其与我们的模块挂钩
const struct file_operations *ops;//file_operations 结构体变量
struct list_head list;
dev_t dev; //设备号 = 主设备号 + 次设备号 dev_t:代表设备号
unsigned int count;//计数,(记录read或write的次数)
};
3——相关函数:cdev_alloc、cdev_init、cdev_add、cdev_del
cdev_alloc :给cdev结构体申请内存空间
cdev_init :初始化
cdev_add :添加一个设备驱动
cdev_del : 注销一个设备驱动
4——设备号
(1)主设备号和次设备号
(2)dev_t类型
(3)MKDEV、MAJOR、MINOR三个宏
MKDEV :将主设备号和次设备号构成设备号
MAJOR :提取主设备号
MINOR :提取次设备号
5——编程实践
(1)使用register_chrdev_region + cdev_init + cdev_add进行字符设备驱动注册,分两步:
//第一步:分配主次设备号
mydev = MKDEV(MYMAJOR, 0); //获取设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)//绑定file_operations结构体和主次设备号
{
return 0;
}
dev_t from :起始设备号 = 主设备号 + 次设备号
unsigned count:次设备号数量
const char *name:设备名称
//第二步:注册字符设备驱动
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
struct cdev *cdev//字符驱动设备,(函数指针)
const struct file_operations *fops//file_operations结构体(函数指针)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
struct cdev *p//字符驱动设备,(函数指针)
dev_t dev //获取的设备号
6——注销设备驱动
// 第一步真正注销字符设备驱动用cdev_del
void cdev_del(struct cdev *p); //struct cdev *p字符驱动设备,(函数指针)
// 第二步去注销申请的主次设备号
unregister_chrdev_region(USB_DEVICE_DEV, USB_DEVICE_MAX);
USB_DEVICE_DEV//获取的设备号
USB_DEVICE_MAX//次设备号数量
7——//自动分配设备号alloc_chrdev_region + cdev
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev_t *dev //输出型参数——也就是系统自动分配的主设备号
unsigned baseminor //次设备号的基准
unsigned count //次设备号数量
const char *name //