ST Modbus协议解析:使用ST手动构建和拆解Modbus报文
在工业现场,PLC之间、PLC与HMI/上位机/智能仪表之间频繁交换数据,Modbus因其简洁、开放、易实现而成为最广泛使用的通信协议之一。当标准库函数(如MB_CLIENT或MB_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 → 十进制0 → WORD值0 → 拆为高位字节0x00、低位字节0x00:
写入 aRequest[2] := 16#00; // 地址高位
写入 aRequest[3] := 16#00; // 地址低位
寄存器数量10 → WORD值10 → 拆为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 8B 是 0x05 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]=01 → wValue[0] = 1;aResponse[5]=00, aResponse[6]=02 → wValue[1] = 2,依此类推。
四、处理异常响应:识别并定位故障
当从站无法执行请求时,返回异常响应:地址不变,功能码最高位置1(即原功能码 OR 16#80),后跟1字节异常码。
例如:请求 05 03 00 00 00 0A C5 8B 后收到 05 83 02 B0 9F
05:地址正确83:0x80 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: BYTEwStartAddr: WORDwQuantity: WORDaWriteData: ARRAY[0..124] OF WORD(写入值,最大125寄存器)
内部逻辑:
- 根据
eFuncCode自动选择数据域模板(如0x03填地址+数量;0x10填地址+数量+字节数+数据) - 自动计算
nLen与CRC - 输出
aFrame: ARRAY[0..255] OF BYTE和nFrameLen: 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);
七、调试技巧:快速定位通信故障
- 环回测试:将PLC串口TX/RX短接,发送报文后立即收到相同字节 → 验证本地发送逻辑无误。
- ASCII对照法:用串口助手(如Modbus Poll)发送标准帧,抓取十六进制数据,与ST生成的
aRequest逐字节比对。 - CRC交叉验证:用在线CRC计算器(输入
05 03 00 00 00 0A,选择CRC-16-MODBUS)核对结果是否为C5 8B。 - 时序抓包:使用USB-RS485+逻辑分析仪,确认帧间静默时间≥3.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
暂无评论,快来抢沙发吧!