linux驱动开发之路(二)—— 按键中断(misc设备)

实验内容介绍见《GEC210嵌入式系统开发教材20131120(更新)》第93页--“5.3 嵌入式 Linux 下的按键中断实验”


5.3 嵌入式 Linux 下的按键中断实验


 实验目的:
1、 掌握基本的字符设备的驱动程序设计。
2、 掌握基本的文件操作。
3、 掌握在操作系统下普通 IO 端口的使用方法。

4、 掌握内核中断、定时器及等待队列的使用。


 实验内容:
1、 阅读 S5PV210 的数据手册,熟悉 IO 端口及中断的原理。
2、 编写独立按键的应用程序。
3、 编写 makefile 文件。

4、 下载并调试独立按键应用程序。


 预备知识:
1、 C 语言基础知识.
2、 Linux 下常用命令的使用及 vim 编辑器的使用。
3、 程序调试的基本知识和方法。

4、 ARM 应用程序的基本架构。


 实验设备及工具:
1、 硬件:GEC210 开发板

2、 软件:PC 操作系统 ubuntu10.04、minicom、arm-linux-gcc 交叉编译环境。


 基础知识:
1、 硬件原理:

a) 原理图:


由硬件原理图可知按键分别接在 KP_COL0---KP_COL3 和 KP_ROW0---KP-ROW3 上即是 GPH2 0-3)
(和 GPH3(0-3)上。


b) 寄存器简介:
GPH2 包含四个寄存器用于控制 IO 端口的功能,包括 GPH2CON、GPH2DAT、GPH2DRV、GPH2PUD。(GPH3 包含类似寄存器,详细请查看 S5PV210 数据手册)
GPH2CON 用 于 设 置 GPH2 的 功 能 , 例 如 GPH2CON0[0:3] 为 0b0000 时 GPH2[0] 作 为 输 入 ;
GPH2CON0[0:3] 为 0b0001 时 GPH2[0] 作 为 输 出 端 口 ; GPH2CON0[0:3] 为 0b0010 时 为 保 留 未 用 ;
GPH2CON0[0:3] 为 0b0011 时 作 为 按 键 的 列 ; GPH2CON0[0:3] 为 0b0011~0b1110 保 留 未 使 用 ;
GPH2CON0[0:3]为 0b1111 时 GPH2[0]作为外部中断使用。
(GPH3CON 功能类似,这里不再赘述,详细请查看S5PV210数据手册)


GPH2DAT[7:0]: 当 GPH2CON 将 GPH2 设置成输入模式时,GPH2DAT 相应的位对应着引脚的状态;
当 GPH2CON 将 GPH2 设置成输出模式是,GPH2DAT 相应的位和引脚的状态一致。 GPH2CON 将 GPH2当设置成其他功能模式时,GPH2DAT 相应的位的状态是未知的。GPH2PUD 用于设置 GPH2 IO 端口的上拉电阻 0b00 禁止上拉/下拉电阻;0b01 禁止下拉电阻;0b10禁止上拉电阻;0b11 保留未使用。


 实验原理:


1、
该驱动设计为一个字符设备。9个 GPIO 引脚设置为外部中断引脚,双边沿触发(上升沿和下降沿均触发信号)。向系统注册的两个中断,调用共同的中断处理函数。当有键按下时,在中断中查询引脚状态,以确定是哪个 GPIO 脚被按下。


2、 注册中断处理函数,驱动程序可以通过下面的函数注册并激活一个中断处理程序,以便中断:
int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), unsigned long irq_flags, const char * devname, void *dev_id)
第一个参数 irg 表示要分配的中断号。对某些设备,如传统 PC 设备上的系统时钟或键盘,这个值通常是预先设定死的。而对于大多数其他设备来说,这个值要么是可以通过探测获取,要么可以动态确定。
第二个参数 handler 是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。要注意,handler 函数的原型是特定的——它接受三个参数,并有一个类型为 irqreturn_t的返回值。我们将在本章随后的部分讨论这个函数。
第三个参数 irqflags 可以为 0,也可能是下列一个或多个标志的位掩码:

 IRQF_DISABLED: 

此 标 志 表 明 给 定 的 中 断 处 理 程 序 是 一 个 快 速 中 断 处 理 程 序 ( fast interrupt handler)。过去,Linux 将中断处理程序分为快速和慢速两种。那些可以迅速执行但调用频率可能会很高的中断服务程序,会被贴上这样的标签。通常这样做需要修改中断处理程序的行为,使它们能够尽可能快地执行。现在,加不加此标志的区别只剩下一条了:在本地处理器上,快速中断处理程序在禁止所有中断的情况下运行。这使得快速中断处理程序能够不受其他中断干扰,得以迅速执行。而缺省情况下(没有这个标志),除了正运行的中断处理程序对应的那条中断线被屏蔽外,其他所有中断都是激活的。除了时钟中断外,绝大多数中断都不使用标志。
 IRQF_SAMPLE_RANDOM:

此标志表明这个设备产生的中断对内核熵池(entropy pool)有贡献。内核熵池负责提供从各种随机事件导出的真正的随机数。如果指定了该标志,那么来自该设备的中断间隔时间就会作为墒填充到熵池。如果你的设备以预知的速率产生中断(比如系统定时器),或者可能受外部攻击者(例如连网设备)的影响,那么就不要设置这个标志。相反,有其他很多硬件产生中断的速率是不可预知的,所以都能成为一种较的熵源。关于内核熵池更多的信息。请参考附录 C。

 IRQF_SHARED:

此标志表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标志;否则,在每条线上只能有一个处理程序。有关共享中断处理程序的更多信息将在下面的小节中提供。

第四个参数 devname 是与中断相关的设备的 ASCII 文本表示法。例如,PC 机上键盘中断对应的这个值为“keyboard”。这些名字会被/proc/irq 和/proc/interrupt 文件使用,以便与用户通信。
第五个参数 dev_id 主要用于共享中断线 request_irq()成功执行会返回 0。如果返回非 0 值,就表示有犯错误发生,在这种情况下,指定的中断处理程序不会被注册。最常见的错误是-EBUSY,它表示给定的中断线已经在使用(或者当前用户或者你没有指定 SA_SHIRQ)。

释放中断处理线,可以调用 void free_irq(unsigned int irq, void *dev_id)

中断处理程序的返回值是一个特殊类型:irqreturn_t。中断处理程序可能返回两个特殊的值:IRQ_NONE和 IRQ_HANDLED。


3、 内核定时器的使用:
定时器由结构 time_list 表示,定义在文件中。
struct timer_list{
struct list_head entry; /*包含定时器的链表*/
unsigned long expires; /*以jiffies为单位的定时值*/
spinlock_t lock; /*保护定时器的锁*/
void (*function)(unsigned long);/*定时器处理函数*/
unsigned long data; /*传给处理函数的长整形参数*/
struct tvec_t_base_s *base;/*定时器内部值,用户不要使用*/
};

幸运的是,使用定时器并不需要深入了解该数据结构。事实上,过深的陷入该结构,反而会使你的代码不能保证对可能发生的变化提供支持。内核提供了一组与定时器相关的接口用来简化管理定时器的操作。所有这些接口都声明在文件中,大多数接口在文件 kernel/timer.c 中获得实现。
创建定时器时需要先定义它:
struct timer_list my_timer;
接着需要通过一个辅助函数初始化定时器数据结构的内部值,初始化必须在使用其他定时器管理函数对定时器进行操作前完成。
Init_timer(&my_timer);
现在你可以填充结构中需要的值了:
my_timer.expires=jiffies+delay; /*定时器超时时的节拍数*/
my_timer.data=0; /*给定时器处理函数传入0值*/
my_timer.function=my_function; /*定时器超时时调用的函数*/

my_timet.expires 表示超时时间,它是以节拍为单位的绝对计数值。如果当前 jiffies 计数等于或大于
my_timet.expires,那么 my_timer.function 指向的处理函数就会开始执行,另外该函数还要使用长整型参数
my_timer.data。所以正如我们从 timer_list 结构看到的形式,处理函数必须符合下面的函数原形:
void my_timer_function (unsigned long data);
data 参数使你可以利用同一个处理函数注册多个定时器,只需通过该参数就能区别对待它们。如果你不需要这个参数,可以简单地传递 0(或任何其他值)给处理函数。
最后,你必须激活定时器:
add_timer(&my_timer);
有时可能需要更改已经激活的定时器超时时间,所以内核通过函数 mod_timer()来实现该功能,该函数可以改变指定的定时器超时时间:
mod_timer (&my_timer,jiffies+mew_delay);
mod_timer()函数也可操作那些已经初始化,但还没有被激活的定时器,如果定时器未被激活,Mod_timer()会激活它。如果调用时定时器未被激活,该函数返回 0;否则返回 1。但不论哪种情况,一旦从 mod_timer()函数返回,定时器都将被激活而且设置了新的定时值。
如果需要在定时器超时前停止定时器,可以使用 del_timer()函数:
del_timer(&my_timer);
被激活或未被激活的定时器都可以使用该函数,如果定时器还未被激活,该函数返回 0;否则返回 1。


4、 睡眠和唤醒
当进程等待事件(可以是输入数据,子进程的终止或是其他什么)时,它需要进入睡眠状态以便其他进程可以使用计算资源。你可以调用如下函数之一让进程进入睡眠状态:
void interruptible_sleep_on(struct wait_queue **q);
void sleep_on(struct wait_queue **q);
int wait_event_interruptible(wait_queue_head_t q, int condition);
然后用如下函数之一唤醒进程:
void wake_up(struct wait_queue **q);
void wake_up_interruptible(struct wait_queue **q);


5、 驱动代码简析(详细代码请查看附件)

驱动代码:

< driver / button_driver.c >

/*
**
**This is a button driver.
**The author is Alan.
**
*/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 

#define DEVICE_NAME  "buttons"//设备名称

struct button_desc {
	int gpio;
	int number;
	char *name;
	struct timer_list timer;	
};

static struct button_desc buttons[]={// 设备中断号和名称
	{ S5PV210_GPH2(0), 0, "KEY0" },
	{ S5PV210_GPH2(1), 1, "KEY1" },
	{ S5PV210_GPH2(2), 2, "KEY2" },
	{ S5PV210_GPH2(3), 3, "KEY3" },
	{ S5PV210_GPH3(0), 4, "KEY4" },
	{ S5PV210_GPH3(1), 5, "KEY5" },
	{ S5PV210_GPH3(2), 6, "KEY6" },
	{ S5PV210_GPH3(3), 7, "KEY7" },
}; 

static volatile char key_values[]={
	'0','0','0','0','0','0','0','0'
};

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);//声明一个等待队列并初始化

static volatile int ev_press = 0;

static void gec210_buttons_timer(unsigned long _data)
{
	struct button_desc *bdata = (struct button_desc *)_data;
	int down;
	int number;
	unsigned tmp;

	tmp=gpio_get_value(bdata->gpio);
	
	/* active low */
	down=!tmp;
	printk("KEY %d: %08x\n",bdata->number,down);

	number=bdata->number;
	if(down != (key_values[number] & 1))//down为0,确定按键被按下
	{
		key_values[number]='0'+down;
		
		ev_press =1;
		wake_up_interruptible(&button_waitq);//唤醒等待队列
	}
}

static irqreturn_t button_interrupt(int irq,void *dev_id)//中断处理函数
{
	struct button_desc *bdata =(struct button_desc *)dev_id;
	mod_timer(&bdata->timer,jiffies+msecs_to_jiffies(40));

	return IRQ_HANDLED;//return 1
}

static int gec210_buttons_open(struct inode *inode,struct file *file)
{
	int irq;
	int i;
	int err;

	for(i=0;i=0;i--)
		{		
			if(!buttons[i].gpio)
				continue;
			irq=gpio_to_irq(buttons[i].gpio);
			disable_irq(irq);//禁止中断
			free_irq(irq,(void *)&buttons[i]);//释放中断
			del_timer_sync(&buttons[i].timer);//清楚定时器	
		}
		return -EBUSY;
	}
	ev_press = 1;
	return 0;
}

static int gec210_buttons_close(struct inode *inode,struct file *file)
{
	int irq,i;

	for(i=0;if_flags & O_NONBLOCK)
			return -EAGAIN;
		else
			wait_event_interruptible(button_waitq,ev_press);
	}
	ev_press =0;
	err = copy_to_user((void *)buffer,(const void*)(&key_values),
			min(sizeof(key_values),count));
	return err ? -EFAULT :min(sizeof(key_values),count);
}

static unsigned int gec210_buttons_poll(struct file *file,
				struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	
	poll_wait(file,&button_waitq,wait);//查询有无按键中断
	if(ev_press)
		mask |= POLLIN | POLLRDNORM;//有数据可读\有普通数据可读
	return mask;
}

static struct file_operations dev_fops = {
	.owner		=	THIS_MODULE,
	.open		=	gec210_buttons_open,
	.release	=	gec210_buttons_close,
	.read		=	gec210_buttons_read,
	.poll		=	gec210_buttons_poll,
};

static struct miscdevice misc = {
	.minor		=	MISC_DYNAMIC_MINOR,
	.name		=	DEVICE_NAME,
	.fops		=	&dev_fops,
};

static int __init button_dev_init(void)
{
	int ret;
	ret = misc_register(&misc);//注册misc设备
	printk(DEVICE_NAME "\tinitialized\n");
	return ret;
}

static void __exit button_dev_exit(void)
{

	misc_deregister(&misc);//注销misc设备
}

module_init(button_dev_init);
module_exit(button_dev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALAN");


< driver / Makefile >
ifneq ($(KERNELRELEASE),)
	obj-m :=button_driver.o
else
	module-objs :=button_driver.o
	KERNELDIR :=/home/gec/linux_kernel/linux2.6.35.7/
	PWD :=$(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
	$(RM)  *.ko *.mod.c *.mod.o *.o *.order *.symvers *.cmd

测试应用程序代码:


< app / button_test.c >
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ARRY_SIZE(x) (sizeof(x)/sizeof(x[0]))

int main(int argc,char **argv)
{

	int button_fd;
	char current_button_value[8]={0};//板载八个按键 key2-9
	char prior_button_value[8]={0};//用于保存按键的前键值
	
	button_fd = open("/dev/buttons",O_RDONLY);
	if(button_fd < 0)
	{
		perror("open device :");
		exit(1);
	}
	while(1)
	{
		int i;
		if(read(button_fd,current_button_value,
			sizeof(current_button_value))!=sizeof(current_button_value))
		{
			perror("read buttons:");
			exit(1);//exit(0) 表示程序正常, exit(1)/exit(-1)表示程序异常退出,exit() 结束当前进程/当前程序/,在整个程序中,只要调用 exit ,就结束
		}
		for(i=0;i

< app / Makefile >
Exec := button_test
Obj := button_test.c
CC := arm-linux-gcc

$(Exec) : $(Obj)
	$(CC) -o $@ $(Obj) $(LDLIBS$(LDLIBS-$(@)))

clean:
	rm -vf $(Exec) *.elf *.o

测试结果:
















你可能感兴趣的:(驱动程序)