前言
这里我首先介绍初学者一般的键盘检测程序会如何写,然后再介绍按键状态机的方式来写按键,对比之后,你会发现按键状态机比普通的delay程序检测好太多的。
按键状态机是一个核心要点,为什么说呢,因为围绕一个按键的功能就能带来很多的题目,几乎每一届的省赛都会考到,并且按键的方式有多种,短按、长按、按下才有效松开无效等等。并且根据键盘的线路连接又分为独立键盘和矩阵键盘。
按键基本原理
独立键盘是跳线帽J5的3和2相连,也就是开发板上BTN(button)那个英文标识
这样一来,只保留第一列,其他列不看就形成了
当按键按下的时候,相应的引脚读到的电平就为0,在没有按下的时候电平是为高的,为什么为高呢?
因此上电后,P30-P33都是高电平
那么在知道了按键原理后,画一下按键的流程图:
按键抖动
这个检测到低电平就非常重要了,它是决定按键是否按下,是否按键处理的关键!
在平常的电子按键中,由于按键抖动,往往按下按键的时候波形不是完美的跳变,而是存在一定的抖动的。
按键按下的时候由于存在抖动,因此在没有硬件消抖的器件支持的原因下,我们只有采取软件消抖。软件消抖的原理
也就是说在第一次检测到了后,再次等待一小段时间,这段时间中如果还检测的到低电平就说明按键真正被按下了!这样就使用软件的角度解决了按键抖动的问题
那么通俗意义上面的按键检测代码为
#include "STC15F2K60S2.h"
typedef unsigned char uchar;
typedef unsigned int uint;
enum{
LED = 4 , EXT , DIG , CODE };
#define Select( x ) P2 = ( P2 & 0x1f ) | ( x << 5 ); P2 = ( P2 & 0x1f )
sbit BEEP = P0^6; sbit RELAY = P0^4;
sbit K1 = P3^0; sbit K2 = P3^1; sbit K3 = P3^2; sbit K4 = P3^3;
uchar code CA[] = {
0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90};
uchar buffer[] = {
1, 1, 1, 1, 2, 2, 2, 2};
uchar digSel = 0;
void Delay10ms() //@11.0592MHz
{
unsigned char i, j;
i = 108;
j = 145;
do
{
while (--j);
} while (--i);
}
void main()
{
if(K1 == 0)
{
Delay10ms()
if(k1 == 0)
{
//按键处理程序在这里进行
}
}
}
这种非常的不好
为什么呢?因为它消抖程序使用的是Delay10ms(),在此程序中使用的是非常没有意义的等待,这样极大减少了CPU的使用效率,降低了程序的响应能力。
因此我们必须把这部分进行改进,让延时的部分通过定时器来解决。并且以一种更加高效的状态机方式来改进按键检测程序,让按键检测更加富有健壮性,也更加具有对不同功能的实用性。
按键状态机程序
首先这里先给出按键的程序
#include "STC15F2K60S2.h"
#define SEL(x) P2 = (P2 & 0x1f) | (x << 5); P2 = (P2 & 0x1f)
#define P0FF P0 = 0xff
typedef unsigned char uchar;
typedef unsigned int uint;
enum{
LED = 4, EXT, DIG, CODE};
sbit BP = P0^6; sbit RY = P0^4;
sbit K1 = P3^0; sbit K2 = P3^1; sbit K3 = P3^2; sbit K4 = P3^3;
enum{
KS_GET, KS_ASSIGN, KS_WAITDEAL}KeyState = KS_GET;
uchar TempKey = 0,Key = 0, KeyCnt = 0;
uchar GetKey()
{
if(K1 == 0) return 1;
else if(K2 ==0) return 2;
else if(K3 ==0) return 3;
else if(K4 ==0) return 4;
else return 0;
}
void Init()
{
P0FF;
SEL(LED);
BP = 0;RY = 0;
SEL(EXT);
}
void Timer1Init(void) //2毫秒@12.000MHz
{
AUXR |= 0x40; //定时器时钟1T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x9A; //设置定时初值
TH1 = 0xA9; //设置定时初值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
//加上
ET1 = 1; //允许定时器1中断
EA = 1; //全局允许中断开启
}
void Timer1Handle() interrupt 3
{
switch(KeyState)
{
case KS_GET:
TempKey = GetKey();
KeyState = KS_ASSIGN;
KeyCnt = 10;
break;
case KS_ASSIGN:
if(KeyCnt != 0)
{
KeyCnt--;
}
else if(TempKey == GetKey())
{
if(TempKey != Key)
{
Key = TempKey;
KeyState = KS_WAITDEAL;
}
else
{
Key = KS_GET;
}
}
else
{
KeyState = KS_GET;
}
break;
}
}
void main()
{
Init();
Timer1Init();
while(1)
{
if(KeyState == KS_WAITDEAL)
{
switch(Key)
{
case 1:
P0 = 0xff;
SEL(LED);
break;
case 2:
P0 = 0x00;
SEL(LED);
}
KeyState = KS_GET;
}
}
}
这里实现的是按键按下S7关闭所有LED,按下按键S7打开所有的LED.
我先画一个流程图方便理解
主程序中
中断处理程序
解释
其实,就是将按键的消抖使用的定时器来解决。并且将按键的过程分为了几个状态,让整个过程更加清晰了,通过定时器每次2ms就会进入中断程序,将延时通过KeyCnt来达到相同的效果,并且通过在定时器的KeyGet()程序扫描键盘,省去了在主程序中去判断按键是否按下,而当按键真正被按下的时候,KeyState的值会为KS_WAITDEAL,这个时候主程序对标志进行判断并处理。提高了整个程序的响应性。
按键实现松手检测
通过上述的按键状态分离,我们可以利用此程序的特性来实现按键的松手检测。而按键的松手检测我记得是在某国赛还考过的。下面先上代码
#include "STC15F2K60S2.h"
#define SEL(x) P2 = (P2 & 0x1f) | (x << 5); P2 = (P2 & 0x1f)
#define P0FF P0 = 0xff
typedef unsigned char uchar;
typedef unsigned int uint;
enum{
LED = 4, EXT, DIG, CODE};
sbit BP = P0^6; sbit RY = P0^4;
sbit K1 = P3^0; sbit K2 = P3^1; sbit K3 = P3^2; sbit K4 = P3^3;
enum{
KS_GET, KS_ASSIGN, KS_WAITDEAL}KeyState = KS_GET;
uchar TempKey = 0,Key = 0, KeyCnt = 0;
uchar GetKey()
{
if(K1 == 0) return 1;
else if(K2 ==0) return 2;
else if(K3 ==0) return 3;
else if(K4 ==0) return 4;
else return 0;
}
void Init()
{
P0FF;
SEL(LED);
BP = 0;RY = 0;
SEL(EXT);
}
void Timer1Init(void) //2毫秒@12.000MHz
{
AUXR |= 0x40; //定时器时钟1T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x9A; //设置定时初值
TH1 = 0xA9; //设置定时初值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
//加上
ET1 = 1; //允许定时器1中断
EA = 1; //全局允许中断开启
}
void Timer1Handle() interrupt 3
{
switch(KeyState)
{
case KS_GET:
TempKey = GetKey();
KeyState = KS_ASSIGN;
KeyCnt = 10;
break;
case KS_ASSIGN:
if(KeyCnt != 0)
{
KeyCnt--;
}
else if(TempKey == GetKey())
{
if(TempKey != Key)
{
Key = TempKey;
KeyState = KS_WAITDEAL;
}
else
{
Key = KS_GET;
}
}
else
{
KeyState = KS_GET;
}
break;
}
}
void main()
{
Init();
Timer1Init();
while(1)
{
if(KeyState == KS_WAITDEAL)
{
switch(Key)
{
case 0:
P0 = 0xff;
SEL(LED);
break;
case 1:
P0 = 0x00;
SEL(LED);
}
KeyState = KS_GET;
}
}
}
这个程序可以实现按下S7,LED全亮,松开S7,LED全部熄灭。
为什能够实现这个功能呢?原因就在于
这个0上面,其实你仔细思考整个流程,你会发现其实你在按下按键后
GetKey()程序会返回(1-4)得到值(取决你按下的按键),然后会触发KS_WAITDEAL的事件,在主程序中会根据Key来处理。然后你松开手后,这个时候GetKey没有检测到按键,但是会返回0的!,这个0会也会触发KS_WAITDEAL的事件,然后主程序中会根据Key为0来处理事件。然后下一次扫描的时候,还是返回的0,但是这个0就不会触发KS_WAITDEAL的事件了!
这是因为
程序中有对重复的按键的检测的。这部分需要读者带入特定的值在脑子中走一遍流程就懂了。因此只要在上述的基础上增加按键对0值的按键处理即可实现松手检测了,是不是很简单?
这里加一个问题:请实现按下S7全亮松开S7全灭,按下S6亮第一个灯,松开S6灭掉第一个灯
实现长按按键
如何才能实现长按呢?长按即是按下按键后,并没有松开,而是以一种继续按下的状态保持。根据上面提供的按键状态机程序,在按键按下后,会马上触发按键处理程序。处理完后,这个时候定时器中断会继续按键扫描,这个时候返回的按键值还是之前按下的按键值(因为一直处于按键按下状态),这个时候按键中断每次扫描到
else if(TempKey == GetKey())
{
if(TempKey != Key)
{
Key = TempKey;
KeyState = KS_WAITDEAL;
}
else
{
Key = KS_GET;
}
}
到
就不会进入了,就直接进入
然后会一直处于获取状态。因此要解决长按的问题,就必须在这部分进行变化。
代码
这部分实现了,长按LED会呈现流水灯的效果
#include "STC15F2K60S2.h"
#define SEL(x) P2 = (P2 & 0x1f) | (x << 5); P2 = (P2 & 0x1f)
#define P0FF P0 = 0xff
typedef unsigned char uchar;
typedef unsigned int uint;
enum{
LED = 4, EXT, DIG, CODE};
sbit BP = P0^6; sbit RY = P0^4;
sbit K1 = P3^0; sbit K2 = P3^1; sbit K3 = P3^2; sbit K4 = P3^3;
enum{
KS_GET, KS_ASSIGN, KS_WAITDEAL}KeyState = KS_GET;
uchar TempKey = 0,Key = 0, KeyCnt = 0;
uint HoldCnt = 500;
uchar i = 0;
uchar GetKey()
{
if(K1 == 0) return 1;
else if(K2 ==0) return 2;
else if(K3 ==0) return 3;
else if(K4 ==0) return 4;
else return 0;
}
void Init()
{
P0FF;
SEL(LED);
BP = 0;RY = 0;
SEL(EXT);
}
void Timer1Init(void) //2毫秒@12.000MHz
{
AUXR |= 0x40; //定时器时钟1T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x9A; //设置定时初值
TH1 = 0xA9; //设置定时初值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
//加上
ET1 = 1; //允许定时器1中断
EA = 1; //全局允许中断开启
}
void Timer1Handle() interrupt 3
{
switch(KeyState)
{
case KS_GET:
TempKey = GetKey();
KeyState = KS_ASSIGN;
KeyCnt = 10;
break;
case KS_ASSIGN:
if(KeyCnt != 0)
{
KeyCnt--;
}
else if(TempKey == GetKey())
{
if(TempKey != Key)
{
Key = TempKey;
KeyState = KS_WAITDEAL;
}
else if(HoldCnt-- != 0);
else
{
Key = TempKey;
KeyState = KS_WAITDEAL;
HoldCnt = 500;
}
}
else
{
KeyState = KS_GET;
}
break;
}
}
void main()
{
Init();
Timer1Init();
while(1)
{
if(KeyState == KS_WAITDEAL)
{
switch(Key)
{
case 0:
P0 = 0xff;
SEL(LED);
break;
case 1:
P0 = ~(1 << i);
SEL(LED);
i = (i + 1)%8;
}
KeyState = KS_GET;
}
}
}
这部分就是添加为了长按做出处理的代码。它在进行到
这部分的时候,在条件不满足的时候,不是将按键状态设置为KS_GET
而是给出了else if(HoldCnt-- != 0);
HoldCnt是长按多久触发一次按键的计数变量,这里在程序首部给出了其值为500,因此是按500*2ms=1s触发一次,也就是说你长按后,后面每一秒都会触发
这部分,我再上面流程图的基础上面再加一点,方便理解
其实,只要理解了整个按键状态机的流程就自然清楚如何在上面去增加一些按键的功能了。
结束语
按键状态机这部分是非常重要的一个重点!如果实在有些理解不了,请将上述代码记忆。先用着,慢慢理解!