如何用状态机解决按键状态识别问题
任务目标
在单片机的一众外设中我们早期接触到的就有按键,按键可以作为信号发生源来触发对应事件,本次的实验目标就是能够以非阻塞模式来识别按键的不同状态,其中包括短按、长按以及双击,同时还要对按键进行软件消抖
状态机编程
什么是状态机编程
解决此问题的主要思路就是依靠状态机编程,所以在此先简单介绍一下什么是状态机编程:
状态机就是一种逻辑模型,用来描述一个系统在不同阶段(状态)下,面对不同的输入(事件)时,应该做出什么反应(动作),以及如何进入下一个阶段(状态转移)。
状态机编程的核心概念
想要理解状态机编程就需要先了解状态机编程中的几大核心概念
- 状态:系统当前所处的状态或阶段(例如:按键的按下、抬起以及等待抬起等状态)
- 事件:指触发状态发生改变的条件(例如:按键按下、收到串口数据等)
- 动作:在状态转移或者处于某个状态时需要执行的程序(例如:开启LED灯、发送串口数据以及修改状态变量的值)
- 转换:从一个状态切换到另一个状态的过程
状态机编程的核心思想
综上我们不难得出状态机的核心思想为:程序不是简单的线性执行,而是根据当前的状态和输入的事件来决定下一步的行为。
为什么需要使用状态机编程
在早期的单片机学习中我们经常需要使用顺序执行的思维来写代码,比如:开灯->延时->关灯->延时
这种编写代码的方式极为简单,同时也限制了这种编程方法只能在简单项目中使用,在稍微复杂的项目中就会导致不同外设之间进行互相影响,从而让整个程序无法正常运行,其主要缺陷如下:
CPU会被死锁:在延时过程中CPU无法响应其它的任务,例如按键按下以及屏幕刷新等- 代码逻辑混乱:为了处理多个逻辑,程序中会出现多个
if-else语句和全局标志位,直言会降低代码的可读性和可维护性
状态机的优势
因此此时引入状态机的概念就能很好的解决这一问题,状态机的优点如下
- 实现非阻塞式编程:在状态机编程模式中
CPU每次只检查当前输入是否满足跳转条件,不满足就立刻退出去执行其它任务 - 实现多任务操作:配合定时器可以在逻辑环境下让单片机同时处理按键、通信等多个任务
- 逻辑清晰、易于维护:将复杂的业务拆解后修改某一个逻辑时不会出现牵一发而动全身的情况
程序编写
本程序烧写所使用的开发板的主控芯片型号为STM32G431R8T6,所使用的开发板为蓝桥杯17届嵌入式赛道官方开发板
单键按下判断程序编写
在判断多个按键状态之前我们先实现最基础的按键按下状态判断,同时在判断按键按下时添加对应的软件消抖逻辑。
代码逻辑
代码的大致编写思路如下:首先我们需要创建按键的不同状态机,随后编写按键扫描函数,在函数中对按键的当前状态进行判断,并做出下一步的行动,我们的目标是在不阻塞CPU的情况下进行按键的状态判断,因此我们不能直接在主循环中直接调用按键扫描函数,否则会影响到其它外设的正常运行。对此我们的解决方案是配置定时器让其每10ms产生一次中断,在定时器中断服务函数中调用按键扫描函数,这样就能减少对CPU资源的占用

定时器配置
首先打开CubeMX软件,挑选一个空闲的定时器,这里我选择的是单片机的基本定时器TIM6,在CubeMX的控制界面中对定时器的基本参数进行配置,由于此前预先在时钟树配置界面将单片机的主频配置为了170MHZ,因此这里选择将定时器6的预分频系数设置为170-1(169),自动重装载值设置为10000-1(9999),同时开启自动重装载
此时我们不难推出当前定时器的周期为(170 * 1000000) / (169 + 1) / (9999 + 1) = 100,这说明此时的频率为100hz,周期为10ms,刚好就是我们预想的周期值,定时器配置到此结束
按键配置
由于不同开发板的按键外设对应的引脚不同,因此按键的配置在此我们仅作简单演示

通过开发板的原理图我们得出按键的默认状态是高电平,因此按下时按键对应引脚状态会变为低电平,此时我们选择下降沿触发中断
中断配置
在中断配置界面使能三个按键的中断并开启定时器6的中断

至此与该程序有关的外设的配置就基本完成,接下来就是代码编写环节
代码编写
本阶段主要需要实现的代码就是按键扫描函数,该函数的作用为扫描按键的状态做出相应的行为后更新状态,在按键消抖的过程中我们先来明确一下有哪些状态,涉及的按键状态如下:
- 空闲状态:等待按键按下
- 确认状态:按键引脚电平发生变化后检测按键是否真的按下
- 按下状态:经过消抖确认,按键确定已经按下,此时开始等待用户松开按键
- 释放等待状态:检测到电平变高时进入的状态,用于处理松手时的机械抖动
按键扫描函数逻辑
函数开始时需要先检测按键引脚电平的状态,随后开始后续的状态判断和相关状态转换操作
- 第一步:闲置检测
- 如果读取到低电平则说明按键可能被按下了
- 动作:立即转换状态为确认状态,准备在下一次进入函数时进行验证
- 第二步:按下消抖验证
10ms之后再次进入定时器中断中并调用按键扫描函数,此时再次扫描按键电平状态,如果此时仍旧是低电平说明按键确实被按下了- 动作:将状态切换为按下状态,同时将全局标志位置一(通知主函数此时按键已经按下)
- 如果此时按键引脚电平为高电平则说明上一次的电平变化是干扰杂波,此时将状态退回到空闲状态
- 第三步:等待按键松开
- 按键已经被确定按下,此时系统不断循环并检查电平
- 如果电平转换为高电平则说明用户松开了按键
- 动作:将状态切换为等待释放状态,准备进行抬起消抖处理
- 第四步:松手消抖验证
10ms之后再次进入函数,如果此时电平仍然是高电平,则说明用户已经彻底抬起- 动作:将状态重置为空闲状态,完成一次完整的按键判断周期
- 如果此时电平又变回了低电平,说明松手过程中存在抖动,此时将状态切换为按下状态继续等待
按键扫描函数代码
综合上述逻辑分析我们不难写出函数对应代码
c++
void Key_scan(void)
{
//静态变量只会被初始化一次,后续会跳过当前语句
static KeyState key_state = KEY_STATE_IDLE;
GPIO_PinState curr_level = HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin);
switch (key_state)
{
case KEY_STATE_IDLE:
//此时为低电平按键疑似按下
if (curr_level == GPIO_PIN_RESET)
{
key_state = KEY_STATE_CONFIRM;
}
break;
case KEY_STATE_CONFIRM:
//10ms后按键对应引脚仍旧为低电平,说明此时按键确实按下
if (curr_level == GPIO_PIN_RESET)
{
key_state = KEY_STATE_PRESSED;
key1_flag = 1;
}
//10ms后按键引脚变回高电平说明刚才是错误信息
else
{
//再次将按键状态转变为空闲态
key_state = KEY_STATE_IDLE;
}
break;
//按键按下后等待按键松手
case KEY_STATE_PRESSED:
if (curr_level == GPIO_PIN_SET)
{
key_state = KEY_STATE_WAIT_RELEASE;
}
break;
//等待用户松手
case KEY_STATE_WAIT_RELEASE:
//如果电平变化之后10ms之后仍旧不变说明已经彻底松手了
if (curr_level == GPIO_PIN_SET)
{
key_state = KEY_STATE_IDLE;
}
//如果短期内再次回到原来的状态则保持状态位不变
else
{
key_state = KEY_STATE_PRESSED;
}
break;
default:
break;
}
}
大致的状态转化图如下

函数调用
当按键扫描函数编写完成后我们需要在定时器中断服务函数中调用对应代码,在调用函数之前需要先手动开启定时器中断,在main函数中调用函数进行定时器使能操作
HAL_TIM_Base_Start_IT(&htim6);//开启定时器中断
定时器中断服务函数内容如下
c++
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6) //判断当前中断是否是由定时器6触发的
{
Key_scan(); //调用按键扫描函数
}
}
至此简单的单个按键的按下抬起状态判断以及软件消抖操作的代码就编写完成了
状态 VS 状态机
在编写代码的时候你或许会混淆状态和状态机的概念,它们二者之间虽然只有一字之差但是所指代的内容却大不相同,因此在此我们对这两个概念再进行对比辨析以扫清接下来学习的障碍
-
状态
状态描述的是一个对象或系统在特定时间点的具体情况、属性或者数据快照
- 本质:它是一种静态的数据或事实,例如电灯在某一时刻处于亮起或者熄灭的状态,在特定的状态下对象的状态是确定的,不会发生发生改变
-
状态机
状态机通常指一个模型或者控制系统,它不仅包含了系统所有可能的状态,还定义了这些状态之间进行转换 的规则
- 本质:它是一套动态的逻辑
- 状态机的四大核心要素:
- 状态:系统所拥有的所有可能的状态
- 事件/输入:触发状态发生改变的动作(如按键按下等)
- 转换:系统从一个状态切换到另一个状态的过程
- 动作:状态转换发生时系统执行的操作(例如控制
LED灯亮起)
总结
本文介绍了简单的状态机编程思想,并通过状态机编程实现了在非阻塞模式下对按键的状态进行捕获,下次我会将当前的按键扫描函数进行改进,为按键添加更多的状态并增加判断的按键的数量