GPIO应用之按键FIFO驱动

本文详细介绍了如何使用FIFO机制设计按键驱动程序,包括按键按下、弹起、长按和自动连发的事件处理。通过裸机编程和状态机的概念,解释了FIFO的工作原理,以及如何在按键检测中应用滤波机制消除抖动。同时讨论了中断和查询方式在按键检测中的优缺点,特别是在操作系统环境下的资源消耗和稳定性考虑。
摘要由CSDN通过智能技术生成

驱动设计:

此案件驱动程序来源于安富莱,用于扫描独立按键,具有软件滤波机制,采用FIFO保存键值。可以检测如下事件:

       按键按下

       按键弹起

       长按键

       长按时自动连发

问题来了,什么是FIFO?

eg:FIFO机制和状态机一样,都是在裸机编程中非常重要的编程思想。

FIFO是先入先出的意思,即谁先进入队列,谁先出去。比如我们需要串口打印数据,当使用缓存将该数据保存的时候,在输出数据时必然是先进入的数据先出去,那么该如何实现这种机制呢?首先就是建立一个缓存空间,这里假设为10个字节空间进行说明。

从上图可知,使用FIFO就先要定义一个结构体,来储存他的三个成员。缓存区Buf[]数组,缓存区写指针Write,缓存区读指针Read。

初始状态下,Read=Write=0;

typedef struct 
{
	uint8_t Buf[10];  //缓存区
	uint8_t Write;    //缓存区写指针
	uint8_t Read;    //缓存区读指针 
	
}KEY_FIFO_T;

我们依次按下k1,k2,FIFO中的数据变为:

如果Write不等于Read,我们就认为有新的按键事件发生。

读取一个按键进行处理后,Read变量变为1,Write变量保持不变

然后我们继续读取三个按键进行处理,Read变量变为4,此时Read=Write=4。两个变量已经相等,就表示没有新的按键进行处理。 

注意: 当FIFO缓存区写满后,write会被重新赋值为0,也就是从第一个字节空间填数据进去,如果这个地址空间的数据还没有被都出去,就会被后来的数据覆盖调,这里开辟了十个字节的缓存区,一般够用。

设计FIFO按键的三个好处:

  1. 可靠地记录每一个按键事件,避免遗漏按键事件。特别是需要实现按键的按下、长按、自动连发、弹起等事件时。

  2. 读取按键的函数可以设计为非阻塞的,不需要等待按键抖动滤波处理完毕。

  3. 按键 FIFO 程序在嘀嗒定时器中定期的执行检测,不需要在主程序中一直做检测,这样可以有效地降低系统资源消耗。

按键FIFO的实现

一,定义结构体:

在我们的key.h文件中定义一个结构体类型为KEY_FIFO_T的结构体。就是前面说的那个结构体。这只是类型声明,并没有分配变量空间。

typedef struct 
{
	uint8_t Buf[10];  //缓存区
	uint8_t Write;    //缓存区写指针
	uint8_t Read;    //缓存区读指针 
	
}KEY_FIFO_T;

接着在key.c 中定义 s_tKey 结构变量, 此时编译器会分配一组变量空间。

static KEY_FIFO_T s_tkey; //按键FIFO结构体变量
二,将按键状态写入到FIFO缓存区:

当检测到按键事件发生后,就可以调用KEY_FIFO_Put函数将键值压入FIFO中。

void KEY_FIFO_Put(uint8_t Key_Code)
{
    s_tkey.Buf[s_tkey.Write]=Key_Code;
    if(++s_tkey.Write>=KEY_FIFO_SIZE)
    {
        s_tkey.Write=0;
    }

}

每压一次,就是每调用一次KEY_FIFO_Put()函数,写指针write就++一次,也就是向后移动一个空间,如果FIFO空间写满了,也就是s_tKey.Write >= KEY_FIFO_SIZE,Write会被重新赋值为 0。

三,将按键状态从FIFO中读出来:
//从缓存区数组中读取按键键值
uint8_t KEY_FIFO_Get(void)
{   uint8_t ret;
    if(s_tkey.Read==s_tkey.Write)  //表示缓存区为空
        return KEY_NONE;  //
    else {
        ret=s_tkey.Buf[s_tkey.Read];
        if(++s_tkey.Read>=KEY_FIFO_SIZE)
        {
            s_tkey.Read=0;
        }
        return ret;
    }

}

如果返回值KEY_NONE为0,表示写入的指针和读出的指针相等,那么,按键缓冲区为空,所有的按键时间已经处理完毕。如果不相等就说明FIFO的缓冲区不为空,将Buf中的数读出给ret变量。同样,如果FIFO空间读完了,没有缓存了,也就是s_tKey.Read >= KEY_FIFO_SIZE,Read也会被重新赋值为 0。按键的键值定义在key.h 文件,下面是具体内容:

//按次序定义按键的按下,弹起,长按事件
typedef enum 
{
 KEY_NONE=0,
	
 KEY_1_DOWN,	
 KEY_1_UP,	
 KEY_1_LONG,	
	
 KEY_2_DOWN,	
 KEY_2_UP,	
 KEY_2_LONG,	

 KEY_3_DOWN,	
 KEY_3_UP,	
 KEY_3_LONG,
	
 KEY_4_DOWN,	
 KEY_4_UP,	
 KEY_4_LONG,	
  	
 KEY_5_DOWN,	
 KEY_5_UP,	
 KEY_5_LONG,

 KEY_6_DOWN,	
 KEY_6_UP,	
 KEY_6_LONG,
	
	
}KEY_ENUM;

必须按次序定义每个键的按下、弹起和长按事件,即每个按键对象占用 3 个数值。推荐使用枚举enum, 不用#define的原因是便于新增键值,方便调整顺序。使用{ } 将一组相关的定义封装起来便于理解。编译器也可帮我们避免键值重复。

四,按键检测程序分析

上面说了如何将按键的键值存入和读出FIFO,但是既然是按键操作,就肯定涉及到按键消抖处理,还有按键的状态是按下还是弹起,是长按还是短按。所以为了以示区分,我们用还需要给每一个按键设置很多参数,就需要再定义一个结构体KEY_T,让每个按键对应1个全局的结构体变量。

//检测按键是长按还是短按,为它设置一系列对应的参数

typedef struct
{
	/*下面是一个函数指针,指向判断按键是否按下的函数*/
	//1表示按下
	uint8_t (*IsKeyDownFunc)(void);
    
    uint8_t count; //滤波计数器
    uint16_t	LongCount;  //长按计数器
	uint16_t LongTime;   //按键按下持续时间,0表示按键不检测长按
	uint8_t State;    //按键当前状态
	uint8_t RepeatSpeed;  //连续按键周期
	uint8_t RepeatCount; //连续按键计数器
	
}KEY_T;

在key.c 中定义s_tBtn结构体数组变量。

static KEY_FIFO_T s_tkey; //按键FIFO结构体变量
static KEY_T s_tBtn[HARD_KEY_NUM ]= {0}; //目前有四个按键,要为每一个按键分配结构体变量,所以定义一个KEY_T结构体类型的数组变量

每个按键对象都分配一个结构体变量,这些结构体变量以数组的形式存在将便于我们简化程序代码行数。因为我的硬件有6个按键,所以在key.h里有 #define HARD_KEY_NUM   6。使用函数指针IsKeyDownFunc可以将每个按键的检测以及组合键的检测代码进行统一管理。

因为函数指针必须先赋值,才能被作为函数执行。因此在定时扫描按键之前,必须先执行一段初始化函数来设置每个按键的函数指针和参数。这个函数是void KEY_Init(void)

//初始化各个按键的指针参数
void KEY_FIFO_Init(void)
{
    uint8_t i;

    /* 对按键FIFO读写指针清零 */
    s_tkey.Read = 0;
    s_tkey.Write = 0;

    /* 给每个按键结构体成员变量赋一组缺省值 */
    for (i = 0; i < HARD_KEY_NUM; i++)
    {
        s_tBtn[i].LongTime = KEY_LONG_TIME;/* 长按时间 0 表示不检测长按键事件 */
        s_tBtn[i].count = KEY_FILTER_TIME/ 2;	/* 计数器设置为滤波时间的一半 */
        s_tBtn[i].State = 0;/* 按键缺省状态,0为未按下 */
        s_tBtn[i].RepeatSpeed = 6;/* 按键连发的速度,0表示不支持连发 */
        s_tBtn[i].RepeatCount = 0;/* 连发计数器 */
    }
    /* 判断按键按下的函数 */
    s_tBtn[0].IsKeyDownFunc = IsKey1Down;
    s_tBtn[1].IsKeyDownFunc = IsKey2Down;
    s_tBtn[2].IsKeyDownFunc = IsKey3Down;
    s_tBtn[3].IsKeyDownFunc = IsKey4Down;
    s_tBtn[4].IsKeyDownFunc = IsKey5Down;
    s_tBtn[5].IsKeyDownFunc = IsKey6Down;
}

我们知道按键会有机械抖动,你以为按键按下就是低电平,其实在按下的一瞬间会存在机械抖动,如果不做延时处理,可能会出错,一般如果按键检测到按下后再延时50ms检测一次,如果还是检测低电平,才能说明按键真正的被按下了。反之按键弹起时也是一样的。所以我们程序设置按键滤波时间50ms, 因为代码每10ms扫描一次按键,所以按键的单位我们可以理解为10ms,滤波的次数就为5次。这样只有连续检测到50ms状态不变才认为有效,包括弹起和按下两种事件,即使按键电路不做硬件滤波(没有电容滤波),该滤波机制也可以保证可靠地检测到按键事件。

判断按键是否按下,用一个HAL_GPIO_ReadPin就可以搞定。

//按键检测
uint8_t IsKey1Down(void)
{
    if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)
        return 1;

    else
        return 0;
}
uint8_t IsKey2Down(void)
{
    if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)
        return 1;
    else
        return 0;
}
uint8_t IsKey3Down(void)
{
    if(HAL_GPIO_ReadPin(KEY3_GPIO_Port,KEY3_Pin)==GPIO_PIN_RESET)
        return 1;
    else
        return 0;
}
uint8_t IsKey4Down(void)
{
    if(HAL_GPIO_ReadPin(KEY4_GPIO_Port,KEY4_Pin)==GPIO_PIN_RESET)
        return 1;
    else
        return 0;
}
uint8_t IsKey5Down(void)
{
    if(HAL_GPIO_ReadPin(KEY4_GPIO_Port,KEY4_Pin)==GPIO_PIN_RESET)
        return 1;
    else
        return 0;
}
uint8_t IsKey6Down(void)
{
    if(HAL_GPIO_ReadPin(KEY4_GPIO_Port,KEY4_Pin)==GPIO_PIN_RESET)
        return 1;
    else
        return 0;
}
五,按键扫描

按键扫描函数KEY_Scan()每隔 10ms 被执行一次。RunPer10ms函数在 systick中断服务程序中执行。我这里因为用到了操作系统,所以直接创建一个10ms为周期的任务调用函数,当然你也可以使用定时器。

void Soft_Init(void)
{
	 KEY_FIFO_Init();
	
	xTaskCreate(    vDisplay_Task, //任务函数
		"vDisplay_Task",  //任务名
				128,     //STACK大小
				NULL,    //任务参数
				2,        //任务优先级
				NULL);    //任务句柄
	while (1)
	{
		vTaskDelay(20);
		
		//RunPer10ms();
		 KEY_Scan();
		// printf("写入\r\n");
	} 
}
void KEY_Scan(void)
{

    uint8_t i;
    for (i = 0; i < HARD_KEY_NUM; i++)
    {

        KEY_Detect(i);

    }

}

每隔10ms所有的按键GPIO均会被扫描检测一次。KEY_Detect函数实现如下:

void KEY_Detect(uint8_t i)
{

    KEY_T *pBtn;

    pBtn =&s_tBtn[i];  //读取相应的结构体地址赋值给结构体变量pBtn

    if(pBtn->IsKeyDownFunc()) {  //这里执行的是按键按下的处理


        //进行50ms滤波,滤除不正常的初始值状态,按键滤波前给count一个初值,前面说按键初始化的时候已经设置了 Count = KEY_FILTER_TIME/2
        if(pBtn->count<KEY_FILTER_TIME)
        {

            pBtn->count=KEY_FILTER_TIME;

        }
        //实现KEY_FILTER_TIME的延迟
        else if (pBtn->count<2*KEY_FILTER_TIME)
        {

            pBtn->count++;
        }
        else     //判断按键是否长按,进行处理  
        {
            /*
            **********************************************************************************
            这个 State 变量是有其实际意义的,如果按键按下了,这里就将其设置为 1,如果没有按下这个
            变量的值就会一直是 0,这样设置的目的可以有效的防止一种情况的出现:比如按键 K1 在某个
            时刻检测到了按键有按下,那么它就会做进一步的滤波处理,但是在滤波的过程中,这个按键
            按下的状态消失了,这个时候就会进入到上面第二步 else 语句里面,然后再做按键松手检测滤波
            ,滤波结束后判断这个 State 变量,如果前面就没有检测到按下,这里就不会记录按键弹起。
            **********************************************************************************
            */

            
            if(pBtn->State==0) {  //首次按下
                pBtn->State=1;
                KEY_FIFO_Put((uint8_t)(3*i+1));
            }
            if (pBtn->LongTime > 0) /*LongTime初始值是100。单位10ms,持续1秒,认为支持长按事件*/
            {
                if (pBtn->LongCount < pBtn->LongTime) /*LongCount长按计数器。单位10ms,持续1秒,认为长按事件*/
                {
                    /* 发送按钮持续按下的消息 */
                    if (++pBtn->LongCount == pBtn->LongTime)/*LongCount等于LongTime(100),10ms进来一次,进来了100次也就是说按下时间为于1s*/
                    {
                        /* 键值放入按键FIFO */
                        KEY_FIFO_Put((uint8_t)(3 * i + 3));
                    }
                }
                else/*LongCount大于LongTime(100),也就是说按下时间大于1s,触发长按连发事件*/
                {
                    if (pBtn->RepeatSpeed > 0)/* RepeatSpeed连续按键周期,控制发送长按按键长按的速度 */
                    {
                        if (++pBtn->RepeatCount >= pBtn->RepeatSpeed)
                        {
                            pBtn->RepeatCount = 0;
                            /* 长按键后,每隔60ms发送1个按键。因为长按也是要发送键值得,10ms进来一次,进来了6次,也就是说发送时间为60ms。*/
                            KEY_FIFO_Put((uint8_t)(3 * i + 1));
                        }
                    }
                }
            }
        }
    }



    //这里面执行的是按键松手的处理或者按键没有按下的处理
    else {
        if(pBtn->count>KEY_FILTER_TIME)
        {
            pBtn->count=KEY_FILTER_TIME;
        }
        else if(pBtn->count!=0)
        {
            pBtn->count--;
        }
        else
        {
            if(pBtn->State==1)
            {
                pBtn->State=0;
                KEY_FIFO_Put((uint8_t)(3*i+2));
            }


        }

        pBtn->LongCount = 0;
        pBtn->RepeatCount = 0;

    }
}

这个函数还是比较难以理解的,主要是结构体的操作。所以好好学习结构体,不要见了结构体就跑。

分析:首先读取相应按键的结构体地址赋值给结构体指针变量pBtn ,因为程序里面每个按键都有自己的结构体,只有通过这个方式才能对具体的按键进行操作。(在前面我们使用软件定时器时也使用了这中操作,在滴答定时器的中断服务函数中)。

KEY_T *pBtn;

    pBtn =&s_tBtn[i];  //读取相应的结构体地址赋值给结构体变量pBtn

然后接着就是给按键滤波前给Count设置一个初值,前面说按键初始化的时候已经设置了Count =5/2。然后判断是否按下的标志位,如果按键按下了,这里就将其设置为 1,如果没有按下这个变量的值就会一直是 0。这里可能不理解是就是按键按下发送的键值是3 * i + 1。按键弹起发送的键值是3 * i + 2,按键长按发送的键值是3 * i + 3。也就是说按键按下发送的键值是1和4和7。按键弹起发送的键值是2和5和8,按键长按发送的键值是3和6和9。看下面这个枚举enum你就明白了。

//按次序定义按键的按下,弹起,长按事件
typedef enum 
{
 KEY_NONE=0,
	
 KEY_1_DOWN,	
 KEY_1_UP,	
 KEY_1_LONG,	
	
 KEY_2_DOWN,	
 KEY_2_UP,	
 KEY_2_LONG,	

 KEY_3_DOWN,	
 KEY_3_UP,	
 KEY_3_LONG,
	
 KEY_4_DOWN,	
 KEY_4_UP,	
 KEY_4_LONG,	
  	
 KEY_5_DOWN,	
 KEY_5_UP,	
 KEY_5_LONG,

 KEY_6_DOWN,	
 KEY_6_UP,	
 KEY_6_LONG,
	
	
}KEY_ENUM;
六,实验演示
while(1)
	{
		
		uint8_t key_code;
		key_code=KEY_FIFO_Get();
		 //printf("key_code=%d\r\n",key_code);
		if(key_code!=KEY_NONE)
		{
			switch(key_code)
			{
				
				case KEY_1_DOWN:			/* K1键按下 */
					printf("K1键按下\r\n");
					break;
				case KEY_1_UP:				/* K1键弹起 */
					printf("K1键弹起\r\n");
					break;
				case KEY_1_LONG:				/* K1键弹起 */
					printf("K1键弹起\r\n");
					break;
				case KEY_2_DOWN:			/* K2键按下 */
					printf("K2键按下\r\n");
					break;
				case KEY_2_UP:				/* K2键弹起 */
					printf("K2键弹起\r\n");
					break;
				case KEY_2_LONG:				/* K2键弹起 */
					printf("K2键长按\r\n");
					break;
				case KEY_3_DOWN:			/* K3键按下 */
					printf("K3键按下\r\n");
					break;
				case KEY_3_UP:				/* K3键弹起 */
					printf("K3键弹起\r\n");
					break;	
				case KEY_3_LONG:				/* K3键弹起 */
					printf("K3键长按\r\n");
					break;	
				default:
					/* 其它的键值不处理 */
					break;
				
				
				
			
				
				
				
			}
			
			
		}
		//BSP_IO_T(LED1);
		//BSP_IO_T(LED2);
	}

检测按键有中断方式和GPIO查询方式两种。这里推荐用GPIO查询方式。

1.从裸机的角度分析

  • 中断方式:中断方式可以快速地检测到按键按下,并执行相应的按键程序,但实际情况是由于按键的机械抖动特性,在程序进入中断后必须进行滤波处理才能判定是否有效的按键事件。如果每个按键都是独立的接一个 IO 引脚,需要我们给每个 IO 都设置一个中断,程序中过多的中断会影响系统的稳定性。中断方式跨平台移植困难。

  • 查询方式:查询方式有一个最大的缺点就是需要程序定期的去执行查询,耗费一定的系统资源。实际上耗费不了多大的系统资源,因为这种查询方式也只是查询按键是否按下,按键事件的执行还是在主程序里面实现。

2.从OS的角度分析

  • 中断方式:在 OS 中要尽可能少用中断方式,因为在RTOS中过多的使用中断会影响系统的稳定性和可预见性。只有比较重要的事件处理需要用中断的方式。

  • 查询方式:对于用户按键推荐使用这种查询方式来实现,现在的OS基本都带有CPU利用率的功能,这个按键FIFO占用的还是很小的,基本都在1%以下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值