一、状态机基础:什么是状态机?为什么从站需要状态机?

大家好,我是你们的老朋友。今天咱们正式开始聊Modbus从站开发里最核心的一个概念——状态机。

说实话,我刚开始做嵌入式开发那会儿,对状态机这东西也是似懂非懂。总觉得不就是几个if-else嘛,至于搞得那么玄乎?直到有一次,我在一个工业现场调试从站设备,遇到了一个极其诡异的bug——设备偶尔会死机,但又不是每次都死。查了三天三夜,最后发现是状态切换逻辑没处理好。嗯,从那以后,我再也不敢小看状态机了。

1.1 到底什么是状态机?

状态机,说白了就是一个“有限状态自动机”。你想想看,我们生活中的很多东西都可以用状态机来描述——比如红绿灯,它只有三个状态:红灯、绿灯、黄灯。每个状态下,遇到特定的事件(比如倒计时结束),就会执行一个动作(切换灯色),然后进入下一个状态。

在Modbus从站里,状态机就是用来管理从站设备“当前在干什么”的。它决定了设备什么时候该接收数据、什么时候该处理数据、什么时候该回复数据。

核心定义:状态机是一种数学模型,它由一组有限的状态、一组输入事件、以及状态转移规则组成。在任何时刻,系统只能处于其中一个状态。

1.2 为什么从站非得用状态机?

这个问题我经常被问到。有人觉得,我写个while循环,里面放几个if判断,不也能干活吗?

能,但你会死得很惨。

我给大家讲个真实案例。几年前我帮一家工厂调试Modbus从站,他们的工程师就是用if-else写的逻辑。设备在实验室跑得好好的,一上产线就出问题——主站发了个读保持寄存器的请求,从站正在处理的时候,主站又发了个写线圈的请求。结果呢?从站直接懵了,数据乱了,设备复位了。

为什么会这样?因为if-else的逻辑是线性的,它处理不了“并发事件”和“异常中断”。而状态机天生就是为这种场景设计的——它把设备的整个生命周期切分成若干个明确的状态,每个状态只关心自己该做的事,遇到不该处理的事件,直接拒绝或者报错。

我的经验:用状态机写的从站代码,可读性高、维护性好、bug少。我见过太多用if-else堆出来的“屎山”代码,改一个功能要翻半天,还不敢动。状态机就不一样,每个状态独立,改一个不影响其他。

1.3 状态机的核心三要素

好,咱们来拆解一下状态机的三个核心要素。我个人习惯把它们记成“三件套”:状态、事件、动作。

要素 英文 说明 Modbus从站中的例子
状态 State 系统当前所处的稳定阶段 空闲态、接收态、处理态、发送态
事件 Event 触发状态切换的外部或内部信号 接收到一帧数据、校验错误、超时
动作 Action 进入/离开状态时执行的操作 清空缓冲区、计算CRC、发送响应

咱们一个一个说。

状态(State)

状态就是设备“正在干什么”。在Modbus从站里,我一般会定义这么几个状态:

  • 空闲态(IDLE):啥也不干,等着主站发命令
  • 接收态(RECV):正在收数据,还没收完
  • 处理态(PROC):数据收完了,正在解析和执行
  • 发送态(SEND):处理完了,准备把结果发回去
  • 异常态(ERROR):出错了,比如CRC校验失败

每个状态都是互斥的——设备不可能同时处于接收态和发送态,对吧?

事件(Event)

事件就是“发生了什么”。它驱动状态机从一个状态跳到另一个状态。常见的事件有:

  • 接收到一个字节
  • 接收超时(3.5个字符时间没收到新数据)
  • 校验错误
  • 命令解析完成
  • 发送完成

这里有个坑,我踩过。事件的定义一定要清晰,不能模棱两可。比如“接收超时”这个事件,它的触发条件是什么?是3.5个字符时间没收到数据,还是10ms没收到?这个必须明确,否则不同硬件平台上的表现会不一样。

动作(Action)

动作就是“要做什么”。它是在状态切换时执行的代码。动作分为三种:

  • 进入动作:进入某个状态时执行一次
  • 退出动作:离开某个状态时执行一次
  • 转移动作:在状态切换过程中执行

避坑指南:我曾经在一个项目里,把动作写在了状态内部而不是切换时。结果状态机在同一个状态下反复触发同一个动作,导致缓冲区被重复清空,数据全丢了。记住:动作要绑定在状态切换上,而不是状态内部。

1.4 状态机长什么样?

光说理论太枯燥,咱们画个图看看。下面这个就是Modbus从站最基础的状态机流程图:

Modbus从站基础状态机 空闲态 IDLE 接收态 RECV 处理态 PROC 发送态 SEND 异常态 ERROR 收到首字节 超时/校验错 接收完成 处理完成 发送完成 命令非法 异常处理完毕 正常状态 异常状态 异常转移

这个图看着复杂,其实逻辑很简单。从站上电后进入空闲态,收到主站发来的第一个字节,就跳到接收态。数据收完了,跳到处理态去解析命令。处理完了,跳到发送态把结果发回去。发完了,回到空闲态,等着下一轮。如果中间出了任何差错——比如校验失败、命令非法——就跳到异常态,处理完异常再回到空闲态。

1.5 代码怎么写?

说了这么多,咱们看看代码长什么样。我习惯用枚举来定义状态,用switch-case来实现状态切换。这样结构清晰,也好调试。

/* 状态定义 */
typedef enum {
    STATE_IDLE,   // 空闲态
    STATE_RECV,   // 接收态
    STATE_PROC,   // 处理态
    STATE_SEND,   // 发送态
    STATE_ERROR   // 异常态
} ModbusState;

/* 事件定义 */
typedef enum {
    EVT_RX_BYTE,      // 收到一个字节
    EVT_RX_TIMEOUT,   // 接收超时
    EVT_RX_COMPLETE,  // 接收完成
    EVT_CRC_ERROR,    // CRC校验错误
    EVT_PROC_DONE,    // 处理完成
    EVT_SEND_DONE,    // 发送完成
    EVT_CMD_INVALID   // 命令非法
} ModbusEvent;

/* 状态机主循环 */
ModbusState currentState = STATE_IDLE;

void ModbusStateMachine(ModbusEvent event) {
    switch(currentState) {
        case STATE_IDLE:
            if(event == EVT_RX_BYTE) {
                // 进入接收态
                currentState = STATE_RECV;
                // 动作:启动接收超时定时器
                StartRxTimer();
            }
            break;
            
        case STATE_RECV:
            if(event == EVT_RX_BYTE) {
                // 动作:把字节存入缓冲区
                PushToBuffer(rxByte);
                // 重置超时定时器
                ResetRxTimer();
            }
            else if(event == EVT_RX_TIMEOUT) {
                // 超时了,回到空闲态
                currentState = STATE_IDLE;
                // 动作:清空缓冲区
                ClearBuffer();
            }
            else if(event == EVT_RX_COMPLETE) {
                // 收完了,去处理
                currentState = STATE_PROC;
                // 动作:停止定时器,开始解析
                StopRxTimer();
                ParseCommand();
            }
            else if(event == EVT_CRC_ERROR) {
                // CRC错了,进异常
                currentState = STATE_ERROR;
                // 动作:准备异常响应
                PrepareExceptionResponse(CRC_ERROR);
            }
            break;
            
        // 其他状态的处理类似...
    }
}

我的习惯:每个状态的处理函数单独写,不要全堆在switch-case里。比如STATE_RECV的处理逻辑可以封装成HandleRecvState(event),这样代码好维护,也方便单元测试。

1.6 小结

状态机这东西,说白了就是把复杂的逻辑拆成小块,每个小块只做一件事。你想想看,如果没有状态机,Modbus从站要同时处理接收、解析、发送、异常,那代码得乱成什么样?

我个人觉得,状态机是嵌入式开发里性价比最高的设计模式之一。它不复杂,但能解决大问题。尤其是做工业通信协议的时候,状态机几乎是标配。你去看任何一个成熟的Modbus库,底层一定有一个状态机在跑。

嗯,这一章咱们把状态机的基础概念讲清楚了。下一章咱们会深入Modbus从站的具体状态定义,看看每个状态里到底该干什么、不该干什么。到时候我会拿我踩过的坑给大家当反面教材,保证你们印象深刻。


专注资料整理