STM32按键扫描——较为复杂的情况(记录贴②)

一.需求描述:

        遇到有非常多的按键,每个按键又有非常多的需求,这些按键也不能互相干扰的情况,如果用很多定时器,或者用很多中断处理,不仅浪费很多硬件资源,也很有可能会发生冲突,所以需要用到一定的算法加持,让程序对事务的处理更加优雅。

假设现在有四个按键,每个按键要求独立运行,不能互相干扰。按键在按下时,会给串口发送信息,现在要求发送的信息有: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事件结束
​
} //按键第二次按下之后的情况结束

四.总结

        这道题比较难,但是只要理清思路,熟练运用数组这种数据结构,只要按照逻辑进行编程,也能很快解出来,实际上是这种题需要脑子非常清楚,对下一步做什么事了如指掌。希望大家有所收获,有错误或者疏漏恳请在评论区指出,感谢各位大佬。

  • 45
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值