4、状态机设计:有限状态机(FSM)在限位逻辑中的应用

说到限位逻辑,很多新手第一反应就是写一堆 if-else 嵌套。说实话,我早年也这么干过。结果呢?代码越改越乱,边界条件漏了一堆。后来我学乖了——用状态机。有限状态机(FSM)这东西,说白了就是把你的控制逻辑画成一张图,然后照着图写代码。清晰、可靠、好维护。

我个人习惯,但凡涉及到多个条件切换的控制逻辑,第一件事就是画状态转移图。图出来了,代码就是照着填。今天咱们就聊聊,在限位逻辑里怎么用 FSM。

4.1 为什么限位逻辑需要状态机?

你想想看,一个电机运动控制系统,可能同时有正转、反转、急停、限位触发、传感器抖动……这些信号交织在一起。如果用纯组合逻辑或者乱糟糟的 if 判断,很容易出现“竞态条件”或者“状态丢失”。

我在项目中遇到过一个问题:电机已经撞到正向限位了,但因为软件里只判断了“当前是否在限位”,没有记录“之前是否已经进入限位状态”,结果电机在限位边界来回震荡,最后烧了驱动器。嗯,从那以后,我所有限位逻辑都改用状态机。

核心思想:状态机把“当前是什么状态”和“发生了什么事”分开处理。状态决定行为,事件驱动转移。这样逻辑就不会乱。

4.2 状态定义:四个核心状态

在限位逻辑中,我通常定义四个基本状态。不多不少,刚好覆盖所有正常和异常场景。

状态名称 状态标识 含义 典型行为
空闲 IDLE 电机未运动,无限位触发 等待指令,可接受正转/反转命令
正向限位 FWD_LIMIT 正向限位开关被触发 立即停止正向运动,允许反向运动
反向限位 REV_LIMIT 反向限位开关被触发 立即停止反向运动,允许正向运动
错误 ERROR 双侧限位同时触发或异常 紧急停止所有运动,需手动复位
我的经验:ERROR 状态一定要单独定义。我曾经见过有人把双侧限位当成“正向限位”处理,结果电机卡在中间,两边都动不了。单独一个 ERROR 状态,逻辑清晰,调试也方便。

4.3 状态转移图

下面这张图,是我做限位逻辑时必画的状态转移图。你照着这个结构写代码,基本不会出大问题。

空闲 IDLE 正向限位 FWD_LIMIT 反向限位 REV_LIMIT 错误 ERROR 正向限位触发 反向限位触发 限位解除 限位解除 反向也触发 正向也触发 双侧同时触发 手动复位 保持 保持 保持
注意:从“空闲”直接到“错误”的虚线路径,很多人会忽略。实际项目中,如果两个限位传感器同时损坏或者接线短路,就可能出现双侧同时触发。这个转移必须加上,否则系统会卡在未知状态。

4.4 状态转移的触发条件

每个转移都需要明确的触发条件。我习惯用一张表把条件列清楚,写代码时直接对照。

当前状态 触发事件 下一状态 动作
空闲 正向限位开关 = 触发 正向限位 停止正向运动,记录限位标志
空闲 反向限位开关 = 触发 反向限位 停止反向运动,记录限位标志
空闲 双侧限位同时触发 错误 紧急停止,报警
正向限位 正向限位开关 = 解除 空闲 清除限位标志,允许正向运动
正向限位 反向限位开关 = 触发 错误 紧急停止,报警
反向限位 反向限位开关 = 解除 空闲 清除限位标志,允许反向运动
反向限位 正向限位开关 = 触发 错误 紧急停止,报警
错误 手动复位信号 空闲 清除所有标志,重新初始化
避坑指南:我曾经在“正向限位”状态下,忘记处理“反向限位也触发”的情况。结果电机在正向限位处卡住,操作员强行反向运动,导致机械结构损坏。记住:任何限位状态下,另一个限位触发都必须进入 ERROR。

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;
    }
}
关键点:注意 default 分支。我见过太多人写状态机不写 default,结果一旦状态变量被意外修改(比如内存写穿),系统直接跑飞。default 分支一定要回到 ERROR 或者 IDLE,这是最后的防线。

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。

注意:传感器抖动是限位逻辑的头号杀手。如果限位开关有机械抖动,状态机会在 IDLE 和 LIMIT 之间来回跳。解决方案:在传感器输入加软件消抖,或者用硬件施密特触发器。我一般用 10ms 的连续采样确认。

好了,状态机设计这块就聊到这儿。记住:画图、定状态、写转移表、套代码框架、加调试输出。这五步走完,你的限位逻辑基本稳了。