独立按键

常用的按键形式有两种:独立式按键和矩阵式按键,如下图所示。

两种按键形式

独立按键比较简单,它们各自与独立的输入线相连接。

四根输入线分别连接到单片机的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口的结构

上图的红色方框内即即单片机的内部部分。

显然,这种具有上拉的准双向 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; // 更新上次的值即这次的值
        }
    }
}

按键消抖

看似这份代码没啥问题,运行起来好像也是对的。

读者如果运行了上面的代码,可以尝试用手指快速敲打按键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归零
}
最后修改:2024 年 01 月 23 日
如果觉得我的文章对你有用,请随意赞赏