上篇博文我们采用延时的方式用来消除按键接触或者断开时抖动产生的信号波动。
在实际做项目开发的时候,程序量往往很大,各种状态值也很多,while(1)主循环要不停的扫描各种状态值是否发生变化,及时的进行任务调度,如果程序中加入了这种delay延时操作,可能某件事发生或者某个状态改变,程序还在延时操作中。等delay延时结束再去检查,已经晚了。已经无法检测到之前的事件或者状态了,为了避免这种的情况发生,要尽量的缩断while(1)循环一次所需要的时间。
因此采用了另一种消抖方式。启用一个中断,假如2ms一次进入中断,然后扫描一次按键的状态并且储存起来,连续扫描8次后,看看这连续8次的按键状态是否一致。8次按键的时间大概是16ms,如果这16ms内按键状态一直保持一致,那就可以确定现在按键处于稳定的阶段,而非抖动的阶段。
看程序
#include<reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
//sbit LED9 = P0^7;
//sbit LED8 = P0^6;
//sbit LED7 = P0^5;
//sbit LED6 = P0^4;
sbit key1 = P2^4;
sbit key2 = P2^5;
sbit key3 = P2^6;
sbit key4 = P2^7;
unsigned char code LedChar[] ={
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, //数码管真值
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
bit keysta = 1;
void main()
{
bit past = 1; //定义一个变量,保存前一次扫描的按键值,
unsigned char cnt = 0;//定义一个变量,记录按键按下的次数
EA = 1;
ENLED = 0; //选择数码管DS1来进行显示
ADDR3 = 1;
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
TMOD = 0x01;
TH0 = 0XF8; //延时2ms
TL0 = 0xCD;
ET0 = 1;
TR0 = 1;
P2 = 0xF7; //1111 0111 P2.3置0,即keyout1输出低电平
P0 = LedChar[cnt]; //显示按键初值
while(1)
{
if (keysta != past) //检测到当前值与前次值不相等说明此时按键有动作
{
if(key4 == 0) //如果目前按键处于按住状态
{
cnt++; //按键次数加1
if(cnt >= 10)
{
cnt = 0; //只用1个数码管显示,所以到10就被清零重新开始
}
P0 = LedChar[cnt]; //计数值显示到数码管上
}
past = keysta; //更新备份为当前值,以备进行下次比较
}
}
}
/* T0中断服务函数,用于按键的状态的扫描和消抖 */
void interruptTimer0() interrupt 1
{
static unsigned char keybuf = 0xFF;
TH0 = 0xF8;
TL0 = 0xCD;
keybuf = (keybuf<<1) | key4;
if(keybuf == 0x00)
{
keysta = 0; //连续8次扫描都为0,则在16ms内都只检测到按下状态时,可以认为按键已按下
}
else if(keybuf == 0xFF)
{
keysta = 1; //连续8次扫描都为1,即16ms内都检测到弹起状态时,可以认为按键已经弹起
}
else
{} //其他情况则说明按键状态尚未稳定,则不对keysta进行更新
}
看下程序结果视频利用中断扫描状态_哔哩哔哩_bilibili
可以看到结果和用delay语句的没有差异。分析下延时程序里的比较语句
keybuf = (keybuf<<1) | key4;
keyuf的初始值是1111 1111,每次进入中断它左移一次,左移一次后的值为1111 1110,key4的值只是1或者0,它要和keybuf左移后的最低位进行或的运算。运算后的值赋予keybuf。
假设开关处于弹起状态 key4=1;
则第一次进入中断的运算是:左移后1111 1110 | 0000 0001 =1111 1111;运算结果重新赋值给keybuf
则第二次进入中断的运算是:左移后1111 1110 |0000 0001 =1111 1111;运算结果重新赋值给keybuf
同理第三次,第四次知道第8次的运算后的结果都是1111 1111=0xFF;则说明开关处于弹起这一稳定的状态。
同样的如果开关处于一直按住的状态8次判断后keysta =0;通过判断稳态发生变化进入while循环的判断语句让数码管进行加1的动作。
利用这种方法,就可以避免通过延时消抖占用单片机的执行时间,而是转化成一种按键状态的判定而非按键过程的判断。之前的程序是用delay语句跳过了抖动过程,在跳过过程中程序是一直不动的处于等待状态。
矩阵按键的扫描
前文描述了独立按键的扫描,现在引入矩阵按键以本案为例
矩阵按键共4组,每组各4个独立按键,一共16个按键。见原理图
可知PCB板上的开关K1--K16即是原理图上的开关K1-K16 一 一对应 并且keyout1 - keyout4是P2.3 - P2.0端口,keyin1 -keyin4是端口P2.4 - P2.7。 从电路结构和控制方式来说keyout的输出是0还是1由程序控制,keyin的输出是0还是1由操作者的手(即按键开关)和keyout的输出一起控制。
现在通过一个程序实现现象,按下任何一个按键都能准确的在数码管上显示相应的数字。数码管显示范围是0-F对应着开关K1 - K16.即按下K1显示0 K2显示1依次K16显示F。
前一篇博文有提到按键按下通常会保持100ms以上,如果在按键扫描中断中,每次让矩阵按键的一个keyout输出低电平,其他三个输出高电平,判断当前所有Keyin的状态,下次中断时再让下一个keyout输出低电平,其他三个输出高电平,再判断一次所有keyin。通过快速的中断不停的循环进行判断,就可以最终确定哪个按键按下了。这个原理有点像数码管的动态扫描类似。数码管在动态赋值,而按键在动态读取。
看程序以及结果视频矩阵按键_哔哩哔哩_bilibili
#include<reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
sbit KEY_IN_1 = P2^4;
sbit KEY_IN_2 = P2^5;
sbit KEY_IN_3 = P2^6;
sbit KEY_IN_4 = P2^7;
sbit KEY_OUT_1 = P2^3;
sbit KEY_OUT_2 = P2^2;
sbit KEY_OUT_3 = P2^1;
sbit KEY_OUT_4 = P2^0;
//sbit key1 = P2^4;
//sbit key2 = P2^5;
//sbit key3 = P2^6;
//sbit key4 = P2^7;
unsigned char code LedChar[] ={
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, //数码管显示字符转换表
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char KeySta[4][4] = { //全部按键当前态
{1,1,1,1},
{1,1,1,1},
{1,1,1,1},
{1,1,1,1}
};
void main()
{
unsigned char i,j ;
unsigned char PastSta[4][4] ={
{1,1,1,1}, //按键值备份,保存前一次的值(前一稳态)
{1,1,1,1},
{1,1,1,1},
{1,1,1,1}
};
EA = 1; //中断使能
ENLED = 0; //选择数码管DS1来进行显示
ADDR3 = 1;
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
TMOD = 0x01;
TH0 = 0XFC; //定时1ms
TL0 = 0x67;
ET0 = 1; //启动定时器0中断
TR0 = 1;
P0 = LedChar[0]; //显示按键初值
while(1)
{
for(i=0; i<4; i++) //循环检测4*4的矩阵按键
{
for(j=0; j<4; j++)
{
if(PastSta[i][j] != KeySta[i][j]) //检测按键动作
{
if(PastSta[i][j] != 0) //按键前一稳态不是0即按键前一稳态是1即现稳态是0,即按住开关触发
{
P0 = LedChar[i*4 + j]; //这个判断语言也可用KeySta[i][j]语句判断
}
PastSta[i][j] = KeySta[i][j]; //把现稳态赋值给前态
}
}
}
}
}
/* T0中断服务函数,用于按键的状态的扫描和消抖 */
void interruptTimer0() interrupt 1
{
unsigned char i;
static unsigned char keyout = 0; //矩阵按键扫描输出索引
static unsigned char keybuf[4][4] = { //矩阵按键扫描缓冲区,16个建初始都是0xff则说明全部处于弹起状态
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF}
};
TH0 = 0xFC; //定时器0重新加载初值
TL0 = 0x67;
//将一行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(i = 0; i<4; i++) //每行4个按键,所以循环4次
{
if((keybuf[keyout][i] & 0x0F) ==0x00)
{ //连续4次扫描值为0,即4*4ms内部都是按下的状态时,可以认为按键已稳定的按下
KeySta[keyout][i] = 0;
}
else if((keybuf[keyout][i] & 0x0F) ==0x0F)
{ //连续扫描4次扫描值为1,即4*4ms内部都是弹起状态,可认为按键已稳定的弹起
KeySta[keyout][i] = 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;
}
}
从视频可以看到按下相应的按键数码管就显示相应的数字。没有发生连续触发或者误触。
解读下程序相关部分的功能,
中断最后部分
keyout = keyout & 0x03;等同于
if(keyout >= 4)
{keyout = 0; }
如果keyout等于4的时候即0x04=0100,0x03=0011 , 0x04 & 0x03=0000=0x00=0。 keyout小于等于3的时候这个 keyout&0x03运算不会改变keyout 的值,它就等于它本身的值。
后续的扫描程序,删除了一些可以删除的句子完全的写法是至于为何可以这么写等分析完程序结构,总结的时候再叙述。前文我描述过关于按键扫描的方式即4个keyout IO口其中1个取低电平其余三个高电平的方式来扫描按键。
本案就以操作者手按着K4让数码管显示3来分析下程序的工作方式。
并且以先keyout输出后keyin输入扫描消抖分析(实际程序是先keyin后keyout)
从程序上来看只有case 0 该句才能使按住k4动作有效,keyin4输出低电平(有效)
如果中断进入了其他的扫描语句这无论k4怎么动作keyout4输出都是高电平。
那这个矩阵开关就可以示意为,只有矩阵[0][3]这个值为0其余开关都处于高电平,而且是每过4ms这个值才检测到一次。
中断最前面部分程序分析
可以看到一开始4x4的矩阵开关初始赋值都是0xff即1111 1111。
然后是本案独立按键相似的消抖方案。
从程序和实际操作来看,只有keyout = 0 且keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4; 这句有不同的结果keybuf[0][3] 左移1位并且和KEY_IN_4进行或运算的结果1111 1110重新赋值给keybuf[0][3],前文我提过本案以按着K4来分析程序。至于keybuf[0][0],keybuf[0][1],keybuf[0][2],K1-k3都是弹起的,它们运算后的结果依然是0xff。则这个keybuf的矩阵的值现在为
{0xFF,0xFF,0xFF,0xFE},
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF}
显而易见当keyout为1,2,3的时候矩阵的值都未发生变化,因为我们只按着K4其他的按键都是弹起的。
因此只有keyout重新为0即第5次进入中断的时候,keyout = 0 且keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4 这时keyout[0][3]第二次左移或运算的结果是1111 1100 =0xFC; 可见当按着k4的时候每过4ms(即进入中断4次)keybuf[0][3]的值就会发生一次变化其它的值不动。可见16ms以后该值为1111 0000,此时的keybuf[4][4]矩阵值是
{0xFF,0xFF,0xFF,0xF0},
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF}
下图按键状态判断语句
由程序结构可知该程序扫描的是由keyout索引的该行4个开关的值,依然以按住K4判断该函数
当keyout的值为0的时候进入该函数可知,当第一次进入中断函数的判断语句
if((keybuf[keyout][i] & 0x0F) ==0x00)
{ KeySta[keyout][i] = 0;}
显然keybuf[0][0] keybuf[0][1] keybuf[0][2] 的值0xff显然不满足进入
第二个判断函数
else if((keybuf[keyout][i] & 0x0F) ==0x0F)
{
KeySta[keyout][i] = 1;
}
显然条件满足则KeySta[0][0] KeySta[0][1] KeySta[0][2] 都赋值为1 因为 按住K4 所以keybuf[0][3]第一次左移
的值0xFE的值与0x0F进行与运算既不等于0也不等于1,即都不进人判断函数,即不动keybuf[0][3]赋值直接进入语句i++。但keybuf[0][3]的值依然保持前一稳定的值。由于一开始按键开关都是弹起的则前一稳态的值是0xff,
则可以看到第一次进入中断的时候KeySta0][0] KeySta[0][1] KeySta[0][2] KeySta[0][3] 的值都是1,第二次进入中断显然KeySta[1][0] KeySta[1][1] KeySta1][2] KeySta[1][3]的值也都1.依次类推
第5次中断的时候keybuf[0][3]的值为0xFC依然无法进入判断函数即KeySta[0][3]依然是1,只有第13ms的时候即第4次扫描caes 0语句 keybuf[0][3]的值为0xF0 =1111 000 这时候满足
if((keybuf[keyout][i] & 0x0F) ==0x00)
{ KeySta[keyout][i] = 0;} 即KeySta[0][3] = 0;
即前12ms KeySta[4][4] 矩阵的值为,当进入13ms的时候KeySta [4][4]的值为
然后我们进入主函数的程序分析。
我们改下这个函数让它更容易理解,即判断语句更直观一点即用 if(KeySta[i][j] ==0)(注:开关现稳态是0即保持按住)代替if(PastSta[i][j] != 0),前12ms的 程序一直在扫描并且按键开关没有变化,无法进入判断语句因此past[4][4] 的值不发生变化一直都是1 ,只有在第13ms的时候,中断函数赋值KeySta[0][3] = 0
显然矩阵按键[0][3]状态发生了变化,这次进入第一个判断函数,一直按着K4则满足第二个判断语句 if(KeySta[i][j] == 0)则P0 = LedChar[i*4+j]=LedChar[3] 如此数码管显示3.符合之前我们提出的要求。
至此按住按键开关K4程序功能分析基本结束,符合一开始提出的要求。接着解决下前文留下的两个问题。
1是把keyin扫描程序放在keyout扫描程序前面 2是keyout 扫描语句删减了几句依然能正常工作
对于PCB的电路来说,信号从输出到输入都需要一个时间,有时它足够快而有时却不够快,这取决于电路的设计,这里输入输出颠倒就是为了让输出信号有足够的时间(一次中断间隔)来稳定,并有足够的时间来完成它对输入影响,如果该电路采用了硬件消抖加了电容的,这就是必须的。也就是说程序这么设计有更强的适用性,足以应对各种恶劣情况。
同样的由于keyout和keyin程序顺序颠倒则程序运行方式和前文所说有些许不同,由程序可知进入中断的keyout扫描程序是由上个中断提供的,本中断选择的keyout扫描程序是为下个中断执行的。
理一下程序的流程,操作者按住K4的按键
第一次进入中断
按照程序的流程得到的值是
{
keyout = 0;
keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4;由于keyout1输出未定因此该输出结果
KEY_IN_4是0还是1不清楚即使你按着开关
keyout =1;(keyout++)
case1;
}
第二次进入中断
{
case1;上个中断选择的
keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4;由于case1语句 keyout1输出高电平,此该即使按住k4赋值的结果依然都是0xff
keyout =2;(keyout++)
case2;
}
第三次进入中断
{
case2;上个中断选择的
keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4;由于case2语句以及case1的影响 keyout1输出高电平,此该即使按住k4赋值的结果依然都是0xff
keyout =3;(keyout++)
case3;
}
第4次进入中断
{
case3;上个中断选择的
keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4;由于case3语句以及case2与case1的影响 keyout1输出高电平,此该即使按住k4赋值的结果依然都是0xff
keyout =4;(keyout++)
keyout=0;( keyout = keyout & 0x03;)逢4归0
case0;
}
第5次进入中断
{
case0;上个中断选择的
keyout = 0;
keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4;由于case0语句 KEY_OUT_1=0输出低电平,而且此时其它的keyout已然都被赋值为1KEY_OUT_2=1, KEY_OUT,3=1,KEY_OUT_4=1.因此按住k4
KEY_IN_4的值才输出低电平为0;
也就是说第5次进入中断,按住k4消抖程序才正在意义上动作了一次结果是keybuf[0][3]=1111 1110
keyout =1;(keyout++)
keyout=1;( keyout = keyout & 0x03;)逢4归0
case1;
}
依次类推第9,13次进入中断keybuf[0][3]的值都移动一位存入矩阵中,当第17次进入该中断即过了17ms的时候
{
case0;上个中断选择的
keyout = 0;
keybuf[keyout][3] = (keybuf[keyout][3]<<1) | KEY_IN_4;由于case0语句 KEY_OUT_1=0输出低电平,而且此时已然都被赋值为1KEY_OUT_2=1, KEY_OUT,3=1,KEY_OUT_4=1.因此按住k4
KEY_IN_4的值才输出低电平为0;
也就是说第17次进入中断,按住k4消抖程序完成4次动作 结果是keybuf[0][3]=1111 0000
KeySta[0][3]=0; (if((keybuf[keyout][i] & 0x0F) ==0x00)判断语句赋值
{ KeySta[keyout][i] = 0;)
keyout =1;(keyout++)
keyout=1;( keyout = keyout & 0x03;)逢4归0
case1;
}
第17次中断结束后进入主函数,按键发生变化然后数码管显示3至此程序分析结束。显然17ms的时间和前文的keyout放keyin前面的时间13ms是有区别,差了4ms。同样的从函数流程来说,4个keyout输出的值在经过4个中断以后所有的输出都已明确不会发生不确定的情况。对于新手来说把扫描程序写全就可以了。
我制作了该程序的逻辑导图
总结:该程序使用两个for语句实现16个按键状态扫描
3个数组分别对应存储16个按键状态的变化转换,实现物理按键与数码管值输出的联系
每4次中断依次完成对16个按键状态变化的监控,16次中断完成一次确定的状态变化监控以 及消抖(与本案开头的扫描8位中断2ms总时间16ms,到矩阵按键的时候中断1ms,每4次中断扫描1位扫描4位总时间也是16ms)
keyin在前keyout在后提高程序适应性。(尤其对于执行速度很快的单片机)