总览
对安富莱按键FIFO的代码进行了分析综合与运用
首先说一句,面对对象的编程思想真的相当重要。相当后悔毕业之后才接触了面对对象编程的思想,其实在大一大二的时候,就喜欢原子和野火的代码,逻辑简单也好用。看到安富莱的stm32代码,有点懵逼的的,就感觉为什么一个简单的按键检测搞这么麻烦,初始化按键的GPIO之后再加一句简单的GPIO_ReadInputDataBit不就行了么,还搞这搞那这么麻烦!当时也不懂什么叫数据结构,也没有人指导。可是学到后来,就觉得仅仅一个GPIO_ReadInputDataBit真的不能解决所有的事情,当然了,按一下再弹起或许可以,但是如果有长按功能呢?有连续按的功能呢? 当时觉得STM32还写得不错,就开始向S3C2440进军,看看uboot,玩玩linux,看看内核代码,在看内核的时候,发现充斥着大量的结构体与数据结构,当时就在想,为啥用这么多的结构体呢,好不明白。立马觉得自己阅读代码的能力不够,就把C语言重新回顾了几遍,又去把C++学了一遍。当明白了面对对象的编程思想之后,才发现过去面对过程编写的思想真的是小儿科啊。
其实用结构体的方式,方便就在好管理!
按键对象
回归正题,编写按键程序的时候,首先想到的就是按键有几种状态,第二是对按键的操作方法。
| 按键对象 | |||
|---|---|---|---|
| 按键状态: | 按下 | 弹起 | 长按 |
| 按键操作: | 按一下 | 长按 | 连续按 |
| 对这个有一个意识之后,面对对象的编程思想就来了,我需要一个按键类型,就像C语言里面的int、char一样,可以声明一种按键类型,那么下面的代码就出来了: |
按键数量
开发板上有两个按键,把这两个按键枚举出来:
typedef enum
{
KID_K1 = 0,
KID_K2
}KEY_ID_E;
//两个按键,再加上组合按键,共有三种方式
//组合键这个可加可不加,加的话这个值就是3,不加就是2
#define KEY_COUNT 3
按键对象类型
利用C语言的typedef和struct:
/*
按键滤波时间50ms, 单位10ms。
只有连续检测到50ms状态不变才认为有效,包括弹起和按下两种事件
即使按键电路不做硬件滤波,该滤波机制也可以保证可靠地检测到按键事件
*/
#define KEY_FILTER_TIME 5
#define KEY_LONG_TIME 100 /* 单位10ms, 持续1秒,认为长按事件 */
typedef struct
{
/*下面是一个函数指针*/
uint8_t (*IsKeyDownFunc)(void);/*按键按下的判断函数,1表示按下*/
uint8_t Count; /* 滤波计数器 */
uint16_t LongCount; /* 长按计数器 */
uint16_t LongTime; /* 按键按下的持续时间,0表示不检测长按 */
uint8_t State; /* 按键当前状态(按下还是弹起) */
uint8_t RepeatSpeed; /* 连续按键周期 */
uint8_t RepeatCount; /* 连续按键计时器 */
}KEY_Typedef;
按键状态
利用枚举定义每个按键的状态,按下、弹起、长按:
typedef enum
{
KEY_NONE = 0,
KEY_1_DOWN, /* 1键按下 1*/
KEY_1_UP, /* 1键弹起 2*/
KEY_1_LONG, /* 1键长按 3*/
KEY_2_DOWN, /* 2键按下 4*/
KEY_2_UP, /* 2键弹起 5*/
KEY_2_LONG, /* 2键长按 6*/
/*组合键*/
KEY_3_DOWN, /* 组合1、2键按下 7*/
KEY_3_UP, /* 组合1、2键弹起 8*/
KEY_3_LONG /* 组合1、2键长按 9*/
}KEY_ENUM;
按键状态存储数据结构
把按键每次的按下的状态数据存储FIFO数据结构中,然后从这个FIFO中读取按键事件,这个FIFO定义一个长度为10的数组:
/* 按键FIFO用到变量 */
#define KEY_FIFO_SIZE 10
typedef struct
{
uint8_t Buf[KEY_FIFO_SIZE]; /* 键值缓冲区 */
uint8_t Read; /* 缓冲区读指针1 */
uint8_t Write; /* 缓冲区写指针 */
uint8_t Read2; /* 缓冲区读指针2 */
}KEY_FIFO_Typedef;
小结
从按键对象上手,总结出按键对象类型、按键状态以及按键状态存储数据结构,上述代码在头文件中实现。
实现方法
一:用宏 定义按键的时钟、端口、引脚
#define RCC_ALL_KEY (RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOC)
#define GPIO_PORT_K1 GPIOA
#define GPIO_PIN_K1 GPIO_Pin_0
#define GPIO_PORT_K2 GPIOC
#define GPIO_PIN_K2 GPIO_Pin_13
二:定义按键个数和按键的FIFO数据结构
static KEY_Typedef s_tBtn[KEY_COUNT]; //两个按键和二合一组合键一共三个按键
static KEY_FIFO_Typedef s_tKey; //定义一个FIFO数据结构
三:用简便的语句实现按键是否被按下
//其实本质上,这就是GPIO_ReadInputDataBit实现方法。不过if语句中的判断是否相等,得看硬件电路,
//如果按键按下后,引脚检测是低电平,则就是等于0,如果按键按下后,引脚检测是高电平,就不等于0
static uint8_t IsKeyDown1(void) {if((GPIO_PORT_K1->IDR & GPIO_PIN_K1) != 0) {return 1;} else {return 0;}}
static uint8_t IsKeyDown2(void) {if((GPIO_PORT_K2->IDR & GPIO_PIN_K2) != 0) {return 1;} else {return 0;}}
static uint8_t IsKeyDown3(void) {if(IsKeyDown1() && IsKeyDown2()) {return 1;} else {return 0;}}
四:往FIFO中写入一个键值
void BspPutKey(uint8_t _KeyCode)
{
s_tKey.Buf[s_tKey.Write] = _KeyCode;
if(++s_tKey.Write >= KEY_FIFO_SIZE)
{
s_tKey.Write = 0;
}
}
五:从按键FIFO缓冲区读取一个键值
uint8_t BspGetKey(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;
}
}
/*
*********************************************************************************************************
* 函 数 名: bsp_GetKey2
* 功能说明: 从按键FIFO缓冲区读取一个键值。独立的读指针。
* 形 参: 无
* 返 回 值: 按键代码
*********************************************************************************************************
*/
uint8_t BspGetKey2(void)
{
uint8_t ret;
if(s_tKey.Read2 == s_tKey.Write)
{
return KEY_NONE;
}
else
{
ret = s_tKey.Buf[s_tKey.Read2];
if(++s_tKey.Read2 >= KEY_FIFO_SIZE)
{
s_tKey.Read2 = 0;
}
return ret;
}
}
对于这个第二个读指针,从功能说明上看,是独立的读指针,与之前那个读指针有区别。
六:获取按键的状态以及设置按键的参数
/*
*********************************************************************************************************
* 函 数 名: bsp_GetKeyState
* 功能说明: 读取按键的状态
* 形 参: _ucKeyID : 按键ID,从0开始
* 返 回 值: 1 表示按下, 0 表示未按下
*********************************************************************************************************
*/
uint8_t BspGetKeyState(KEY_ID_E _ucKeyID)
{
return s_tBtn[_ucKeyID].State;
}
/*
*********************************************************************************************************
* 函 数 名: bsp_SetKeyParam
* 功能说明: 设置按键参数
* 形 参:_ucKeyID : 按键ID,从0开始
* _LongTime : 长按事件时间
* _RepeatSpeed : 连发速度
* 返 回 值: 无
*********************************************************************************************************
*/
void BspSetKeyParam(uint8_t _ucKeyID, uint16_t _LongTime, uint8_t _RepeatSpeed)
{
s_tBtn[_ucKeyID].LongTime = _LongTime; /* 长按时间 0 表示不检测长按键事件 */
s_tBtn[_ucKeyID].RepeatSpeed = _RepeatSpeed; /* 按键连发的速度,0表示不支持连发 */
s_tBtn[_ucKeyID].RepeatCount = 0; /* 连发计数器 */
}
七:清空按键FIFO缓冲区
/*
*********************************************************************************************************
* 函 数 名: bsp_ClearKey
* 功能说明: 清空按键FIFO缓冲区
* 形 参:无
* 返 回 值: 按键代码
*********************************************************************************************************
*/
void BspClearKey(void)
{
s_tKey.Read = s_tKey.Write;
}
八:初始化按键硬件
static void BspInitKeyHard(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*开启按键GPIO口的时钟*/
RCC_AHB1PeriphClockCmd(RCC_ALL_KEY,ENABLE);
/* 第2步:配置所有的按键GPIO为浮动输入模式(实际上CPU复位后就是输入状态) */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_PIN_K1;
GPIO_Init(GPIO_PORT_K1, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_PIN_K2;
GPIO_Init(GPIO_PORT_K2, &GPIO_InitStructure);
}
九:初始化按键变量
/*
*********************************************************************************************************
* 函 数 名: bsp_InitKeyVar
* 功能说明: 初始化按键变量
* 形 参: 无
* 返 回 值: 无
*********************************************************************************************************
*/
static void BspInitKeyVar(void)
{
uint8_t i;
s_tKey.Read = 0;
s_tKey.Write = 0;
s_tKey.Read2 = 0;
for(i = 0; i < KEY_COUNT; 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 = 0; /* 按键连发的速度,0表示不支持连发 */
s_tBtn[i].RepeatCount = 0; /* 连发计数器 */
}
/* 如果需要单独更改某个按键的参数,可以在此单独重新赋值 */
/* 比如,我们希望按键1按下超过1秒后,自动重发相同键值 */
s_tBtn[KID_K1].LongTime = 100;
s_tBtn[KID_K1].RepeatSpeed = 5; /* 每隔50ms自动发送键值 */
s_tBtn[KID_K2].LongTime = 100;
s_tBtn[KID_K2].RepeatSpeed = 5; /* 每隔50ms自动发送键值 */
s_tBtn[0].IsKeyDownFunc = IsKeyDown1;
s_tBtn[1].IsKeyDownFunc = IsKeyDown2;
s_tBtn[2].IsKeyDownFunc = IsKeyDown3;
}
十:最重要的检测按键!最重要!
static void BspDetectKey(uint8_t i)
{
KEY_Typedef *pBtn;
pBtn = &s_tBtn[i];
if(pBtn->IsKeyDownFunc())
{
/*这个里面执行的是按键按下的处理*/
/* 下面这个if语句主要是用于按键滤波前给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 ++;
}
/* 这里实现KEY_FILTER_TIME时间长度的延迟 */
/* 个人理解以上两句就是软件消抖 */
else
{
/*
**********************************************************************************
这个State变量是有其实际意义的,如果按键按下了,这里就将其设置为1,如果没有按下,
这个变量的值就会一直是0,这样设置的目的可以有效的防止一种情况的出现:比如按键K1
在某个时刻检测到了按键有按下,那么它就会做进一步的滤波处理,但是在滤波的过程中,
这个按键按下的状态消失了,这个时候就会进入到上面第二步else语句里面,然后再做按键
松手检测滤波,滤波结束后判断这个State变量,如果前面就没有检测到按下,这里就不会
记录按键弹起。
**********************************************************************************
*/
if(pBtn->State == 0)
{
pBtn->State = 1;
/* 发送按钮按下的消息 */
BspPutKey((uint8_t)(3 * i + 1));
}
if(pBtn->LongTime > 0)
{
if(pBtn->LongCount < pBtn->LongTime)
{
/* 发送按钮按下的消息 */
if(++pBtn->LongCount == pBtn->LongTime)
{
/* 键值放入按键FIFO */
BspPutKey((uint8_t)(3 * i + 3));
}
}
else
{
if(pBtn->RepeatSpeed > 0)
{
if(++pBtn->RepeatCount >= pBtn->RepeatSpeed)
{
pBtn->RepeatCount = 0;
/* 长按键后,每隔10ms发送1个按键 */
BspPutKey((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;
BspPutKey((uint8_t)(3 * i + 2));
}
}
pBtn->LongCount = 0;
pBtn->RepeatCount = 0;
}
}
/* 然后就是逐一扫描按键 */
void BspKeyScan(void)
{
uint8_t i;
for(i = 0; i < KEY_COUNT; i++)
{
BspDetectKey(i);
}
}
十一:初始化按键
void BspInitKEY(void)
{
BspInitKeyVar();
BspInitKeyHard();
}
最后
定时起里对按键进行扫描,主函数对键值进行读取。
//定时器扫描硬件函数部分省略了
while(1)
{
uint8_t ucKeyCode = BspGetKey();
if(++i > 100000)
{
printf(" ucKeyCode = %d\r\n",ucKeyCode);
i = 0;
}
if(ucKeyCode != KEY_NONE)
{
switch (ucKeyCode)
{
case KEY_1_DOWN :
/* User Code 1*/
break;
case KEY_2_DOWN :
/* User Code 2*/
break;
default:
break;
}
}
}
优点
- 可靠地记录每一个按键事件,避免遗漏按键事件。特别是需要实现按键的按下、长按、自动连发、弹起等事件时。
- 读取按键的函数可以设计为非阻塞的,不需要等待按键抖动滤波处理完毕。
- 按键FIFO程序在嘀嗒定时器中定期的执行检测,不需要在主程序中一直做检测,这样可以有效地降低系统资源消耗。
Alipay
Wechat