独立按键
常用的按键形式有两种:独立式按键和矩阵式按键,如下图所示。
独立按键比较简单,它们各自与独立的输入线相连接。
四根输入线分别连接到单片机的I/O端口。当按键K1被按下时,GND与P3.1端口连接,导致P3.1端口呈现低电平状态。当按键释放,连接断开,P3.1端口则回到高电平状态。通过这种方式,我们可以通过检测I/O端口的电平状态来判定按键的开闭状态。
我们已经了解了这个电路中按键工作的原理,但仍有一个疑问:按下按键时,I/O端口呈现低电平,这点我们可以理解。但为什么当I/O端口什么都不连接时,它会显示为高电平呢?此外,如果该端口被设置为高电平输出,长时间按下按键会不会导致单片机损坏?
实际上,在单片机I/O口内部,有一个上拉电阻。按键是接在P3口的,P3口上电默认是准双向I/O口,下面来简单介绍一下准双向I/O口的电路。
准双向I/O口
下图即准双向I/O口的结构。
上图的红色方框内即即单片机的内部部分。
显然,这种具有上拉的准双向 I/O 口,如果要正常读取外部信号的状态,必须首先得保证自己内部输出的是 1,如果内部输出 0,则无论外部信号是 1还是 0,这个引脚读进来的都是 0。
矩阵按键
在某一个系统设计中,如果需要使用很多的按键时,做成独立按键会大量占用 IO 口,因此我们引入了矩阵按键的设计。
在上图中,一共有四组按键。如果P1.7输出一个低电平,P1.7就相当一个GND,那S1\~4不就相当于一组独立按键吗?当然这个时候P1.6\~P1.4必须输出高电平才不会对这组独立按键产生干扰。
独立按键的扫描
这段代码使得矩阵键盘上的S1至S4键能够作为独立按键使用。它能够统计S1键被按下的次数,并将该次数显示在最左侧的数码管上。
#include <REG52.H>
#define LED P0
sbit keyOut_1 = P1 ^ 7;
sbit keyOut_2 = P1 ^ 6;
sbit keyOut_3 = P1 ^ 5;
sbit keyOut_4 = P1 ^ 4;
sbit keyIn_1 = P1 ^ 3;
sbit keyIn_2 = P1 ^ 2;
sbit keyIn_3 = P1 ^ 1;
sbit keyIn_4 = P1 ^ 0;
unsigned char code LedChar[] = {
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E};
void main()
{
unsigned char cnt = 0; // 计数变量,记录按键按下的次数
bit backup = 1; // 保存前一次扫描的按键值
keyOut_1 = 0; // 将矩阵按键的第一行作为独立按键,并把剩余三行屏蔽
LED = ~LedChar[cnt]; // 先输出0,即初值
while (1)
{
if (keyIn_1 != backup) // 当前值与上次值不同,说明按键状态发生变化,可能是摁下或弹起
{
if (backup == 0) // 如果上次是0,则这次的变化一定是弹起
{
cnt++; // 次数++
if (cnt >= 10) // 加到10就清0
{
cnt = 0;
}
LED = ~LedChar[cnt]; // 计数变量的值显示在数码管上
}
backup = keyIn_1; // 更新上次的值即这次的值
}
}
}
在 C51 语言中,bit
是一种数据类型,专门用于存储单个位的值。C51 是一个适用于8051微控制器的C语言方言,由Keil软件等公司为8051微控制器系列开发的编译器支持。bit
类型在微控制器编程中非常有用,因为它允许程序员直接访问和控制微控制器的单个位,这通常与硬件寄存器的特定功能位相关。
bit
类型的特点如下:
bit
变量只能存储两个值:0 或 1。bit
类型通常用于表示布尔值,如真/假、开/关等。bit
类型的变量通常用于访问和控制特殊功能寄存器(SFR)中的位,如控制LED灯或读取按钮状态。bit
类型的操作效率很高,因为它直接映射到微控制器的硬件,可以通过单个机器指令来设置或清除。
在 C51 中定义 bit
类型的变量的示例:
sbit led = P1^0; // 定义一个特殊功能位,映射到端口1的第0位
在这个例子中,sbit
关键字用于定义一个特殊功能位。led
是变量名,P1^0
表示端口1的第0位。这样定义后,你就可以直接通过 led
来控制连接到端口1第0位的LED灯。
在 C51 中操作 bit
类型的变量也很简单:
led = 1; // 打开LED灯
led = 0; // 关闭LED灯
bit
类型在嵌入式系统和微控制器编程中非常常见,因为这些系统通常对内存和处理能力有严格的限制,而 bit
类型的操作可以有效地利用资源。
按键消抖
看似这份代码没啥问题,运行起来好像也是对的。
读者如果运行了上面的代码,可以尝试用手指快速敲打按键S1,会发现数码管的数字一次加了2甚至3。这就让我们引入了按键抖动和消抖的问题。
通常按键所用的开关都是机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定的接通,在断开时也不会一下子彻底断开,而是在闭合和断开的瞬间伴随了一连串的抖动,如下图所示。
按键稳定闭合时间长短是由操作人员决定的,通常都会在 100ms 以上,刻意快速按的话能达到 40-50ms左右,很难再低了。抖动时间是由按键的机械特性决定的,一般都会在 10ms以内,为了确保程序对按键的一次闭合或者一次断开只响应一次,必须进行按键的消抖处理。当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或断开稳定后再进行处理。按键消抖可分为硬件消抖和软件消抖。
这里我就不说硬件消抖了,直接说软件消抖,就是当检测到按键状态变化后,先等待一个 10ms 左右的延时时间,让抖动消失后再进行一次按键状态检测,如果与刚才检测到的状态相同,就可以确认按键已经稳定的动作了。
代码:
#include <REG52.H>
#define LED P0
sbit keyOut_1 = P1 ^ 7;
sbit keyOut_2 = P1 ^ 6;
sbit keyOut_3 = P1 ^ 5;
sbit keyOut_4 = P1 ^ 4;
sbit keyIn_1 = P1 ^ 3;
sbit keyIn_2 = P1 ^ 2;
sbit keyIn_3 = P1 ^ 1;
sbit keyIn_4 = P1 ^ 0;
unsigned char code LedChar[] = {
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E};
void delay();
void main()
{
unsigned char cnt = 0;
bit backup = 1;
bit keyBuff = 1;
keyOut_1 = 0;
LED = ~LedChar[cnt];
while (1)
{
keyBuff = keyIn_1;
if (keyIn_1 != backup)
{
delay(); // 延时大约10ms
if (keyBuff == keyIn_1)
{
if (backup == 0)
{
cnt++;
if (cnt >= 10)
{
cnt = 0;
}
LED = ~LedChar[cnt];
}
backup = keyIn_1;
}
}
}
}
void delay()
{
unsigned char sec = 1000;
while (sec--)
;
}
现在再试试就不会出现这个问题了。
这个程序通过一个简单的算法解决了按键抖动的问题。虽然这个示例程序很简单,我们可以这么编写,但在实际的项目开发中,程序通常很复杂,涉及的状态值也很多。程序需要不断地检查这些状态值是否发生了变化,并且要及时进行任务调度。如果在程序中插入了延时操作,可能会错过某些事件的发生;当事件已经发生并结束,而程序仍在延时中,等到延时结束去检查时,就已经来不及了,错过了捕捉该事件的机会。因此,我们应该尽量减少主循环while(1)的执行时间,并且对于需要长时间延时的操作,我们应该寻找其他解决方案。
对于按键消抖所需的延时,除了简单的延时,我们有更好的方法来处理。例如,我们可以设置一个定时中断,每2毫秒触发一次,每次中断时检查一下按键的状态并保存。连续检查8次,如果这8次的按键状态都是一致的,大约在16毫秒内,我们就可以判断按键已经稳定下来,不再抖动。这种方法比简单的延时更为高效,能够更准确地处理按键抖动问题。如下图所示,这个过程可以帮助我们确定按键是否真正被按下。
假如左边时间是起始 0 时刻,每经过 2ms 左移一次,每移动一次,判断当前连续的 8 次按键状态是不是全 1 或者全 0,如果是全 1 则判定为弹起,如果是全 0 则判定为按下,如果 0 和 1 交错,就认为是抖动,不做任何判定。
利用这种方法,就可以避免通过延时消抖占用单片机执行时间,而是转化成了一种按键状态判定而非按键过程判定,我们只对当前按键的连续 16ms 的 8 次状态进行判断,而不再关心它在这 16ms 内都做了什么事情,那么下面就按照这种思路用程序实现出来。
#include <REG52.H>
#define LED P0
sbit keyOut_1 = P1 ^ 7;
sbit keyOut_2 = P1 ^ 6;
sbit keyOut_3 = P1 ^ 5;
sbit keyOut_4 = P1 ^ 4;
sbit keyIn_1 = P1 ^ 3;
sbit keyIn_2 = P1 ^ 2;
sbit keyIn_3 = P1 ^ 1;
sbit keyIn_4 = P1 ^ 0;
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()
{
unsigned char cnt = 0;
bit backup = 1;
EA = 1;
TMOD = 0x01;
TH0 = 0xF8;
TL0 = 0xCD;
ET0 = 1;
TR0 = 1;
keyOut_1 = 0;
LED = ~LedChar[cnt];
while (1)
{
if (keySta != backup) // 比对前后按键状态
{
if (backup == 0)
{
cnt++;
if (cnt >= 10)
{
cnt = 0;
}
LED = ~LedChar[cnt];
}
backup = keyIn_1;
}
}
}
void interruptTimer0() interrupt 1
{
static unsigned char keyBuff = 0xFF; // 扫描缓存,保存一段时间的扫描值
TH0 = 0xF8; // 重新加载定时器初值
TL0 = 0xCD;
keyBuff = (keyBuff << 1) | keyIn_1; // 缓存左移一位,并将当前扫描值移入最低位。
if (keyBuff == 0x00)
{ // 如果连续8次扫描值都是0,可认为按键已按下
keySta = 0;
}
else if (keyBuff == 0xFF)
{ // 如果连续8次扫描值都是1,可认为按键已弹起
keySta = 1;
}
}
矩阵按键的扫描
我们之前讨论过,通常一个按键被按下时会维持至少100毫秒。如果我们在按键扫描的中断服务中,每次只让键盘矩阵中的一个KeyOut引脚输出低电平,而其余三个输出高电平,然后检测所有KeyIn引脚的状态。下一次中断时,另一个KeyOut引脚输出低电平,其余保持高电平,再次检测所有KeyIn引脚。通过这样在中断中不断循环切换和检测,我们就能确定哪个按键被按下了。这个过程和数码管的动态扫描原理有点相似:数码管是不断地更新显示值,而按键扫描则是不断地读取状态。
关于扫描间隔和消抖时间,现在我们有4个KeyOut引脚,需要中断4次才能完成所有按键的一次完整扫描。如果我们用2毫秒的中断来判断,并重复8次扫描,总时间会达到64毫秒(2毫秒×4次×8次),这样的时间太长了。因此,我们改为使用1毫秒的中断来做判断,并且重复4次采样,这样消抖时间仍然是16毫秒(1毫秒×4次×4次)。
接下来,我们将通过编程实现这个过程。程序会循环扫描电路板上的16个矩阵按键(从K1到K16),识别出按键动作,并在按键被按下时,在一位数码管上显示当前按键的编号(使用0到F表示,显示的数码管值=按键编号-1)。
#include <REG52.H>
#define LED P0
sbit keyOut_1 = P1 ^ 7;
sbit keyOut_2 = P1 ^ 6;
sbit keyOut_3 = P1 ^ 5;
sbit keyOut_4 = P1 ^ 4;
sbit keyIn_1 = P1 ^ 3;
sbit keyIn_2 = P1 ^ 2;
sbit keyIn_3 = P1 ^ 1;
sbit keyIn_4 = P1 ^ 0;
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 backup[4][4] = {{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1}};
EA = 1;
TMOD = 0x01;
TH0 = 0xFC;
TL0 = 0x67;
ET0 = 1;
TR0 = 1;
LED = ~LedChar[0];
while (1)
{
for (i = 0; i < 4; i++)
{
for (j = 0; j < 4; j++)
{
if (backup[i][j] != keySta[i][j])
{
if (backup[i][j] == 1)
{
LED = ~LedChar[i * 4 + j];
}
backup[i][j] = keySta[i][j];
}
}
}
}
}
void interruptTimer0() interrupt 1
{
unsigned char i;
static unsigned char keyOut = 0;
static unsigned char keyBuf[4][4] = {
{0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}};
TH0 = 0xFC;
TL0 = 0x67;
keyBuf[keyOut][0] = (keyBuf[keyOut][0] << 1) | keyIn_1;
keyBuf[keyOut][1] = (keyBuf[keyOut][1] << 1) | keyIn_2;
keyBuf[keyOut][2] = (keyBuf[keyOut][2] << 1) | keyIn_3;
keyBuf[keyOut][3] = (keyBuf[keyOut][3] << 1) | keyIn_4;
for (i = 0; i < 4; i++)
{
if ((keyBuf[keyOut][i] & 0x0F) == 0x0F) // 连续四次为1
{
keySta[keyOut][i] = 1;
}
else if ((keyBuf[keyOut][i] & 0x0F) == 0x00) // 连续四次为0
{
keySta[keyOut][i] = 0;
}
}
switch (keyOut) // 根据当前这组按键的索引值,拉高这组的引脚,拉低下组的引脚
{
case 0:
keyOut_2 = 0;
keyOut_1 = 1;
break;
case 1:
keyOut_3 = 0;
keyOut_2 = 1;
break;
case 2:
keyOut_4 = 0;
keyOut_3 = 1;
break;
case 3:
keyOut_1 = 0;
keyOut_4 = 1;
break;
default:
break;
}
keyOut++; // 更换一组按键
keyOut &= 0x03; // 技巧:加到4归零
}