1.3.5 按键扫描
按键获取一般有两种方式(图1-14):一是按键触发输入脚中断;二是扫描读取输入脚电平。然而,常规按键是一种机械结构,当按下和释放时,存在毛刺问题,如图1-15所示。并且这个毛刺的严重程度跟按键触点的接触良好性有关,典型的例子是鼠标时间用久了,会出现按键失效或者是一次按键出现两次连击现象,这往往是鼠标触点氧化导致的,触点清洗一下即可。
图1-14 按键获取电路图
图1-15 按键出现问题
为了解决按键接触引起的误触发,需要引入一个小抖动处理,中断读取的方式,在中断产生后,关闭中断,延时一定时间在按键稳定区读取按键值,之后再延时一段时间,重新打开中断响应,以避开释放时的抖动二次中断。扫描方式一般利用节拍来采样,当检测到有连续两次及以上有效按键值的,认为产生了一次按键事件,为了防止按键释放时抖动引起二次按键,一般还需要加入防止双击处理。
MS的按键处理是第二种扫描方式,利用节拍例行处理来扫描GPIO获取,考虑到实际按键往往超过10个,直接利用MCU51的P1口的8个脚作为按键扫描脚(图1-16),采用4×4交叉扫描方式实现16个按键(图1-17),解决MCU51端口少的问题。
图1-16 MCU51的P1口按键扫描脚
图1-17 MS按键交叉的扫描方式
4×4按键如图1-16所示,P1口8个端口都配置为双向端口,其中输出为OC门输出,带上拉电阻,在干扰大的场合,需要外部扩展上拉电阻,提高抗干扰能力。假设按下了“A”按键,我们来分析4×4扫描按键的原理。
1)设置X=0,获取Y=4’b1011。
2)设置Y=0,获取X=4’b1101。
3)合并X、Y得到扫描码8’b10111101。
4)扫描码8’b10111101映射到按键码“A”。
这种4×4扫描方式,很适合一次只按一个按键的操作,这时,扫描码与按键码是一一映射的,具有唯一性。若存在多个按键同时按下的情况时,比如PC的Crtl+Alt+Z这种组合按键,这种扫描方式就不适合了,因为存在不唯一性问题。
MS的按键扫描流程如图1-18所示,间隔10ms的系统节拍例行查询按键扫描码,当发现扫描码不为invalid,也就是不等于8’b11111111,说明有按键按下,保存第一次扫描码和第二次扫描码,之后等待按键释放。当检测到扫描码为invalid时,说明按键已经释放。第一步检测当前按键是否跟上一次按键间隔太紧,也就是说是否存在二次连击、按键接触不良引起的误操作,若有,则需要一个二次连击的计数器避免误操作。之后再判断按键值是否正确,若不正确直接退出;若正确,区分按键操作类型,是长按键,还是短按键,(工业设备,按键不是很多)往往用长按和短按来增加按键数,这个类似PC按键的Shift按键或者是Caps Lock按键,把字母分为大写和小写一个道理,工业设备,按键一般没有连按,怕误操作。区分了长短按键之后,再发送按键消息,一个完整的按键获取处理完成,按键设备复位。
图1-18 按键扫描流程
按键设备的数据结构定义如下。
代码清单1-17:按键宏及变量定义
#define ShortInterval 2 // 短按按键间隔 >= 2 #define LongInterval 40 // 长按按键间隔 #define DoubleHitInterval 20 // 防按键双击误动间隔 static Byte idata ScanFirstData = invalid; // 第一次采集按键值 static Byte idata ScanSecondData = invalid; // 第二次采集按键值 static Byte idata ScanCounter = 0; // 按下按键计数器 static Byte idata DoubleHitCounter = 0; // 防按键双击误动计数器
一般采用MCU51的P1口作为按键扫描口,采用4×4矩阵扫描方式,用户只需要修改矩阵扫描部分硬件及代码即可。建议添加上拉电阻,电阻值在10kΩ左右可以提高按键准确性,且抗干扰能力强,若干扰严重时,可将上拉电阻阻值下调。代码如下。
代码清单1-18:按键扫描
static Byte ScanPin(void) { Byte scan; P1 = 0x0F; Delay(1); // P1.7~P1.4置0,延时,等待电路稳定 scan = P1; // 读取低4位扫描值 P1 = 0xF0; Delay(1); // P1.3~P1.0置0,延时,等待电路稳定 scan = scan | P1 ; // 读取高四位扫描值并与低四位合并 return(scan); // 返回值 }
按键扫描必须要基于系统节拍例行扫描,所以要有一个系统节拍例行处理函数,代码如下。
代码清单1-19:按键系统节拍例行程序
void KeySystickRoutine(void) { Byte scan, key; scan = ScanPin(); // 获取扫描码 if (scan != invalid) // 判断是否有按键按下 { ScanCounter++; if (ScanCounter == 1) // 保存第一次扫描码 ScanFirstData = scan; else if(ScanCounter == 2) // 保存第二次扫描码 ScanSecondData = scan; else if (ScanCounter > LongInterval) // 防止计数器循环 ScanCounter = LongInterval; } else { if(DoubleHitCounter) // 防止连续双击按键 { DoubleHitCounter--; ScanCounter = 0; return; } if (ScanCounter == 0) return; // 若没有扫描码,退出 if ((ScanFirstData == invalid) || (ScanFirstData != ScanSecondData)) { ScanCounter = 0; // 判断扫描码是否正确 return; } if (ScanCounter == LongInterval) // 判断是否为长按键 key = RemapLongKey(ScanFirstData); // 通过扫描码获取长按键值 else if (ScanCounter >= ShortInterval) // 判断是否为短按键 key = RemapKey(ScanFirstData); // 通过扫描码获取短按键值 else key = invalid; // 误操作,无效 if (key != invalid) { PostMessage(MessageKey, key); // 发送按键 DoubleHitCounter=DoubleHitInterval; // 设定二次连击计数器 } ScanFirstData = invalid; // 按键设备复位 ScanSecondData = invalid; ScanCounter = 0; } }