首页 > 技术文章 > 按键消抖和矩阵键盘的扫描进阶

Brimon-zZY 2020-12-12 14:00 原文

在按下按键的时候,在闭合和断开的瞬间有一连串的抖动。

这样一次按下的动作可能会触发很多次。

所以,当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或断开稳定后再进行处理。 按键消抖可分为硬件消抖和软件消抖。

消除抖动有软件和硬件两种方法。

通常我们用软件消抖。

最简单的消抖原理,就是当检 测到按键状态变化后,先等待一个 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;
 }
}

 

推荐阅读