目的
在学习了江科大的课程之后,我发现使用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; // 无按键事件
}
处理按键 事件的函数
/**
* @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下,在1秒内触发按键松开,则判定为短按
- 状态1下,在1秒外触发按键松开,则判定为长按
- 状态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再检测两次并返回值。