4、跨协议转换核心原理:协议栈分层映射、数据单元(PDU)与协议单元(ADU)的转换逻辑、状态机设计

各位工程师朋友,这一章咱们来啃硬骨头。

跨协议转换,说白了就是让两个「语言不通」的设备能对上话。比如Modbus RTU要和Modbus TCP握手,或者Modbus ASCII要跟RTU交换数据。你想想看,一个走串口,一个走以太网,物理层都不一样,怎么聊?

嗯,这里就要用到协议栈分层映射的思路了。我个人习惯把这件事拆成三块来理解:分层映射数据单元转换状态机控制。咱们一个一个说。

4.1 协议栈分层映射:别让物理层干扰应用层

我在项目中遇到过不少新手,一上来就想在应用层直接改报文。结果呢?改完发现校验不对,或者地址丢了。其实问题的根源在于——没搞清每一层该干什么

Modbus协议栈,从下往上大致分三层:

  • 物理层:RS-232/485(串口)或以太网(TCP/IP)。这一层只管比特流,别碰它。
  • 数据链路层:对于RTU/ASCII,这一层负责帧定界、地址识别、CRC/LRC校验。对于TCP,这一层是TCP/IP协议栈的事。
  • 应用层:Modbus PDU(协议数据单元)。这才是我们真正要关心的数据内容——功能码+数据。

跨协议转换的核心原则是什么?应用层不变,变的是下面两层

举个例子:RTU转TCP。RTU报文里有设备地址和CRC校验,这些在TCP里根本不需要——因为TCP/IP已经用IP地址和TCP校验和搞定了。所以转换时,我们要把RTU的地址和CRC剥掉,只保留PDU,然后套上TCP的MBAP头。

核心口诀:PDU是灵魂,ADU是衣服。换协议就是换衣服,别把灵魂弄丢了。

4.2 PDU与ADU的转换逻辑:剥洋葱与穿衣服

好,咱们来点实际的。Modbus的报文结构,我习惯用一张表记在脑子里:

协议类型 ADU结构 PDU位置
Modbus RTU 地址(1B) + PDU + CRC(2B) 中间部分
Modbus ASCII ':' + 地址(2字符) + PDU + LRC(2字符) + CR/LF 中间部分
Modbus TCP MBAP头(7B) + PDU 从第8字节开始

你看,不管外面怎么包装,PDU始终是那个PDU。转换逻辑其实就是两步:

  1. 剥洋葱:从源协议的ADU中提取出PDU。
  2. 穿衣服:把PDU塞进目标协议的ADU框架里。

举个例子,RTU转TCP的代码逻辑,我写过很多次了。核心就这几行:

// 伪代码:RTU -> TCP 转换
uint8_t rtu_frame[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xC5, 0xCD};
// 地址:0x01, 功能码:0x03, 数据..., CRC:0xC5CD

// 第一步:提取PDU(从第2字节开始,到CRC前结束)
uint8_t pdu[] = {0x03, 0x00, 0x00, 0x00, 0x0A};
// 功能码:0x03, 起始地址:0x0000, 寄存器数量:0x000A

// 第二步:构建TCP帧(MBAP头 + PDU)
uint8_t tcp_frame[12];
tcp_frame[0] = 0x00;  // 事务标识符高字节
tcp_frame[1] = 0x01;  // 事务标识符低字节
tcp_frame[2] = 0x00;  // 协议标识符(固定为0)
tcp_frame[3] = 0x00;
tcp_frame[4] = 0x00;  // 后续长度(PDU长度+1)
tcp_frame[5] = 0x06;  // 长度=1(单元标识符)+5(PDU长度)
tcp_frame[6] = 0x01;  // 单元标识符(原RTU地址)
// 后面直接拼接PDU
memcpy(&tcp_frame[7], pdu, 5);

这里有个坑,我曾经踩过——单元标识符。RTU里的设备地址,在TCP里变成了单元标识符。但很多网关默认填0x00或0xFF,结果从站不认。我建议:保留原地址值,除非你明确知道对方网关的映射规则。

避坑指南:ASCII转RTU时,记得把ASCII的十六进制字符(比如'3A')转成二进制字节(0x3A)。我见过有人直接字符串拼接,结果CRC算出来全是错的。

4.3 状态机设计:让转换过程有章可循

跨协议转换不是简单的「收到就转发」。你想想看,如果串口收了一半,以太网那边突然发来请求怎么办?或者CRC校验失败,要不要丢弃?

嗯,这时候就需要状态机来管事了。我个人习惯把转换网关的状态机设计成三个主要状态:

  • IDLE(空闲):等待接收数据。收到第一个字节后,进入接收状态。
  • RECV(接收中):根据协议规则,持续接收直到帧结束。RTU用3.5字符时间超时判断,TCP用长度字段判断。
  • PROC(处理中):校验、提取PDU、重新封装、发送。完成后回到IDLE。

状态转移图大概长这样:

        +---------+
        |  IDLE   | <------+
        +----+----+        |
             |             |
       收到首字节        处理完成
             |             |
             v             |
        +----+----+        |
        |  RECV   |        |
        +----+----+        |
             |             |
        帧接收完成          |
             |             |
             v             |
        +----+----+        |
        |  PROC   |--------+
        +---------+

这里有个细节,我在项目里吃过亏——超时处理。RTU协议要求3.5字符时间无数据才算帧结束。但如果你用状态机实现,一定要在RECV状态里加一个超时定时器。否则万一线路干扰,少收了一个字节,状态机就永远卡在RECV里了。

警告:千万别在PROC状态里做阻塞操作!比如等待TCP连接建立。我见过有人直接在中断里调用connect(),结果整个系统卡死。正确的做法是:PROC状态只做数据转换和投递,真正的网络发送交给任务队列或DMA。

4.4 实战中的状态机优化

讲个真实案例。有一次我做Modbus RTU转MQTT网关,RTU侧是9600波特率,MQTT侧是WiFi。问题来了:RTU一帧数据可能只有几十毫秒,但MQTT发布一次可能要几百毫秒。

怎么办?我加了一个缓冲队列。状态机里多了一个状态:

  • BUF(缓冲):当PROC状态发现目标协议忙时,把转换好的数据放入队列,然后立即回到IDLE继续收下一帧。

这样,RTU侧不会因为网络延迟而丢帧。你想想看,如果不用队列,RTU发第二帧时,网关还在等MQTT发完第一帧——那第二帧就丢了。

状态机设计没有标准答案,但有一条铁律:不要阻塞。我建议所有状态都设计成「非阻塞+超时退出」的模式。哪怕处理失败,也要回到IDLE,等待下一帧。

总结一下:跨协议转换,说白了就是三件事——分层剥开、提取PDU、重新打包。状态机是保证这个过程不出乱子的骨架。记住:PDU是核心,ADU是包装,状态机是调度员。

下一章,咱们会深入Modbus TCP和RTU之间的具体转换细节,包括那些让人头疼的字节序和超时问题。到时候再聊。