前言
本次编程实验以IAP15F2K61S2为单片机主控芯片,其编程使用与STC15F2K60S2完全相同,头文件为STC15F2K60S2.H
。若用于51系列单片机,以reg52.h
为头文件,则需将后文中timer0.c中void Timer0_Init(void)
函数中的AUXR &= 0x7F;
一行删除。程序中可能涉及的定时器初始化程序和LED亮灭程序和数码管显示程序,读者可根据自身所用单片机原理图和手册自行修改。
一、使用定时器扫描矩阵键盘按键的原理
1.基本原理
为了检测按键按下和松开,必须记录按键当前状态和上一次状态。如果当前状态为高电平,上一次状态为低电平,则检测到按键松手;如果当前状态为低电平,上一次为高电平,则检测到按键按下。由于短按按下一次只需返回一次按键值,因此只需检测按键松开动作,即上一次状态低电平,当前状态高电平。
2.消抖原理
按键按下和松开时,其连接的IO口电平并非瞬间拉低,而是会有一小段时间的抖动:
考虑到按键按下和松开抖动时间较短,按下时间通常大于50ms。定时器每隔25ms扫描一次按键状态,假设扫描时恰好处于按键按下时的抖动过程,检测到低电平,那么经过25ms此时按键已经稳定处于按下状态,必然检测到低电平,从而上一次状态和现状态均为低电平;如果检测到高电平,则25ms后,上一次状态为高电平,当前状态为低电平,再经过25ms后,上一次状态和现状态均为低电平。当按键松开时,假设恰好检测到抖动过程的高电平,则上一次状态为低电平,当前状态为高电平,符合松手条件,返回按键值;如果检测到抖动过程的低电平,则上一次状态和当前状态均为低电平,经过25ms后,此时按键稳定处于松开状态,必然检测到高电平,则上一次状态为低电平,当前状态为高电平,符合松手条件,返回按键值。
3.矩阵键盘原理图
开发板上矩阵键盘原理图如下图所示,后文代码中关于矩阵键盘行和列IO口的定义和操作皆根据原理图进行,读者可自行修改。代码中对按键值的定义为:从左至右,从上至下,从1递增到16。
二、编程实现
1.矩阵键盘函数
kbd.h
#ifndef __KBD_H
#define __KBD_H
#include <STC15F2K60S2.H>
//矩阵键盘四列IO口重命名,根据原理图修改
sbit colu1=P4^4;
sbit colu2=P4^2;
sbit colu3=P3^5;
sbit colu4=P3^4;
unsigned char kbd_state(); //矩阵键盘按键状态读取
unsigned char kbd_send(unsigned char mode); //矩阵键盘最终按键值返回
#endif
kbd.c
mode用于选择短按长按模式。mode为0 时,通过检测是否满足上一次状态为低电平(kbdlast不为0)且当前状态为高电平(kbdnow为0)决定是否返回按键值;mode为1时,通过检测是否满足上一次状态为低电平(kbdlast不为0)且当前状态为低电平(kbdnow不为0,且kbdlast=kbdnow)决定是否返回按键值。
#include "kbd.h"
typedef unsigned char u8;
unsigned char column=0; //column为矩阵键盘列对应IO口的加权和
/*
* @brief 矩阵键盘按键状态读取函数
* @param
* @reval key:矩阵键盘按键状态
* @note: 按行扫描
*/
unsigned char kbd_state()
{
unsigned char key=0,i=0;
for(i=0;i<4;i++)
{
colu1=colu2=colu3=colu4=1; //将矩阵键盘四列电平拉高
P3=P3&0xf0|(~(0x1<<i)); //按行扫描,依次将第一行、第二行、第三行和第四行电平拉低,而后只需对列进行低电平查找
column=(u8)colu1+(u8)colu2*2+(u8)colu3*4+(u8)colu4*8; //得到行电平拉低后的四列电平加权总和,第1列权1,第2列权2,以此类推,注意应将colu强制转换为unsigned char型
switch (column) //查找低电平所在的列,一旦找到则key不为0,返回key值,结束此函数
{
case 0x0e:key=1+4*i;break; //如果低电平在第一列,从第四列到第一列电平依次为:1 1 1 0
case 0x0d:key=2+4*i;break; //如果低电平在第二列,从第四列到第一列电平依次为:1 1 0 1
case 0x0b:key=3+4*i;break; //如果低电平在第三列,从第四列到第一列电平依次为:1 0 1 1
case 0x07:key=4+4*i;break; //如果低电平在第四列,从第四列到第一列电平依次为:0 1 1 1
}
if(key!=0) //一旦key不为0,返回key,函数结束
return key;
}
return 0; //若无按键按下,返回0
}
/*
* @brief 矩阵键盘最终按键值返回函数
* @param mode:长短按选择标志位,1长按0短按
* @reval kbdlast:矩阵键盘最终按键值
* @note:
*/
unsigned char kbd_send(unsigned char mode)
{
static u8 kbdlast=0,kbdnow=0,i=1;
kbdlast=kbdnow; //不断刷新矩阵键盘上一次状态和当前状态
kbdnow=kbd_state();
for(i=1;i<17;i++) //遍历1-16
{
if(kbdlast==i&&kbdnow==(i*mode)) //如果上一次状态按键值和当前状态按键值满足条件,则返回上一次状态按键值,函数结束
return kbdlast;
}
return 0; //如果上一次状态按键值和当前状态按键值不满足条件,则返回0
}
2.定时器0函数
timer0.h
#ifndef __TIMER0_H
#define __TIMER0_H
#include <STC15F2K60S2.H>
void Timer0_Init(void); //1毫秒@11.0592MHz
#endif
timer0.c
#include "timer0.h"
void Timer0_Init(void) //1毫秒@11.0592MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //使能定时器0中断
EA=1; //使能总中断
}
3.main.c
定时器定时1毫秒基础时间(方便扩展其他时间要求的功能函数),每过25毫秒检测一次矩阵键盘按键,如果按键按下并松开,则返回相应的按键值(只监测按键松手,因此按下一次只能返回一次按键值);如果连续按下2秒,则切换为长按模式,每过500毫秒检测一次矩阵键盘按键,并返回按键值(监测按键持续按下,每过500毫秒返回一次按键值)。
实际上,短按扫描是一直在运行的。在短按模式起返回按键和检测按下时间是否超过2s作用;在长按模式起检测按键是否松手作用。如果在长按模式期间不进行短按扫描,那么一旦进入长按模式就无法返回短按模式。
#include <STC15F2K60S2.H>
#include "kbd.h"
#include "timer0.h"
#include <intrins.h>
typedef unsigned char u8;
#define outputp0(y,x) P0=x,P2&=0x1f,P2|=y,P2&=0x1f; //P2高三位用于74HC138译码器选择锁存器,P0用于发送数据
u8 kbdnum=0,kbdtemp=0; //按键值;按键缓存值
int shownum=0; //数码管显示值
unsigned char code Seg_Table[]={ //数码管段码表,共阳
0xc0,
0xf9,
0xa4,
0xb0,
0x99,
0x92,
0x82,
0xf8,
0x80,
0x90,
0xff //8段全灭
};
void Delay2ms(void) //2ms@11.0592MHz
{
unsigned char data i, j;
_nop_();
_nop_();
i = 22;
j = 128;
do
{
while (--j);
} while (--i);
}
/*
* @brief 1位数码管显示函数
* @param pos:位选;dat:显示数字;dot:小数点选择位,1有0无
* @reval
* @note:
*/
void showbit(unsigned char pos,dat,dot)
{
outputp0(0xc0,0x01<<pos); //位选
outputp0(0xe0,Seg_Table[10]); //段选,这两行起消隐作用
outputp0(0xc0,0x01<<pos); //位选
outputp0(0xe0,Seg_Table[dat]+0x80*dot); //段选
Delay2ms(); //延时
}
void test() //测试程序
{
if(kbdtemp!=0)
{
kbdnum=kbdtemp; //通过缓存量向keynum赋值,防止按下按键松开后立刻被清零
shownum+=kbdnum;
shownum%=10000;
kbdnum=0; //清零,防止一次按下多次执行
kbdtemp=0;
}
showbit(0,shownum/1000%10,0); //数码管显示
showbit(1,shownum/100%10,0);
showbit(2,shownum/10%10,0);
showbit(3,shownum%10,0);
}
void main()
{
Timer0_Init();
while(1)
{
test();
}
}
void Timer0_Isr(void) interrupt 1
{
static u8 kbd_longflag=0; //矩阵键盘长按标志位,1长按0短按
static int kbd_short_t=0,kbd_t=0,kbd_long_t=0;
TL0 = 0x66; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
kbd_short_t++; //矩阵键盘按键短按时间计数
if(kbd_short_t>=(500*kbd_longflag+25*(!kbd_longflag))) //短按每25ms检测一次按键
{
if(!(colu1&colu2&colu3&colu4)) //长按检测,每25ms检测矩阵键盘按键是否仍在按下,如果是
{
kbd_t++; //矩阵键盘按键按下时间计数(以25ms为单位)
if(kbd_t>=80) //如果持续按下2s
{
kbd_t=80; //防止溢出
kbd_longflag=1; //切换为长按模式
}
}
else if(colu1&colu2&colu3&colu4) //长按检测,每25ms检测按键是否仍在按下,如果不是
{
kbd_t=0; //清零按键按下时间计数
kbd_longflag=0; //切换为短按模式
}
kbd_short_t=0; //清零短按时间计数
if(kbd_longflag==0) //如果是短按模式
kbdtemp=kbd_send(0); //按键以短按模式检测
}
kbd_long_t++; //按键长按时间计数
if(kbd_long_t>=500) //长按时每500ms检测一次按键
{
kbd_long_t=0; //清零按键长按时间计数
if(kbd_longflag==1) //如果是长按模式
kbdtemp=kbd_send(1); //按键以长按模式检测
}
}
三、测试程序现象
单片机上电数码管显示0000,短按按下矩阵键盘按键,数码管显示值在原基础上加上按下的按键值;长按按下时,按下2秒时开始在原基础上加上按键值,而后每隔500毫秒增加一次值。
总结
通过上述程序,可以实现定时器扫描矩阵键盘以提高主程序执行效率。通过类比,也可以实现定时器扫描独立按键和数码管,详情见定时器扫描按键(短按/长按)和定时器扫描数码管(含滚动显示)
此外,由于定时器每1ms进入一次中断,对于具有IIC和One-Wire通信等具有严格时序要求的项目来说,容易使通信被干扰,因此不建议使用定时器扫描的方式。当然,通过一些方法也可以实现二者兼容并存,在DS1302可调时钟(按键短按/长按)中提供了其中一种方法。
有任何问题和补充,欢迎评论区交流。