一、Modbus从站标准状态定义

做Modbus从站开发,说白了就是管理好五个状态。

我刚开始接触这个协议时,总觉得状态机这东西太理论。后来在项目里吃过亏,才明白——状态没管好,通讯就会乱套。

今天咱们就把这五个状态掰开揉碎讲清楚。

1.1 空闲态(Idle State)

职责:等待主站发起请求。此时从站不处理任何报文,只监听总线。

转换条件:

  • 检测到起始位(3.5字符静默时间后第一个字节)→ 进入接收态
  • 如果总线持续空闲 → 保持空闲态

我个人的习惯:在空闲态里把接收缓冲区清空,避免上次残留数据干扰新报文。曾经有个项目,就是因为没清缓冲区,导致解析了半截旧数据,从站直接死机。

1.2 接收态(Receive State)

职责:逐字节接收主站发来的报文,同时做基础校验。

转换条件:

  • 接收完成(收到完整帧,含CRC校验)→ 进入处理态
  • 接收超时(超过1.5字符时间无新字节)→ 进入错误态
  • 接收过程中发现帧长度异常 → 进入错误态

避坑指南:我曾经在接收态里只判断了CRC,没判断帧长度。结果有个设备发了超长报文,缓冲区溢出,把后面代码区都覆盖了。嗯,从那以后我规定:接收态必须同时检查长度和CRC。

1.3 处理态(Process State)

职责:解析报文、执行功能码、准备响应数据。

转换条件:

  • 处理成功(正常响应)→ 进入发送态
  • 处理失败(异常响应)→ 进入发送态(发送异常码)
  • 处理过程中检测到致命错误(如内存访问越界)→ 进入错误态

你想想看,处理态其实是最容易出bug的地方。功能码解析、寄存器地址合法性检查、数据边界判断……哪一步漏了都可能出问题。

注意:处理态里不要做耗时操作!比如写EEPROM、等待传感器采样。我见过有人把延时函数写在这里,结果主站超时重发,从站直接卡死。处理态应该只做数据组装,真正的IO操作放到任务调度里去做。

1.4 发送态(Transmit State)

职责:将响应帧(正常或异常)发送到总线上。

转换条件:

  • 发送完成 → 进入空闲态
  • 发送过程中检测到总线冲突(RS485半双工)→ 进入错误态

发送态有个细节容易被忽略:发送完成后要等待3.5字符时间再切回空闲态。为什么?因为要保证帧间隔,防止主站把两帧当成一帧处理。

我建议:发送态里用硬件自动CRC生成,别自己算。以前我手算CRC,结果算错了一位,主站一直收不到正确响应,排查了整整两天。

1.5 错误态(Error State)

职责:处理异常情况,恢复系统到安全状态。

转换条件:

  • 错误恢复完成(如超时复位、缓冲区清理)→ 进入空闲态
  • 连续错误超过阈值(如3次)→ 保持错误态,等待手动复位

警告:错误态里不要无限循环!我曾经犯过这个错:接收超时后进入错误态,然后在错误态里死等。结果主站发了三次请求都没回应,直接标记从站离线。正确的做法是:错误态里设置一个超时计数器,超时后自动回到空闲态。

1.6 状态转换总览

下面这张图是我用SVG画的,把五个状态和转换条件串起来了。你看一眼就能明白整体逻辑。

空闲态 接收态 处理态 发送态 错误态 检测到起始位 接收完成 处理完成 发送完成 超时/长度异常 致命错误 总线冲突 恢复完成

1.7 状态转换表

为了方便你对照开发,我把转换条件整理成了表格:

当前状态 触发事件 下一状态 备注
空闲态 检测到起始位 接收态 同时启动接收超时定时器
接收态 接收完成(含CRC校验通过) 处理态 停止接收超时定时器
接收态 接收超时/帧长度异常 错误态 记录错误码
处理态 处理成功/处理失败(异常码) 发送态 组装响应帧
处理态 致命错误(如内存越界) 错误态 立即停止处理
发送态 发送完成 空闲态 等待3.5字符间隔
发送态 总线冲突 错误态 RS485半双工时发生
错误态 恢复完成(超时/手动复位) 空闲态 清空所有缓冲区

小技巧:实际编码时,我习惯用枚举定义状态:

typedef enum {
    STATE_IDLE,      // 空闲态
    STATE_RECEIVE,   // 接收态
    STATE_PROCESS,   // 处理态
    STATE_TRANSMIT,  // 发送态
    STATE_ERROR      // 错误态
} ModbusState;

这样switch-case写起来清晰,也方便调试时打印状态值。

好了,五个状态的定义和转换条件就讲完了。记住一句话:状态机是Modbus从站的骨架,骨架歪了,功能再多也白搭。


公众号:蓝海资料掘金营,微信deep3321