非阻塞式读取矩阵按键
这个教程是在电协培训新生的一点点心得,旨在解决许多人学习单片机,对按键的一个困惑:按键的非阻塞式消抖。如何在不消耗很多时间的情况下,有效地消抖,具有非常实际的意义。
本文就把一个循序渐进的对按键处理的理解记录下来,方便大家学习,由于本人已投入半导体器件的学习,很久没有接触数字电路,如有错误,还请读者指出。
本文注重方法的连续演化,遵循事物的发展规律,如需要计数式非阻塞矩阵按键读取方法,直接翻到最后即可,最后我将给出本文所讲的所有代码,屏蔽掉,稍加修改端口可以在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;
}
谢谢大家。