字符设备驱动程序分析
字符设备驱动程序分析
下面是针对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也可以使对应函数在运行完成后自动回收内存;