目录
一、前言
二、关于实现非阻塞的办法
2.1 中断类型的选择
2.2 定时器中断
二、程序流程图
2.1 状态S=0空闲状态
2.2 状态S=1按键判断长按还是其他的事件
2.3 状态S=2按键判断双击或者单击
2.4 状态S=3按键已双击状态
2.5 状态S=4长按状态
三、编写代码
3.1 按键初始化
3.2 获取按键当前的状态
3.3 按键状态机处理函数
四、完整代码
4.1 定时器中断初始化
4.2 按键初始化
一、前言
由于普通Delay延时函数会出现阻塞主函数的运行,会发生主函数变慢或者停止的情况,在没有使用RTOS的情况应该如何实现非阻塞呢,以下为个人学习经验分享,学习来自江科大。
二、关于实现非阻塞的办法
2.1 中断类型的选择
肯定是中断的办法来实现非阻塞的作用,但是我们使用的不是普通的IO口接收到什么信号就进入中断,这个办法进入的中断难免会有一些按键抖动,因此,我们使用定时器中断
2.2 定时器中断
那么定时器中断为什么能够实现非阻塞呢?我们是用这个办法来解决的,如下图:
我们设置为每20ms进入定时中断读取一次引脚的值,然后读取本次的引脚值和保存上一次的引脚值,然后进行两次的值的分析:
两次都为1,则为松开按键状态
上次是1,本次是0,则为刚按下按键状态
上次是0,本次是1,则为刚松开按键状态
由于我们使用了定时器隔开读数的原因,所以我们避免了使用Delay延迟函数,所以我们实现了不阻塞主函数!
二、程序流程图
2.1 状态S=0空闲状态
状态:空闲状态,也就是我们按键未按下的状态
功能:检测按键是否按下(跳出当前状态的判断)
状态转移:按键按下,然后转去状态1,并设定长按的时间
2.2 状态S=1按键判断长按还是其他的事件
状态:按键已按下状态,也就是我们按键刚按下去的状态
功能:检测按键是否松开(跳出当前状态的判断)
功能:等待上面设计的长按时间是否结束(跳出当前状态的判断)
状态转移:按键松开,然后转去状态2,并设定双击等待的时间
状态转移:长按时间到,然后转去状态4,并设定重复的时间(多久重复一次),设置长按标志位LONG
2.3 状态S=2按键判断双击或者单击
状态:按键松开状态,也就是我们按键刚松开的状态
功能:检测按键是否按下(跳出当前状态的判断)
功能:等待上面设计的双击时间是否结束(跳出当前状态的判断)
状态转移:按键按下,然后转去状态3,并设置双击标志位DOUBLE
状态转移:双击时间到,然后重置状态,回到状态0并设置单击标志位SINGLE
2.4 状态S=3按键已双击状态
状态:按键已双击状态,也就是已经判断完双击事件啦
功能:检测按键是否松开(跳出当前状态的判断)
状态转移:按键松开,然后重置状态,返回初始状态
2.5 状态S=4长按状态
状态:按键长按状态,一直按着就一直循环,然后置重复事件REPEAT
功能:检测按键是否松开(跳出当前状态的判断)
功能:等待重复时间(当时间到后重复执行当前状态的功能)
状态转移:重复时间到了,然后再设置重复时间,将当前状态再执行一次
状态转移:按键松开,然后重置状态,返回初始状态
三、编写代码
3.1 按键初始化
/*** @brief 按键GPIO初始化函数* @param 无* @retval 无* @description 配置按键对应的GPIO引脚* KEY_1(PB1)、KEY_2(PB11): 上拉输入,按下时读取为0* KEY_3(PB13)、KEY_4(PB15): 下拉输入,按下时读取为1*/
void Key_Init(void)
{// 使能GPIOB时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;// 配置KEY_1(PB1)和KEY_2(PB11)为上拉输入GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);// 配置KEY_3(PB13)和KEY_4(PB15)为下拉输入GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 下拉输入模式GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);
}
初始化就不多说啦,正常输入模式就可以。
3.2 获取按键当前的状态
/* 按键编号定义 */
#define KEY_1 0 // 按键1编号 (对应PB1,上拉输入)
#define KEY_2 1 // 按键2编号 (对应PB11,上拉输入)
#define KEY_3 2 // 按键3编号 (对应PB13,下拉输入)
#define KEY_4 3 // 按键4编号 (对应PB15,下拉输入)/*** @brief 获取按键物理状态* @param n 按键编号 (KEY_1, KEY_2, KEY_3, KEY_4)* @retval KEY_PRESSED: 按键被按下, KEY_UNPRESSED: 按键未被按下* @description 读取指定按键的当前物理状态* KEY_1(PB1)、KEY_2(PB11): 低电平有效* KEY_3(PB13)、KEY_4(PB15): 高电平有效*/
uint8_t Key_GetState(uint8_t n)
{if (n == KEY_1){// KEY_1上拉输入,按下时为低电平if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){return KEY_PRESSED;}}else if (n == KEY_2){// KEY_2上拉输入,按下时为低电平if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0){return KEY_PRESSED;}}else if (n == KEY_3){// KEY_3下拉输入,按下时为高电平if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) == 1){return KEY_PRESSED;}}else if (n == KEY_4){// KEY_4下拉输入,按下时为高电平if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) == 1){return KEY_PRESSED;}}return KEY_UNPRESSED;
}
获取指定按键的物理状态(也就是高低电平),然后来识别是否按下之类的,用来变成之后代码的合成材料
3.3 按键状态机处理函数
/* 按键数量配置 */
#define KEY_COUNT 4 // 系统支持的按键总数/* 按键状态标志位定义 */
#define KEY_HOLD 0x01 // 按键保持按下状态
#define KEY_DOWN 0x02 // 按键刚按下瞬间
#define KEY_UP 0x04 // 按键刚松开瞬间
#define KEY_SINGLE 0x08 // 按键单击
#define KEY_DOUBLE 0x10 // 按键双击
#define KEY_LONG 0x20 // 按键长按
#define KEY_REPEAT 0x40 // 按键连按(长按后的重复触发)/*** @brief 按键状态机处理函数* @param 无* @retval 无* @description 按键扫描和状态处理的核心函数,需要定时调用(建议1ms调用一次)* 实现了完整的按键状态机,支持以下功能:* - KEY_HOLD: 按键保持按下状态* - KEY_DOWN: 按键刚按下瞬间* - KEY_UP: 按键刚松开瞬间* - KEY_SINGLE: 按键单击(在双击检测时间内只按一次)* - KEY_DOUBLE: 按键双击(在指定时间内按两次)* - KEY_LONG: 按键长按(超过长按时间阈值)* - KEY_REPEAT: 按键连按(长按后的重复触发)* * @note 状态机说明:* S[i] = 0: 空闲状态,等待按键按下* S[i] = 1: 第一次按下状态,等待松开或长按超时* S[i] = 2: 第一次松开状态,等待第二次按下或双击超时* S[i] = 3: 双击确认状态,等待第二次松开* S[i] = 4: 长按状态,等待松开或重复触发*/
void Key_Tick(void)
{static uint8_t Count, i; // Count: 扫描计数器, i: 循环变量static uint8_t CurrState[KEY_COUNT], PrevState[KEY_COUNT]; // 当前状态和前一次状态static uint8_t S[KEY_COUNT]; // 状态机状态数组static uint16_t Time[KEY_COUNT]; // 定时器数组// 所有按键的定时器递减for (i = 0; i < KEY_COUNT; i ++){if (Time[i] > 0){Time[i] --;}}// 每20ms扫描一次按键状态(假设本函数每1ms调用一次)Count ++;if (Count >= 20){Count = 0;// 遍历所有按键for (i = 0; i < KEY_COUNT; i ++){// 更新按键状态PrevState[i] = CurrState[i];CurrState[i] = Key_GetState(i);// 处理KEY_HOLD状态if (CurrState[i] == KEY_PRESSED){Key_Flag[i] |= KEY_HOLD; // 按键按下时置位HOLD标志}else{Key_Flag[i] &= ~KEY_HOLD; // 按键松开时清除HOLD标志}// 处理KEY_DOWN状态(上升沿检测)if (CurrState[i] == KEY_PRESSED && PrevState[i] == KEY_UNPRESSED){Key_Flag[i] |= KEY_DOWN;}// 处理KEY_UP状态(下降沿检测)if (CurrState[i] == KEY_UNPRESSED && PrevState[i] == KEY_PRESSED){Key_Flag[i] |= KEY_UP;}// 按键状态机处理if (S[i] == 0) // 状态0: 空闲状态{if (CurrState[i] == KEY_PRESSED){Time[i] = KEY_TIME_LONG; // 设置长按计时器S[i] = 1; // 转到状态1}}else if (S[i] == 1) // 状态1: 第一次按下状态{if (CurrState[i] == KEY_UNPRESSED){Time[i] = KEY_TIME_DOUBLE; // 设置双击检测计时器S[i] = 2; // 转到状态2}else if (Time[i] == 0) // 长按时间到{Time[i] = KEY_TIME_REPEAT; // 设置重复触发计时器Key_Flag[i] |= KEY_LONG; // 置位长按标志S[i] = 4; // 转到状态4}}else if (S[i] == 2) // 状态2: 第一次松开状态{if (CurrState[i] == KEY_PRESSED){Key_Flag[i] |= KEY_DOUBLE; // 置位双击标志S[i] = 3; // 转到状态3}else if (Time[i] == 0) // 双击检测时间到{Key_Flag[i] |= KEY_SINGLE; // 置位单击标志S[i] = 0; // 回到状态0}}else if (S[i] == 3) // 状态3: 双击确认状态{if (CurrState[i] == KEY_UNPRESSED){S[i] = 0; // 回到状态0}}else if (S[i] == 4) // 状态4: 长按状态{if (CurrState[i] == KEY_UNPRESSED){S[i] = 0; // 回到状态0}else if (Time[i] == 0) // 重复触发时间到{Time[i] = KEY_TIME_REPEAT; // 重新设置重复触发计时器Key_Flag[i] |= KEY_REPEAT; // 置位重复触发标志S[i] = 4; // 保持状态4}}}}
}
该代码是放在定时器中断函数里面的!!这个记住了,定时器是每隔1ms调用一次这个函数;
由于Count ++,每1ms加一次,记到20次则为20ms,所以每隔20ms处理一次状态机;
然后和上述的一样,进行当前和之前电平状态的对比,就可以知道当前的按键的物理状态;
然后根据流程图进行代码的编写,比如状态0通过按下按键跳到状态1,然后设置长按定时,也就是这样
if (S[i] == 0) // 状态0: 空闲状态
{
if (CurrState[i] == KEY_PRESSED)
{
Time[i] = KEY_TIME_LONG; // 设置长按计时器
S[i] = 1; // 转到状态1
}
}
以此类推即可!!
四、完整代码
4.1 定时器中断初始化
#include "stm32f10x.h" // Device headervoid Timer_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);TIM_InternalClockConfig(TIM2);TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM_TimeBaseInitStructure.TIM_Period = 1000 - 1;TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);TIM_ClearFlag(TIM2, TIM_FLAG_Update);TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;NVIC_Init(&NVIC_InitStructure);TIM_Cmd(TIM2, ENABLE);
}/*
void TIM2_IRQHandler(void)
{if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
*/
4.2 按键初始化
/*** @file Key.c* @brief 全功能按键非阻塞式实现源文件* @author STM32江科大* @date 2025年8月6日* @description 实现多种按键检测功能,包括按下、松开、单击、双击、长按、连按等* 采用状态机方式实现,支持非阻塞式检测*/#include "stm32f10x.h" // Device header
#include "Key.h"/* 按键物理状态定义 */
#define KEY_PRESSED 1 // 按键被按下的物理状态
#define KEY_UNPRESSED 0 // 按键未被按下的物理状态/* 按键时间参数定义 (单位: ms) */
#define KEY_TIME_DOUBLE 200 // 双击间隔时间阈值
#define KEY_TIME_LONG 2000 // 长按时间阈值
#define KEY_TIME_REPEAT 100 // 连按重复间隔时间/* 按键状态标志数组 */
uint8_t Key_Flag[KEY_COUNT]; // 存储各按键的状态标志位/*** @brief 按键GPIO初始化函数* @param 无* @retval 无* @description 配置按键对应的GPIO引脚* KEY_1(PB1)、KEY_2(PB11): 上拉输入,按下时读取为0* KEY_3(PB13)、KEY_4(PB15): 下拉输入,按下时读取为1*/
void Key_Init(void)
{// 使能GPIOB时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;// 配置KEY_1(PB1)和KEY_2(PB11)为上拉输入GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);// 配置KEY_3(PB13)和KEY_4(PB15)为下拉输入GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 下拉输入模式GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);
}/*** @brief 获取按键物理状态* @param n 按键编号 (KEY_1, KEY_2, KEY_3, KEY_4)* @retval KEY_PRESSED: 按键被按下, KEY_UNPRESSED: 按键未被按下* @description 读取指定按键的当前物理状态* KEY_1(PB1)、KEY_2(PB11): 低电平有效* KEY_3(PB13)、KEY_4(PB15): 高电平有效*/
uint8_t Key_GetState(uint8_t n)
{if (n == KEY_1){// KEY_1上拉输入,按下时为低电平if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){return KEY_PRESSED;}}else if (n == KEY_2){// KEY_2上拉输入,按下时为低电平if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0){return KEY_PRESSED;}}else if (n == KEY_3){// KEY_3下拉输入,按下时为高电平if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) == 1){return KEY_PRESSED;}}else if (n == KEY_4){// KEY_4下拉输入,按下时为高电平if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) == 1){return KEY_PRESSED;}}return KEY_UNPRESSED;
}/*** @brief 检测按键状态* @param n 按键编号 (KEY_1, KEY_2, KEY_3, KEY_4)* @param Flag 要检测的状态标志位 (KEY_HOLD, KEY_DOWN, KEY_UP, KEY_SINGLE, KEY_DOUBLE, KEY_LONG, KEY_REPEAT)* @retval 1: 指定状态发生, 0: 指定状态未发生* @description 检测指定按键的指定状态是否发生* 除了KEY_HOLD状态会持续保持外,其他状态在检测后会自动清除*/
uint8_t Key_Check(uint8_t n, uint8_t Flag)
{// 检查指定按键的指定标志位是否置位if (Key_Flag[n] & Flag){// 除了KEY_HOLD外,其他标志位检测后自动清除if (Flag != KEY_HOLD){Key_Flag[n] &= ~Flag; // 清除对应标志位}return 1;}return 0;
}/*** @brief 按键状态机处理函数* @param 无* @retval 无* @description 按键扫描和状态处理的核心函数,需要定时调用(建议1ms调用一次)* 实现了完整的按键状态机,支持以下功能:* - KEY_HOLD: 按键保持按下状态* - KEY_DOWN: 按键刚按下瞬间* - KEY_UP: 按键刚松开瞬间* - KEY_SINGLE: 按键单击(在双击检测时间内只按一次)* - KEY_DOUBLE: 按键双击(在指定时间内按两次)* - KEY_LONG: 按键长按(超过长按时间阈值)* - KEY_REPEAT: 按键连按(长按后的重复触发)* * @note 状态机说明:* S[i] = 0: 空闲状态,等待按键按下* S[i] = 1: 第一次按下状态,等待松开或长按超时* S[i] = 2: 第一次松开状态,等待第二次按下或双击超时* S[i] = 3: 双击确认状态,等待第二次松开* S[i] = 4: 长按状态,等待松开或重复触发*/
void Key_Tick(void)
{static uint8_t Count, i; // Count: 扫描计数器, i: 循环变量static uint8_t CurrState[KEY_COUNT], PrevState[KEY_COUNT]; // 当前状态和前一次状态static uint8_t S[KEY_COUNT]; // 状态机状态数组static uint16_t Time[KEY_COUNT]; // 定时器数组// 所有按键的定时器递减for (i = 0; i < KEY_COUNT; i ++){if (Time[i] > 0){Time[i] --;}}// 每20ms扫描一次按键状态(假设本函数每1ms调用一次)Count ++;if (Count >= 20){Count = 0;// 遍历所有按键for (i = 0; i < KEY_COUNT; i ++){// 更新按键状态PrevState[i] = CurrState[i];CurrState[i] = Key_GetState(i);// 处理KEY_HOLD状态if (CurrState[i] == KEY_PRESSED){Key_Flag[i] |= KEY_HOLD; // 按键按下时置位HOLD标志}else{Key_Flag[i] &= ~KEY_HOLD; // 按键松开时清除HOLD标志}// 处理KEY_DOWN状态(上升沿检测)if (CurrState[i] == KEY_PRESSED && PrevState[i] == KEY_UNPRESSED){Key_Flag[i] |= KEY_DOWN;}// 处理KEY_UP状态(下降沿检测)if (CurrState[i] == KEY_UNPRESSED && PrevState[i] == KEY_PRESSED){Key_Flag[i] |= KEY_UP;}// 按键状态机处理if (S[i] == 0) // 状态0: 空闲状态{if (CurrState[i] == KEY_PRESSED){Time[i] = KEY_TIME_LONG; // 设置长按计时器S[i] = 1; // 转到状态1}}else if (S[i] == 1) // 状态1: 第一次按下状态{if (CurrState[i] == KEY_UNPRESSED){Time[i] = KEY_TIME_DOUBLE; // 设置双击检测计时器S[i] = 2; // 转到状态2}else if (Time[i] == 0) // 长按时间到{Time[i] = KEY_TIME_REPEAT; // 设置重复触发计时器Key_Flag[i] |= KEY_LONG; // 置位长按标志S[i] = 4; // 转到状态4}}else if (S[i] == 2) // 状态2: 第一次松开状态{if (CurrState[i] == KEY_PRESSED){Key_Flag[i] |= KEY_DOUBLE; // 置位双击标志S[i] = 3; // 转到状态3}else if (Time[i] == 0) // 双击检测时间到{Key_Flag[i] |= KEY_SINGLE; // 置位单击标志S[i] = 0; // 回到状态0}}else if (S[i] == 3) // 状态3: 双击确认状态{if (CurrState[i] == KEY_UNPRESSED){S[i] = 0; // 回到状态0}}else if (S[i] == 4) // 状态4: 长按状态{if (CurrState[i] == KEY_UNPRESSED){S[i] = 0; // 回到状态0}else if (Time[i] == 0) // 重复触发时间到{Time[i] = KEY_TIME_REPEAT; // 重新设置重复触发计时器Key_Flag[i] |= KEY_REPEAT; // 置位重复触发标志S[i] = 4; // 保持状态4}}}}
}
/*** @file Key.h* @brief 全功能按键非阻塞式实现头文件* @author STM32江科大* @date 2025年8月6日* @description 实现多种按键检测功能,包括按下、松开、单击、双击、长按、连按等*/#ifndef __KEY_H
#define __KEY_H/* 按键数量配置 */
#define KEY_COUNT 4 // 系统支持的按键总数/* 按键编号定义 */
#define KEY_1 0 // 按键1编号 (对应PB1,上拉输入)
#define KEY_2 1 // 按键2编号 (对应PB11,上拉输入)
#define KEY_3 2 // 按键3编号 (对应PB13,下拉输入)
#define KEY_4 3 // 按键4编号 (对应PB15,下拉输入)/* 按键状态标志位定义 */
#define KEY_HOLD 0x01 // 按键保持按下状态
#define KEY_DOWN 0x02 // 按键刚按下瞬间
#define KEY_UP 0x04 // 按键刚松开瞬间
#define KEY_SINGLE 0x08 // 按键单击
#define KEY_DOUBLE 0x10 // 按键双击
#define KEY_LONG 0x20 // 按键长按
#define KEY_REPEAT 0x40 // 按键连按(长按后的重复触发)/*** @brief 按键初始化函数* @param 无* @retval 无* @description 初始化按键对应的GPIO引脚,配置为输入模式*/
void Key_Init(void);/*** @brief 按键状态检测函数* @param n 按键编号 (KEY_1, KEY_2, KEY_3, KEY_4)* @param Flag 要检测的按键状态标志位* @retval 1: 指定状态发生, 0: 指定状态未发生* @description 检测指定按键的指定状态是否发生,除KEY_HOLD外,其他状态检测后会自动清除*/
uint8_t Key_Check(uint8_t n, uint8_t Flag);/*** @brief 按键扫描处理函数* @param 无* @retval 无* @description 按键状态机处理函数,需要定时调用(建议1ms调用一次)* 处理所有按键的状态转换和事件检测*/
void Key_Tick(void);#endif