用数电真值表的方式看嵌入式按键扫描

来源:漫长当下 嵌入式 13 次阅读
摘要:按键在嵌入式单板中几乎是必不可少的,可见其非常之重要,那么今天我们就来讲一下关于按键扫描的内容。 先讲一下按键抖动的内容吧,现在的单板系统中按键基本上都是机械式按键,那么机械式按键就存在抖动的问题,图1表示一个按键由按下到弹起的过程,理想波形是假设按键不存在抖动的状态,可以看到按键按下和弹起的界限是非常明显的,然而实际上机械按键都是有抖动的,也就如实际波形显示的那样,在按下和弹起的这段时间,按键存

按键在嵌入式单板中几乎是必不可少的,可见其非常之重要,那么今天我们就来讲一下关于按键扫描的内容。

先讲一下按键抖动的内容吧,现在的单板系统中按键基本上都是机械式按键,那么机械式按键就存在抖动的问题,图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*/    }}

后台回复“按键扫描”四个字可获取本文源码。

评论区

登录后即可参与讨论

立即登录