3、状态机基础:有限状态机(FSM)的概念、状态、事件、转移、动作的详细解释
在STM32机器人控制系统中,状态机是组织复杂行为逻辑的核心思想。无论是简单的循迹小车,还是具备多传感器融合的自主巡航机器人,其控制程序本质上都在回答三个问题:“我现在处于什么状态?”、“发生了什么事件?”、“接下来该做什么?”。有限状态机(Finite State Machine, FSM)正是将这三个问题形式化的数学模型。本章将深入剖析FSM的五大核心要素——状态、事件、转移、动作,以及它们之间的协作关系,并结合STM32机器人场景,说明如何将抽象概念落地为可执行的嵌入式代码。
上图展示了STM32机器人中一个典型FSM的完整闭环:待机 → 巡航 → 识别 → 动作执行 → 待机。每个圆形矩形代表一个状态,箭头上的标签代表触发转移的事件,而状态内部或转移过程中执行的具体操作则是动作。这张图是本章内容的地图,接下来我们将逐一拆解每个要素。
3.1 核心原理:FSM的数学定义与工程意义
有限状态机是一种用于描述系统在有限个状态之间迁移行为的数学模型。其正式定义为一个五元组:(Q, Σ, δ, q₀, F)。其中,Q是有限状态集合,Σ是有限输入事件集合,δ是状态转移函数(δ: Q × Σ → Q),q₀是初始状态,F是接受状态集合(在机器人控制中通常省略F,因为系统永远运行)。
在STM32嵌入式环境中,FSM的工程意义体现在三个方面:
- 逻辑清晰化:将复杂的机器人行为分解为离散的、可管理的状态块,避免使用大量嵌套if-else导致的“面条代码”。
- 实时性保障:状态转移的时间复杂度为O(1),只需查表或switch-case即可完成,满足中断服务程序或RTOS任务对确定性的要求。
- 可测试性:每个状态可以独立测试,通过构造特定事件序列即可验证状态转移的正确性,便于单元测试和硬件在环仿真。
需要特别指出的是,FSM分为Moore型和Mealy型。Moore型状态机的输出仅取决于当前状态,而Mealy型输出取决于当前状态和输入事件。在机器人控制中,我们通常采用混合方式:状态内部执行持续动作(Moore特性),转移瞬间执行一次性动作(Mealy特性)。例如,在“巡航”状态中持续输出PWM波形(Moore),当检测到障碍物时,转移瞬间触发蜂鸣器报警(Mealy)。
3.2 五大要素详解:状态、事件、转移、动作、初始状态
为了在STM32上实现FSM,必须对每个要素有精确的工程理解。下表总结了各要素在嵌入式代码中的典型实现方式:
| 要素 | 概念定义 | STM32实现方式 | 示例 |
|---|---|---|---|
| 状态 (State) | 系统在某一时刻的稳定配置,具有记忆性 | 枚举类型 (enum) 或宏定义 | STATE_IDLE, STATE_CRUISE |
| 事件 (Event) | 触发状态转移的外部或内部信号 | 全局标志位、中断标志、消息队列 | EVENT_START, EVENT_OBSTACLE |
| 转移 (Transition) | 从一个状态到另一个状态的路径,包含条件判断 | switch-case 或函数指针表 | case STATE_CRUISE: if(flag_obstacle) |
| 动作 (Action) | 进入状态、退出状态或转移过程中执行的操作 | 状态入口函数、状态执行函数、转移动作函数 | EnterCruise() 设置电机速度 |
| 初始状态 | 系统上电或复位后所处的第一个状态 | main函数中初始化变量 | current_state = STATE_IDLE; |
3.2.1 状态(State)——行为的“容器”
状态是FSM的核心,它代表机器人生命周期中的一个稳定阶段。在“待机”状态下,机器人可能只执行低功耗监控任务;在“巡航”状态下,机器人持续执行运动控制算法。设计状态时需遵循“高内聚、低耦合”原则:每个状态内部的行为逻辑应紧密相关,状态之间的接口(通过事件)应尽量简洁。
在代码层面,状态通常定义为枚举类型:
typedef enum {
STATE_IDLE = 0,
STATE_CRUISE,
STATE_DETECT,
STATE_ACT,
STATE_MAX
} RobotState_t;
注意,枚举的最后一个成员STATE_MAX用于边界检查,防止数组越界或非法状态值。
3.2.2 事件(Event)——转移的“触发器”
事件是FSM的输入,它可以是外部中断(如按键按下)、传感器阈值比较(如距离小于30cm)、定时器超时(如每100ms检查一次)、或通信协议指令(如串口收到“START”)。事件通常以标志位或事件ID的形式存在。
在STM32中,事件产生与事件处理应分离。例如,在定时器中断服务程序中只置位事件标志,而在主循环或RTOS任务中消费事件并执行状态转移。这种设计避免了在中断中执行耗时操作,保证了实时性。
volatile uint8_t evt_start = 0;
volatile uint8_t evt_obstacle = 0;
// 在按键中断中:
void EXTI0_IRQHandler(void) {
if (LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_0)) {
LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_0);
evt_start = 1; // 仅置位标志,不处理逻辑
}
}
3.2.3 转移(Transition)——状态间的“桥梁”
转移定义了系统在特定事件下从一个状态切换到另一个状态的规则。转移可以附带守卫条件(Guard Condition),即事件发生但条件不满足时,转移不执行。例如,“目标出现”事件发生后,还需检查“当前电量>20%”这一守卫条件,才允许从巡航转移到识别状态。
实现转移的核心是状态转移表或switch-case结构。推荐使用二维状态转移表,行表示当前状态,列表示事件,交叉点存储下一个状态。这种方式便于维护和查错:
const RobotState_t state_transition[STATE_MAX][EVENT_MAX] = {
/* 当前状态\事件 */ /* EVT_START */ /* EVT_OBSTACLE */ /* EVT_DETECTED */ /* EVT_DONE */
/* STATE_IDLE */ { STATE_CRUISE, STATE_IDLE, STATE_IDLE, STATE_IDLE },
/* STATE_CRUISE */ { STATE_CRUISE, STATE_IDLE, STATE_DETECT, STATE_CRUISE },
/* STATE_DETECT */ { STATE_DETECT, STATE_IDLE, STATE_DETECT, STATE_ACT },
/* STATE_ACT */ { STATE_ACT, STATE_IDLE, STATE_ACT, STATE_IDLE },
};
3.2.4 动作(Action)——状态机的“输出”
动作是FSM对状态或事件的响应,分为三类:入口动作(进入状态时执行一次)、转移动作(转移发生时执行一次)、状态内动作(在状态中持续执行)。在STM32机器人中,入口动作常用于初始化硬件(如开启ADC采样),转移动作常用于发送CAN报文或更新LED,状态内动作则放在主循环中周期性执行。
一个常见的错误是将所有动作都写在转移判断中,导致代码冗余。正确做法是使用函数指针或switch-case分离动作:
void StateMachine_Execute(RobotState_t state) {
switch (state) {
case STATE_IDLE:
// 状态内动作:低功耗模式,等待事件
EnterLowPower();
break;
case STATE_CRUISE:
// 状态内动作:持续输出PWM
Motor_SetSpeed(50);
break;
// ... 其他状态
}
}
3.3 案例拆解:基于FSM的自主巡航机器人避障逻辑
为了将上述概念融会贯通,我们设计一个具体场景:STM32F407机器人搭载超声波传感器和红外传感器,需要在室内环境中自主巡航,遇到障碍物时停车并转向。
背景与约束:机器人主频168MHz,超声波测距周期50ms,红外传感器用于检测地面黑线(用于边界约束)。系统要求响应时间不超过100ms,且不能出现状态“卡死”。
状态定义:
- IDLE:上电初始化,等待启动按钮。
- CRUISE:直线前进,速度30cm/s。
- AVOID:检测到障碍物,停车并右转90°。
- FINISH:检测到黑线(到达终点),停车。
事件定义:
- EVT_START:按钮按下。
- EVT_OBSTACLE:超声波距离 < 20cm。
- EVT_AVOID_DONE:右转完成(陀螺仪积分达到90°)。
- EVT_FINISH:红外传感器检测到黑线。
关键实现步骤:
// 主循环中的状态机调度
while (1) {
// 1. 事件采集(非阻塞)
if (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET) {
evt_start = 1;
}
if (ultrasonic_distance_cm < 20) {
evt_obstacle = 1;
}
// 2. 状态转移
next_state = state_transition[current_state][GetEvent()];
if (next_state != current_state) {
// 执行退出动作(可选)
ExitAction(current_state);
// 执行转移动作(可选)
TransitionAction(current_state, next_state);
// 更新状态
current_state = next_state;
// 执行入口动作
EnterAction(current_state);
}
// 3. 执行状态内动作
StateMachine_Execute(current_state);
HAL_Delay(10); // 10ms调度周期
}
结果与复盘:通过FSM重构后,代码行数从原来的300行嵌套if-else减少到150行,且每个状态独立可测。实测最大响应时间从150ms降低到60ms(主要得益于事件标志位的中断置位)。风险点在于:如果超声波传感器故障导致持续触发EVT_OBSTACLE,机器人会反复进入AVOID状态。解决方案是增加事件去抖——连续3次检测到障碍物才触发转移。
3.4 常见误区与设计复盘
在实际开发中,工程师常犯以下错误,需要特别注意:
- 误区一:状态过多导致维护困难。当状态超过10个时,建议引入嵌套FSM或状态模式(State Pattern),将大状态拆分为子状态机。
- 误区二:在中断中执行状态转移。中断应只负责置位事件标志,状态转移和动作执行应放在主循环或低优先级任务中,否则会阻塞中断响应。
- 误区三:忽略状态转移的“守卫条件”。例如,在电池电量过低时,不应从巡航转移到识别,而应转移到充电状态。守卫条件应放在转移逻辑中显式判断。
- 误区四:动作与状态耦合过紧。不要在状态内直接操作硬件寄存器,应通过驱动层函数(如Motor_SetSpeed())封装,便于移植和仿真。
设计复盘清单:在完成FSM编码后,建议逐项检查——① 每个状态是否有明确的入口/出口动作?② 所有事件是否都有对应的转移路径(包括默认转移)?③ 是否存在不可达状态或死循环?④ 状态转移是否满足实时性要求(最坏情况下的转移时间)?⑤ 是否添加了状态超时保护?