本文记录一种非阻塞按键的软件管理机制。
前言
大多数单片机的按键教程中,按键防抖大多使用延时来处理,一般延时阻塞个10ms,然而就算对于51单片机的运行速度来说,10ms也能做很多很多事情,比如前面讨论过的刷新8位数码管消耗15ms,显然刷4位就要不了10ms。再比如之前常讨论的协调中断与主循环的运行时间问题,10ms的时间可太长了。
一、按键的硬件电路连接
使用最简单的按键电路,组件仅包含一个单片机IO和一个按键。由于STC89C52RC的P3口内部有上拉电阻,所以也不需要在外面加什么东西,K1按下的时候单片机P3.1的电平是低电平,没按下P3.1就是高电平。
二、软件管理机制
1.实体按键与虚拟按键,按键管理结构与按键轮询
有一些东西的参数设置页面需要长按按键才能进入,可以有效地杜绝大多数的误触,不小心碰到了呀,小孩子好奇这里摸一下那里摸一下呀。
按键既可以短按也可以长按,可以看作是一个实体按键衍生出了两个按键。按键长短按的判别本质上是通过计算按键按下的时间来进行的,通过设定不同的目标时间,就可以区别出按键的长短按。因而可以构造出按键管理器。
typedef struct
{
unsigned int keyTrigCounter;
unsigned char activeState;
unsigned int keyTrigCounterTarget1; //短按计数目标值
unsigned int keyTrigCounterTarget2; //长按计数目标值
}realKeyTrigMrg_t;
约定keyTrigCounterTarget1是短按计数目标值,keyTrigCounterTarget2对应长按。当程序轮询到按键低电平时,keyTrigCounter就加一,并且不等待继续向下执行,如果按键无效,那么keyTrigCounter总是被清零以去抖动。程序如此往复运行,当keyTrigCounter加到目标值1时认为是按键短按,加到目标值2时认为是按键长按。但是这里面有几个说道,直接看下面的按键轮询伪代码。
if("识别到按键管脚低电平"){
if(keyTrigCounter < 0xFFFF)
keyTrigCounter++;
if(keyTrigCounter >= keyTrigCounterTarget2){
if(activeState == 0){ //防止长按重复触发,但是如果要使能长按快加/快减就不应使用
"将虚拟按键2添加到活动的按键列表";
activeState = 1;
}
}
}
else{ //短按模式,抬手才进行统计
if(keyTrigCounter >= keyTrigCounterTarget1){
if(activeState == 0){ //防止长按抬手导致的短按触发
"将虚拟按键1添加到活动的按键列表";
}
}
keyTrigCounter = 0;
activeState = 0;
}
- 通常来说人们有可能会知道按键有长按功能,但不见得知道长按需要保持多少秒。所以按键扫描机制里面长按识别不等待按键释放,只要按下的时间够了就立即认为长按有效。短按的话就需要按键释放之后才开始统计判定比较贴近实际。
- keyTrigCounter自加之前要先判断是否小于某个值,因为这是个双字节整型变量,取值范围0-65535,当变量为65535时再加一就回零了。
- 由于要到达长按一定会经过短按,所以增加了一个activeState标志用于使长按和短按成为互斥事件。
- activeState标志在长按判定中的使用,如果不使用它,那么长按触发一次之后如果不曾释放按键,那么长按事件就会重复触发,这个功能在长按快速增加或减少设定值时比较有用。如果不希望长按持续触发,那就需要在长按中加入activeState判定标志。
- keyTrigCounterTarget1和keyTrigCounterTarget2的值的设定问题,由于按键运行涉及到实际手按输入,并且扫描代码中有不少if分支,所以就依靠感觉就可以的那种感觉,装载一个初值实际慢慢试。
考虑当按键按下时显示某个帮助页面,当按键释放时回到主页面,讨论主题是按键释放检测。在上面的按键轮询伪代码的基础上,进行简单的改动如下所示(修改1,2)。经过修改,程序就获得了按键释放检测的能力,不过这时候只能识别出按下(伪代码中:虚拟按键2)和释放(伪代码中:虚拟按键3)两种状态。
if("识别到按键管脚低电平"){
if(keyTrigCounter < 0xFFFF)
keyTrigCounter++;
if(keyTrigCounter >= keyTrigCounterTarget2){
if(activeState == 0){ //防止长按重复触发,但是如果要使能长按快加/快减就不应使用
"将虚拟按键2添加到活动的按键列表";
activeState = 1;
}
}
}
else{ //短按模式,抬手才进行统计
if(keyTrigCounter >= keyTrigCounterTarget1){
if(activeState == 0) //防止长按抬手导致的短按触发
"将虚拟按键1添加到活动的按键列表";
}
#error "修改1:在下面增加长按检测的代码并删除activeState内嵌判断语句"
#error "修改2:将keyTrigCounterTarget2缩短为和keyTrigCounterTarget1一样的值使得长按变成短按"
if(keyTrigCounter >= keyTrigCounterTarget2){
"按键有效并且正确释放,在这里将虚拟按键3添加到活动的按键列表";
}
keyTrigCounter = 0;
activeState = 0;
}
2.事件
所有的按键行为都可以抽象为事件,比如单个按键长按、短按、释放,组合按键等等,不管实际表现出来的是什么方式,都可以抽象为各种独立的事件。
事件的好处是使系统的程序模块之间的接口更明晰,因为按键识别的任务仅仅只是将按键行为转化成虚拟事件。而处理函数只接收事件输入,这样一来它并不需要知道输入是由按键产生的,事件源也可以是外部中断、电源保护等等。
//虚拟按键事件,支持最多16个事件
#define KEY_SW_1_PRESSED 0x0001
#define KEY_SW_2_PRESSED 0x0002
#define KEY_SW_3_PRESSED 0x0004
#define KEY_SW_4_PRESSED 0x0008
...
#define KEY_SW_16_PRESSED 0x8000
上面定义的每个事件都占用双字节的某一位,那么实际的事件列表只需要一个双字节变量就可以容纳,假定使用virtualKeys来充当事件列表。要往事件列表里面添加虚拟按键事件1,使用下面的操作。
static unsigned int virtualKeys = 0x0000;
virtualKeys |= KEY_SW_1_PRESSED; //向事件列表中添加虚拟按键事件1
要查看事件列表中是否存在虚拟按键事件1,使用下面的操作。
if(virtualKeys & KEY_SW_1_PRESSED){
//to do
}
要从事件列表中清除虚拟按键事件1,使用下面的操作。
virtualKeys ^= KEY_SW_1_PRESSED;
3.多按键管理
有多个实体按键的情况只是单个按键情况的组合,并且得益于正在讨论的按键扫描机制,组合按键检测得以实现,不过组合按键检测从另一个角度来看是“独立互斥事件”的组合,这显然是处理函数该考虑的事情。
为了使管理器更加清晰明了,使用结构体数组和宏来进行管理。
#define NUMBER_OF_REAL_KEY 3
#define KEY_REAL_A 0
#define KEY_REAL_B 1
#define KEY_REAL_C 2
typedef struct
{
unsigned int keyTrigCounter;
unsigned char activeState;
unsigned int keyTrigCounterTarget1;
unsigned int keyTrigCounterTarget2;
}realKeyTrigMrg_t;
static realKeyTrigMrg_t realKeyGroupTrigMrg[NUMBER_OF_REAL_KEY];
4. 完整的按键模块代码
头文件。
#ifndef _KEY_H_
#define _KEY_H_
/*********与单片机平台相关的内容开始*********/
#include "reg52.h"
sbit KEY_REAL_A_PIN = P3^1;
/*********与单片机平台相关的内容结束*********/
//虚拟按键事件,支持最多16个事件
#define KEY_SW_1_PRESSED 0x0001
#define KEY_SW_2_PRESSED 0x0002
#define KEY_SW_3_PRESSED 0x0004
#define KEY_SW_4_PRESSED 0x0008
typedef void (*keyResultProcess_p)(unsigned int keyMasks);
extern void keyInit(keyResultProcess_p keyResultProcessFn); //按键初始化,初始化时将用户按键处理函数传入
extern void getKeyStateAndDoUserProcess(void); //扫描按键并执行用户函数
extern void addKeyEventToVirtualKeys(unsigned int keyMasks);//向按键列表添加指定的按键事件
#endif
C文件。
#include "key.h"
#include <string.h>
#define NUMBER_OF_REAL_KEY 1
#define KEY_REAL_A 0
typedef struct
{
unsigned int keyTrigCounter;
unsigned char activeState;
unsigned int keyTrigCounterTarget1;
unsigned int keyTrigCounterTarget2;
}realKeyTrigMrg_t;
static unsigned int virtualKeys = 0x0000;
static keyResultProcess_p keyResultProcessFunction = NULL;
static realKeyTrigMrg_t realKeyGroupTrigMrg[NUMBER_OF_REAL_KEY];
void keyInit(keyResultProcess_p keyResultProcessFn)
{
if(keyResultProcessFn != NULL)
keyResultProcessFunction = keyResultProcessFn;
memset(realKeyGroupTrigMrg,0,sizeof(realKeyGroupTrigMrg));
realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounterTarget1 = 305; //短按
realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounterTarget2 = 15000; //长按大概3秒
}
void getKeyStateAndDoUserProcess(void){
//按键A
if(KEY_REAL_A_PIN == 0){
if(realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounter < 0xFFFF)
realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounter++;
if(realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounter >= realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounterTarget2){
if(realKeyGroupTrigMrg[KEY_REAL_A].activeState == 0){ //防止长按重复触发
virtualKeys |= KEY_SW_2_PRESSED;
realKeyGroupTrigMrg[KEY_REAL_A].activeState = 1;
}
}
}
else{ //短按模式,抬手才进行统计
if(realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounter >= realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounterTarget1){
if(realKeyGroupTrigMrg[KEY_REAL_A].activeState == 0){ //防止长按抬手导致的短按触发
virtualKeys |= KEY_SW_1_PRESSED;
}
}
if(realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounter >= realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounterTarget2){
virtualKeys |= KEY_SW_3_PRESSED; //按键释放事件,将长按时间改短充当短按,屏蔽掉防止长按重复触发,释放得以执行
}
realKeyGroupTrigMrg[KEY_REAL_A].keyTrigCounter = 0;
realKeyGroupTrigMrg[KEY_REAL_A].activeState = 0;
}
if(keyResultProcessFunction != NULL)
keyResultProcessFunction(virtualKeys);
virtualKeys = 0x0000;
}
//所有的输入控制源均被抽象为虚拟按键
void addKeyEventToVirtualKeys(unsigned int keyMasks)
{
virtualKeys |= keyMasks;
}