STM32标准库教程:独立按键的短按、长按和双击实现方法详解

目的 

在学习了江科大的课程之后,我发现使用delay和while的按键检测程序会阻塞程序的运行,因此我决定进行代码改进,在不使用delay函数消抖,以不阻塞程序的方式在stm32上实现独立按键的短按,长按,双按

短按,长按,双按的个人定义

短按

在1以内松开了一次按键

长按

在1s以后松开了一次按键

双按

在0.5s内松开两次按键

特殊情况

如果1s内按键按下两次却不松开,返回短按

原理       

  • 使用定时器进行每5ms一次检测,当触发边沿(上升沿,下降沿)后,将之后的三次数据整合并返回一次数据包
  • 数据包 包含一个 状态值( EventVal) 和一个 稳定警告值(Warning)
  • 实际上 我只返回了一个数据 状态值( EventVal), 但警告值其实计算过了,打击可以按自己需求进行返回
  • 代码思路讲解

    总论

            我这里时使用了stm32f1系列的芯片,使用标准库进行编程,除了库文件,我们还需要main.c,Key.c,Key.h,Timer.c,Timer.h,LED.c,LED.h这几个文件

            main.c

            在这里我实现了每5ms(通过判断自定义时间标志位)进行一次按键扫描,并将获取的扫描值放入按键事件判断,并在0.96Oled屏幕上显示短按(Single), 长按(Long),双按(Double)

    。并且短按实现LED灯亮,长按LED灯灭

            同时我还每1s将Num++,并在Oled屏幕上显示

    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "Key.h"
    #include "LED.h"
    #include "OLED.h"
    #include "Timer.h"
    /*定时器实现按键扫描*/
    /*原理是每5ms中断扫描依次按键,当key产生变化,连续记录三次*/
    
    uint16_t Num = 0;
    uint8_t Key_Val = 2;
    
    int main(void)
    {
    	OLED_Init();
    	Timer_Init();
    	Key_Init();
    	LED_Init();
    	OLED_ShowNum(1, 1, Num, 3);
    	while (1)
    	{
    		if (Time_5ms)
    		{
    			Time_5ms = 0;                //标志位清0
    			Key_Val = Key_Scan();
    			switch (Key_Event(Key_Val))
    			{
    				case 0:
    					OLED_ShowString(1, 1, "Single");
    					LED1_On();
    					break;
    				case 1:
    					OLED_ShowString(1, 1, "Long  ");
    					LED1_Off();
    					break;	
    				case 2:
    					OLED_ShowString(1, 1, "Double");
    					break;	
    			}
    		}
    		if (Time_1000ms)
    		{
    			Time_1000ms = 0;
    			Num++;
    			OLED_ShowNum(2, 1, Num, 3);
    		}
    	}
    }
    

            Timer.c

                  在这里我们需要创建两个记录时间状态的标志位Time_1000ms(1000ms标志位)用于数字自增 和Time_5ms(5ms标志位以及用来判断时间标志位是否可以置一的计数值Time_1000ms_Count Time_5ms_Count 用于按键扫描

    #include "stm32f10x.h"                  // Device header
    
    uint8_t Time_1000ms = 0;                // 1000ms标志位
    uint8_t Time_5ms = 0;                   // 5ms标志位
    
    uint16_t Time_1000ms_Count = 0;         // 1000ms计数
    uint16_t Time_5ms_Count = 0;            // 5ms计数

                    我们将设定一个周期是1ms,也就是频率1000Hz的计数器,并设定NVIC中断,在每次进入中断将计数+1,并在到达对应时间计数后将标志位置1,在main.c判断标志位后会软件置0

            Timer初始化配置
    void Timer_Init(void)
    {
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    
        TIM_InternalClockConfig(TIM2);
    
        TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = 
        {
            .TIM_Period = 1000,                               // 定时器溢出时间是1ms               
            .TIM_Prescaler = 72,
            .TIM_ClockDivision = TIM_CKD_DIV1,
            .TIM_CounterMode = TIM_CounterMode_Up
        };
        TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
        TIM_ClearFlag(TIM2, TIM_FLAG_Update);                 //clear 更新flag to 避免马上进入更新中断
    
        TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    
    
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
        NVIC_InitTypeDef NVIC_InitStructure = 
        {
            .NVIC_IRQChannel = TIM2_IRQn,
            .NVIC_IRQChannelPreemptionPriority = 0,
            .NVIC_IRQChannelSubPriority = 0,
            .NVIC_IRQChannelCmd = ENABLE
        };
        NVIC_Init(&NVIC_InitStructure);
    
        TIM_Cmd(TIM2, ENABLE);
    }
    
            中断函数

            计数并在达到对应值置标志位1

    //定时器中断服务函数
    /**
     * @brief TIM2中断处理函数
     * 
     * 定时器溢出时间是1ms,所以每1ms会进入一次中断。
     * 
     * 此函数是TIM2的中断处理函数。当TIM2更新中断发生时,会调用此函数。
     * 它会增加Time_1000ms_Count和Time_5ms_Count变量的值,并检查Time_1000ms_Count是否达到1000,以 
     * 及Time_5ms_Count是否达到5。如果满足任何条件,
     * 则会将相应的Time_1000ms、Time_5ms标志设置为1。
     * 
     * @param None
     * @retval None
     */
    void TIM2_IRQHandler(void)
    {
        if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
        {
            TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
            Time_1000ms_Count++;
            Time_5ms_Count++;
            if (Time_1000ms_Count >= 1000)
            {
                Time_1000ms_Count = 0;
                Time_1000ms = 1;
            }
    
            if (Time_5ms_Count >= 5)
            {
                Time_5ms_Count = 0;
                Time_5ms = 1;
            }
        }
    }
    

            Key.c

            枚举体和变量的创建

            首先我定义了一些枚举体用于将一些数据可视化,大家也可以不使用直接用0 1 2 3代替,此外还创建了一些变量用于数据的计算,可以看下以下的代码,注释很完善哟

    #include "stm32f10x.h"                  // Device header
    
    enum Key_Warning_Type
    {
        Key_Warning_Accurate = 0,                // 数据精准标志位 0.准  1.不准 使用三次异或实现,如果三次全一样则为
        Key_Warning_Inaccurate                   
    };
    
    enum Key_Value_Type
    {
        Key_Value_Pressed = 0,                    // 按键值     1.松开 0.按下
        Key_Value_Released,
        Key_Value_NoEvent          
    };
    
    enum Key_State_Type
    {
        Key_State_0 = 0,                          // 按键状态 0.无 1.检测消抖 
        Key_State_1
    };
    
    enum Event_Val_Type
    {
        Event_Val_ShortPress = 0,                    // 短按
        Event_Val_LongPress,                         // 长按
        Event_Val_DoublePress,                       // 双按
        Event_Val_NoEvent                            // 无事件
    };
    
    enum Event_State_Type
    {
        Event_State_0 = 0,                 // 状态0
        Event_State_1                      // 状态1
    };
    
    uint8_t Key_Warning = Key_Warning_Accurate;         // 数据精准标志位 0.准  1.不准 使用三次异或实现,如果三次全一样则为
    uint8_t Key_Value = Key_Value_Released;             // 按键值        1.松开 0.按下
    uint8_t Key_Value_Old = Key_Value_Released;         // 上一次按键值
    uint8_t Key_Value_Sum = 0;                          // 按键值累加
    uint8_t Key_Count = 0;                              // 按键计数 1 - 3
    uint8_t Key_State = Key_State_0;                    // 按键状态 0.无 1.检测消抖 
    
    uint8_t Event_Val = Event_Val_NoEvent;              // 事件状态值 0.短按 1.长按 2.双按 3.无事件
    
    uint8_t Event_State = Event_State_0;                // 事件状态阶段 0.无 1.检测
    uint16_t Event_Count = 0;                           // 200次为1s
            按键初始化

            我使用了GPIOB11作为Key输入检测引脚,使用上拉输入

    void Key_Init(void)
    {
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //1. Peripheral 2. Write Enable
    
        GPIO_InitTypeDef GPIO_InitStructure = 
        {
            .GPIO_Pin = GPIO_Pin_11,
            .GPIO_Mode = GPIO_Mode_IPU,                 // 上拉输入
            .GPIO_Speed = GPIO_Speed_50MHz
        };
        GPIO_Init(GPIOB, &GPIO_InitStructure);
    }
             按键边沿检测

            我们将本次的按键值和上次的进行异或操作(两次数据一样返回0 两次数据不一样和返回1)

    // 按键边沿检测
    uint8_t Key_IF_Edge(uint8_t Key_Val, uint8_t Key_Val_Old)
    {
        return Key_Val ^ Key_Val_Old;
    }
             按键状态扫描

            使用了状态机的思想,在触发边沿之后,再检测两次数据并返回一个键值,因为我们有误差允许,加上边沿总共三次检测 返回出现两次以上的那个值

            我们使用了下面这个式子来计算,大家仔细想想哦

    (Key_Value_Sum + 1) / 3; 
                     代码主体
    /**
     * @brief   扫描按键的状态。
     * @return  表示按键状态的值:
     *              - 0:无按键事件
     *              - 1:按键按下
     *              - 2:按键松开
     *
     * @brief   Scans the state of the key.
     * @return  The value representing the key state:
     *              - 0: No key event
     *              - 1: key pressed
     *              - 2: key released
     */
    uint8_t Key_Scan(void)
    {
        // 保存上一个按键值
        Key_Value_Old = Key_Value;
        
        // 读取当前按键值
        Key_Value = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
        
        switch (Key_State)
        {
            case Key_State_0:
                if (Key_IF_Edge(Key_Value, Key_Value_Old))
                {
                    Key_State = Key_State_1;
                    Key_Count = 0;
                    Key_Value_Sum = Key_Value;
                    Key_Warning = Key_Warning_Accurate;
                }
                break;
            case Key_State_1:
                Key_Value_Sum += Key_Value;
                Key_Count++;
                if (Key_IF_Edge(Key_Value, Key_Value_Old))
                {
                    Key_Warning = Key_Warning_Inaccurate;
                }
                if (Key_Count >= 2)
                {
                    Key_State = Key_State_0;
                    return (Key_Value_Sum + 1) / 3;         // 两次都是1 那么(2+1)/3 = 1 两次都是0 那么(1+1)/3 = 0 
                }
                break;
        }
        
        return Key_Value_NoEvent; // 无按键事件
    }
             处理按键 事件的函数

            同样我们使用了状态机的思想,这里的思路是

    1.  当按键按下,进入状态1
    2.  状态1下,在1秒内触发按键松开,则判定为短按
    3.  状态1下,在1秒外触发按键松开,则判定为长按
    4.  状态1下,在0.5s内触发两次按键松开,则判定为双按
            特殊情况

            在0.5s内按下两次却不松开,判定为短按

            LED.c(不是必须)

            这里我们使用GPIOA1作为LED的驱动引脚通用开漏另一端接VCC,下面是初始化代码

    void LED_Init(void)
    {
        // Reset and clock control APB2 Peripheral Line Clock Commond
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //1. Peripheral 2. Write Enable
        GPIO_InitTypeDef GPIO_InitStructure; // 通过结构体配置初始化数据
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; 
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
        GPIO_Init(GPIOA, &GPIO_InitStructure); //1. Peripheral Name 2. 结构体地址
        GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1);
    }

            为了方便使用,我还写了两个函数用来控制灯的亮灭

    void LED_On(void)
    {
        GPIO_ResetBits(GPIOA, GPIO_Pin_1);
    }
    
    void LED_Off(void)
    {
        GPIO_SetBits(GPIOA, GPIO_Pin_1);
    }

             OLED.c 

            这个我是使用江科大提供的,会用函数就行

    最终效果

            

    不足之处

            1.每5ms进行一次按键检测,依然占用了一定的软件资源

            2.因为有双按的存在,所以短按不能被立刻反馈,是有一定延迟的

    后续改进

            使用外部中断进行按键扫描,在边沿触发按键中断后,每5ms再检测两次并返回值。

    作者:MaikieMaiky

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32标准库教程:独立按键的短按、长按和双击实现方法详解

    发表评论