非阻塞式读取矩阵按键:保姆教程

非阻塞式读取矩阵按键

这个教程是在电协培训新生的一点点心得,旨在解决许多人学习单片机,对按键的一个困惑:按键的非阻塞式消抖。如何在不消耗很多时间的情况下,有效地消抖,具有非常实际的意义。

本文就把一个循序渐进的对按键处理的理解记录下来,方便大家学习,由于本人已投入半导体器件的学习,很久没有接触数字电路,如有错误,还请读者指出。

本文注重方法的连续演化,遵循事物的发展规律,如需要计数式非阻塞矩阵按键读取方法,直接翻到最后即可,最后我将给出本文所讲的所有代码,屏蔽掉,稍加修改端口可以在51单片机上实现,体会每种方法的利弊,和更好的方法的思想。

另外我将给出一个应用,证明非阻塞,特征次数可变的按键检测的优势。

1、按键的基本读取

请添加图片描述

如上图,是单片机常用的两种开关,其内部结构,由于弹性金属触点的接触,进而触发了两个引脚的开路和短路。

请添加图片描述

独立按键的检测原理:

从单片机核心板的原理图上可以看到,在按键未按下时,KEY是断开的,单片机IO口通过限流电阻与VCC相连,则IO口处于被拉高的状态。

按键被按下时,则KEY接通,电阻右端为理想的0V状态,IO口接在电阻右端,处于被拉低的状态。

单片机通过读这些引脚状态就能够判断按键处于什么状态。

根据这个思想,代码如下:

void main(void)
{
    while (1)
    {
        if (key0 == 0)
        {
            led = !led;
        }
    }
}

2、delay延时消抖:解决抖动问题

上面的代码,效果不好?那一定是按键消抖的问题

请添加图片描述

通常按键所用的开关都是机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹 性作用,一个按键开关在闭合时不会马上就稳定的接通,在断开时也不会一下子彻底断开, 而是在闭合和断开的瞬间伴随了一连串的抖动。

那么,如何消抖呢?

一种是硬件消抖,用电容导走信号的高频分量,也就是抖动部分。

请添加图片描述

另一种就是软件消抖:

void main(void)
{
    while (1)
    {
        if (!key0)
        {
            delay_ms(10);
            if (!key0)
            {
                led = !led;
                //while (!key0);
            }
        }
    }
}

这种方法也有弊端,不用while卡住,那么按住按键,则每次都会进入循环,大多数应用情景,我们都希望按一次按键,单片机做一次响应,用了while,那么主循环就会卡在这里不能往下进行,这对于大多数情况下都是致命的,应该尽量避免。

3、标志位:解决单次按下难题

这种方法基于上面提到的问题做了改进,主要思想是定义一个变量,储存按键的状态,只有在弹起状态的时候按下,才算做一次有效按下,在else if中更新为按键弹起状态。

void main(void)
{
    unsigned char key0_stat;
    while (1)
    {
        if (key0_stat == 1 && key0 == 0)
        {
            delay_ms(10);
            key0_stat = 0;
            led = !led;
        }
        else if (key0_stat == 0 && key0 == 1) key0_stat = 1;
    }
}

4、 计数:解决阻塞问题

上一种方法已经能用,但是远远称不上好用,为什么?因为那个10ms的延时,我们说延时的目的是为了避开那个我们处理不了的抖动过程,但是延时必然有弊端,尤其是这种延时这么长时间,什么事都不干,无论从系统实时性角度,还是资源利用率角度,都是不能允许存在的。那么有没有别的方法避免呢?

请添加图片描述

上面的图,展示了按键从按下到松开,单片机以较高频率采集IO口状态的过程,那么,我们能从中提取出标志着按下并稳定的一种关键特征吗?

没错,就是能够在一段时间内持续、稳定保持在低电平状态,也就是按下的前几个0。

这样是不是可以避免那个恼火的延时过程呢?

先看代码:

void main(void)
{
    unsigned int count = 0;
    while (1)
    {
        if (!key0)
        {
            if (count <= 100) count++;
        }
        else count = 0;
        if (count == 100)
        {
            led = !led;
        }
    }
}

根据上面的理论分析,我们想到了连续多次读引脚状态,只有在稳定按下的时候,才判断为真正按下了按键。

第一个if,判断按键按下的过程,只要检测到按下,就会进入,只要count小于101,就+1,这个变量就通过值来反应按下的时间,中间有任何抖动,一旦被读入高电平,这个count就会被重新置0,打断了连续计数的状态,只有按键稳定,count才能被加到101,并且只要按键不松开,count的值就会稳定在101。

然后在主循环中另外判断,只要conut等于100,就说明经历了一次按下的状态,并且一次按下,只会触发一次,这样就避免了疯狂按下的bug,这样的程序也几乎不会干扰主循环中的其他程序。

5、扩展到矩阵按键

请添加图片描述

矩阵按键的特点:

电路结构复杂,但提高了I/O引脚的利用率,软件编程较复杂,适用于所需按键较多的场合。

矩阵按键程序实现的两种方法:

一是行扫描法,二是线反转法。两种方法大同小异,我们这里用最常用的行扫描法。

可以看到,矩阵按键通过8个IO口,就能够驱动16个按键,这是因为分别动用了4个IO口来操作4行4列的线,通过这样的排布,以及扫描软件,就能实现读。那么在程序中我们又如何进行这项操作呢?

void main(void)
{
    int i, j;
    unsigned int cnt[16] = {0};
    while (1)
    {
        P2 = 0xff;  //P2口全部置高
        for (i = 0; i < 4; i++)  //循环,每次拉低一列
        {
            set_row(i, 0);  //拉低一列
            for (j = 0; j < 4; j++)  //循环,每次检测一行
            {
                if (get_line() == j)  //检测到行被拉低
                {
                    if(cnt[4*i+j] <= 100) cnt[4*i+j]++;  //对应的按键计数加一
                }
                else cnt[4*i+j] = 0;  //低电平,按键松开或抖动过程,对应的按键计数清零
            }
            set_row(i, 1);  //重新拉高这一列
        }
        
        //if (cnt[0] == 100) led = !led;
        for (i = 0; i < 4; i++)  //检测计数值经过100的按键,并用LED显示
        {
            for (j = 0; j < 4; j++)
            {
                if (cnt[4*i+j] == 100) P0 = ~(4*i+j);
            }
        }
    }
}

为了方便使用,两个函数定义为:
void set_row(unsigned char row, unsigned char stat)
{
    switch (row)
    {
        case 0: row0 = stat; break;
        case 1: row1 = stat; break;
        case 2: row2 = stat; break;
        case 3: row3 = stat; break;
    }
}

unsigned char get_line(void)
{
    unsigned char line = 4;
    if (line0 == 0) line = 0;
    if (line1 == 0) line = 1;
    if (line2 == 0) line = 2;
    if (line3 == 0) line = 3;
    return line;
}

这就是按键扫描检测的核心代码,可以看到,代码形式与上面刚刚讲过的最优的独立按键检测方式非常相似。

不同就在于两点:

  • 1、通过外层for循环,依次拉低一列,通过内层循环检测每一行,这段对应之前的检测单个按键的代码
  • 2、通过一个16个u8的数组,存储16个按键对应的计数状态,低电平加一,高电平清零,另外的循环检测是否计数足够也是在循环中检测,这段对应之前的判断if(count==100)_的代码。

这样,便实现了非阻塞的形式读取矩阵按键。

6、全部代码

为了方便大家学习,我贴出全部代码,可以在STC89C52平台跑通,请自行屏蔽注释不需要的main函数。

#include "reg52.h"
#include <intrins.h>

sbit led = P0^0;

sbit key0 = P3^3;
sbit key1 = P2^3;
sbit key2 = P2^4;

sbit row0 = P2^3;
sbit row1 = P2^2;
sbit row2 = P2^1;
sbit row3 = P2^0;
sbit line0 = P2^7;
sbit line1 = P2^6;
sbit line2 = P2^5;
sbit line3 = P2^4;

void delay_ms(unsigned int a);		//@11.0592MHz
void set_row(unsigned char row, unsigned char stat);
unsigned char get_line(void);

void main(void)
{
    while (1)
    {
        if (key0 == 0)
        {
            led = !led;
        }
    }
}

//void main(void)
//{
//    while (1)
//    {
//        if (!key0)
//        {
//            delay_ms(10);
//            if (!key0)
//            {
//                led = !led;
//                //while (!key0);
//            }
//        }
//    }
//}

//void main(void)
//{
//    unsigned char key0_stat;
//    while (1)
//    {
//        if (key0_stat == 1 && key0 == 0)
//        {
//            delay_ms(10);
//            key0_stat = 0;
//            led = !led;
//        }
//        else if (key0_stat == 0 && key0 == 1) key0_stat = 1;
//    }
//}

//void main(void)
//{
//    unsigned int count = 0;
//    while (1)
//    {
//        if (!key0)
//        {
//            if (count <= 100) count++;
//        }
//        else count = 0;
//        if (count == 100)
//        {
//            led = !led;
//        }
//    }
//}

//void main(void)
//{
//    int i, j;
//    unsigned int cnt[16] = {0};
//    while (1)
//    {
//        P2 = 0xff;  //P2口全部置高
//        for (i = 0; i < 4; i++)  //循环,每次拉低一列
//        {
//            set_row(i, 0);  //拉低一列
//            for (j = 0; j < 4; j++)  //循环,每次检测一行
//            {
//                if (get_line() == j)  //检测到行被拉低
//                {
//                    if(cnt[4*i+j] <= 100) cnt[4*i+j]++;  //对应的按键计数加一
//                }
//                else cnt[4*i+j] = 0;  //低电平,按键松开或抖动过程,对应的按键计数清零
//            }
//            set_row(i, 1);  //重新拉高这一列
//        }
//        
//        //if (cnt[0] == 100) led = !led;
//        for (i = 0; i < 4; i++)  //检测计数值经过100的按键,并用LED显示
//        {
//            for (j = 0; j < 4; j++)
//            {
//                if (cnt[4*i+j] == 100) P0 = ~(4*i+j);
//            }
//        }
//    }
//}

void delay_ms(unsigned int a)		//@11.0592MHz
{
	unsigned char i, j;
    unsigned int k;
    for (k = 0; k < a; k++)
    {
        _nop_();
        i = 2;
        j = 199;
        do
        {
            while (--j);
        } while (--i);
    }
}

void set_row(unsigned char row, unsigned char stat)
{
    switch (row)
    {
        case 0: row0 = stat; break;
        case 1: row1 = stat; break;
        case 2: row2 = stat; break;
        case 3: row3 = stat; break;
    }
}

unsigned char get_line(void)
{
    unsigned char line = 4;
    if (line0 == 0) line = 0;
    if (line1 == 0) line = 1;
    if (line2 == 0) line = 2;
    if (line3 == 0) line = 3;
    return line;
}

谢谢大家。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值