字符设备驱动程序分析

字符设备驱动程序分析

下面是针对jz2440开发板写的一个led驱动程序,重点不在于该程序,而是以此为例,对字符设备驱动程序框架的分析总结;

/*
 * jz2440 leds driver
**/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static int major;
static struct class *leds_drv_class;
static struct class_device *leds_class_device[4];

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;

#define DEVICE_NAME  "leds"  /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
/*
 * 在应用程序中使用open(),打开/dev/led*某个led设备文件时会调用该函数,
 * 具体的处理就是根据子设备号的不同用来区分不同的led设备,进而分别处理;
**/
static int jz2440_led_drv_open (struct inode *inode, struct file *file)
{
	int minor;
	
	/* 得到打开设备的子设备号,也可以使用minor = MINOR(inode->i_cdev) */
	minor= MINOR(inode->i_rdev); 

	switch(minor){
	case 0: /* 控制所有led */
		/* 设置GPF4(D10)、GPF5(D11)、GPF6(D12) 为output mode*/
		*gpfcon &= ~((3<<8) | (3<<10) | (3<<12));
		*gpfcon |=  ((1<<8) | (1<<10) | (1<<12));
		break;
		
	case 1: /* 控制/dev/led1(D10) */
		/* 设置GPF4为output mode*/
		*gpfcon &= ~(3<<8);
		*gpfcon |=  (1<<8);
		break;
		
	case 2: /* 控制/dev/led2(D11) */
		/* 设置GPF5为output mode*/
		*gpfcon &= ~(3<<10);
		*gpfcon |=  (1<<10);
		break;
		
	case 3: /* 控制/dev/led3(D12) */
		/* 设置GPF6为output mode*/
		*gpfcon &= ~(3<<12);
		*gpfcon |=  (1<<12);
		break;
	}

	return 0;
}

/*
 * 在应用程序中使用write(),对不同的led设备进行写操作时会调用该函数,
 * 具体的处理就是根据子设备号的不同用来区分不同的led设备,进而分别处理;
**/
static ssize_t jz2440_led_drv_write (struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	int minor;
	char val;

	minor = MINOR(file->f_dentry->d_inode->i_rdev); /* 得到子设备号 */
	copy_from_user(&val, buf, count); /* 从用户空间获取数据 */

	switch(minor){
	case 0:
		if(val == 1){
			*gpfdat &= ~((1<<6) | (1<<5) | (1<<4));
		}else{
			*gpfdat |=  ((1<<6) | (1<<5) | (1<<4));
		}
		break;
		
	case 1:
		if(val == 1){
			*gpfdat &= ~(1<<6);
		}else{
			*gpfdat |=  (1<<6);
		}
		break;
		
	case 2:
		if(val == 1){
			*gpfdat &= ~(1<<5);
		}else{
			*gpfdat |=  (1<<5);
		}
		break;
		
	case 3:
		if(val == 1){
			*gpfdat &= ~(1<<4);
		}else{
			*gpfdat |=  (1<<4);
		}
		break;
	}

	return 0;
}

static struct file_operations jz2440_leds_drv_fops = {
	.owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
	.open   =   jz2440_led_drv_open,     
	.write	=	jz2440_led_drv_write,
};

/*
 * 扩展说明:
 * 当应用程序使用open("/dev/leds")打开一个设备文件时,其中/dev/leds设备文件如何而来?
 * 有两种方式:
 *     1. 手动创建
 *        mknod /dev/leds c 主设备号 次设备号
 *     2. 自动创建
 *        mdev机制,会根据/sys目录下的设备信息,创建对应的设备节点
 * 所以重点就是驱动程序中如何来提供这些设备信息,具体的操作如下:
 * 1. 使用class_create创建一个类;
 * 2. 使用class_device_create来创建对应类的设备,里面包含了设备名、主次设备号等信息;
 * 总结:(结合以下程序)当insmod模块后,就会在/sys/class目录下新建一个名为ledsdrv的类,在类的下面会创
 *      建出led1、led2、led3、leds等四个设备信息,而mdev则会根据这些信息自动创建/dev/led1
 *      、/dev/led2、/dev/led3、/dev/leds等四个设备节点,而在rmmod模块后会自动这些设备信息和节点;
 * 问题来了,mdev为什么能够动态识别这些设备信息并创建设备节点?
 * 因为在构建根文件系统时,/etc/init.d/rcS这个脚本中,设置了如下:
 * echo /sbin/mdev > /proc/sys/kernel/hotplug
 * 意思就是当内核检测到有设备热插拔时,会调用/sbin/mdev程序;
 */
static int __init s3c2440_leds_init(void)
{
	int ret;
	int minor;
	
	/* 向内核注册file_operations结构变量 */
	ret = register_chrdev(0, DEVICE_NAME, &jz2440_leds_drv_fops);
	if(ret < 0){
		printk(DEVICE_NAME "can't register\n");
		return ret;
	}
	major = ret;
	
	leds_drv_class = class_create(THIS_MODULE, "ledsdrv");
	if (IS_ERR(leds_drv_class))
		return PTR_ERR(leds_drv_class);
	
	leds_class_device[0] = class_device_create(leds_drv_class, NULL, MKDEV(major, 0), NULL, "leds");
    
	for(minor=1; minor<4; minor++){
		leds_class_device[minor] = class_device_create(leds_drv_class, NULL, MKDEV(major, minor), NULL, "led%d", minor);
		if (unlikely(IS_ERR(leds_class_device[minor])))
			return PTR_ERR(leds_class_device[minor]);
	}
	
	/* 地址映射 */
	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = (volatile unsigned long *)ioremap(0x56000054, 8);
	
	printk(DEVICE_NAME " initialization\n");
	
	return 0;
}

static void __exit s3c2440_leds_exit(void)
{
	int minor;
	unregister_chrdev(major, DEVICE_NAME);
	
	for(minor=0; minor<4; minor++){
		class_device_unregister(leds_class_device[minor]);
	}
	
	class_destroy(leds_drv_class);

	iounmap(gpfcon);
	iounmap(gpfdat);

	printk(DEVICE_NAME " exit\n");
}

module_init(s3c2440_leds_init);
module_exit(s3c2440_leds_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("jason.tian");
MODULE_VERSION("0.0.1");
MODULE_DESCRIPTION("S3C2440 LED Driver");

在上面的s3c2440_leds_init函数加上了__init修饰,具体作用说明如下:

在Llinux中,所有标识为__init(两个下划线)的函数在连接的时候都放在.init.text这个区段内,此外,所有的__init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些__init函数,并在初始化完成后,释放init区段(包括.init.text、.initcall.init等);

__init一样,__exit也可以使对应函数在运行完成后自动回收内存;

posted @ 2019-08-27 16:30  SKILL-RABBIT  阅读(234)  评论(0编辑  收藏  举报