一.需求描述:
遇到有非常多的按键,每个按键又有非常多的需求,这些按键也不能互相干扰的情况,如果用很多定时器,或者用很多中断处理,不仅浪费很多硬件资源,也很有可能会发生冲突,所以需要用到一定的算法加持,让程序对事务的处理更加优雅。
假设现在有四个按键,每个按键要求独立运行,不能互相干扰。按键在按下时,会给串口发送信息,现在要求发送的信息有:1.按键短按,2.按键长按,3.按键松手时。短按和长按是两种不同的事件,要求短按事件大概按下和松手的间隔小于2s,而按下和松手的间隔大于2s。
二.题目分析
由题可知,如果用硬件资源完成这项任务,虽然对编程的人比较友好,但是对单片机资源是极大的浪费,并且很可能会引发资源之间的冲突。如果想要尽量少使用单片机资源,可以通过定义大量标志位,保存每一个按键的每一个阶段的操作来达到减少单片机资源的目的。但是这样代码又会显得非常冗杂,移植性和可读性都非常差。笔者记录下一种相当巧妙的通过while循环不断扫描所有按键,利用while循环的重复性进行复杂的任务处理的方法(这里用标准库,因为笔者在写本篇博客时HAL库还不是很精通)。
代码如下
//按键1
void OnKey1Presseds(void)//°按键一的短按事件
{
printf("%s\r\n","Key1_Pressed");
OLED_ShowString(1,1," ");
OLED_ShowString(1,1,"Key1_Pressed");
}
void OnKey1Pressedl(void)//按键一长按事件
{
printf("%s\r\n","Key1_Press_Long");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"Key1_Press_Long");
}
void OnKey1Released(void)//按键一松手事件
{
printf("%s\r\n","Key1_Released");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"Key1_Released");
}
//按键2
void OnKey2Presseds(void)//按键二短按事件
{
printf("%s\r\n","Key2_Pressed");
OLED_ShowString(1,1," ");
OLED_ShowString(1,1,"Key2_Pressed");
}
void OnKey2Pressedl(void)//按键二长按事件
{
printf("%s\r\n","Key2_Press_Long");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"Key2_Press_Long");
}
void OnKey2Released(void)//按键二松手事件
{
printf("%s\r\n","Key2_Released");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"Key2_Released");
}
//按键三
void OnKey3Presseds(void)//按键三短按事件
{
printf("%s\r\n","Key3_Pressed");
OLED_ShowString(1,1," ");
OLED_ShowString(1,1,"Key3_Pressed");
}
void OnKey3Pressedl(void)//按键三长按事件
{
printf("%s\r\n","Key3_Press_Long");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"Key3_Press_Long");
}
void OnKey3Released(void)//按键三松手事件
{
printf("%s\r\n","Key3_Released");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"Key3_Released");
}
//按键四
void OnKey4Presseds(void)//按键四短按事件
{
printf("%s\r\n","Key4_Pressed");
OLED_ShowString(1,1," ");
OLED_ShowString(1,1,"Key4_Pressed");
}
void OnKey4Pressedl(void)//按键四长按事件
{
printf("%s\r\n","Key4_Press_Long");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"Key4_Press_Long");
}
void OnKey4Released(void)//按键四松手事件
{
printf("%s\r\n","Key4_Released");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"Key4_Released");
}
#define KEY_NUM 4 //按键数量
#define GPIO_KEY1 GPIO_Pin_3 //按键1
#define GPIO_KEY2 GPIO_Pin_4 //2
#define GPIO_KEY3 GPIO_Pin_5 //3
#define GPIO_KEY4 GPIO_Pin_6 //4
uint8_t KeyPinNum[]={GPIO_KEY1,GPIO_KEY2,GPIO_KEY3,GPIO_KEY4};
void (*pOnKeyPressedS[])(void)={OnKey1Presseds,OnKey2Presseds,OnKey3Presseds,OnKey4Presseds,OnKey5Presseds,};//短按数组
void (*pOnKeyPressedL[])(void)={OnKey1Pressedl,OnKey2Pressedl,OnKey3Pressedl,OnKey4Pressedl,OnKey5Pressedl,};//长按数组
void (*pOnKeyReleased[])(void)={OnKey1Released,OnKey2Released,OnKey3Released,OnKey4Released,OnKey5Released,};//松开数组
u8 KeyPressed[]={0,0,0,0};//按键是否被按下
long KeyLastPressedTime[]={0,0,0,0};//按键按下的时间
long KeyLastReleasedTime[]={0,0,0,0};//按键松手的时间
u8 isSendKeyLongPressedEvent[]={0,0,0,0};//是否触发过按键长按的事件
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_3| GPIO_Pin_4|GPIO_Pin_5| GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
}
void HandleKey(void)
{
for(u8 i=0;i<KEY_NUM;i++)//不断扫描五个按键
{
if(KeyPressed[i])//如果按键已经按下
{
if(CurrentTimes-KeyLastPressedTime[i]>50)//按键按下超过50ms,CurrentTimes在定时器中,大概1ms会增大一位
{
if(GPIO_ReadInputDataBit(GPIOA, KeyPinNum[i])==1)//如果按键松手
{
if(isSendKeyLongPressedEvent[i]==0)//如果这时没有触发长按事件
{
if(pOnKeyReleased[i])//如果对应的松开事件存在
{
pOnKeyReleased[i]();//触发对应的松开事件
}
}
KeyPressed[i]=0;//按键触发数组归零并且开始下一轮扫描
KeyLastReleasedTime[i]=CurrentTimes;//记录按键松手的时间(暂时没用)
}//按键松开事件结束
else//如果一直按住按键
{
if(CurrentTimes-KeyLastPressedTime[i]>1000)//按住的时间超过1000ms
{
if(isSendKeyLongPressedEvent[i]==0)//没有触发过长按事件
{
isSendKeyLongPressedEvent[i]=1;//将长按事件置一
if(pOnKeyPressedL[i])//如果存在长按事件
{
pOnKeyPressedL[i]();//触发chang'an
}
}
}
}//长按事件结束
}//按键按下超过50ms事件结束
} //按键第二次按下之后的情况结束
else//按键如果第一次按下
{
if( (GPIO_ReadInputDataBit(GPIOA, KeyPinNum[i])==0)&&(CurrentTimes-KeyLastReleasedTime[i]>200))
{
//首次按下
KeyPressed[i]=1;//对应的按键已经按下
KeyLastPressedTime[i]=CurrentTimes;//记录按键按下的时间
isSendKeyLongPressedEvent[i]=0;//清零上次按下触发长按事件的标志
if(pOnKeyPressedS[i])//如果存在按下按键的事件
{
pOnKeyPressedS[i]();//触发按下事件
}
}
}
}//for
}
定时器中断处理(配置部分省略)
void TIM2_IRQHandler(void)//中断
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)//获取中断标志位
{
CurrentTimes++;//当前的时间
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
三.解析代码:
代码整体有点长,但是封装的比较成功(个人觉得),可移植性非常强,而且可扩展性也非常强,拥有这串代码,哪怕有一万个按键,也可以只需要在代码开头增加事件,以及更改最开头的数组的大小即可轻松实现庞大事件的处理,而且按键之间完全没有任何干扰。下面我开始详细介绍这段代码:
1.事件区:
这个不再赘述:
//按键x
void OnKey1Presseds(void)//按键x的短按事件
{}
void OnKey1Pressedl(void)//按键x长按事件
{}
void OnKey1Released(void)//按键x松手事件
{}
2.定义区:
这个地方也没有什么好定义的,只是为了写程序的时候方便调用。值得一提的是,这里用了数组去存储各种标志位,同时用一个数组去存储所有的事件,这里用数组的原因是可以用索引i去表示,也是为了方便调用对应的事件。
#define KEY_NUM 4 //按键数量
#define GPIO_KEY1 GPIO_Pin_3 //按键1
#define GPIO_KEY2 GPIO_Pin_4 //2
#define GPIO_KEY3 GPIO_Pin_5 //3
#define GPIO_KEY4 GPIO_Pin_6 //4
uint8_t KeyPinNum[]={GPIO_KEY1,GPIO_KEY2,GPIO_KEY3,GPIO_KEY4};
void (*pOnKeyPressedS[])(void)={OnKey1Presseds,OnKey2Presseds,OnKey3Presseds,OnKey4Presseds,OnKey5Presseds,};//短按数组
void (*pOnKeyPressedL[])(void)={OnKey1Pressedl,OnKey2Pressedl,OnKey3Pressedl,OnKey4Pressedl,OnKey5Pressedl,};//长按数组
void (*pOnKeyReleased[])(void)={OnKey1Released,OnKey2Released,OnKey3Released,OnKey4Released,OnKey5Released,};//松开数组
u8 KeyPressed[]={0,0,0,0};//按键是否被按下
long KeyLastPressedTime[]={0,0,0,0};//按键按下的时间
long KeyLastReleasedTime[]={0,0,0,0};//按键松手的时间
u8 isSendKeyLongPressedEvent[]={0,0,0,0};//是否触发过按键长按的事件
3.扫描区
void HandleKey(void)
{
for(u8 i=0;i<KEY_NUM;i++)//不断扫描五个按键
{
if(KeyPressed[i])//如果按键已经按下
{
if(CurrentTimes-KeyLastPressedTime[i]>50)//按键按下超过50ms,CurrentTimes在定时器中,大概1ms会增大一位
{
if(GPIO_ReadInputDataBit(GPIOA, KeyPinNum[i])==1)//如果按键松手
{
if(isSendKeyLongPressedEvent[i]==0)//如果这时没有触发长按事件
{
if(pOnKeyReleased[i])//如果对应的松开事件存在
{
pOnKeyReleased[i]();//触发对应的松开事件
}
}
KeyPressed[i]=0;//按键触发数组归零并且开始下一轮扫描
KeyLastReleasedTime[i]=CurrentTimes;//记录按键松手的时间(暂时没用)
}//按键松开事件结束
else//如果一直按住按键
{
if(CurrentTimes-KeyLastPressedTime[i]>1000)//按住的时间超过1000ms
{
if(isSendKeyLongPressedEvent[i]==0)//没有触发过长按事件
{
isSendKeyLongPressedEvent[i]=1;//将长按事件置一
if(pOnKeyPressedL[i])//如果存在长按事件
{
pOnKeyPressedL[i]();//触发chang'an
}
}
}
}//长按事件结束
}//按键按下超过50ms事件结束
} //按键第二次按下之后的情况结束
else//按键如果第一次按下
{
if( (GPIO_ReadInputDataBit(GPIOA, KeyPinNum[i])==0)&&(CurrentTimes-KeyLastReleasedTime[i]>200))
{
//首次按下
KeyPressed[i]=1;//对应的按键已经按下
KeyLastPressedTime[i]=CurrentTimes;//记录按键按下的时间
isSendKeyLongPressedEvent[i]=0;//清零上次按下触发长按事件的标志
if(pOnKeyPressedS[i])//如果存在按下按键的事件
{
pOnKeyPressedS[i]();//触发按下事件
}
}
}
}//for
}
这个区域逻辑较为复杂,如果想要探究其运行逻辑的同好可以继续看下去,而想要实现功能的同学可以直接复制,不用修改任何一个地方。在了解这串代码的运行逻辑时,笔者建议将以上代码复制到编辑器,因为格式原因,代码的括号出现了对不齐的情况,看起来非常困难。
扫描:
这串代码的最外层是一个for循环,这个for循环的作用就是不断扫描几个按键,将这个函数整体放到主循环中,就可以实现对所有循环的不断扫描,因为代码中几乎没有延时,所以扫描速度非常快,基本上可以相当于同时检测所有代码。是的,这也是数码管的显示原理,通过对不同数码管快速的扫描,利用视觉暂留吧达到同时显示的原理。
for(u8 i=0;i<KEY_NUM;i++)
{}
判断是否为第一次:
接下来就是判断按键是否为第一次按下,因为按键如果不是第一次按下,就需要判断是否属于长按的情况,因为我们对按键的判断逻辑是:是长按还是短按,那么拆分一下就是,首先判断按键是否已经按下,如果按下了,再判断按下的时间是否满足长按的条件。
if(KeyPressed[i])
{}
else
{}
判断是否为长按:
分为两种情况,按下超过50ms的时候会进入触发松开事件和长按事件的区间,其中又分为是否松开,按住超过一秒则是长按,如果没有超过一秒,则是触发松开事件。
if(KeyPressed[i])//如果按键已经按下
{
if(CurrentTimes-KeyLastPressedTime[i]>50)//按键按下超过50ms,CurrentTimes在定时器中,大概1ms会增大一位
{
if(GPIO_ReadInputDataBit(GPIOA, KeyPinNum[i])==1)//如果按键松手
{
if(isSendKeyLongPressedEvent[i]==0)//如果这时没有触发长按事件
{
if(pOnKeyReleased[i])//如果对应的松开事件存在
{
pOnKeyReleased[i]();//触发对应的松开事件
}
}
KeyPressed[i]=0;//按键触发数组归零并且开始下一轮扫描
KeyLastReleasedTime[i]=CurrentTimes;//记录按键松手的时间(暂时没用)
}//按键松开事件结束
else//如果一直按住按键
{
if(CurrentTimes-KeyLastPressedTime[i]>1000)//按住的时间超过1000ms
{
if(isSendKeyLongPressedEvent[i]==0)//没有触发过长按事件
{
isSendKeyLongPressedEvent[i]=1;//将长按事件置一
if(pOnKeyPressedL[i])//如果存在长按事件
{
pOnKeyPressedL[i]();//触发chang'an
}
}
}
}//长按事件结束
}//按键按下超过50ms事件结束
} //按键第二次按下之后的情况结束
四.总结
这道题比较难,但是只要理清思路,熟练运用数组这种数据结构,只要按照逻辑进行编程,也能很快解出来,实际上是这种题需要脑子非常清楚,对下一步做什么事了如指掌。希望大家有所收获,有错误或者疏漏恳请在评论区指出,感谢各位大佬。