文章目录
在采用轻量级任务调度器的裸机开发中,为了实现"非阻塞"的任务执行,状态机(FSM)是不可或缺的基石。本篇文章是基于系统调度器的基础下进行拓展进阶,具体链接如下:
系统调度器文章参考:STM32 别再死等延时了!教你一招:不用 RTOS 也能让多个任务同时跑
什么是状态机?
状态机全称有限状态机(Finite State Machine)。简单来说,它将一个任务的生命周期拆分为多个离散的状态(State)。系统根据当前的状态和输入条件(如电平变化、计时到期),决定执行什么动作并跳转到下一个状态。
想象一下你在煮一壶水。
-
普通思维:盯着水壶看,直到水开了(while 循环 + Delay 等待)。这时候如果有人敲门,你根本听不见,因为你正"死等"着水开。
-
状态机思维:你给煮水分了几个状态:
-
空闲态:水壶没动静。
-
加热态:水正冒热气。
-
沸腾态:水开了,该关火了。
-
你每隔几分钟看一眼水壶(这就是轮询),看完立刻去干别的(比如扫地)。哪怕水没开,你也知道现在处于"加热态",下次再来看。
这就是状态机的核心:不原地等待,只记住进度。
为什么要配合系统调度器?
在嵌入式工程领域,抛开感性的比喻,状态机(FSM) 与 系统调度器(Task Scheduler) 的结合实际上是逻辑维度与时间维度的精准对齐。
状态机的行为逻辑高度依赖于物理时间阈值。以按键交互为例,其核心逻辑(如 20ms 消抖、2s 长按)必须建立在稳定的时间参考之上。
- 非调度环境:在 while(1) 裸跑中,循环周期受代码分支影响(如:这帧处理了屏幕,下帧只读了 GPIO),循环周期是不确定的。这会导致状态机内部的计数逻辑(ticks++)失去物理参考意义。
- 调度架构:调度器基于硬件定时器(如 SysTick)产生固定频率。当调度器确保任务每 10ms 执行一次时,状态机内的计数器就获得了绝对的时间标尺。
调度器不仅提供了时间基准,还通过分时复用解决了多任务冲突。
-
传统阻塞逻辑: 在没有调度的系统中,实现 20ms 消抖通常依赖 HAL_Delay(20)。这会强制 CPU 进入空转状态,阻塞其他实时任务(如串口数据接收或电机控制)。
-
调度器 + 状态机: 状态机将长达 2s 的长按动作拆解为 200 个离散的逻辑切片。每次任务轮询时,状态机仅执行当前状态的瞬时判定(耗时微秒级)后立即返回(Return),释放 CPU 所有权。
架构优势: 这种"快进快出"的模式,使系统能在宏观上实现多个状态机(按键、传感器、通信协议)的并发运行。
案例演示
现在,我们通过一个经典的按键状态机案例,演示如何在 10ms 调度任务中实现"消抖、单击、长按"功能。
为了让按键驱动具备工业级的扩展性,我们首先将按键的所有属性封装进一个结构体(Struct)。这种"面向对象"的处理方式,允许我们用同一套逻辑管理成百上千个按键。
按键对象的定义
c
typedef struct
{
GPIO_TypeDef *gpiox; // 硬件端口 (如 GPIOB)
uint16_t pin; // 硬件引脚 (如 GPIO_PIN_0)
uint16_t ticks; // 状态内计时器
uint8_t id; // 按键唯一识别 ID
uint8_t state; // 当前状态机状态
uint8_t debouce_cnt; // 消抖计数器
uint8_t repeat; // 记录点击次数(用于双击判定)
} button;
// 初始化按键列表
button btn_list[4] = {
{GPIOB, GPIO_PIN_0, 0, 0, 1, 0, 0, 0}, // ID: 1 (PB0)
{GPIOB, GPIO_PIN_1, 0, 0, 2, 0, 0, 0}, // ID: 2 (PB1)
{GPIOB, GPIO_PIN_2, 0, 0, 3, 0, 0, 0}, // ID: 3 (PB2)
{GPIOA, GPIO_PIN_0, 0, 0, 4, 0, 0, 0} // ID: 4 (PA0)
};
状态机核心算法
这是整个系统的"引擎"。它被调度器每 10ms 调用一次,每次进入只进行一次电平采样和状态跳转,绝不原地停留。
在双击下,传统的做法通常是阻塞等待双击,而这里进入 State 4 后立即返回。调度器会在接下来的 150ms 内反复询问。如果这期间没按下且计时到期,才判定为单击。这种设计保证了在等待双击时,系统的其他任务(如 PWM 控制)依然在运行。
c
/**
* @brief 按键核心逻辑,建议每 10ms 调用一次
* @return 返回触发按键的 ID,如果没有事件则返回 0
*/
uint8_t button_process(button* btn) {
uint8_t cur_level = HAL_GPIO_ReadPin(btn->gpiox, btn->pin);
uint8_t event_id = 0;
switch (btn->state) {
case 0: // 【空闲】
if (cur_level == 0)
{
btn->state = 1; // 去消抖
btn->debouce_cnt = 0;
}
break;
case 1: // 【消抖】
if (cur_level == 0)
{
if (++btn->debouce_cnt >= 3)
{
btn->state = 2;
}
}
else
btn->state = 0;
break;
case 2: // 【确认按下】
if (cur_level == 1)// 弹起了
{
btn->repeat++; // 记录点击次数
btn->ticks = 0; // 重置计时器,准备计算双击间隔
btn->state = 4; // 进入双击等待判定
}
else
{
if (++btn->ticks >= 150)
{ // 长按 1.5s
btn->state = 3;
btn->repeat = 0;
// event_id = btn->id + 20; // 如果需要长按,可以返回 ID+20
}
}
break;
case 3: // 【长按结束等待松手】
btn->ticks = 0;
if (cur_level == 1)
btn->state = 0;
break;
case 4: // 【双击等待判定】
if (cur_level == 0)
{ // 在时间内再次按下,说明是双击
btn->state = 1; // 回到消抖,去确认第二次按下
// 注意:这里由于 repeat 已经是 1 了,下次弹起就会变成 2
}
else
{
btn->ticks++;
if (btn->ticks >= 13)
{ // 等待约 150ms (10ms * 15)
// 超时了,根据 repeat 次数判定
if (btn->repeat == 1)
event_id = btn->id; // 单击:返回原 ID (1,2,3,4)
else if
(btn->repeat >= 2) event_id = btn->id + 10; // 双击:返回 ID+10 (11,12,13,14)
btn->repeat = 0;
btn->state = 0;
btn->ticks = 0;
}
}
break;
}
return event_id;
}
业务层对接:事件分发
有了底层的事件输出,业务层逻辑(Key_Proc)变得异常清爽。我们不再关心按键是怎么消抖的,只关心"发生了什么事件"。通过判断返回来不同的ID进行相应的事件处理。
c
void Key_Proc(void) {
for (int i = 0; i < 4; i++) {
uint8_t key_event = button_process(&btn_list[i]);
if (key_event == 0) continue; // 无事发生,跳过
switch (key_event) {
case 1: // KEY1 单击
uled ^= 0x01;
break;
case 11: // KEY1 双击
uled &= ~0x01;
break;
case 21: // KEY1 长按
Save_System_Params(); // 执行耗时保存操作
break;
// ... 其他按键事件
}
}
}
在调度器的节拍下,我们只需遍历按键列表并处理返回的 event_id。这种方式将底层的"电平扫描"与上层的"业务逻辑"彻底解耦。
通过这套"调度器 + 状态机"的架构,原本复杂的按键交互变得极易扩展和维护。底层驱动只需关注状态跳转,而业务层只需关注事件结果。
众多应用场景
-
通信协议解析(UART/SPI/I2C)
在接收串口数据包时,使用状态机逐个字节处理:
- 状态 A:等待帧头(如 0x55)。
- 状态 B:接收长度字节。
- 状态 C:接收数据荷载。
- 状态 D:校验和验证。
- 优势:无需使用 Delay 或长时间阻塞等待完整包,收一个字节处理一次,极大地提高了串口的吞吐率。
-
传感器异步读取(DS18B20/DHT11)
这类传感器通常需要发送指令后等待数百毫秒才能读取数据。
- 状态 A:发送启动信号。
- 状态 B:记录当前 Tick,进入"等待态"。
- 状态 C:调度器检测时间到期,跳转到"读取态"。
- 优势:在传感器转换数据的 750ms 漫长等待中,CPU 可以自由执行按键扫描、屏显更新等任务。
通过这套"系统调度器 + 状态机"的架构,原本杂乱无章的按键信号、传感器数据和通信协议,在物理世界中被赋予了精确的时间尺度与执行秩序。
很多开发者会问:既然这种方案如此高效,我们还需要 RTOS(实时操作系统)吗?
事实上,这种架构是裸机开发的天花板,也是理解 RTOS 核心逻辑的敲门砖:
- 资源占用:本架构只需极小的 RAM 和 Flash,在 Flash 空间吃紧、RAM 只有几 KB 的单片机(如 STM8、STM32G0/F0)上,它是平衡性能与资源的最优解。
- 执行效率:由于省去了 RTOS 繁重的上下文切换(Context Switch)和任务堆栈开销,它的执行效率甚至高于 RTOS。
- 思维转变:RTOS 允许你"阻塞"任务,而本架构强制你使用"非阻塞"的状态跳转。这种**"分时复用"**的思维,是每一位高级嵌入式工程师必备的底层内功。
如果你面对的是资源有限、实时性要求高且逻辑复杂的工程,这套架构将是你手中最锋利的武器。它不仅让你的程序告别了低效的 Delay,更让你的裸机代码拥有了足以媲美操作系统的流畅度。