目录
前言
按键是许多单片机初学者必学的元件,按键的驱动程序中,消抖是很重要的一个部分,网上许多教程的按键消抖思路如图所示:
根据这个思路,这些教程给出的程序写法大概如下所示:
sbit Button0 = P1^0;
void main()
{
while(1)
{
if(Button == 0)
{
delay_ms(50);
if(Button0 == 0)
{
printf("Button pressed!");
}
}
}
}
其中delay_ms(50)函数起延时消抖作用,咋一看好像没什么问题,但是,问题就出现在这个地方。且看,在程序中,当按键按下的时候,程序就停留在delay_ms函数中,单纯地做加减法运算,最终实现延时50ms的目的,而对于其他的事情呢,在这50ms内单片机一概不管。要知道50ms对于单片机来说是可以执行很多任务的,这样的程序在简单的项目里倒没什么,但是在复杂的项目里,它的缺点就特别明显。
这么说来读者或许感觉不是特别深刻,我举个例子。对许多初学者来说,数码管也早就有所了解,数码管的使用中有动态数码管这种显示方法,倘若将动态数码管与按键结合使用,以上我所描述的情况即会出现,读者不妨尝试尝试。(仅对初学者而言昂,用其他方法规避这个问题当我没说)。
不知读者是否已经体会到了?那么今天我就是为解决这个问题而来的。
消抖流程
首先上面展示的消抖思路是没问题的,只是实现的过程出现了问题,根据上面的思路以及一些例程,我为读者提供了一种实现方法:
额……读者可能看不明白,但是不要慌,我解读一下。在整个流程里,单片机每1ms检测一次按键是否按下,当连续50次(50ms)检测到按键按下时,触发按键程序,其余时间段的则执行其他程序(除按键以外的程序)。
同样是实现50ms的延时消抖,但是与之前不同的是,在这50ms里,单片机并没有停留在这个地方,而是在执行其他程序,直到50ms达到,执行按键程序。(读者应该能get到意思吧?不懂的评论区留言)
实现代码
根据以上的思路,接下来来看代码的实现,我将一步步搭建程序的框架,直接使用的读者可直接跳到后面获取最终程序。需要说明的是,以下的代码是在STC8H8K64U单片机中实现的,不同的单片机代码会有些许差异,但是整体的实现思路是一样的。
初始化定时器
要达到流程图中的效果,硬核延时是难以实现的,但好在单片机有定时器,我们可以利用定时器来实现每1ms扫描一次按键,只需设置一个1ms的定时器中断即可。
#include<stc8h.h>
extern bit button0ScanBit_ms; //外部变量声明(来自按键程序)
#define FOSC 24000000UL //系统时钟频率,24MHz
#define TIMER4_US 1000U //中断时间(单位:us)
//初始化定时器4
void TIMER4_Init()
{
T4T3M &= ~(0x01 << 4);
T4T3M |= (0x01 << 5);
// T4T3M &= ~(0x01 << 5);
T4T3M &= ~(0x01 << 6);
TM4PS = 0;
if((T4T3M >> 5) & 0x01)
{
T4H = (65536 - FOSC/1000000UL/(TM4PS + 1) *TIMER4_US) >> 8;
T4L = (65536 - FOSC/1000000UL/(TM4PS + 1) *TIMER4_US) & 0xff;
}
else
{
T4H = (65536 - FOSC/1000000UL/(TM4PS + 1) *TIMER4_US/12) >> 8;
T4L = (65536 - FOSC/1000000UL/(TM4PS + 1) *TIMER4_US/12) & 0xff;
}
T4T3M |= (0x01 << 7);
IE2 |= (0x01 << 6);
EA = 1;
}
//每TIMER4_US进入一次中断
void TIMER4_Interrupt() interrupt 20
{
button0ScanBit_ms = 1;
}
定时器的初始化函数写法来自单片机数据手册,不是本次的重点,不作过多介绍。这里读者只需要注意到定时器每过1ms(1000us)进入中断函数将变量button0ScanBit_ms 置1。
每1ms扫描一次按键
在搭建出按键的扫描框架之前,我们先来规划一下按键的状态,按键的状态,有未按下、短按、长按、双击等等。在本次的讲解中,我只讲述短按和长按的实现,其余的读者自行发挥。
下面我用一个枚举类型将这些状态罗列出来:
typedef enum
{
ButtonState_No, //无按下
ButtonState_Short, //短按
ButtonState_Long //长按
}ButtonState; //按键扫描结果
接下来把扫描的框架搭建出来:
#include<stc8h.h>
bit button0ScanBit_ms = 0; //按键扫描标志位,每1ms扫描一次
typedef enum
{
ButtonState_No, //无按下
ButtonState_Short, //短按
ButtonState_Long //长按
}ButtonState; //按键扫描结果
#define BUTTON_PUSHED 0 //按键按下的电平
#define BUTTON_NOPUSH !BUTTON_PUSHED
sbit Button0 = P1^0;
//Button0
ButtonState BUTTON_Button0Scan()
{
ButtonState xdata button_NowState = ButtonState_No; //按键当下的状态
if(button0ScanBit_ms == 1)
{
button0ScanBit_ms = 0;
if(Button0 == BUTTON_PUSHED)
{
}
else
{
}
}
return button_NowState;
}
void main()
{
ButtonState buttonResult;
TIMER4_Init();
BUTTON_Init();
while(1)
{
buttonResult = BUTTON_Button0Scan();
if(buttonResult == ButtonState_Short)
{
}
else if(buttonResult == ButtonState_Long)
{
}
}
}
按键计数触发
在扫描到按键按下时,就应该开始计数了,当计数达到预定值就就表示按键真的按下了,而如果检测到按键并未按下,那么就要清空计数器。
#include<stc8h.h>
bit button0ScanBit_ms = 0; //按键扫描标志位,每1ms扫描一次
typedef enum
{
ButtonState_No, //无按下
ButtonState_Short, //短按
ButtonState_Long //长按
}ButtonState; //按键扫描结果
#define BUTTON_PUSHED 0 //按键按下的电平
#define BUTTON_NOPUSH !BUTTON_PUSHED
#define BUTTON_TRIGGER_NUM 50 //扫描到按键按下BUTTON_TRIGGER_NUM次时,触发短按
sbit Button0 = P1^0;
//Button0
ButtonState BUTTON_Button0Scan()
{
ButtonState xdata button_NowState = ButtonState_No; //按键当下的状态
static unsigned char xdata button_Counter = 0; //按键扫描次数(次数达到BUTTON_TRIGGER_NUM后,触发短按)
if(button0ScanBit_ms == 1)
{
button0ScanBit_ms = 0;
if(Button0 == BUTTON_PUSHED) //按键按下
{
button_Counter++;
if(button_Counter > BUTTON_TRIGGER_NUM) //计数到达,按键真的按下了
{
button_Counter = 0;
button_NowState = ButtonState_Short;
}
}
else //按键未按下
{
button_NowState = ButtonState_No;
button_Counter = 0;
}
}
return button_NowState;
}
void main()
{
ButtonState buttonResult;
TIMER4_Init();
BUTTON_Init();
while(1)
{
buttonResult = BUTTON_Button0Scan();
if(buttonResult == ButtonState_Short)
{
printf("Button0 Short Pressed!\r\n");
}
else if(buttonResult == ButtonState_Long)
{
}
}
}
短按的拓展
上面的的程序算是已经实现了我们想要的功能,但先别急,读者在使用的时候应该能感觉出来,按键一按下的时候就疯狂地触发短按,如果遇到那种按一次触发一次的该作何解?下面我为按键添加了一个标志,表示在本次按下按键之后触发过的状态,如果触发过短按,记录一次,直到按键抬起才清除该标志。
#include<stc8h.h>
bit button0ScanBit_ms = 0; //按键扫描标志位,每1ms扫描一次
typedef enum
{
ButtonState_No, //无按下
ButtonState_Short, //短按
ButtonState_Long //长按
}ButtonState; //按键扫描结果
#define BUTTON_PUSHED 0 //按键按下的电平
#define BUTTON_NOPUSH !BUTTON_PUSHED
#define BUTTON_TRIGGER_NUM 50 //扫描到按键按下BUTTON_TRIGGER_NUM次时,触发短按
sbit Button0 = P1^0;
//Button0
ButtonState BUTTON_Button0Scan()
{
ButtonState xdata button_NowState = ButtonState_No; //按键当下的状态
static ButtonState xdata button_PastState = ButtonState_No; //在按键触发后已经返回过的按键状态(主要用于不要重复返回)
static unsigned char xdata button_Counter = 0; //按键扫描次数(次数达到BUTTON_TRIGGER_NUM后,触发短按)
if(button0ScanBit_ms == 1)
{
button0ScanBit_ms = 0;
if(Button0 == BUTTON_PUSHED) //按键按下
{
button_Counter++;
if(button_Counter > BUTTON_TRIGGER_NUM) //计数到达,按键真的按下了
{
button_Counter = 0;
button_NowState = ButtonState_Short;
{
if(button_PastState == ButtonState_No) //在本次按键按下之后是否已经返回过短按状态
{
button_PastState = ButtonState_Short; //否
}
else
{
button_NowState = ButtonState_No; //是,清除状态,避免重复返回
}
}
}
}
else //按键未按下
{
button_NowState = ButtonState_No;
button_Counter = 0;
button_PastState = ButtonState_No;
}
}
return button_NowState;
}
void main()
{
ButtonState buttonResult;
TIMER4_Init();
BUTTON_Init();
while(1)
{
buttonResult = BUTTON_Button0Scan();
if(buttonResult == ButtonState_Short)
{
printf("Button0 Short Pressed!\r\n");
}
else if(buttonResult == ButtonState_Long)
{
}
}
}
这样下来,按键算是实现了。慢着!前面我还说个一个长按的状态,下面将为读者展示。
长按
按键的长按,可以理解成很多次的短按累计而成的,因此在函数中还需要引入一个变量来记录触发短按的次数,达到一定数量之后触发长按。
#include<stc8h.h>
bit button0ScanBit_ms = 0; //按键扫描标志位,每1ms扫描一次
typedef enum
{
ButtonState_No, //无按下
ButtonState_Short, //短按
ButtonState_Long //长按
}ButtonState; //按键扫描结果
#define BUTTON_PUSHED 0 //按键按下的电平
#define BUTTON_NOPUSH !BUTTON_PUSHED
#define BUTTON_TRIGGER_NUM 50 //扫描到按键按下BUTTON_TRIGGER_NUM次时,触发短按
#define BUTTON_LONG_S 3 //长按时间(单位:s)
const unsigned char xdata BUTTON_LTRIGGER_NUM = TIMER4_US/BUTTON_TRIGGER_NUM *BUTTON_LONG_S; //扫描到多少次短按之后代表长按
sbit Button0 = P1^0;
//Button0
ButtonState BUTTON_Button0Scan()
{
ButtonState xdata button_NowState = ButtonState_No; //按键当下的状态
static ButtonState xdata button_PastState = ButtonState_No; //在按键触发后已经返回过的按键状态(主要用于不要重复返回)
static unsigned char xdata button_Counter = 0; //按键扫描次数(次数达到BUTTON_TRIGGER_NUM后,触发短按)
if(button0ScanBit_ms == 1)
{
button0ScanBit_ms = 0;
if(Button0 == BUTTON_PUSHED) //按键按下
{
button_Counter++;
if(button_Counter > BUTTON_TRIGGER_NUM) //计数到达,按键真的按下了
{
button_Counter = 0;
button_NowState = ButtonState_Short;
{
if(button_PastState == ButtonState_No) //在本次按键按下之后是否已经返回过短按状态
{
button_PastState = ButtonState_Short; //否
}
else
{
button_NowState = ButtonState_No; //是,清除状态,避免重复返回
}
if(button_LongCounter < BUTTON_LTRIGGER_NUM)
{
button_LongCounter++; //短按累计次数未达到预定值
}
else //短按累计次数达到预定值
{
button_NowState = ButtonState_Long; //触发长按
}
}
}
}
else //按键未按下
{
button_NowState = ButtonState_No;
button_Counter = 0;
button_PastState = ButtonState_No;
button_LongCounter = 0;
}
}
return button_NowState;
}
void main()
{
ButtonState buttonResult;
TIMER4_Init();
BUTTON_Init();
while(1)
{
buttonResult = BUTTON_Button0Scan();
if(buttonResult == ButtonState_Short)
{
printf("Button0 Short Pressed!\r\n");
}
else if(buttonResult == ButtonState_Long)
{
printf("Button0 Long Pressed!\r\n");
}
}
}
长按的拓展
在长按的程序中,同样面临着疯狂触发的情况,这一点的处理方法跟短按是一样的。
#include<stc8h.h>
bit button0ScanBit_ms = 0; //按键扫描标志位,每1ms扫描一次
typedef enum
{
ButtonState_No, //无按下
ButtonState_Short, //短按
ButtonState_Long //长按
}ButtonState; //按键扫描结果
#define BUTTON_PUSHED 0 //按键按下的电平
#define BUTTON_NOPUSH !BUTTON_PUSHED
#define BUTTON_TRIGGER_NUM 50 //扫描到按键按下BUTTON_TRIGGER_NUM次时,触发短按
#define BUTTON_LONG_S 3 //长按时间(单位:s)
const unsigned char xdata BUTTON_LTRIGGER_NUM = TIMER4_US/BUTTON_TRIGGER_NUM *BUTTON_LONG_S; //扫描到多少次短按之后代表长按
sbit Button0 = P1^0;
//Button0
ButtonState BUTTON_Button0Scan()
{
ButtonState xdata button_NowState = ButtonState_No; //按键当下的状态
static ButtonState xdata button_PastState = ButtonState_No; //在按键触发后已经返回过的按键状态(主要用于不要重复返回)
static unsigned char xdata button_Counter = 0; //按键扫描次数(次数达到BUTTON_TRIGGER_NUM后,触发短按)
if(button0ScanBit_ms == 1)
{
button0ScanBit_ms = 0;
if(Button0 == BUTTON_PUSHED) //按键按下
{
button_Counter++;
if(button_Counter > BUTTON_TRIGGER_NUM) //计数到达,按键真的按下了
{
button_Counter = 0;
button_NowState = ButtonState_Short;
{
if(button_PastState == ButtonState_No) //在本次按键按下之后是否已经返回过短按状态
{
button_PastState = ButtonState_Short; //否
}
else
{
button_NowState = ButtonState_No; //是,清除状态,避免重复返回
}
if(button_LongCounter < BUTTON_LTRIGGER_NUM)
{
button_LongCounter++; //短按累计次数未达到预定值
}
else //短按累计次数达到预定值
{
button_NowState = ButtonState_Long; //触发长按
if(button_PastState != ButtonState_Long) //在本次按键按下之后是否已经返回过长按状态
{
button_PastState = ButtonState_Long; //否
}
else
{
button_NowState = ButtonState_No; //是,(返回过长按必定返回过短按,根据实际情况确定是否要避免重复返回长按状态)
}
}
}
}
}
else //按键未按下
{
button_NowState = ButtonState_No;
button_Counter = 0;
button_PastState = ButtonState_No;
button_LongCounter = 0;
}
}
return button_NowState;
}
void main()
{
ButtonState buttonResult;
TIMER4_Init();
BUTTON_Init();
while(1)
{
buttonResult = BUTTON_Button0Scan();
if(buttonResult == ButtonState_Short)
{
printf("Button0 Short Pressed!\r\n");
}
else if(buttonResult == ButtonState_Long)
{
printf("Button0 Long Pressed!\r\n");
}
}
}
在程序里,读者可能对常变量BUTTON_LTRIGGER_NUM和宏定义BUTTON_LONG_S有所疑惑,这里其实是将比较的次数转换成了时间,因为我们对长按一般都习惯说长按多少秒。
拓展按键
在实际的应用中,我们面对的可能并不是一个按键,而是多个按键,这就需要将按键拓展加以应用,而我上面所写的程序对于拓展按键也是十分方便的,假设我现在要拓展按键1,只需要添加一个bit变量,并到定时器中断中加入这个变量,然后将按键0的程序复制粘贴,最后把关于Button0的全改成Button1即可。(是不是很简单?)
//每TIMER4_US进入一次中断
void TIMER4_Interrupt() interrupt 20
{
button0ScanBit_ms = 1;
button1ScanBit_ms = 1;
}
bit button1ScanBit_ms = 0;
sbit Button1 = P1^1;
//Button1
ButtonState BUTTON_Button1Scan()
{
ButtonState xdata button_NowState = ButtonState_No; //按键当下的状态
static ButtonState xdata button_PastState = ButtonState_No; //在按键触发后已经返回过的按键状态(主要用于不要重复返回)
static unsigned char xdata button_Counter = 0; //按键扫描次数(次数达到BUTTON_TRIGGER_NUM后,触发短按)
if(button1ScanBit_ms == 1)
{
button1ScanBit_ms = 0;
if(Button1 == BUTTON_PUSHED) //按键按下
{
button_Counter++;
if(button_Counter > BUTTON_TRIGGER_NUM) //计数到达,按键真的按下了
{
button_Counter = 0;
button_NowState = ButtonState_Short;
{
if(button_PastState == ButtonState_No) //在本次按键按下之后是否已经返回过短按状态
{
button_PastState = ButtonState_Short; //否
}
else
{
button_NowState = ButtonState_No; //是,清除状态,避免重复返回
}
if(button_LongCounter < BUTTON_LTRIGGER_NUM)
{
button_LongCounter++; //短按累计次数未达到预定值
}
else //短按累计次数达到预定值
{
button_NowState = ButtonState_Long; //触发长按
if(button_PastState != ButtonState_Long) //在本次按键按下之后是否已经返回过长按状态
{
button_PastState = ButtonState_Long; //否
}
else
{
button_NowState = ButtonState_No; //是,(返回过长按必定返回过短按,根据实际情况确定是否要避免重复返回长按状态)
}
}
}
}
}
else //按键未按下
{
button_NowState = ButtonState_No;
button_Counter = 0;
button_PastState = ButtonState_No;
button_LongCounter = 0;
}
}
return button_NowState;
}
其他
在main函数中还有一个函数——BUTTON_Init(),这其实是因为STC8H8K64U单片机配置的原因,在这函数里面我将按键配置为高阻输入并使能上拉电阻,读者使用的单片机根据实际情况配置即可。
最终代码
#include<stc8h.h>
bit button0ScanBit_ms = 0; //按键扫描标志位,每1ms扫描一次
typedef enum
{
ButtonState_No, //无按下
ButtonState_Short, //短按
ButtonState_Long //长按
}ButtonState; //按键扫描结果
#define BUTTON_PUSHED 0 //按键按下的电平
#define BUTTON_NOPUSH !BUTTON_PUSHED
#define BUTTON_TRIGGER_NUM 50 //扫描到按键按下BUTTON_TRIGGER_NUM次时,触发短按
#define BUTTON_LONG_S 3 //长按时间(单位:s)
const unsigned char xdata BUTTON_LTRIGGER_NUM = TIMER4_US/BUTTON_TRIGGER_NUM *BUTTON_LONG_S; //扫描到多少次短按之后代表长按
sbit Button0 = P1^0;
//Button0
ButtonState BUTTON_Button0Scan()
{
ButtonState xdata button_NowState = ButtonState_No; //按键当下的状态
static ButtonState xdata button_PastState = ButtonState_No; //在按键触发后已经返回过的按键状态(主要用于不要重复返回)
static unsigned char xdata button_Counter = 0; //按键扫描次数(次数达到BUTTON_TRIGGER_NUM后,触发短按)
if(button0ScanBit_ms == 1)
{
button0ScanBit_ms = 0;
if(Button0 == BUTTON_PUSHED) //按键按下
{
button_Counter++;
if(button_Counter > BUTTON_TRIGGER_NUM) //计数到达,按键真的按下了
{
button_Counter = 0;
button_NowState = ButtonState_Short;
{
if(button_PastState == ButtonState_No) //在本次按键按下之后是否已经返回过短按状态
{
button_PastState = ButtonState_Short; //否
}
else
{
button_NowState = ButtonState_No; //是,清除状态,避免重复返回
}
if(button_LongCounter < BUTTON_LTRIGGER_NUM)
{
button_LongCounter++; //短按累计次数未达到预定值
}
else //短按累计次数达到预定值
{
button_NowState = ButtonState_Long; //触发长按
if(button_PastState != ButtonState_Long) //在本次按键按下之后是否已经返回过长按状态
{
button_PastState = ButtonState_Long; //否
}
else
{
button_NowState = ButtonState_No; //是,(返回过长按必定返回过短按,根据实际情况确定是否要避免重复返回长按状态)
}
}
}
}
}
else //按键未按下
{
button_NowState = ButtonState_No;
button_Counter = 0;
button_PastState = ButtonState_No;
button_LongCounter = 0;
}
}
return button_NowState;
}
void main()
{
ButtonState buttonResult;
TIMER4_Init();
BUTTON_Init();
while(1)
{
buttonResult = BUTTON_Button0Scan();
if(buttonResult == ButtonState_Short)
{
printf("Button0 Short Pressed!\r\n");
}
else if(buttonResult == ButtonState_Long)
{
printf("Button0 Long Pressed!\r\n");
}
}
}
最后
该按键程序是我在实际开发中而来的,或许读者所遇到的情况与我遇到的不一样,所以希望读者能根据实际情况进行应用。由于个人的水平实在有限,如有错误或改进之处,请赐教。如读者在文章中有所疑问,欢迎评论区留言!