按键消抖和矩阵键盘的扫描进阶
将按键检测驱动放到定时器中断中
在按下按键的时候,在闭合和断开的瞬间有一连串的抖动。
这样一次按下的动作可能会触发很多次。
所以,当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或断开稳定后再进行处理。 按键消抖可分为硬件消抖和软件消抖。
消除抖动有软件和硬件两种方法。
通常我们用软件消抖。
最简单的消抖原理,就是当检 测到按键状态变化后,先等待一个 10ms 左右的延时时间,让抖动消失后再进行一次按键状 态检测,如果与刚才检测到的状态相同,就可以确认按键已经稳定的动作了。
但是。
当我们的工程庞大的时候,这种的方法就会出现某些问题。
while(1) 这个主循环要不停的扫描各种状态值是否有发生变化,及时的进行任务调度,如果程序中间 加了这种 delay 延时操作后,很可能某一事件发生了,但是我们程序还在进行 delay 延时操作 中,当这个事件发生完了,程序还在 delay 操作中,当我们 delay 完事再去检查的时候,已经 晚了,已经检测不到那个事件了。
就出现了按键消抖的优化:
启用定时器中断,每 2ms 进一次中断,扫描 一次按键状态并且存储起来,连续扫描 8 次后,看看这连续 8 次的按键状态是否是一致的。 8 次按键的时间大概是 16ms,这 16ms 内如果按键状态一直保持一致,那就可以确定现在按 键处于稳定的阶段,而非处于抖动的阶段。
这样就会避免delay占用单片机程序执行时间。
而是变成了按键状态判定,而不是按键过程判定。
这只是按键消抖算法之一。
下面的代码是按键消抖和识别的模块化代码:
定义部分:
uint8 code KeyCodeMap[4] = { //4位独立按键到标注按键的映射表 0x0d,//回车键 0x26,//上键 0x28,//下键 0x1b //ESC键 }; uint8 pdata KeySta[4] = { //4位独立按键当前状态 1, 1, 1, 1 };
code KeyCodeMap是按键对应的功能值
KeyDriver():
void KeyDriver() { uint8 i; static uint8 pdata backup[4] = { //4位独立按键备份值 1, 1, 1, 1 }; for (i=0; i<4; i++)//循环检测4个独立按键 { if (backup[i] != KeySta[i])//检测按键 { if(backup[i] != 0) //如果按键按下 { KeyAction(KeyCodeMap[i]); //调用按键动作函数 } backup[i] = KeySta[i];//刷新备份值 } } }
KeyScan():
/* 按键扫描函数,需在定时中断中调用,间隔4ms*/ void KeyScan() { uint8 i; static uint8 flag = 0; //消抖计数 static uint8 keybuf[4] = { //4位独立按键扫描缓冲区 0xff, 0xff, 0xff, 0xff }; //将4个独立按键值移入缓冲区 keybuf[0] = (keybuf[0] << 1) | KEY_S2; keybuf[1] = (keybuf[1] << 1) | KEY_S3; keybuf[2] = (keybuf[2] << 1) | KEY_S4; keybuf[3] = (keybuf[3] << 1) | KEY_S5; flag++; //间隔5ms扫描一次 if(flag == 4)//4次就是20ms 完成消抖 { flag = 0;//扫描次数清零 for (i=0; i<4; i++) //读取4个独立的值 { if ((keybuf[i] & 0x0f) == 0x00) { KeySta[i] = 0;//如果4次扫描的值都为0,即按下状态 } else if ((keybuf[i] & 0x0f) == 0x0f) { KeySta[i] = 1;//如果4次扫描的值都为1,即弹起状态 } } } }
中断设置:
void Init_Timer0() { //定时器中断0 TMOD = 0x01; TH0 = 0xee; TL0 = 0x00; //5ms定时 ET0 = 1; TR0 = 1; } void Timer0() interrupt 1 //中断服务函数 { TH0 = 0xee; TL0 = 0x00; KeyScan(); }
主函数:
void main() { EA = 1; Init_Timer0();while(1) {
KeyDriver(); } }
下面是矩阵按键的扫描:
通常我们会先把列线拉高把行线拉低,然后等待按键按下判断那一列被拉低,用switch语句记录key值。
再把行线拉高把列线拉低,等待行线的值变换,然后记录key值,两个key值相加,得到最后的key值。
现在,我们每次让矩 阵按键的一个 KeyOut 输出低电平,其它三个输出高电平,判断当前所有 KeyIn 的状态,下 次中断时再让下一个 KeyOut 输出低电平,其它三个输出高电平,再次判断所有 KeyIn,通过 快速的中断不停的循环进行判断,就可以最终确定哪个按键按下了
用 1ms 中断判断 4 次采样值,这样消抖时间还是 16ms(1*4*4)
下面是两种矩阵按键的硬件实现形式,他们的实现代码是通用的。
实现代码:
代码的数码管为74Hc573驱动。
keyout和keyin的引脚参照图1修改。
#include <reg52.h> #define uint unsigned int #define uchar unsigned char uchar code SMGduan[]= {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F}; uchar code SMGwei[] = {0xfe, 0xfd, 0xfb}; uchar KeySta[4][4] = { //全部矩阵按键的当前状态 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; uchar keyvalue = 0; sbit DU = P2^6; sbit WE = P2^7; sbit KEY_OUT_1 = P3^0; sbit KEY_OUT_2 = P3^1; sbit KEY_OUT_3 = P3^2; sbit KEY_OUT_4 = P3^3; sbit KEY_IN_1 = P3^4; sbit KEY_IN_2 = P3^5; sbit KEY_IN_3 = P3^6; sbit KEY_IN_4 = P3^7; /*定时器中断0初始化函数*/ void timer0Init() { EA = 1; TMOD = 0x01; //设置 T0 为模式 1 TH0 = 0xFC; //为 T0 赋初值 0xFC67,定时 1ms TL0 = 0x66; ET0 = 1; //使能 T0 中断 TR0 = 1; //启动 T0 } /*三位数码管显示函数*/ void display(uchar i) { static uchar wei; P0 = 0XFF; WE = 1; P0 = SMGwei[wei]; WE = 0; switch(wei) { case 0: DU = 1; P0 = SMGduan[i / 100]; DU = 0; break; case 1: DU = 1; P0 = SMGduan[i % 100 / 10]; DU = 0; break; case 2: DU = 1; P0 = SMGduan[i % 10]; DU = 0; break; } wei++; if(wei == 3) { wei = 0; } } void main() { uchar i,j; uchar backup[4][4] = { //按键值备份,保存前一次的值 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; timer0Init(); while(1){//循环检测 4*4 的矩阵按键 for (i=0; i<4; i++){ for (j=0; j<4; j++){ if (backup[i][j] != KeySta[i][j]){//检测按键动作 if (backup[i][j] != 0){ //按键按下时执行动作 keyvalue = i * 4 + j; } backup[i][j] = KeySta[i][j]; //更新前一次的备份值 } } } } } /*定时器中断0服务函数*/ void timer0() interrupt 1 { uchar m; static uchar keyout = 0; //矩阵按键扫描输出索引 static uchar flags = 0; static uchar keybuf[4][4] = { //矩阵按键扫描缓冲区 {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF} }; TH0 = 0xFC; //重新加载初值1ms TL0 = 0x66; /*数码管*/ flags++; if(flags == 5) { //5ms flags = 0; display(keyvalue); //数码管动态扫描 } /*消抖并更新按键状态*/ //将一行的 4 个按键值移入缓冲区 keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1; keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2; keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3; keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4; //消抖后更新按键状态 for (m = 0; m < 4; m++){ //每行 4 个按键,所以循环 4 次 if ((keybuf[keyout][m] & 0x0F) == 0x00){ //连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下 KeySta[keyout][m] = 0; } else if ((keybuf[keyout][m] & 0x0F) == 0x0F){ //连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起 KeySta[keyout][m] = 1; } } /*进行矩阵按键扫描*/ //执行下一次的扫描输出 keyout++; //输出索引递增 keyout = keyout & 0x03; //索引值加到 4 即归零 switch (keyout){ //根据索引,释放当前输出引脚,拉低下次的输出引脚 case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break; case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break; case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break; case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break; default: break; } }