【第27期】有限状态机 (FSM):逻辑控制的灵魂

核心痛点:随着项目变大,全局标志位(Flags)越来越多,代码里充斥着 if (flag1 && !flag2) ... else if (flag3) 的逻辑,牵一发而动全身,改一个 Bug 冒出来三个 Bug。

解决方案:将系统抽象为 状态 (State) + 事件 (Event) -> 动作 (Action) + 新状态

1. 初阶:Switch-Case 法 (最常见)

这是大多数人入门 FSM 的写法。以一个简单的“电子门锁”为例。 状态:LOCKED (锁定), UNLOCKED (解锁), ALARM (报警)。

typedef enum { STATE_LOCKED, STATE_UNLOCKED, STATE_ALARM } State_t;
State_t CurrentState = STATE_LOCKED;

void FSM_Run(Event_t event) {
    switch (CurrentState) {
        case STATE_LOCKED:
            if (event == EVENT_PASSWORD_CORRECT) {
                Unlock_Door();
                CurrentState = STATE_UNLOCKED; // 状态迁移
            } else if (event == EVENT_TAMPER) {
                CurrentState = STATE_ALARM;
            }
            break;

        case STATE_UNLOCKED:
            if (event == EVENT_TIMEOUT) {
                Lock_Door();
                CurrentState = STATE_LOCKED;
            }
            break;
            
        // ... 省略 ALARM 处理
    }
}

  • 优点:直观,逻辑简单时很好用。

  • 缺点:当状态有 20 个,事件有 10 个时,这个 switch 会变成几千行的怪物,维护极其痛苦。


2. 进阶:二维状态表 (空间换时间)

高手为了消除 switch-case 和嵌套 if,会使用 函数指针矩阵

这也叫“表驱动法 (Table-Driven)”。我们将逻辑抽象为一个二维数组:Table[当前状态][发生事件]。数组中的元素是一个函数指针,指向“该情况下要执行的动作”。

第一步:定义函数指针类型

// 定义状态处理函数的原型:输入参数可选,返回新的状态
typedef State_t (*StateHandler_t)(void);

第二步:实现具体的动作函数

把原本塞在 case 里的逻辑拆分成独立的小函数。

// 1. 锁定状态下,收到正确密码
State_t Action_Unlock(void) {
    Unlock_Door();
    return STATE_UNLOCKED; // 返回新状态
}

// 2. 锁定状态下,收到拆机信号
State_t Action_Alarm(void) {
    Horn_On();
    return STATE_ALARM;
}

// 3. 什么都不做 (保持原状态)
State_t Action_DoNothing(void) {
    return CurrentState;
}

第三步:构建二维查找表 (The Magic Matrix)

这是 FSM 的灵魂

// 假设有 3 种状态,3 种事件
// 这里的行代表 State,列代表 Event
const StateHandler_t FSM_Table[3][3] = {
    //             EVENT_PWD_OK    EVENT_TIMEOUT     EVENT_TAMPER
    /* LOCKED */  {Action_Unlock,  Action_DoNothing, Action_Alarm},
    /* UNLOCKED*/ {Action_DoNothing, Action_Lock,    Action_Alarm},
    /* ALARM */   {Action_Reset,   Action_DoNothing, Action_DoNothing}
};

第四步:驱动引擎 (极简)

无论逻辑多复杂,驱动代码只有一行!

void FSM_Run(State_t state, Event_t event) {
    // 越界保护(生产环境必备)
    if (state >= MAX_STATE || event >= MAX_EVENT) return;

    // 核心:查表 -> 执行 -> 更新状态
    // O(1) 的时间复杂度,快如闪电
    StateHandler_t handler = FSM_Table[state][event];
    CurrentState = handler(); 
}

3. 这种写法的好处

  1. 消除分支:代码里没有 if-else,也没有 switch。逻辑流就是查表。

  2. 维护性无敌

    • 如果想修改“锁定状态下收到密码”的行为,只需要去改 Action_Unlock 函数。

    • 如果想修改逻辑(比如报警后不能直接复位),只需要修改 FSM_Table 里的函数指针,完全不动驱动代码。

  3. 可视化FSM_Table 本身就是一张代码版的“状态转移图”,一眼就能看出系统的逻辑全貌。


4. 结构体状态机 (面向对象风格)

如果你的每个状态除了动作,还有“进入时执行(Entry)”和“退出时执行(Exit)”的需求,可以定义一个结构体:

typedef struct {
    void (*Entry)(void);       // 进入该状态时跑一次
    void (*Run)(Event_t evt);  // 状态持续期间跑
    void (*Exit)(void);        // 离开该状态时跑一次
} FSM_Node_t;

const FSM_Node_t StateList[] = {
    {LCD_On,   Menu_Loop, LCD_Off},  // 菜单状态
    {Motor_On, PID_Loop,  Motor_Off} // 运行状态
};

关键点小结

  1. Switch-Case 适合简单逻辑(< 5 个状态)。

  2. 查表法 (Table-Driven) 是中大型 FSM 的标准解法,它用空间(数组)换取了时间(O(1)查找)和可维护性

  3. 思想:把变化的逻辑(Action)和不变的框架(Engine)分离开来。

/***************************************************
 * 本文为作者《嵌入式开发基础与工程实践》系列文章之一。
 * 关注即可订阅后续内容更新,翻阅往期信息,采用异步推送机制,无需主动轮询。
 * 转发本文可视为一次网络广播,有助于更多节点接收该信息。
 ***************************************************/

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值