一、状态机基础:什么是状态机、为什么用在运动控制中、状态机的三要素
大家好,我是老张。在运动控制这行摸爬滚打了十几年,今天咱们聊聊状态机。
说实话,我刚入行那会儿,写运动控制程序就是一堆 if-else 堆砌。电机正转、反转、急停、回零……代码越写越长,最后自己都看不懂了。直到有一次,设备在客户现场出了故障——电机该停的时候没停,差点撞了夹具。那次之后,我才真正重视起状态机。
什么是状态机?
状态机,说白了就是一个“有限状态自动机”。
你想想看,一台运动控制器,它不可能同时做所有事。要么在“空闲”状态,要么在“运动中”,要么在“报警”状态。每个时刻,它只能处于一个状态。这就是状态机的核心思想。
我习惯用一个比喻来解释:状态机就像一台自动售货机。你投币(事件),它根据当前状态(比如“待机”或“已投币”),决定下一步做什么(转移)。
核心定义:状态机是一种数学模型,它由有限个状态、事件和转移规则组成。在任何时刻,系统只能处于一个状态,并通过事件触发转移到另一个状态。
为什么用在运动控制中?
这个问题我问过很多新人。答案五花八门,但核心就三点:
- 安全可靠——状态机强制规定了“什么状态下能做什么事”。比如,电机在“高速运行”状态下,不允许直接进入“回零”状态。这种约束能避免很多低级错误。
- 逻辑清晰——代码不再是面条式的 if-else。每个状态独立处理自己的事件,出了问题一眼就能定位。
- 易于维护——我遇到过最夸张的项目,状态机有 30 多个状态。但因为有清晰的状态转移图,新同事接手时,看一遍图就能上手改代码。
嗯,这里要注意一点:不是所有运动控制场景都需要状态机。如果你只是控制一个风扇正反转,用 if-else 就够了。但一旦涉及多轴联动、安全互锁、复杂流程,状态机就是你的救命稻草。
状态机的三要素
状态机的三要素,我记了十年都没忘:状态、事件、转移。
1. 状态(State)
状态就是系统在某一时刻的“快照”。在运动控制中,常见的状态有:
| 状态名称 | 含义 | 典型行为 |
|---|---|---|
| IDLE(空闲) | 电机未使能,等待指令 | 响应启动指令 |
| HOMING(回零) | 正在寻找原点 | 执行回零算法 |
| MOVING(运动) | 正在执行定位或轨迹 | 实时插补计算 |
| ALARM(报警) | 发生故障 | 停止输出,记录错误 |
我个人习惯给每个状态分配一个枚举值,比如 STATE_IDLE = 0、STATE_HOMING = 1。这样在调试时,直接看变量值就知道当前状态,非常方便。
2. 事件(Event)
事件是触发状态转移的“导火索”。它可以是外部输入(比如按钮按下),也可以是内部条件(比如位置到达)。
我曾经犯过一个错:把“超时”和“位置到达”两个事件混在一起处理。结果有一次超时了,程序却误判为位置到达,直接执行了下一步动作……嗯,从那以后,我坚持每个事件独立编码。
常见的事件类型:
- 用户指令(启动、停止、复位)
- 传感器信号(限位开关、原点信号)
- 内部定时器(超时、延时完成)
- 计算完成(插补结束、路径规划完成)
3. 转移(Transition)
转移定义了“当事件发生时,从当前状态到下一个状态的路径”。
转移通常包含三个要素:
- 源状态——当前状态
- 触发事件——什么条件下转移
- 目标状态——转移到哪里
- 动作——转移过程中执行的操作(可选)
举个例子:
// 状态转移示例
当前状态: IDLE
触发事件: EVT_START
条件: 电机已使能 && 无报警
动作: 清除位置误差,设置目标位置
目标状态: MOVING
我的小技巧:在写转移逻辑时,我习惯用二维表格来梳理。行是当前状态,列是事件,交叉点就是目标状态。这样一眼就能看出有没有遗漏的转移路径。
状态机核心逻辑图
下面这张图,是我做运动控制项目时最常用的状态机框架。它包含了三个基本状态和它们之间的转移关系:
注意:这张图是最简版本。实际项目中,状态数可能多达几十个。但核心逻辑不变——每个状态只处理自己该处理的事,事件来了就按规则转移。
一个简单的代码示例
最后,给你看一段我常用的状态机框架代码。这是 C 语言风格,但思路在任何语言中都通用:
// 状态枚举
typedef enum {
STATE_IDLE,
STATE_HOMING,
STATE_MOVING,
STATE_ALARM
} MotorState;
// 事件枚举
typedef enum {
EVT_NONE,
EVT_START,
EVT_HOME_DONE,
EVT_MOVE_DONE,
EVT_ERROR,
EVT_RESET
} MotorEvent;
// 状态机处理函数
MotorState stateMachine(MotorState current, MotorEvent event) {
switch (current) {
case STATE_IDLE:
if (event == EVT_START) {
// 执行启动动作
enableMotor();
return STATE_HOMING;
}
break;
case STATE_HOMING:
if (event == EVT_HOME_DONE) {
return STATE_MOVING;
}
if (event == EVT_ERROR) {
disableMotor();
return STATE_ALARM;
}
break;
case STATE_MOVING:
if (event == EVT_MOVE_DONE) {
return STATE_IDLE;
}
if (event == EVT_ERROR) {
disableMotor();
return STATE_ALARM;
}
break;
case STATE_ALARM:
if (event == EVT_RESET) {
clearAlarm();
return STATE_IDLE;
}
break;
}
return current; // 无转移,保持当前状态
}
你看,代码结构非常清晰。每个状态只处理自己关心的事件,不相关的直接忽略。这就是状态机的魅力——逻辑不会乱,bug 无处藏。
避坑指南:我曾经在状态机里忘了处理“ALARM 状态下收到 EVT_START”的情况。结果客户按了启动按钮,设备没反应,他们以为死机了。记住:每个状态都要考虑“如果收到不该来的事件怎么办”。要么忽略,要么报错,但绝不能崩溃。
好了,状态机的基础就聊到这儿。三要素——状态、事件、转移——你记住了吗?下一节咱们聊聊怎么设计一个真正能用的运动控制状态机。
公众号:蓝海资料掘金营,微信deep3321