欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

程序员文章站 2022-06-08 19:48:06
...

参考:https://blog.csdn.net/fengyuwuzu0519/article/details/71046343

 

字符设备驱动程序之中断方式的按键驱动_编写代码

 

使用中断方式,那么肯定有一个中断的初始化注册,就是告诉内核,我按下按键的时候会触发一个中断,同时一定有一个中断处理函数来处理中断发生时应该做什么。

linux内核中 如何告诉内核我按下按键了给我触发中断并实现中断处理函数呢。

 

注册中断(open驱动程序时调用):int request_irq(unsigned int irq, irq_handler_t handler,unsigned long irqflags, const char *devname, void *dev_id)

request_irq()函数参数解析:

※※※重要!!void *:void即“无类型”,void *则为“无类型指针”,可以指向任何数据类型。所以后面的代码传进来结构体。

1、从原理图可知IRQ中断号irq:(IRQ_EINT0……)

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

 

2、向系统注册的中断处理函数,中断发生时,系统调用这个函数,dev_id参数被传递给它,中断处理函数handler的格式:

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

3、触发方式:irqflags:type(IRQT_BOTHEDGE双边沿触发:上升沿和下降沿都可以触发中断)。

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

4、devname:中断名称,可以使用cat /proc/interrupts 查看此名称

5、dev_id:用法很简单。在free_irq卸载时,通过irq与dev_id结合在一起,来确定卸载哪一个irqaction结构。

 

 

释放中断(卸载驱动程序时,解除按键中断)

void free_irq(unsigned int irq, void *dev_id)

参数:irq中断号。dev_id用法很简单,在free_irq卸载时,通过irq与dev_id结合在一起,来确定卸载哪一个irqaction结构。

 

驱动程序:third_drv.c

/*
	一、驱动框架:

	1.先定义file_operations结构体,其中有对设备的打开,读和写的操作函数。
	2.分别定义相关的操作函数
	3.定义好对设备的操作函数的结构体(file_operations)后,将其注册到内核的file_operations结构数组中。
	  此设置的主设备号为此结构在数组中的下标。
	4.定义出口函数:卸载注册到内核中的设备相关资源
	5.修饰 入口 和 出口函数
	6.给系统提供更多的内核消息,在sys目录下提供设备的相关信息。应用程序udev可以据此自动创建设备节点,
	  创建一个class设备类,在此类下创建设备
*/

#include <linux/module.h>	//内涵头文件,含有一些内核常用函数的原形定义。
#include <linux/kernel.h>	//最基本的文件,支持动态添加和卸载模块。Hello World驱动要这一个文件就可以。
#include <linux/fs.h>		//包含了文件操作相关的struct的定义,例如struct file_operations
#include <linux/init.h>		
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h>	//包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义
#include <asm/irq.h>
#include <asm/io.h>			//包含了ioremap、ioread等内核访问IO内存等函数的定义
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>


static struct class *thirddrv_class;	//一个类
static struct class_device	*thirddrv_class_dev;	//一个类里面再建立一个设备

volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;

volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;


static irqreturn_t button_irq(int irq, void *dev_id)
{
	printk("irq = %d\n",irq);
	return IRQ_HANDLED;
}


static int third_drv_open(struct inode *inode, struct file *file)
{
	/* 配置GPF0,2为输入引脚 */
	/* 配置GPF3,11为输入引脚 */
	request_irq(IRQ_EINT0,  button_irq, IRQT_BOTHEDGE, "S2", 1);	//配置为中断引脚
	request_irq(IRQ_EINT2,  button_irq, IRQT_BOTHEDGE, "S3", 1);
	request_irq(IRQ_EINT11, button_irq, IRQT_BOTHEDGE, "S4", 1);
	request_irq(IRQ_EINT19, button_irq, IRQT_BOTHEDGE, "S5", 1);
	
	return 0;
}

ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	/* 返回4个引脚的电平 */
	unsigned char key_vals[4];
	int regval;

	//如果传进来的size不等于我们返回的4个字节,返回一个错误值
	if	(size != sizeof(key_vals))
		return -EINVAL;
	
	/* 读GPF0,2为输入引脚 */
	regval = *gpfdat;
	key_vals[0]=(regval & (1<<0)) ? 1 : 0;
	key_vals[1]=(regval & (1<<2)) ? 1 : 0;

	/* 读GPG3,11为输入引脚 */
	regval = *gpgdat;
	key_vals[2]=(regval & (1<<3)) ? 1 : 0;
	key_vals[3]=(regval & (1<<11)) ? 1 : 0;

	copy_to_user(buf, key_vals, sizeof(key_vals));
	
	return sizeof(key_vals);
}

int third_drv_close(struct inode *inode, struct file *file)
{
	free_irq(IRQ_EINT0,  1);
	free_irq(IRQ_EINT2,  1);
	free_irq(IRQ_EINT11, 1);
	free_irq(IRQ_EINT19, 1);
	return 0;
}

static struct file_operations third_drv_fops = {
    .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open    =  third_drv_open,     
	.read	 =	third_drv_read,
	.release = 	third_drv_close,
};

int major;

static int third_drv_init(void)
{
	major = register_chrdev(0, "third_drv", &third_drv_fops);

	//创建一个类
	thirddrv_class = class_create(THIS_MODULE, "firstdrv");

	//在这个类下面再创建一个设备
	//mdev是udev的一个简化版本
	//mdev应用程序,就会被内核调用,会根据类和类下面的设备这些信息
	thirddrv_class_dev = class_device_create(thirddrv_class, NULL, MKDEV(major, 0), NULL, "buttons");/* /dev/buttons */

	//建立地址映射:物理地址->虚拟地址
	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);	//指向的是虚拟地址,第一个参数是物理开始地址,第二个是长度(字节)
	gpfdat = gpfcon + 1; //加1,实际加4个字节

	gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);	//指向的是虚拟地址,第一个参数是物理开始地址,第二个是长度(字节)
	gpgdat = gpgcon + 1; //加1,实际加4个字节

	return 0;
}

static void third_drv_exit(void)
{
	unregister_chrdev(major, "third_drv");
	
	class_device_unregister(thirddrv_class_dev);
	class_destroy(thirddrv_class);

	iounmap(gpfcon);
	iounmap(gpgcon);
	
	return 0;
}


module_init(third_drv_init);
module_exit(third_drv_exit);

MODULE_LICENSE("GPL");

验证驱动中断:(下面步骤,不写应用程序来验证驱动程序

使用命令:exec 5</dev/buttons,打开/dev/buttons这个设备,定位到文件描述符fd5,挂载到5下,会调用open

cat /proc/interrupts (产生的中断的信息)

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

使用ps命令查看,当前进程是-sh(shell),PID是772。

使用命令,ls -l /proc/772/fd,文件描述符fd5指向/dev/buttons,以后通过文件描述符5来访问buttons设备。

 

exec是用来执行一个进程的

 

linux中,所有设备都是文件,/dev/buttons也是文件,有文件描述符exec 5</dev/button  将  /dev/button文件关联到文件描述符5,以后对5的操作,就是对设备文件的操作) 

在Shell里执行exec 5</dev/button,因此5是Shell新打开的文件描述符,Shell的进程ID是772

在proc文件系统里772的fd目录,表示772进程打开的文件描述符) 

ps查看当前进程(可以查看进程状态s:休眠)

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

使用命令,exec 5<&-,关闭文件描述符fd5,释放中断。(会调用release

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

测试:(按下按键)

IRQ_EINT0:16=16+0,IRQ_EINT2:18=16+2,IRQ_EINT11:55=16+39,IRQ_EINT19(复位键):63=16+47。

因为是双边沿触发,所以每次按下按键,松开按键,打印两次。

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

 

 

 

优化上面的程序(读出按键值)

1、内核有一个系统函数s3c2410_gpio_getpin(引脚PIN):读出引脚的值。

2、定义了一个结构体pin_desc

     S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

    这个结构体在request_irq函数里传进去。

3、在read函数中,如果没有按键动作发生,休眠(让出CPU,不返回);如果有按键动作发生,直接返回。

     休眠:wait_event_interruptible(button_waitq, ev_press)

     (把进程挂在button_waitq队列里面)

     如果ev_press=0,让应用程序休眠,不返回,程序停止在此处。当被唤醒时,从此处继续执行。

     定义上面两个参数:

     static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
     /* 中断时间标志,中断服务程序将它置1,third_drv_read将它清0 */

     static volatile int ev_press=0;

4、当中断发生,执行中断处理函数,此时唤醒队列中次应用的进程,继续执行,返回结果。

      唤醒:(去button_waitq队列,把挂在这个队列的进程唤醒)

      ev_press = 1; /* 表示中断发生了 */

      wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程,去button_wq队列的进程唤醒 */

 

 

完整的驱动代码:third_drv.c

/*
	一、驱动框架:

	1.先定义file_operations结构体,其中有对设备的打开,读和写的操作函数。
	2.分别定义相关的操作函数
	3.定义好对设备的操作函数的结构体(file_operations)后,将其注册到内核的file_operations结构数组中。
	  此设置的主设备号为此结构在数组中的下标。
	4.定义出口函数:卸载注册到内核中的设备相关资源
	5.修饰 入口 和 出口函数
	6.给系统提供更多的内核消息,在sys目录下提供设备的相关信息。应用程序udev可以据此自动创建设备节点,
	  创建一个class设备类,在此类下创建设备
*/

#include <linux/module.h>	//内涵头文件,含有一些内核常用函数的原形定义。
#include <linux/kernel.h>	//最基本的文件,支持动态添加和卸载模块。Hello World驱动要这一个文件就可以。
#include <linux/fs.h>		//包含了文件操作相关的struct的定义,例如struct file_operations
#include <linux/init.h>		
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h>	//包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义
#include <asm/irq.h>
#include <asm/io.h>			//包含了ioremap、ioread等内核访问IO内存等函数的定义
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>


static struct class *thirddrv_class;	//一个类
static struct class_device	*thirddrv_class_dev;	//一个类里面再建立一个设备

volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;

volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;


/* 下面两个是定义休眠函数的参数 */
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

/* 中断时间标志,中断服务程序将它置1,third_drv_read将它清0 */
static volatile int ev_press=0;

/* 引脚描述的结构体 */
struct pin_desc{
	unsigned int pin;
	unsigned int key_val;
};


/* 键值:按下时,0x01,0x02,0x03,0x04 */
/* 键值:松开时,0x81,0x82,0x83,0x84 */

static unsigned char keyval;	//键值


/* 在request_irq函数中把结构体传进去 */
struct pin_desc pins_desc[4] = {	//键值先赋初始值0x01,0x02,0x03,0x04
	{S3C2410_GPF0,  0x01},	//pin=S3C2410_GPF0,  key_val(按键值)=0x01
	{S3C2410_GPF2,  0x02},	//pin=S3C2410_GPF2,  key_val(按键值)=0x02
	{S3C2410_GPG3,  0x03},	//pin=S3C2410_GPF3,  key_val(按键值)=0x03
	{S3C2410_GPG11, 0x04},	//pin=S3C2410_GPF11, key_val(按键值)=0x04
};


/*
 * 确定按键值
 */
static irqreturn_t button_irq(int irq, void *dev_id)	//中断处理函数
{
	/* irq = IRQ_EINT0 …… */
	/* dev_id = 结构体struct pins_desc */

	struct pin_desc * pindesc = (struct pin_desc *)dev_id;
	unsigned int pinval;

	/* 读取引脚PIN值 */
	pinval = s3c2410_gpio_getpin(pindesc->pin);

	/* 确定按键值,按下管脚低电平,松开管脚高电平 */
	if(pinval)
	{
		/* 松开 */				
		keyval = 0x80 | pindesc->key_val;	//规定的:0x8X
	}
	else
	{
		/* 按下 */
		keyval = pindesc->key_val;	//0x0X
	}

	/* 唤醒 */
	ev_press = 1;	/* 表示中断发生了 */
	wake_up_interruptible(&button_waitq);	/* 唤醒休眠的进程,去button_wq队列,把挂在队列下的进程唤醒 */	
	
	return IRQ_RETVAL(IRQ_HANDLED);
}


static int third_drv_open(struct inode *inode, struct file *file)
{
	/* 配置GPF0,2为输入引脚 */
	/* 配置GPF3,11为输入引脚 */

	/* request_irq函数的第五个参数是void *,为无类型指针,可以指向任何数据类型 */
	request_irq(IRQ_EINT0,  button_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  button_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, button_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, button_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);
	
	return 0;
}

ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	if	(size != 1)
		return -EINVAL;

	/* 如果没有按键动作,休眠,休眠:让出CPU */
	/* 休眠时,把进程挂在button_wq        队列里 */
	/* 如果休眠后被唤醒,就会从这里继续往下执行 */
	/* 一开始没有按键按下,ev_press = 0 */
	wait_event_interruptible(button_waitq, ev_press);//ev_press=0,休眠,让我们的测试程序休眠;ev_press!=0,直接往下运行

	/* 如果有按键动作,返回键值 */
	copy_to_user(buf, &keyval, 1);	//把键值 拷回去
	ev_press = 0;	//清零,如果不清零,下次再读,立马往下执行,返回原来的值
	
	return 1;
}

int third_drv_close(struct inode *inode, struct file *file)
{
	free_irq(IRQ_EINT0,  &pins_desc[0]);
	free_irq(IRQ_EINT2,  &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	return 0;
}

static struct file_operations third_drv_fops = {
    .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open    =  third_drv_open,     
	.read	 =	third_drv_read,
	.release = 	third_drv_close,
};

int major;

static int third_drv_init(void)
{
	major = register_chrdev(0, "third_drv", &third_drv_fops);

	//创建一个类
	thirddrv_class = class_create(THIS_MODULE, "firstdrv");

	//在这个类下面再创建一个设备
	//mdev是udev的一个简化版本
	//mdev应用程序,就会被内核调用,会根据类和类下面的设备这些信息
	thirddrv_class_dev = class_device_create(thirddrv_class, NULL, MKDEV(major, 0), NULL, "buttons");/* /dev/buttons */

	//建立地址映射:物理地址->虚拟地址
	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);	//指向的是虚拟地址,第一个参数是物理开始地址,第二个是长度(字节)
	gpfdat = gpfcon + 1; //加1,实际加4个字节

	gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);	//指向的是虚拟地址,第一个参数是物理开始地址,第二个是长度(字节)
	gpgdat = gpgcon + 1; //加1,实际加4个字节

	return 0;
}

static void third_drv_exit(void)
{
	unregister_chrdev(major, "third_drv");
	
	class_device_unregister(thirddrv_class_dev);
	class_destroy(thirddrv_class);

	iounmap(gpfcon);
	iounmap(gpgcon);
	
	return 0;
}


module_init(third_drv_init);
module_exit(third_drv_exit);

MODULE_LICENSE("GPL");

 

 

 

测试程序:thirddrvtest.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

/* thirddrvtest
 */
int main(int argc, char **argv)
{
	int fd;
	unsigned char key_val;
	int cnt = 0;
	
	fd = open("/dev/buttons", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
	}

	while (1)
	{
		//用查询方式读按键坏处:占用CPU大
		//根本不知道按键什么时候按下,不可预料,只能不断地读,知道它返回
		read(fd, &key_val, 1);
		printf("key_val = 0x%x\n", key_val);
	}
	
	return 0;
}

 

Makefile文件

KERN_DIR = /work/system/linux-2.6.22.6

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= third_drv.o

 

最后进行测试:

 

insmod third_drv.ko

./thirddrvtest &  (在后台执行)

然后按下开发板的四个按键,再松开。。

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

 

怎么卸载模块呢?

因为模块正在被使用

所以,先找到在后台运行的程序thirddrvtest,查看它的进程号为845,

用命令kill -9 845杀死进程,

再卸载模块。

S3C2440 字符设备驱动程序之中断方式的按键驱动_编写代码(七)

 

 

 

 

书上或者光盘的参考代码:s3c24xx_button.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

#define DEVICE_NAME     "buttons"   /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
#define BUTTON_MAJOR    232         /* 主设备号 */

struct button_irq_desc {
    int irq;
    unsigned long flags;
    char *name;
};

/* 用来指定按键所用的外部中断引脚及中断触发方式, 名字 */
static struct button_irq_desc button_irqs [] = {
    {IRQ_EINT19, IRQF_TRIGGER_FALLING, "KEY1"}, /* K1 */
    {IRQ_EINT11, IRQF_TRIGGER_FALLING, "KEY2"}, /* K2 */
    {IRQ_EINT2,  IRQF_TRIGGER_FALLING, "KEY3"}, /* K3 */
    {IRQ_EINT0,  IRQF_TRIGGER_FALLING, "KEY4"}, /* K4 */
};

/* 按键被按下的次数(准确地说,是发生中断的次数) */
static volatile int press_cnt [] = {0, 0, 0, 0};

/* 等待队列: 
 * 当没有按键被按下时,如果有进程调用s3c24xx_buttons_read函数,
 * 它将休眠
 */
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

/* 中断事件标志, 中断服务程序将它置1,s3c24xx_buttons_read将它清0 */
static volatile int ev_press = 0;


static irqreturn_t buttons_interrupt(int irq, void *dev_id)
{
    volatile int *press_cnt = (volatile int *)dev_id;
    
    *press_cnt = *press_cnt + 1; /* 按键计数加1 */
    ev_press = 1;                /* 表示中断发生了 */
    wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */
    
    return IRQ_RETVAL(IRQ_HANDLED);
}


/* 应用程序对设备文件/dev/buttons执行open(...)时,
 * 就会调用s3c24xx_buttons_open函数
 */
static int s3c24xx_buttons_open(struct inode *inode, struct file *file)
{
    int i;
    int err;
    
    for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) {
        // 注册中断处理函数
        err = request_irq(button_irqs[i].irq, buttons_interrupt, button_irqs[i].flags, 
                          button_irqs[i].name, (void *)&press_cnt[i]);
        if (err)
            break;
    }

    if (err) {
        // 释放已经注册的中断
        i--;
        for (; i >= 0; i--)
            free_irq(button_irqs[i].irq, (void *)&press_cnt[i]);
        return -EBUSY;
    }
    
    return 0;
}


/* 应用程序对设备文件/dev/buttons执行close(...)时,
 * 就会调用s3c24xx_buttons_close函数
 */
static int s3c24xx_buttons_close(struct inode *inode, struct file *file)
{
    int i;
    
    for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) {
        // 释放已经注册的中断
        free_irq(button_irqs[i].irq, (void *)&press_cnt[i]);
    }

    return 0;
}


/* 应用程序对设备文件/dev/buttons执行read(...)时,
 * 就会调用s3c24xx_buttons_read函数
 */
static int s3c24xx_buttons_read(struct file *filp, char __user *buff, 
                                         size_t count, loff_t *offp)
{
    unsigned long err;
    
    /* 如果ev_press等于0,休眠 */
    wait_event_interruptible(button_waitq, ev_press);

    /* 执行到这里时,ev_press等于1,将它清0 */
    ev_press = 0;

    /* 将按键状态复制给用户,并清0 */
    err = copy_to_user(buff, (const void *)press_cnt, min(sizeof(press_cnt), count));
    memset((void *)press_cnt, 0, sizeof(press_cnt));

    return err ? -EFAULT : 0;
}

/* 这个结构是字符设备驱动程序的核心
 * 当应用程序操作设备文件时所调用的open、read、write等函数,
 * 最终会调用这个结构中的对应函数
 */
static struct file_operations s3c24xx_buttons_fops = {
    .owner   =   THIS_MODULE,    /* 这是一个宏,指向编译模块时自动创建的__this_module变量 */
    .open    =   s3c24xx_buttons_open,
    .release =   s3c24xx_buttons_close, 
    .read    =   s3c24xx_buttons_read,
};

/*
 * 执行“insmod s3c24xx_buttons.ko”命令时就会调用这个函数
 */
static int __init s3c24xx_buttons_init(void)
{
    int ret;

    /* 注册字符设备驱动程序
     * 参数为主设备号、设备名字、file_operations结构;
     * 这样,主设备号就和具体的file_operations结构联系起来了,
     * 操作主设备为BUTTON_MAJOR的设备文件时,就会调用s3c24xx_buttons_fops中的相关成员函数
     * BUTTON_MAJOR可以设为0,表示由内核自动分配主设备号
     */
    ret = register_chrdev(BUTTON_MAJOR, DEVICE_NAME, &s3c24xx_buttons_fops);
    if (ret < 0) {
      printk(DEVICE_NAME " can't register major number\n");
      return ret;
    }
    
    printk(DEVICE_NAME " initialized\n");
    return 0;
}

/*
 * 执行”rmmod s3c24xx_buttons.ko”命令时就会调用这个函数 
 */
static void __exit s3c24xx_buttons_exit(void)
{
    /* 卸载驱动程序 */
    unregister_chrdev(BUTTON_MAJOR, DEVICE_NAME);
}

/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(s3c24xx_buttons_init);
module_exit(s3c24xx_buttons_exit);

/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("http://www.100ask.net");             // 驱动程序的作者
MODULE_DESCRIPTION("S3C2410/S3C2440 BUTTON Driver");   // 一些描述信息
MODULE_LICENSE("GPL");                              // 遵循的协议