1. GPIO概述
Linux 内核中已经设计好了相关 GPIO 驱动模型,并设计了相关的操作函数接口。
int gpio_request(unsigned gpio, const char *tag) //申请一个空闲的GPIO
int gpio_direction_input(unsigned gpio) //设置GPIO为输入
int gpio_direction_output(unsigned gpio, int value) //设置GPIO为输出或输入
void gpio_set_value(unsigned gpio, int value) //设置GPIO输出高电平或低电平
int gpio_get_value(unsigned gpio) //获取GPIO的当前状态,高电平或低电平
int gpio_to_irq(unsigned gpio) //返回的中断编号
request_irq() //注册中断服务
free_irq() //注销服务
三星官方把在源码中把 GPIO 分为了A、B、C、D、E组,每组32个引脚,通过上述函数申请GPIO 资源时,比如申请 GPB31 这个引脚为 GPIO 时,只需提交参数 PAD_GPIO_B + 31 就可以了。GPIO 的中断号在s5p4418_irq.h
中定义:
既可以通过gpio_to_irq (PAD_GPIO_B +31)
来获取 GPB31 引脚对应的中断号,也可以通过64 + PAD_GPIO_B + 31得到中断号。
2. 驱动代码
本例以 GPB31 作为 GPIO 的外部中断输入引脚,GPB30 作为 GPIO 输出得出中断的状态
2.1 头文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/gpio.h>#include <mach/platform.h>
#include <mach/gpio_desc.h>
#include <mach/gpio.h>
2.2 全局变量
#define LED_GPIO (PAD_GPIO_B + 30) //GPB30为LED输出控制引脚
#define KEY_GPIO (PAD_GPIO_B + 31) //GPB31为触发终端源
#define KEY_GPIO_IRQ gpio_to_irq(KEY_GPIO) //根据GPIO编号获取对应的引脚的中断编号
#define DEVICE_NAME "key_irq"static int major;
static int minor;
struct cdev *key_irq; /* cdev 数据结构 */
static dev_t devno; /* 设备编号 */
static struct class *key_irq_class;static int flag = 0; //屏蔽因驱动初始化中添加定时器导致的在没有按下按键,定时器计时到,出现没有中断而调用了LED点亮动作static struct timer_list key_timer; /*消抖定时器*/char const irq_types[5] = {IRQ_TYPE_EDGE_RISING,IRQ_TYPE_EDGE_FALLING,IRQ_TYPE_EDGE_BOTH,IRQ_TYPE_LEVEL_HIGH,IRQ_TYPE_LEVEL_LOW
};
2.3 中断服务函数
由于按键本身存在的抖动现象,必须滤除掉按键按下后20ms内的不稳定抖动信号,这里采用内核定时器,按键信号触发中断,并进入中断服务程序后,修改定时器的定时值,如果20ms内没有其他信号触发中断,则执行定时器定时结束后的回调函数,否则从新开始计时20ms过程,直到按键信号稳定为止。中断服务函数如下:
static irqreturn_t key_irq_irq_handler(unsigned int irq, void *dev_id)
{/*20ms后启动定时器*/flag= 1; //只有flag为1时才能说明是按键中断导致的定时器函数调用mod_timer(&key_timer,jiffies+ HZ/50); /*去抖时间设为20ms*/returnIRQ_HANDLED;
}
2.4 定时器回调函数
当中断信号确实稳定后,定时器20ms到时,才执行定时器回调函数。在本例程中,定时器回调函数主要执行打印信息,以及翻转LED的亮灭,说明驱动程序工作正常。
在试用过程中,发现一个奇怪现象,通过insmod加载驱动后,即使没有按下按键,LED自动点亮。后来发现,在驱动的初始化函数中,即使我们设置了LED的GPIO引脚为高电平,即LED灭,由于在初始化并添加内核定时器后,会自动执行一次内核定时器的回调函数,导致即使没有中断发生,也会出现加载驱动模块时的LED点亮的现象。
为了解决这一问题,我设置了一个全局的flag,只用flag为1时,才允许在定时器的回调函数中判断LED灯的翻转逻辑;并且只有在真正的中断发生时,在中断服务函数中将flag设置为1。定时器回调函数如下:
static void key_timer_function(unsignedlong data)
{static int count = 0;if(1== flag){printk(KERN_INFO"KEY IRQ HAPPENED!\n");/*根据按键KEY按下的次数交替点亮/熄灭LED灯*/if(count% 2 == 0)gpio_set_value(LED_GPIO,0); /*点亮LED灯*/elsegpio_set_value(LED_GPIO,1); /*熄灭LED灯*/count++;}
}
2.5 驱动初始化函数
驱动初始化函数完成 GPIO 资源申请,获取 GPIO 的中断号,将 GPIO 中断号与中断服务函数绑定。申请字符设备驱动主设备号,创建设备类,和设备实体,以及自动在/dev
目录下自动生成设备节点。本例程,采用按键上升沿作为中断的触发条件,即按键松开后,触发中断。初始化函数如下:
static int __init key_irq_init(void)
{intret;/*注册定时器*/init_timer(&key_timer);key_timer.function= key_timer_function;add_timer(&key_timer);/*申请GPIO*/gpio_free(KEY_GPIO); /*首先释放GPIO*/gpio_free(LED_GPIO); /*首先释放GPIO*/ret= gpio_request_one(KEY_GPIO, GPIOF_IN, "KEY IRQ"); /* 申请IO为输入*/if(ret < 0) {printk(KERN_ERR"Failed to request GPIO for KEY\n");}ret= gpio_request_one(LED_GPIO, GPIOF_OUT_INIT_HIGH, "LED OUTPUT"); /* 申请IO为输出*/if(ret < 0) {printk(KERN_ERR"Failed to request GPIO for LED\n");}gpio_direction_input(KEY_GPIO);/* 设置 GPIO 为输入 */gpio_direction_output(LED_GPIO,0);/* 设置 GPIO 为输出,参数0代表输出*/gpio_set_value(LED_GPIO,1);/*初始化LED为熄灭状态*//*申请GPB31对应的中断*/if(request_irq(KEY_GPIO_IRQ, key_irq_irq_handler, IRQF_TRIGGER_RISING, "key_irqirq", NULL) ){
/* 申请中断 */printk(KERN_WARNINGDEVICE_NAME": Can't get IRQ: %d!\n", KEY_GPIO_IRQ);}irq_set_irq_type(KEY_GPIO_IRQ,irq_types[0]); /*按键上升沿出发中断*/disable_irq(KEY_GPIO_IRQ);enable_irq(KEY_GPIO_IRQ);ret= alloc_chrdev_region(&devno, minor, 1, DEVICE_NAME); /* 从系统获取主设备号 */major= MAJOR(devno);if(ret < 0) {printk(KERN_ERR"cannot get major %d \n", major);return-1;}key_irq= cdev_alloc(); /* 分配 key_irq 结构 */if(key_irq != NULL) {cdev_init(key_irq,&key_irq_fops); /* 初始化 key_irq 结构 */key_irq->owner= THIS_MODULE;if(cdev_add(key_irq, devno, 1) != 0) { /* 增加 key_irq 到系统中 */printk(KERN_ERR"add cdev error!\n");gotoerror;}}else{printk(KERN_ERR"cdev_alloc error!\n");return-1;}key_irq_class= class_create(THIS_MODULE, "key_irq_class");if(IS_ERR(key_irq_class)) {printk(KERN_INFO"create class error\n");return-1;}device_create(key_irq_class,NULL, devno, NULL, DEVICE_NAME);printk("Initcompleted!\n");return0;error:unregister_chrdev_region(devno,1); /* 释放已经获得的设备号 */returnret;
}
2.6 驱动退出函数
在驱动退出函数中,释放申请到的 GPIO 资源,释放 GPIO 对应的中断号,释放系统自动分配的字符设备驱动主设备号,删除设备类和设备实体,删除去抖定时器函数等。
static void __exit key_irq_exit(void)
{gpio_set_value(LED_GPIO,1); /*退出时设置LED为熄灭状态*/gpio_free(KEY_GPIO); /*释放为按键KEY申请的GPIO资源*/gpio_free(LED_GPIO); /*释放为LED申请的GPIO资源*/disable_irq(KEY_GPIO_IRQ);free_irq(KEY_GPIO_IRQ,NULL);cdev_del(key_irq);/* 移除字符设备 */unregister_chrdev_region(devno,1); /* 释放设备号 */device_destroy(key_irq_class,devno);class_destroy(key_irq_class);del_timer(&key_timer); /*删除定时器*/printk(KERN_INFO"Exit completed!\n");
}
2.7 编写 Makefile
KERN_DIR = /home/fa/linux-3.4.y
all:make-C $(KERN_DIR) M=`pwd` modules
clean:make-C $(KERN_DIR) M=`pwd` modules cleanrm-rf modules.order
obj-m += keyinterrupt.o
KERN_DIR 要修改成源码路径,
keyinterrupt
要修改成驱动.c
文件名称
2.8 小结
本例程只是简单的实现了基于中断的按键驱动程序,还没有实现异步功能,在中断服务程序中向用户态的程序发送SIG,然后让用户态的驱动程序在信号处理函数中读取 GPIO 引脚状态。