文章目录

ST Modbus协议解析:使用ST手动构建和拆解Modbus报文

发布于 2026-03-18 15:45:00 · 浏览 6 次 · 评论 0 条

ST Modbus协议解析:使用ST手动构建和拆解Modbus报文

在工业现场,PLC之间、PLC与HMI/上位机/智能仪表之间频繁交换数据,Modbus因其简洁、开放、易实现而成为最广泛使用的通信协议之一。当标准库函数(如MB_CLIENTMB_SERVER)无法满足特殊需求——例如需要动态构造异常响应、插入自定义校验字段、调试非标设备、或绕过协议栈直接控制帧时序——就必须手动构建(assemble)和拆解(disassemble)Modbus报文。本指南专为使用结构化文本(Structured Text, ST)编程的工程师编写,全程不依赖任何高级函数块,仅用基础ST语法完成从字节级到功能码级的完整控制。


一、Modbus RTU报文结构:必须牢记的5个字段

Modbus RTU是电气自动化中最常用的物理层封装形式(RS-485总线),其报文为二进制字节流,无分隔符,靠静默时间判断帧边界。一个完整RTU报文由以下5个连续字节段组成(顺序不可变):

字段名 字节数 说明
地址域 1 从站设备地址(1–247),0为广播地址(仅写操作允许)
功能码域 1 标识操作类型(如0x03=读保持寄存器,0x10=写多个寄存器)
数据域 N(≥0) 承载实际参数:起始地址、寄存器数量、写入值等;长度随功能码动态变化
CRC校验域 2 低字节在前、高字节在后(Little-Endian)的16位循环冗余校验
(无帧头/帧尾) 无起始符、无结束符;靠3.5字符时间(T3.5)静默判定帧结束

⚠️ 注意:

  • 所有数值均以大端序(Big-Endian) 表示,即高位字节在前。例如寄存器地址40001对应十进制0,在报文中编码为两个字节:0x00 0x00
  • CRC计算范围仅包含地址域 + 功能码域 + 数据域不包括CRC自身两个字节。
  • 报文总长度 = 1 + 1 + N + 2 = N + 4 字节。最小报文(如0x01读线圈)为6字节(N=2);最大合法长度为256字节(含CRC),故数据域最多250字节。

二、ST中构建Modbus请求报文:5步手写法

假设目标:向地址为5的从站发送“读保持寄存器”请求,起始地址40001(即寄存器0),读取10个寄存器。

步骤1:声明字节数组变量

VAR
    aRequest: ARRAY[0..255] OF BYTE; // 预留足够空间(最大256字节)
    nLen: INT;                         // 实际有效字节数(不含CRC)
END_VAR

步骤2:填入固定字段(地址 + 功能码)

写入 aRequest[0] := 16#05; // 从站地址 = 5
写入 aRequest[1] := 16#03; // 功能码 = 0x03(读保持寄存器)

步骤3:填入数据域(起始地址 + 寄存器数量)

起始地址40001 → 十进制0WORD0 → 拆为高位字节0x00、低位字节0x00
写入 aRequest[2] := 16#00; // 地址高位
写入 aRequest[3] := 16#00; // 地址低位

寄存器数量10WORD10 → 拆为0x00 0x0A
写入 aRequest[4] := 16#00; // 数量高位
写入 aRequest[5] := 16#0A; // 数量低位

此时 nLen := 6;(地址1 + 功能码1 + 数据4 = 6字节)

步骤4:计算并追加CRC校验

CRC-16-MODBUS算法是确定性查表法。在ST中无需实时计算,直接调用预置函数或内联实现。以下是零依赖、纯ST查表实现(精简版,仅需32字节ROM空间):

// CRC查表(部分,完整表共256项;此处仅列前8项示意,实际需补全)
CONST
    crcTable: ARRAY[0..255] OF WORD := [
        16#0000, 16#C0C1, 16#C181, 16#0140, 16#C301, 16#03C0, 16#0280, 16#C241,
        (* ... 省略中间248项 ... *)
        16#8201, 16#42C0, 16#4380, 16#8341, 16#4100, 16#81C1, 16#8081, 16#4040
    ];
END_CONST

// CRC计算函数(输入:字节数组首地址,长度;输出:CRC值)
FUNCTION CalcCRC : WORD
VAR_INPUT
    pBuf: POINTER TO BYTE;
    nSize: INT;
END_VAR
VAR
    i: INT;
    crc: WORD := 16#FFFF;
    b: BYTE;
END_VAR
FOR i := 0 TO nSize - 1 DO
    b := pBuf^[i];
    crc := crcTable[(crc XOR b) AND 16#FF] XOR (crc / 16#100);
END_FOR;
CalcCRC := crc;

调用计算:
执行 wCRC := CalcCRC(ADR(aRequest), nLen);
写入 aRequest[nLen] := BYTE(wCRC AND 16#FF); // CRC低字节(先发)
写入 aRequest[nLen + 1] := BYTE((wCRC / 16#100) AND 16#FF); // CRC高字节(后发)
更新 nLen := nLen + 2; // 总长变为8字节

最终报文(十六进制):
05 03 00 00 00 0A C5 8B
→ 其中 C5 8B0x05 03 00 00 00 0A 的CRC校验值。

步骤5:发送报文

aRequest[0]aRequest[nLen-1]nLen 个字节通过串口发送函数(如SERIAL_SEND)逐字节发出。严禁添加任何额外字节(如换行、空格、帧头)。


三、ST中拆解Modbus响应报文:4步逆向解析

收到响应后,需验证合法性并提取数据。假设收到8字节响应:05 03 0A 00 01 00 02 00 03 00 04 00 05 C7 5D(地址5,功能码3,字节数10,5个寄存器值,CRC=C7 5D)。

步骤1:校验报文长度与地址/功能码回显

设接收缓冲区为 aResponse: ARRAY[0..255] OF BYTE,实际接收字节数 nRcvLen
验证 nRcvLen >= 5(最小响应:地址1+功能码1+字节数1+至少1字节数据+CRC2 = 6字节;但含错误时可能更短)
验证 aResponse[0] = 16#05(地址匹配)
验证 aResponse[1] = 16#03(功能码匹配,非异常响应)

若不满足,丢弃或标记通信错误。

步骤2:提取字节数字段并验证数据域长度

读取 nByteCount := aResponse[2]; // 数据域字节数(不含地址/功能码/CRC)
验证 nRcvLen = 5 + nByteCount(总长 = 5字节头 + 数据字节数)
验证 nByteCount MOD 2 = 0(寄存器数据必为偶数字节)

若任一失败,视为帧错误,丢弃。

步骤3:校验CRC

提取 wRcvCRC := WORD(aResponse[nRcvLen - 2]) + (WORD(aResponse[nRcvLen - 1]) * 16#100);
重算 wCalcCRC := CalcCRC(ADR(aResponse), nRcvLen - 2);
比较 wRcvCRC = wCalcCRC
不相等则CRC错误,丢弃。

步骤4:解析寄存器数据

数据起始位置:aResponse[3]
寄存器数量:nRegCount := nByteCount / 2
逐个读取寄存器(每个寄存器2字节,大端序):

FOR i := 0 TO nRegCount - 1 DO
    wValue[i] := WORD(aResponse[3 + i * 2]) * 16#100 + WORD(aResponse[3 + i * 2 + 1]);
END_FOR;

例如:aResponse[3]=00, aResponse[4]=01wValue[0] = 1aResponse[5]=00, aResponse[6]=02wValue[1] = 2,依此类推。


四、处理异常响应:识别并定位故障

当从站无法执行请求时,返回异常响应:地址不变,功能码最高位置1(即原功能码 OR 16#80),后跟1字节异常码。

例如:请求 05 03 00 00 00 0A C5 8B 后收到 05 83 02 B0 9F

  • 05:地址正确
  • 830x80 OR 0x03 → 功能码0x03的异常响应
  • 02:异常码 = 2(非法地址)
  • B0 9F:CRC

解析逻辑
判断 aResponse[1] AND 16#80 <> 0 → 是异常帧
提取 nExceptCode := aResponse[2];

查表映射:

异常码 含义 常见原因
01 非法功能码 从站不支持该功能码
02 非法数据地址 起始地址超出设备寄存器范围
03 非法数据值 寄存器数量为0或>125
04 从站设备故障 从站硬件异常或看门狗复位
05 确认 请求已接收,正处理(需轮询)
06 从站忙 从站正在执行长耗时操作
08 存储奇偶校验错 从站EEPROM数据损坏
0A 网关路径不可用 网关无法访问目标子网
0B 网关目标设备失败 网关转发时目标设备无响应

动作建议:记录异常码、暂停重试、触发报警、切换备用通道。


五、关键陷阱与避坑清单

风险点 错误示例 正确做法
字节序混淆 将寄存器地址0x0001写成01 00 严格按大端序:高位字节在前 → 00 01
CRC范围错误 对整个报文(含CRC)计算校验 仅对地址+功能码+数据域计算,CRC本身不参与
静默时间失控 发送后立即发下一帧,无T3.5间隔 帧间必须插入 ≥3.5字符时间(如9600bps下≈3.5ms)
广播请求误加CRC 给地址0的广播帧计算并发送CRC 广播帧不计算CRC,且主站不等待响应
数组越界写入 aRequest[256] := ...(索引超限) 声明数组时预留256字节,但nLen最大为255
未处理字节填充 写单个字节寄存器时,传入01而非00 01 所有寄存器操作均以WORD(2字节)为单位传输
浮点数误拆 直接将REAL变量地址转为POINTER TO BYTE 浮点数需先转为DWORD再拆高低字(BYTE数组取4字节)

六、实战扩展:构建通用Modbus构造器函数块

为提升复用性,可封装为ST函数块(FB)。输入参数:

  • eFuncCode: E_MODBUS_FUNC(枚举:READ_HOLDING=3, WRITE_MULTIPLE=16
  • wSlaveAddr: BYTE
  • wStartAddr: WORD
  • wQuantity: WORD
  • aWriteData: ARRAY[0..124] OF WORD(写入值,最大125寄存器)

内部逻辑:

  1. 根据eFuncCode自动选择数据域模板(如0x03填地址+数量;0x10填地址+数量+字节数+数据)
  2. 自动计算nLen与CRC
  3. 输出aFrame: ARRAY[0..255] OF BYTEnFrameLen: INT

此设计使调用侧代码降至3行:
调用 fbModbusBuilder(eFuncCode := READ_HOLDING, wSlaveAddr := 5, wStartAddr := 0, wQuantity := 10);
发送 SERIAL_SEND(pData := ADR(fbModbusBuilder.aFrame), nLen := fbModbusBuilder.nFrameLen);
解析 fbModbusParser(aRcvData := aRxBuffer, nRcvLen := nRx);


七、调试技巧:快速定位通信故障

  1. 环回测试:将PLC串口TX/RX短接,发送报文后立即收到相同字节 → 验证本地发送逻辑无误。
  2. ASCII对照法:用串口助手(如Modbus Poll)发送标准帧,抓取十六进制数据,与ST生成的aRequest逐字节比对。
  3. CRC交叉验证:用在线CRC计算器(输入05 03 00 00 00 0A,选择CRC-16-MODBUS)核对结果是否为C5 8B
  4. 时序抓包:使用USB-RS485+逻辑分析仪,确认帧间静默时间≥3.5字符、无毛刺、无粘连。
  5. 异常注入测试:手动修改从站地址为不存在值(如0xFF),观察主站是否收到异常帧FF 83 01

八、安全边界:为什么手动构造比调用库更可靠?

标准Modbus库(如Codesys内置MB_MASTER)在以下场景存在固有缺陷:

  • 超时僵死:网络中断时,库函数可能无限等待,导致PLC扫描周期超时;手动实现可精确控制重试次数与间隔。
  • 内存泄漏:长期运行后,某些库的内部缓冲区未释放,引发通讯缓慢;手动管理数组杜绝此风险。
  • 协议变异支持:某仪表要求功能码0x43(非标)、数据域末尾附加1字节设备ID;库函数无法扩展,而手动构造可任意填充。
  • 硬实时保障:在运动控制同步场景中,报文发出时刻需精准到毫秒级;手动控制发送时机,避免库调度延迟。

因此,核心设备、高可靠性系统、定制化协议桥接,必须掌握手动构造能力——它不是备选方案,而是工程底线。


九、附录:完整CRC-16-MODBUS查表(256项,可直接复制)

16#0000, 16#C0C1, 16#C181, 16#0140, 16#C301, 16#03C0, 16#0280, 16#C241,
16#C401, 16#04C0, 16#0580, 16#C541, 16#0700, 16#C7C1, 16#C681, 16#0640,
16#CC01, 16#0CC0, 16#0D80, 16#CD41, 16#0F00, 16#CFC1, 16#CE81, 16#0E40,
16#0A00, 16#CAC1, 16#CB81, 16#0B40, 16#C901, 16#09C0, 16#0880, 16#C841,
16#D801, 16#18C0, 16#1980, 16#D941, 16#1B00, 16#DBC1, 16#DA81, 16#1A40,
16#1E00, 16#DEC1, 16#DF81, 16#1F40, 16#DD01, 16#1DC0, 16#1C80, 16#DC41,
16#1400, 16#D4C1, 16#D581, 16#1540, 16#D701, 16#17C0, 16#1680, 16#D641,
16#D201, 16#12C0, 16#1380, 16#D341, 16#1100, 16#D1C1, 16#D081, 16#1040,
16#F001, 16#30C0, 16#3180, 16#F141, 16#3300, 16#F3C1, 16#F281, 16#3240,
16#3600, 16#F6C1, 16#F781, 16#3740, 16#F501, 16#35C0, 16#3480, 16#F441,
16#3C00, 16#FCC1, 16#FD81, 16#3D40, 16#FF01, 16#3FC0, 16#3E80, 16#FE41,
16#3A00, 16#FAC1, 16#FB81, 16#3B40, 16#F901, 16#39C0, 16#3880, 16#F841,
16#2800, 16#E8C1, 16#E981, 16#2940, 16#EB01, 16#2BC0, 16#2A80, 16#EA41,
16#EE01, 16#2EC0, 16#2F80, 16#EF41, 16#2D00, 16#EDC1, 16#EC81, 16#2C40,
16#E401, 16#24C0, 16#2580, 16#E541, 16#2700, 16#E7C1, 16#E681, 16#2640,
16#2200, 16#E2C1, 16#E381, 16#2340, 16#E101, 16#21C0, 16#2080, 16#E041,
16#A001, 16#60C0, 16#6180, 16#A141, 16#6300, 16#A3C1, 16#A281, 16#6240,
16#6600, 16#A6C1, 16#A781, 16#6740, 16#A501, 16#65C0, 16#6480, 16#A441,
16#6C00, 16#ACC1, 16#AD81, 16#6D40, 16#AF01, 16#6FC0, 16#6E80, 16#AE41,
16#6A00, 16#AAA1, 16#AB81, 16#6B40, 16#A901, 16#69C0, 16#6880, 16#A841,
16#7800, 16#B8C1, 16#B981, 16#7940, 16#BB01, 16#7BC0, 16#7A80, 16#BA41,
16#BE01, 16#7EC0, 16#7F80, 16#BF41, 16#7D00, 16#BDC1, 16#BC81, 16#7C40,
16#B401, 16#74C0, 16#7580, 16#B541, 16#7700, 16#B7C1, 16#B681, 16#7640,
16#7200, 16#B2C1, 16#B381, 16#7340, 16#B101, 16#71C0, 16#7080, 16#B041,
16#5000, 16#90C1, 16#9181, 16#5140, 16#9301, 16#53C0, 16#5280, 16#9241,
16#9601, 16#56C0, 16#5780, 16#9741, 16#5500, 16#95C1, 16#9481, 16#5440,
16#9C01, 16#5CC0, 16#5D80, 16#9D41, 16#5F00, 16#9FC1, 16#9E81, 16#5E40,
16#5A00, 16#9AC1, 16#9B81, 16#5B40, 16#9901, 16#59C0, 16#5880, 16#9841,
16#8801, 16#48C0, 16#4980, 16#8941, 16#4B00, 16#8BC1, 16#8A81, 16#4A40,
16#4E00, 16#8EC1, 16#8F81, 16#4F40, 16#8D01, 16#4DC0, 16#4C80, 16#8C41,
16#4400, 16#84C1, 16#8581, 16#4540, 16#8701, 16#47C0, 16#4680, 16#8641,
16#8201, 16#42C0, 16#4380, 16#8341, 16#4100, 16#81C1, 16#8081, 16#4040

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文