按键在嵌入式单板中几乎是必不可少的,可见其非常之重要,那么今天我们就来讲一下关于按键扫描的内容。
先讲一下按键抖动的内容吧,现在的单板系统中按键基本上都是机械式按键,那么机械式按键就存在抖动的问题,图1表示一个按键由按下到弹起的过程,理想波形是假设按键不存在抖动的状态,可以看到按键按下和弹起的界限是非常明显的,然而实际上机械按键都是有抖动的,也就如实际波形显示的那样,在按下和弹起的这段时间,按键存在抖动,导致输入到单片机的信号也存在电平上的抖动,这样就会出现误检测,那么如何避免或者减少误检测的这种情况呢,大致上可以分为两种处理办法(现在有些芯片使用施密特触发器解决抖动,自带消抖功能),一种就是硬件消抖,另一种就是软件消抖,两种方法没有孰优孰劣,硬件消抖会增加硬件成本,软件消抖会稍微增加软件开发人员的工作量,目前普遍的处理方法是使用软件消抖,当然两者结合肯定效果更好。

图1
本文的重点不在于消抖,而在于按键扫描的方式和单按键多功能的实现思想,但此方法解决了抖动的问题,相信电子专业的同学都学过数字电路这门课,对真值表应该不陌生了,今天介绍的方法和真值表非常相似,假设我们将按键的扫描状态分为上次的扫描状态和本次的扫描状态,按键的有效电平记作1,无效(空闲)电平记作0,那么就可以形成以下的“真值表”:
| 上次扫描状态 | 本次扫描状态 | 按键真实状态 |
|---|---|---|
| 0 | 0 | 释放 |
| 0 | 1 | 按下 |
| 1 | 0 | 弹起 |
| 1 | 1 | 任然按下 |
既然得到了真值表,那么代码就非常简单了,其实现如下(硬件平台使用的STM32,但是除了硬件初始化和读取IO电平状态相关的内容,其它都是与硬件平台无关的):
#ifndef __KEY_BOARD_H#define __KEY_BOARD_H
#include "stm32f4xx_hal.h"#include <stdbool.h>
/*内部上拉输入,低有效*/#define KEY_PORT_1 GPIOB#define KEY_PIN_1 GPIO_PIN_10#define KEY_PRESS_LEVEL_1 GPIO_PIN_RESET#define KEY_PORT_2 GPIOE#define KEY_PIN_2 GPIO_PIN_14#define KEY_PRESS_LEVEL_2 GPIO_PIN_RESET#define KEY_PORT_3 GPIOE#define KEY_PIN_3 GPIO_PIN_11#define KEY_PRESS_LEVEL_3 GPIO_PIN_RESET#define KEY_PORT_4 GPIOE#define KEY_PIN_4 GPIO_PIN_15#define KEY_PRESS_LEVEL_4 GPIO_PIN_RESET#define KEY_PORT_5 GPIOE#define KEY_PIN_5 GPIO_PIN_12#define KEY_PRESS_LEVEL_5 GPIO_PIN_RESET#define KEY_PORT_6 GPIOE#define KEY_PIN_6 GPIO_PIN_13#define KEY_PRESS_LEVEL_6 GPIO_PIN_RESET/*推挽输出,低使能,高失能*/#define KEY_CTL_LINE_ENABLE GPIO_PIN_RESET#define KEY_CTL_LINE_DISABLE GPIO_PIN_SET
typedef enum { KEY_UP, KEY_LEFT, KEY_DOWN, KEY_ENTER, KEY_RIGHT, KEY_EXIT, KEY_NUM_CNT,}_keyIdSig_e;typedef enum { KEY_CTL_LINE_CNT,}_keyIdCtl_e;/*每条控制线上的信号线数量,仅使用在矩阵式键盘中*/#define SIGLINE_PER_CTLLINE (KEY_NUM_CNT / (KEY_CTL_LINE_CNT ? KEY_CTL_LINE_CNT : 1))typedef enum { KEY_NONE, /*未按下*/ KEY_RELEASE, /*弹起*/ KEY_PRESS, /*按下*/ KEY_PRESSING, /*任然按下*/ KEY_PRESS_DOUBLE, /*双击(两次按下)*/ KEY_RELEASE_DOUBLE, /*双击(两次弹起)*/}_keyState_e;typedef struct { uint32_t ifKey; uint32_t spaceTime;}doubleClickTypeDef;typedef struct { _keyState_e keyState_e; bool keyLastSatus; bool keyThisStatus; doubleClickTypeDef doubleClick_Press; doubleClickTypeDef doubleClick_Release;}_keyCom_t;typedef union { uint32_t keyValAll; struct { uint32_t bitKeyUp : 1; uint32_t bitKeyLeft : 1; uint32_t bitKeyDown : 1; uint32_t bitKeyEnter : 1; uint32_t bitKeyRight : 1; uint32_t bitKeyExit : 1; }_keyBit;}_keyVal_u;
/*按键 GPIO 初始化*/void GPIO_Key_Board_Init(void);/*使能按键*/void KeyEnable(void);/*失能按键*/void KeyDisable(void);/*按键扫描*/void Key_Scan(void);/*获取按键的状态*/_keyState_e KeyGetState(_keyIdSig_e keyIdSig);/*检查是否有键按下(如果是弹起有效则是检测弹起)*/bool IfAnyKeyPress(void);
#endif/*KEY_BOARD_H*/
#include "./key_board/key_board.h"#include "debug.h"
/*定义按键公用结构体数组*/_keyCom_t aKeyCom[KEY_NUM_CNT];/*定义一个联合体存储键值*/_keyVal_u keyVal;
/*信号线*/uint16_t aGPIO_Pin_SigLine[] = { KEY_PIN_1,KEY_PIN_2,KEY_PIN_3, KEY_PIN_4,KEY_PIN_5,KEY_PIN_6,};GPIO_TypeDef* aGPIO_Port_SigLine[] = { KEY_PORT_1,KEY_PORT_2,KEY_PORT_3, KEY_PORT_4,KEY_PORT_5,KEY_PORT_6,};GPIO_PinState aKeyPresslevel_SigLine[] = { KEY_PRESS_LEVEL_1,KEY_PRESS_LEVEL_2,KEY_PRESS_LEVEL_3, KEY_PRESS_LEVEL_4,KEY_PRESS_LEVEL_5,KEY_PRESS_LEVEL_6,};/*控制线*/uint16_t aGPIO_Pin_CtlLine[] = { NULL,};GPIO_TypeDef* aGPIO_Port_CtlLine[] = { NULL,};/*按键 GPIO 初始化*/void GPIO_Key_Board_Init(void){ GPIO_InitTypeDef GPIO_InitStruct; __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pin = KEY_PIN_1; HAL_GPIO_Init(KEY_PORT_1,&GPIO_InitStruct); GPIO_InitStruct.Pin = KEY_PIN_2; HAL_GPIO_Init(KEY_PORT_2,&GPIO_InitStruct); GPIO_InitStruct.Pin = KEY_PIN_3; HAL_GPIO_Init(KEY_PORT_3,&GPIO_InitStruct); GPIO_InitStruct.Pin = KEY_PIN_4; HAL_GPIO_Init(KEY_PORT_4,&GPIO_InitStruct); GPIO_InitStruct.Pin = KEY_PIN_5; HAL_GPIO_Init(KEY_PORT_5,&GPIO_InitStruct); GPIO_InitStruct.Pin = KEY_PIN_6; HAL_GPIO_Init(KEY_PORT_6,&GPIO_InitStruct);}/*使能按键*/void KeyEnable(void){}/*失能按键*/void KeyDisable(void){}/*按键扫描的间隔时间*/uint32_t Key_GetScanPeriod(void){ //此处返回按键扫描的间隔时间(即每隔多久调用一次Key_Scan();函数,根据实际值修改) return 30;}/*控制线选择*/void GPIO_CtlLineChoose(_keyIdCtl_e keyIdCtl){ _keyIdCtl_e tmp; /*即将扫描的控制线拉到有效电平*/ HAL_GPIO_WritePin(aGPIO_Port_CtlLine[keyIdCtl],aGPIO_Pin_CtlLine[keyIdCtl],KEY_CTL_LINE_ENABLE); if(keyIdCtl == (_keyIdCtl_e)0) { tmp = (_keyIdCtl_e)(KEY_CTL_LINE_CNT - (_keyIdCtl_e)1); } else { tmp = (_keyIdCtl_e)(keyIdCtl - (_keyIdCtl_e)1); } /*上一次扫描的控制线拉到无效电平*/ HAL_GPIO_WritePin(aGPIO_Port_CtlLine[tmp],aGPIO_Pin_CtlLine[tmp],KEY_CTL_LINE_DISABLE);}/*读取 IO 电平*/bool GPIO_ReadLevel(_keyIdSig_e keyIdSig){ return HAL_GPIO_ReadPin(aGPIO_Port_SigLine[keyIdSig],aGPIO_Pin_SigLine[keyIdSig]) == aKeyPresslevel_SigLine[keyIdSig]?true:false;}/*按键扫描*/void Key_Scan(void){ _keyIdCtl_e tmp = (_keyIdCtl_e)0; for(uint32_t i = 0;i < sizeof(aKeyCom) / sizeof(*aKeyCom);i++) { if(KEY_CTL_LINE_CNT && ((i % SIGLINE_PER_CTLLINE) == 0)) { GPIO_CtlLineChoose((_keyIdCtl_e)(tmp++)); } aKeyCom[i].keyThisStatus = GPIO_ReadLevel((_keyIdSig_e)(i % SIGLINE_PER_CTLLINE)); /*对应真值表的四种状态*/ if((!(aKeyCom[i].keyThisStatus)) && (!(aKeyCom[i].keyLastSatus)))//00 { aKeyCom[i].keyState_e = KEY_NONE; keyVal.keyValAll &= (~(0x01UL << i)); } else if((!(aKeyCom[i].keyThisStatus)) && (aKeyCom[i].keyLastSatus))//01 { aKeyCom[i].keyState_e = KEY_RELEASE;// keyVal.keyValAll &= (~(0x01UL << i)); keyVal.keyValAll |= (0x01UL << i); } else if((aKeyCom[i].keyThisStatus) && (!(aKeyCom[i].keyLastSatus)))//10 { aKeyCom[i].keyState_e = KEY_PRESS;// keyVal.keyValAll |= (0x01UL << i); } else if((aKeyCom[i].keyThisStatus) && (aKeyCom[i].keyLastSatus))//11 { aKeyCom[i].keyState_e = KEY_PRESSING; } aKeyCom[i].keyLastSatus = aKeyCom[i].keyThisStatus; } /*如果不要按键的双击事件,此循环体不需要执行*/ for(uint32_t i = 0;i < sizeof(aKeyCom) / sizeof(*aKeyCom);i++) { if(KeyGetState((_keyIdSig_e)i) == KEY_PRESS) { //标记按下次数 if(++aKeyCom[i].doubleClick_Press.ifKey >= 2) { aKeyCom[i].keyState_e = KEY_PRESS_DOUBLE; aKeyCom[i].doubleClick_Press.ifKey = 0; aKeyCom[i].doubleClick_Press.spaceTime = 0; } } if(aKeyCom[i].doubleClick_Press.ifKey) { if(++aKeyCom[i].doubleClick_Press.spaceTime >= 1000 / Key_GetScanPeriod()) { aKeyCom[i].doubleClick_Press.ifKey = 0; aKeyCom[i].doubleClick_Press.spaceTime = 0; } } if(KeyGetState((_keyIdSig_e)i) == KEY_RELEASE) { //标记弹起次数 if(++aKeyCom[i].doubleClick_Release.ifKey >= 2) { aKeyCom[i].keyState_e = KEY_RELEASE_DOUBLE; aKeyCom[i].doubleClick_Release.ifKey = 0; aKeyCom[i].doubleClick_Release.spaceTime = 0; } } if(aKeyCom[i].doubleClick_Release.ifKey) { if(++aKeyCom[i].doubleClick_Release.spaceTime >= 1000 / Key_GetScanPeriod()) { aKeyCom[i].doubleClick_Release.ifKey = 0; aKeyCom[i].doubleClick_Release.spaceTime = 0; } } }}/*获取按键的状态 枚举类型*/_keyState_e KeyGetState(_keyIdSig_e keyIdSig){ if(keyIdSig >= KEY_NUM_CNT){ return KEY_NONE; } return aKeyCom[keyIdSig].keyState_e;}/*检查是否有键按下(如果是弹起有效则是检测弹起)*/bool IfAnyKeyPress(void){ return ((keyVal.keyValAll != 0)?true:false);}
此代码移植性高,短短几行便实现了最重要的功能,不像使用状态机,复杂度高,代码难以理解,一定要拒绝使用任何带阻塞延时的方法,使用阻塞延时的代码移植性非常差,且不稳定。
使用示例如下:
/*用户函数*/void UserFun(void){ if(GetKeyState(KEY1) == KEY_PRESS) { /*do something*/ } if(GetKeyState(KEY1) == KEY_RELEASE) { /*do something*/ } if(GetKeyState(KEY1) == KEY_PRESSING) { /*do something*/ } if(GetKeyState(KEY2) == KEY_PRESS) { /*do something*/ } if(GetKeyState(KEY2) == KEY_RELEASE) { /*do something*/ } if(GetKeyState(KEY2) == KEY_PRESSING) { /*do something*/ }}
后台回复“按键扫描”四个字可获取本文源码。
评论区
登录后即可参与讨论
立即登录