07. 按键输入
一、按键简介
常态下,独立按键是断开的,按下的时候才闭合。每个独立按键会单独占用一个 IO 口,通过 IO 口的高低电平判断按键的状态。但是按键在闭合和断开的时候,都存在抖动现象,即按键在闭合时不会马上就稳定的连接,断开时也不会马上断开。这是机械触点,无法避免。
按键抖动波形图如下:
按下抖动和释放抖动的时间一般为 5~10ms,如果在抖动阶段采样,其不稳定状态可能出现一次按键动作被认为是多次按下的情况。为了避免抖动可能带来的误操作,我们要做的措施就是给按键消抖(即采样稳定闭合阶段)。消抖方法分为硬件消抖和软件消抖,我们常用软件的方法消抖。
软件消抖,最简单的延时消抖。检测到按键按下后,一般进行10ms 延时,用于跳过抖动的时间段,如果消抖效果不好可以调整这个 10ms 延时,因为不同类型的按键抖动时间可能有偏差。待延时过后再检测按键状态,如果没有按下,那我们就判断这是抖动或者干扰造成的;如果还是按下,那么我们就认为这是按键真的按下了。对按键释放的判断同理。
硬件消抖,利用 RC 电路的电容充放电特性来对抖动产生的电压毛刺进行平滑出来,从而实现消抖,但是成本会更高一点。
二、原理图
K1、K2 和 K3 设计为采样到按键另一端的低电平为有效电平,而 KEY_UP 则需要采样到高电平才为按键有效,并且按键外部没有上下拉电阻,所以需要在 STM32F407 内部设置上下拉。因此,K1、K2 和 K3 配置为 上拉输入,KEY_UP 配置为 下拉输入。
三、程序源码
#define WKUP_GPIO_PORT GPIOA
#define WKUP_GPIO_PIN GPIO_PIN_0
#define WKUP_GPIO_CLK_ENABLE() do{ \
__HAL_RCC_GPIOA_CLK_ENABLE(); \
} while(0);
#define KEY1_GPIO_PORT GPIOE
#define KEY1_GPIO_PIN GPIO_PIN_4
#define KEY1_GPIO_CLK_ENABLE() do{ \
__HAL_RCC_GPIOE_CLK_ENABLE(); \
} while(0);
#define KEY2_GPIO_PORT GPIOE
#define KEY2_GPIO_PIN GPIO_PIN_3
#define KEY2_GPIO_CLK_ENABLE() do{ \
__HAL_RCC_GPIOE_CLK_ENABLE(); \
} while(0);
#define KEY3_GPIO_PORT GPIOE
#define KEY3_GPIO_PIN GPIO_PIN_2
#define KEY3_GPIO_CLK_ENABLE() do{ \
__HAL_RCC_GPIOE_CLK_ENABLE(); \
} while(0);
/* 读取按键对应的GPIO引脚的电平状态 */
#define WK_UP HAL_GPIO_ReadPin(WKUP_GPIO_PORT, WKUP_GPIO_PIN)
#define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_PORT, KEY1_GPIO_PIN)
#define KEY2 HAL_GPIO_ReadPin(KEY2_GPIO_PORT, KEY2_GPIO_PIN)
#define KEY3 HAL_GPIO_ReadPin(KEY3_GPIO_PORT, KEY3_GPIO_PIN)
/* 对应按键按下时代表的数值 */
#define WKUP_PRESS 1
#define KEY1_PRESS 2
#define KEY2_PRESS 3
#define KEY3_PRESS 4
按键初始化函数内容如下:
/**
* @brief 按键初始化函数
*
*/
void Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
WKUP_GPIO_CLK_ENABLE();
KEY1_GPIO_CLK_ENABLE();
KEY2_GPIO_CLK_ENABLE();
KEY3_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = WKUP_GPIO_PIN; // GPIO引脚
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式
GPIO_InitStruct.Pull = GPIO_PULLDOWN; // 使用下拉
HAL_GPIO_Init(WKUP_GPIO_PORT, &GPIO_InitStruct); // GPIO初始化
GPIO_InitStruct.Pin = KEY1_GPIO_PIN; // GPIO引脚
GPIO_InitStruct.Pull = GPIO_PULLUP; // 使用上拉
HAL_GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStruct); // GPIO初始化
GPIO_InitStruct.Pin = KEY2_GPIO_PIN; // GPIO引脚
HAL_GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStruct); // GPIO初始化
GPIO_InitStruct.Pin = KEY3_GPIO_PIN; // GPIO引脚
HAL_GPIO_Init(KEY3_GPIO_PORT, &GPIO_InitStruct); // GPIO初始化
}
按键扫描函数内容如下:
/**
* @brief 按键扫描函数
*
* @param mode 0:不支持连续按;1:支持连续按
* @return uint8_t WKUP_PRES,1,WKUP按下
* KEY1_PRES,2,KEY0按下
* KEY2_PRES,3,KEY1按下
* KEY3_PRES,4,KEY2按下
*
* @note 该函数有响应优先级,同时按下多个按键:WK_UP > KEY3 > KEY2 > KEY1
*/
uint8_t Key_Scan(uint8_t mode)
{
static uint8_t flag = 1; // 按键按松开标志
uint8_t keyValue = 0;
flag = (mode ? 1 : flag); // 支持连按
if (flag && ( WK_UP == 1 || KEY1 == 0 || KEY2 == 0 || KEY3 == 0)) // 按键松开标志为1, 且有任意一个按键按下了
{
HAL_Delay(10); // 按键消抖
flag = 0;
// 再次读取GPIO引脚的电平
keyValue = ((KEY1 == 0) ? KEY1_PRESS : keyValue);
keyValue = ((KEY2 == 0) ? KEY2_PRESS : keyValue);
keyValue = ((KEY3 == 0) ? KEY3_PRESS : keyValue);
keyValue = ((WK_UP == 1) ? WKUP_PRESS : keyValue);
}
else if (WK_UP == 0 && KEY1 == 1 && KEY2 == 1 && KEY3 == 1) // 没有任何按键按下, 标记按键松开
{
flag = 1;
}
return keyValue; // 按键没有按下返回0
}
main() 函数内容如下:
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
LED_Init();
Key_Init();
while (1)
{
switch (Key_Scan(0))
{
case WKUP_PRESS:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_RESET); // 点亮LED1
break;
case KEY1_PRESS:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_10, GPIO_PIN_RESET); // 点亮LED2
break;
case KEY2_PRESS:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_SET); // 熄灭LED1
break;
case KEY3_PRESS:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_10, GPIO_PIN_SET); // 熄灭LED2
break;
}
}
return 0;
}