4、状态机设计:有限状态机(FSM)在限位逻辑中的应用
说到限位逻辑,很多新手第一反应就是写一堆 if-else 嵌套。说实话,我早年也这么干过。结果呢?代码越改越乱,边界条件漏了一堆。后来我学乖了——用状态机。有限状态机(FSM)这东西,说白了就是把你的控制逻辑画成一张图,然后照着图写代码。清晰、可靠、好维护。
我个人习惯,但凡涉及到多个条件切换的控制逻辑,第一件事就是画状态转移图。图出来了,代码就是照着填。今天咱们就聊聊,在限位逻辑里怎么用 FSM。
4.1 为什么限位逻辑需要状态机?
你想想看,一个电机运动控制系统,可能同时有正转、反转、急停、限位触发、传感器抖动……这些信号交织在一起。如果用纯组合逻辑或者乱糟糟的 if 判断,很容易出现“竞态条件”或者“状态丢失”。
我在项目中遇到过一个问题:电机已经撞到正向限位了,但因为软件里只判断了“当前是否在限位”,没有记录“之前是否已经进入限位状态”,结果电机在限位边界来回震荡,最后烧了驱动器。嗯,从那以后,我所有限位逻辑都改用状态机。
4.2 状态定义:四个核心状态
在限位逻辑中,我通常定义四个基本状态。不多不少,刚好覆盖所有正常和异常场景。
| 状态名称 | 状态标识 | 含义 | 典型行为 |
|---|---|---|---|
| 空闲 | IDLE | 电机未运动,无限位触发 | 等待指令,可接受正转/反转命令 |
| 正向限位 | FWD_LIMIT | 正向限位开关被触发 | 立即停止正向运动,允许反向运动 |
| 反向限位 | REV_LIMIT | 反向限位开关被触发 | 立即停止反向运动,允许正向运动 |
| 错误 | ERROR | 双侧限位同时触发或异常 | 紧急停止所有运动,需手动复位 |
4.3 状态转移图
下面这张图,是我做限位逻辑时必画的状态转移图。你照着这个结构写代码,基本不会出大问题。
4.4 状态转移的触发条件
每个转移都需要明确的触发条件。我习惯用一张表把条件列清楚,写代码时直接对照。
| 当前状态 | 触发事件 | 下一状态 | 动作 |
|---|---|---|---|
| 空闲 | 正向限位开关 = 触发 | 正向限位 | 停止正向运动,记录限位标志 |
| 空闲 | 反向限位开关 = 触发 | 反向限位 | 停止反向运动,记录限位标志 |
| 空闲 | 双侧限位同时触发 | 错误 | 紧急停止,报警 |
| 正向限位 | 正向限位开关 = 解除 | 空闲 | 清除限位标志,允许正向运动 |
| 正向限位 | 反向限位开关 = 触发 | 错误 | 紧急停止,报警 |
| 反向限位 | 反向限位开关 = 解除 | 空闲 | 清除限位标志,允许反向运动 |
| 反向限位 | 正向限位开关 = 触发 | 错误 | 紧急停止,报警 |
| 错误 | 手动复位信号 | 空闲 | 清除所有标志,重新初始化 |
4.5 状态机代码实现要点
写状态机代码,我推荐用 switch-case 结构。简单、直观、容易调试。下面是一个伪代码框架,你可以直接套用。
// 状态枚举
typedef enum {
STATE_IDLE,
STATE_FWD_LIMIT,
STATE_REV_LIMIT,
STATE_ERROR
} limit_state_t;
// 全局状态变量
static limit_state_t current_state = STATE_IDLE;
// 状态机主函数,每个周期调用一次
void limit_fsm_run(void) {
// 读取传感器输入
uint8_t fwd_trigger = read_fwd_limit_sensor();
uint8_t rev_trigger = read_rev_limit_sensor();
switch (current_state) {
case STATE_IDLE:
if (fwd_trigger && rev_trigger) {
// 双侧同时触发 -> 错误
current_state = STATE_ERROR;
emergency_stop();
set_alarm(ALARM_DUAL_LIMIT);
} else if (fwd_trigger) {
current_state = STATE_FWD_LIMIT;
stop_motor(MOTOR_FWD);
set_limit_flag(LIMIT_FWD);
} else if (rev_trigger) {
current_state = STATE_REV_LIMIT;
stop_motor(MOTOR_REV);
set_limit_flag(LIMIT_REV);
}
// 无触发则保持空闲
break;
case STATE_FWD_LIMIT:
if (rev_trigger) {
// 另一个限位也触发 -> 错误
current_state = STATE_ERROR;
emergency_stop();
set_alarm(ALARM_DUAL_LIMIT);
} else if (!fwd_trigger) {
// 限位解除 -> 回到空闲
current_state = STATE_IDLE;
clear_limit_flag(LIMIT_FWD);
}
// 保持正向限位状态
break;
case STATE_REV_LIMIT:
if (fwd_trigger) {
current_state = STATE_ERROR;
emergency_stop();
set_alarm(ALARM_DUAL_LIMIT);
} else if (!rev_trigger) {
current_state = STATE_IDLE;
clear_limit_flag(LIMIT_REV);
}
break;
case STATE_ERROR:
if (is_reset_signal_received()) {
// 手动复位
current_state = STATE_IDLE;
clear_all_flags();
clear_alarm();
}
// 否则保持错误状态
break;
default:
// 异常状态保护
current_state = STATE_ERROR;
emergency_stop();
break;
}
}
4.6 调试技巧:状态机可视化
调试状态机,最怕的就是“不知道当前在哪个状态”。我个人的做法是:在状态机里加一个调试输出,把当前状态和触发事件打印出来。
// 调试辅助函数
void limit_fsm_debug(void) {
static const char* state_names[] = {
"IDLE", "FWD_LIMIT", "REV_LIMIT", "ERROR"
};
printf("[FSM] State: %s | FWD=%d REV=%d\n",
state_names[current_state],
read_fwd_limit_sensor(),
read_rev_limit_sensor());
}
把这个函数放在主循环里,每秒打印一次。配合逻辑分析仪抓传感器信号,基本上一眼就能看出状态转移对不对。我曾经用这个方法,半小时就找到了一个“限位抖动导致状态来回跳”的 bug。
好了,状态机设计这块就聊到这儿。记住:画图、定状态、写转移表、套代码框架、加调试输出。这五步走完,你的限位逻辑基本稳了。