嗨嗨嗨!独立按键的短按,长按,双按(stm32 标准库)

本文介绍了一种在STM32单片机上改进的按键检测程序,避免了使用delay导致的程序阻塞。通过定时器和中断处理,实现在不消耗过多软件资源的情况下,区分短按、长按和双按事件,并讨论了使用外部中断进行进一步优化的可能性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目的 

在学习了江科大的课程之后,我发现使用delaywhile的按键检测程序会阻塞程序的运行,因此我决定进行代码改进,在不使用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; // 无按键事件
}
         处理按键 事件的函数
/**
 * @brief 处理按键事件的函数
 * @param Key_Val 按键值
 * @return 事件值
 *          Event_Val_ShortPress = 0,                     短按
 *          Event_Val_LongPress,                          长按
 *          Event_Val_DoublePress,                        双按
 *          Event_Val_NoEvent                             无事件
 *
 * This function handles the key events.
 * @param Key_Val The value of the key
 * @return The event value
 * 
 * @note 如果1s内按键按下两次却不松开,返回Event_Val_ShortPress.
 */
uint8_t Key_Event(uint8_t Key_Val)
{
    switch (Event_State)
    {
        case Event_State_0:
            if (Key_Val == Key_Value_Pressed)               // 按下 (Pressed)
            {
                Event_State = Event_State_1;                // 进入状态1
                Event_Count = 1;                            // 计数器清零
                Event_Val = Event_Val_NoEvent;              // 事件值清零
            }
            break;
        case Event_State_1:
            Event_Count++;  
            if (Event_Count < 100)                        // 100次为500ms
            {
                // 如果在500ms内松开过按键,则事件变为短按
                if (Key_Val == Key_Value_Released)           // 松开 (Released)
                {
                    // 如果在500ms内再次松开按键,也就是按键再次被按下和松开,则事件变为双按
                    if (Event_Val == Event_Val_ShortPress)
                    {
                        Event_Val = Event_Val_DoublePress;
                        Event_State = Event_State_0;
                        return Event_Val;
                    }
                    Event_Val = Event_Val_ShortPress;
                }
            }
            else if (Event_Count < 200)               // 200次为1s
            {
                // 如果事件是短按 说明按键已经松开了 可以返回事件值了
                if (Event_Val == Event_Val_ShortPress)           
                {
                    Event_State = Event_State_0;
                    return Event_Val;
                }
            }
            else
            {
                // 如果在1s内按键一直按下,则事件变为长按
                if (Key_Val == Key_Value_Released)           // 松开 (Released)
                {
                    Event_Val = Event_Val_LongPress;
                    Event_State = Event_State_0;
                    return Event_Val;
                }
            }
            break;
        default:
            break;
    }
    return Event_Val_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再检测两次并返回值。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值