前言
本次编程实验以IAP15F2K61S2为单片机主控芯片,其编程使用与STC15F2K60S2完全相同,头文件为STC15F2K60S2.H
。若用于51系列单片机,以reg52.h
为头文件,则需将后文中timer0.c中void Timer0_Init(void)
函数中的AUXR &= 0x7F;
一行删除。程序中涉及的定时器初始化程序和LED亮灭程序和数码管显示程序,读者可根据自身所用单片机原理图和手册自行修改。
一、为什么使用定时器扫描按键?
在对主程序执行效率要求低、项目工程复杂度低的情况下,通常可以使用以下代码检测按键:
void key()
{
if(key==0) //如果按键按下
{
delay(20); //延时消除抖动
if(key==0) //确认按键按下
{
while(key==0); //等待按键松开
}
}
}
当按键按下迟迟不松开时,主程序一直卡在while(key==0);
里,变相停止了主程序向下运行,影响程序正常执行。为此我们考虑使用定时器间隔扫描按键的方式实现按键检测。
二、定时器扫描按键的原理
1.基本原理
为了检测按键按下和松开,必须记录按键当前状态和上一次状态。如果当前状态为高电平,上一次状态为低电平,则检测到按键松手;如果当前状态为低电平,上一次为高电平,则检测到按键按下。由于短按按下一次只需返回一次按键值,因此只需检测按键松开动作,即上一次状态低电平,当前状态高电平。
2.消抖原理
按键按下和松开时,其连接的IO口电平并非瞬间拉低,而是会有一小段时间的抖动:
考虑到按键按下和松开抖动时间较短,按下时间通常大于50ms。定时器每隔25ms扫描一次按键状态,假设扫描时恰好处于按键按下时的抖动过程,检测到低电平,那么经过25ms此时按键已经稳定处于按下状态,必然检测到低电平,从而上一次状态和现状态均为低电平;如果检测到高电平,则25ms后,上一次状态为高电平,当前状态为低电平,再经过25ms后,上一次状态和现状态均为低电平。当按键松开时,假设恰好检测到抖动过程的高电平,则上一次状态为低电平,当前状态为高电平,符合松手条件,返回按键值;如果检测到抖动过程的低电平,则上一次状态和当前状态均为低电平,经过25ms后,此时按键稳定处于松开状态,必然检测到高电平,则上一次状态为低电平,当前状态为高电平,符合松手条件,返回按键值。
三、编程实现
1.独立按键函数
key.h:
#ifndef __KEY_H
#define __KEY_H
#include <STC15F2K60S2.H>
sbit key1=P3^0; //根据原理图上独立按键连接的IO口定义
sbit key2=P3^1;
sbit key3=P3^2;
sbit key4=P3^3;
unsigned char key_state(); //按键状态获取
unsigned char key_send(unsigned char mode); //按键值返回
#endif
key.c:
mode用于选择短按长按模式。mode为0 时,通过检测是否满足上一次状态为低电平(keylast不为0)且当前状态为高电平(keynow为0)决定是否返回按键值;mode为1时,通过检测是否满足上一次状态为低电平(keylast不为0)且当前状态为低电平(keynow不为0,且keylast=keynow)决定是否返回按键值。
#include "key.h"
unsigned char key_state() //得到按键状态
{
unsigned char keyget=0;
if(!key1)keyget=1; //如果按键1按下
if(!key2)keyget=2; //如果按键2按下
if(!key3)keyget=3; //如果按键3按下
if(!key4)keyget=4; //如果按键4按下
return keyget; //返回按键状态
}
/*
* @brief 按键值发送函数
* @param mode:按键长短按模式标志,1长按0短按
* @reval keyend:最终返回的按键值
* @note
*/
unsigned char key_send(unsigned char mode)
{
static unsigned char keynow=0,keylast=0; //按键当前状态;按键上一次状态
unsigned char keyend=0; //最终要发送的按键值,每次扫描按键时均清零
keylast=keynow; //不断刷新按键上一次状态和当前状态
keynow=key_state();
if(keylast==1&&keynow==(1*mode)) //如果按键1按下
keyend=1;
if(keylast==2&&keynow==(2*mode)) //如果按键2按下
keyend=2;
if(keylast==3&&keynow==(3*mode)) //如果按键3按下
keyend=3;
if(keylast==4&&keynow==(4*mode)) //如果按键4按下
keyend=4;
return keyend; //发送最终按键值
}
2.定时器部分函数
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模式,若以reg52.h为头文件时删除这行代码
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 "key.h"
#include "timer0.h"
typedef unsigned char u8;
#define outputp0(y,x) P0=x,P2&=0x1f,P2|=y,P2&=0x1f; //P2高三位用于74HC138译码器选择锁存器,P0用于发送数据
u8 keynum=0,keytemp=0,light=0xff;
int showkey=0;
void test() //测试程序
{
if(keytemp!=0)
keynum=keytemp; //通过缓存量向keynum赋值,防止按下按键松开后立刻被清零
if(keynum!=0)
{
light=0xff<<keynum;
outputp0(0x80,light); //选择数据锁存器通道,向LED灯发送数据
keynum=0; //清零,防止一次按下多次执行
keytemp=0;
}
}
void main()
{
Timer0_Init();
while(1)
{
test();
}
}
void Timer0_Isr(void) interrupt 1
{
static u8 longflag=0; //长短按模式标志,1为长按,0为短按
static int tkey=0,tlongflag=0,tlongkey=0; //短按时间计数;按键按下时间计数;长按时间计数
TL0 = 0x66; //设置定时初始值 //1ms
TH0 = 0xFC; //设置定时初始值
tkey++; //按键短按扫描时间计数
if(tkey>=25) //1.短按每25ms检测一次按键;2.作长时间按下监测
{
if(!(key1&key2&key3&key4)) //长按检测,每25ms检测按键是否仍在按下,如果是
{
tlongflag++; //按键按下时间计数(以25ms为单位)
if(tlongflag>=80) //如果持续按下2s
{
tlongflag=80; //防止溢出
longflag=1; //切换为长按模式
}
}
else if(key1&key2&key3&key4) //长按检测,如果2s内任意一个25ms时间点检测时按键不按下
{
tlongflag=0; //清零按键按下时间计数
longflag=0; //切换为短按模式
}
tkey=0; //清零短按扫描时间计数
if(longflag==0) //如果是短按模式
keytemp=key_send(0); //按键以短按模式检测
}
tlongkey++; //按键长按扫描时间计数
if(tlongkey>=500) //长按时每500ms检测一次按键
{
tlongkey=0; //清零按键长按扫描时间计数
if(longflag==1) //如果是长按模式
keytemp=key_send(1); //按键以长按模式检测
}
}
四、测试程序现象
测试程序现象:按键1按下并松开,LED1亮;按键2按下并松开,LED1、2亮;按键3按下并松开,LED1、2、3亮;按键4按下并松开,LED1、2、3、4亮。
上述测试程序证明了按键值可以正常读取,但是并未检验按键扫描不会卡住主程序,也未检验按键长按时的成功性。为此,我们需要加上数码管对其进行检验。
向main.c文件中添加以下代码:
#include <intrins.h>
//注意变量定义应放在main.c文件的开头部分
int shownum=0; //数码管显示数字
unsigned char code Seg_Table[]={
0xc0, //共阳数码管段码表
0xf9,
0xa4,
0xb0,
0x99,
0x92,
0x82,
0xf8,
0x80,
0x90,
0xff //0xff熄灭
};
void Delay2ms(void) //2ms@11.0592MHz
{
unsigned char data i, j;
_nop_();
_nop_();
i = 22;
j = 128;
do
{
while (--j);
} while (--i);
}
void showbit(unsigned char pos,dat,dot) //数码管显示函数(1位)
{
outputp0(0xc0,0x01<<pos); //向数码管位选端发送数据
outputp0(0xe0,Seg_Table[10]); //向数码管段选端发送数据,这两行代码起消影作用
outputp0(0xc0,0x01<<pos); //向数码管位选端发送数据
outputp0(0xe0,Seg_Table[dat]+0x80*dot); //向数码管段选端发送数据
Delay2ms(); //延时
}
将main.c
中的test()
函数更改为:
void test()
{
keynum=keytemp; //通过缓存量向keynum赋值,防止按下按键松开后立刻被清零
if(keynum)
{
light=0xff<<keynum; //LED部分
outputp0(0x80,light);
shownum+=keynum;
keynum=0;
keytemp=0;
}
showbit(0,shownum/100%10,0); //数码管显示部分
showbit(1,shownum/10%10,0);
showbit(2,shownum%10,0);
}
测试现象:短按时,按键1按下并松开,LED1亮;按键2按下并松开,LED1、2亮;按键3按下并松开,LED1、2、3亮;按键4按下并松开,LED1、2、3、4亮。数码管显示数字根据按键返回值而增加。长按时,按键按下2s后开始有现象:LED点亮(与短按相同),数码管显示值从按下2s时开始每隔500ms变化一次。
五、总结
通过上述程序,可以实现定时器扫描按键以提高主程序执行效率。通过类比,也可以实现定时器扫描矩阵键盘和数码管,详情见定时器扫描矩阵键盘简易代码(短按/长按)和定时器扫描8位数码管(含滚动显示)
此外,由于定时器每1ms进入一次中断,对于具有IIC和One-Wire通信等具有严格时序要求的项目来说,容易使通信被干扰,因此不建议使用定时器扫描的方式。当然,通过一些方法也可以实现二者兼容并存,在DS1302可调时钟(按键短按/长按)中提供了其中一种方法。
有任何问题和补充,欢迎评论区交流。