新手都能看懂的使用状态机实现非阻塞按键的单击双击长按功能

发布时间:2026/7/18 3:09:02
新手都能看懂的使用状态机实现非阻塞按键的单击双击长按功能 首先我们得明白用一个按键实现单机双击长按的原理通过输入高电平实现控制引脚输出这就是图中的l1单击在俩个高电平间会有间隔我们定义其长度让两次高电平时间控制在间隔时间内执行双击对于长按来说只要在一定时间内持续输出高电平我们就可以认为是执行长按操作。我们以图中l1-3举个例子l1 输出一个高电平代表按键的单击l2则为俩个高电平加上其中的窗口期如300ms我们就可以得到理想状态下在300ms内再次点击按键即可实现双击操作如果超过了300ms的窗口期后再按下按键则回到l1实现单击若持续按着按键不放达到一定时间如800ms后就执行长按操作。通过控制高低电平持续与窗口期时间达到一个按键控制三种事件的操作。但上述实现情况都是建立在理想状态下在现实硬件中会有一个抖动时间抖动时间的长短由按键的机械特性决定一般为5ms10ms。在刚接触stm32的同学们最喜欢使用的就是delay延时函数进行消抖这种方法有一个弊端那就是会阻塞程序的运行delay多少时间程序就会在delay中执行多少时间的空循环这样大大的降低了程序的效率导致程序无法同时执行多个操作为解决这种情况我们使用状态机来实现那问题来了什么是状态机状态机状态机Finite State Machine, FSM是一种描述系统行为的数学模型核心思想是系统在任意时刻只能处于有限个状态中的一个在事件触发下按规则转移到新状态。进入代码部分首先我们在key.h文件中先定义并声明变量与状态机#ifndef __key_H #define __key_H #include main.h // Device header typedef enum{ KEY_IDLE 0, //空闲状态 KEY_DEBOUNCE_DOWN, //按键按下消抖状态 KEY_PRESSED, //按键确认状态 KEY_DEBOUNCE_UP, //按键抬起消抖状态 KEY_WAIT_DOUBLE, //等待双击状态 KEY_LONG_PRESS //按键长按状态 }KEY_STATE; extern KEY_STATE keystates; //给KEY_STATE命名为keystates并声明为跨文件的全局变量 extern volatile uint8_t flag; //声明全局变量flag extern uint32_t now_time; //声明全局变量now_time void key_scan(void); //声明函数key_scan #endif进入key.c文件定义变量KEY_STATE keystates KEY_IDLE; //设置keystates 的初始状态为KEY_IDLE volatile uint8_t flag; //定义变量 uint32_t now_time0; //定义变量 uint32_t now_long_time 0; //定义变量 uint32_t now_double_time 0; //定义变量 uint8_t count ; //定义变量其中我们的flag为标志位因为本次代码将会在中断中执行所以采用传输标志位的方式。now_time0; 为消抖的开始时间uint32_t now_long_time 0;为长按的判定时间uint32_t now_double_time 0; 双击窗口期的判定时间uint8_t count ; 计数来判断本次按下是否是双击现在进行到最激动人心也是最复杂的按键扫码函数的编写为方便解读说明我将拆分开逐段解释说明最后会附上完整代码空闲状态//空闲状态 case KEY_IDLE: //判断按键是否按下 if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) GPIO_PIN_RESET) { //将系统时间赋值给now_time now_time HAL_GetTick(); //将系统时间赋值给now_long_time now_long_time HAL_GetTick(); //初始化变量 flag 0; count 0; //转移状态为KEY_DEBOUNCE_DOWN按下消抖 keystates KEY_DEBOUNCE_DOWN; } break;本状态用于初始化变量进入下一层状态的条件为按键是否按下按下消抖状态case KEY_DEBOUNCE_DOWN: if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) GPIO_PIN_SET) { keystates KEY_IDLE; } else if (HAL_GetTick() - now_time 20) { now_time HAL_GetTick(); keystates KEY_PRESSED; } break;本状态用于检测按键是否按下如果为高电平则为误触状态退回到空闲状态在代码中我用了GetTick函数此函数为为返回系统启动以来的时间戳在空闲状态我已经获取了时间并放在now_time中所以我现在用now_time减去当前的时间大于等于20的if句来实现delay20的操作。如果满足条件就再次记录时间并将状态转移到KEY_PRESSED状态按键确认状态case KEY_PRESSED: //判断按键是否松开如果松开则跳转到抬起消抖状态 if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) GPIO_PIN_SET) { now_time HAL_GetTick(); keystates KEY_DEBOUNCE_UP; } else if(HAL_GetTick() - now_long_time 800) { flag 3; keystates KEY_LONG_PRESS; } break;本状态为确认按键是否按下与长按检测若按键松开恢复高电平则进入下一层状态按键释放消抖状态在这里记录时间的作用和上一个状态一样用于替代delay。在空闲状态中我们记录了第一次按键按下的时间在这里我们可以利用它来检测是否长按时间是否达到如果达到了就记录标志位跳入KEY_LONG_PRESS状态按键释放消抖状态case KEY_DEBOUNCE_UP:// 按键释放消抖状态 if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) GPIO_PIN_RESET )//判断按键是否还是按下 { keystates KEY_PRESSED;//是则表示未成功消抖跳回到按下状态 } else if(HAL_GetTick() - now_time 20) { count ; if(count 2) { count 0; flag 2; keystates KEY_IDLE; } else { now_double_time HAL_GetTick(); keystates KEY_WAIT_DOUBLE; } } break;本状态最为复杂if嵌套if的但实际上很好理解首先我们先判断按键是否按下没松开为真则跳回上一状态为假则表明已经松开那这时我们再判断是否延时真则表示我们已经完成了一次按键按下的操作count就1此时我们再继续判断count是否等于2若为真则表明这是我们第二次按下就计数器清零标志位为2跳转到空闲状态。如果不为2那就表示我们只按了一次按键就记录双击窗口期时间now_double_time并跳入KEY_WAIT_DOUBLE中等待双击状态case KEY_WAIT_DOUBLE: if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) GPIO_PIN_RESET ) { now_time HAL_GetTick(); keystates KEY_DEBOUNCE_DOWN; } else if( HAL_GetTick() - now_double_time 400) { flag 1; keystates KEY_IDLE; } break;本状态为双击等待状态意思是我们已经完成了一次按键按下等待在窗口期内会不会有第二次按键如果有就跳转到按键按下消抖阶段如果没有那表明本次流程结束得到想要的标志位flag值为1状态回到空闲状态。长按检测状态case KEY_LONG_PRESS: if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) GPIO_PIN_SET) { keystates KEY_IDLE; } break;本段状态特别简单算是一个过渡状态用于检测长按后是否松手如果松手则返会空闲状态否则继续检测直至松开在代码中我们的长按是安到一定时间就执行命令不用等到松手但在这里我加入松手检测命令主要是为了让整个状态更完整至于会不会有不检测松手会程序跑飞等问题暂未考虑如果有同学在实操中发现问题请分享与我我们相互学习。完整KEY.C代码#include main.h // Device header #include key.h KEY_STATE keystates KEY_IDLE; volatile uint8_t flag; uint32_t now_time 0; uint32_t now_long_time 0; uint32_t now_double_time 0; uint8_t count; void key_scan() { switch (keystates) { case KEY_IDLE: if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { now_time HAL_GetTick(); now_long_time HAL_GetTick(); flag 0; count 0; keystates KEY_DEBOUNCE_DOWN; } break; case KEY_DEBOUNCE_DOWN: if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { keystates KEY_IDLE; } else if (HAL_GetTick() - now_time 20) { now_time HAL_GetTick(); keystates KEY_PRESSED; } break; case KEY_PRESSED: if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { now_time HAL_GetTick(); keystates KEY_DEBOUNCE_UP; } else if (HAL_GetTick() - now_long_time 800) { flag 3; keystates KEY_LONG_PRESS; } break; case KEY_DEBOUNCE_UP: if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { keystates KEY_PRESSED; } else if (HAL_GetTick() - now_time 20) { count; if (count 2) { count 0; flag 2; keystates KEY_IDLE; } else { now_double_time HAL_GetTick(); keystates KEY_WAIT_DOUBLE; } } break; case KEY_WAIT_DOUBLE: if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { now_time HAL_GetTick(); keystates KEY_DEBOUNCE_DOWN; } else if (HAL_GetTick() - now_double_time 400) { flag 1; keystates KEY_IDLE; } break; case KEY_LONG_PRESS: if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { keystates KEY_IDLE; } break; default: keystates KEY_IDLE; break; } }主文件调用本次代码使用定时器中断所以得先配置回调函数void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim -Instance TIM2) { key_scan(); } }在cubemx中开启tim2开启内部时钟配置参数nvic配置开启定时中断并定义变量key_flag 来存放flag值HAL_TIM_Base_Start_IT(htim2); uint8_t key_flag 0;最后激动人心的时候while程序编写while (1) { key_flag flag; switch (key_flag) { case 1: HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_1); flag 0; break; case 2: HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_6); flag 0; break; case 3: HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_11); flag 0; break; default: break; } }这里我们把scan中得到的flag值赋给key_flag用于避免竞态条件防止每一次读取到不同的值在主程序中我们每执行完一个操作后都将flag值归零以免读到相同的值完整代码HAL_TIM_Base_Start_IT(htim2); uint8_t key_flag 0; while (1) { key_flag flag; switch (key_flag) { case 1: HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_1); flag 0; break; case 2: HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_6); flag 0; break; case 3: HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_11); flag 0; break; default: break; } } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim -Instance TIM2) { key_scan(); } }注意每次我们写的代码都需放这俩注释之间/* USER CODE BEGIN */ 中间填写我们的代码 /* USER CODE END */结语我也是个从0开始的小白从delay不离手到学习到状态机再到应用其中的每一步都折磨的我整晚整晚睡不着觉在此感谢并推荐B站up主江协科技成电应电科协铁头山羊以及在csdn分享技术心得的大佬们 感谢他们让我学习到了学校外学不到的知识。若发现内容有误或表达的不准确的地方麻烦各位大佬们及时提出为了千千万万个懵懂却又对嵌入式充满热情的同学们