驱动注册的两种方式(一)——file_operations结构体

使用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");			// 描述模块的别名信息

相关问题分析:
1、
cdev_alloc() & cdev_init()

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 //


你可能感兴趣的:(Linux,驱动开发)