‖ 系统学习
人生就像一场马拉松,偶尔停下来摸摸鱼,才能走得更远
---
W: 小莫啊,愁眉苦脸的,又被 Modbus 卡住了?
M: 王工救命啊!设备死活不响应我的读取命令,我查了接线、波特率都对,感觉问题出在我发的报文上。可那一串十六进制数字,我看着就头大...
W: 哈哈,报文就是 Modbus 设备间的“对话语言”。别怕,把它拆开看,其实很简单。来,坐下,我泡壶茶,咱慢慢聊。Modbus 的核心思想就是“主从问答”。你(主站)发个“问题”(请求报文),设备(从站)回个“答案”(响应报文)或者执行个动作。报文就是包裹这个“问题”或“答案”的“信封”。
M: “信封”?这个比喻好!那这个“信封”里都装了啥?
W: 问得好!Modbus 报文最基本、最重要的几个部分就是:
地址 (Address): 就像收件人门牌号。总线上可能挂了很多设备(从站),这个地址(1个字节,范围 1-247)告诉报文是发给谁的。地址 0 是广播地址(所有从站都听,但不回复)。
功能码 (Function Code): 这是“问题”的核心!告诉从站你要它干什么(1个字节)。比如:
0x01:读线圈状态 (Read Coils) - 读一堆开关量输出(DO)的状态(ON/OFF)。
0x02:读离散量输入 (Read Discrete Inputs) - 读一堆开关量输入(DI)的状态。
0x03:读保持寄存器 (Read Holding Registers) - 读一堆可读写的模拟量数据(比如温度设定值、电机转速)。
0x04:读输入寄存器 (Read Input Registers) - 读一堆只读的模拟量数据(比如实际温度、压力)。
0x05:写单个线圈 (Write Single Coil) - 改变一个 DO 的状态(强制 ON 或 OFF)。
0x06:写单个寄存器 (Write Single Register) - 改变一个保持寄存器的值。
0x0F:写多个线圈 (Write Multiple Coils) - 一次性改变多个 DO 的状态。
0x10:写多个寄存器 (Write Multiple Registers) - 一次性改变多个保持寄存器的值。
数据 (Data): 这个部分根据功能码不同,内容和长度都变。它包含具体操作的细节。比如:
起始地址 (Starting Address): 从哪个线圈/寄存器开始写?(2字节,高位在前)
数量 (Quantity): 要写多少个线圈/寄存器?(2字节,高位在前)
字节计数 (Byte Count): 后面紧跟着的实际数据有多少个字节?(1字节)
输出值/寄存器值 (Outputs Value/Registers Value): 要写入的多个值(N 字节)。线圈状态会按位打包(8个线圈压成1个字节),寄存器值则每个占2字节。
输出地址/寄存器地址 (Output Address/Register Address): 你要写哪个线圈/寄存器?(2字节,高位在前)
输出值/寄存器值 (Output Value/Register Value): 你要把它写成什么值?(2字节。对于线圈,0xFF00 表示 ON,0x0000 表示 OFF;对于寄存器就是数值)。
起始地址 (Starting Address): 你要从哪个线圈/寄存器开始读?(2字节,高位在前)
数量 (Quantity): 你要连续读多少个线圈/寄存器?(2字节,高位在前)
对于“读”请求 (0x01, 0x02, 0x03, 0x04):数据区通常包含:
对于“写单个”请求 (0x05, 0x06):数据区包含:
对于“写多个”请求 (0x0F, 0x10):数据区包含:
错误校验 (Error Check): 这个就像“信封”的封口漆和防伪码,确保报文在传输过程中没被干扰出错。有两种主要形式:
CRC (循环冗余校验): 用在 Modbus RTU/ASCII 串行链路上(RS-232/485)。对地址、功能码、数据区所有字节进行计算,得到2个字节的校验码,附加在报文最后。RTU 模式最常用。
LRC (纵向冗余校验): 用在 Modbus ASCII 链路上。计算得到一个字节的校验码(转换为两个ASCII字符表示)。
(对于 Modbus TCP): TCP/IP 协议栈底层(如以太网)已经有很强的校验机制(CRC32),所以 Modbus TCP 报文本身不需要额外的 CRC/LRC! 它用另一种机制保证完整性(见后面)。
M: 哦!我好像有点感觉了!地址找对人,功能码说明要干啥,数据区说明具体怎么干,最后加个校验保安全。那... 我常用的 RTU 和 TCP 报文结构一样吗?
W: 非常好!抓住了核心要素。RTU 和 TCP 的“应用数据单元”是一样的(就是地址+功能码+数据),但它们的“包装”不同:
Modbus RTU 报文 (串行 RS-232/485):
[ 从站地址 (1 Byte) ] [ 功能码 (1 Byte) ] [ 数据 (N Bytes) ] [ CRC 校验 (2 Bytes) ]特点: 紧凑,所有数据都是二进制,直接传输效率高。
关键点: 报文之间必须由至少 3.5个字符时间 的静默间隔来分隔。否则设备分不清哪是头哪是尾。波特率越高,这个静默时间越短。
示例 (读保持寄存器): 主站请求读取从站地址 1 的设备,从保持寄存器地址 40001(对应协议地址 0x0000)开始,连续读 2 个寄存器。
地址: 0x01功能码: 0x03 (读保持寄存器)数据: 起始地址高字节 0x00, 起始地址低字节 0x00, 数量高字节 0x00, 数量低字节 0x02CRC: (计算前面 01 03 00 00 00 02 这6个字节的CRC) -> 假设是 0xC4 0x0B完整RTU请求帧 (十六进制): `01 03 00 00 00 02 C4 0B`正常响应: 如果成功,从站 1 回复:
地址: 0x01 (我是谁)功能码: 0x03 (你要我读寄存器)数据: 字节计数 (后面数据有多少字节) 0x04 (因为2个寄存器 * 2字节/寄存器=4字节), 寄存器1值高字节, 寄存器1值低字节, 寄存器2值高字节, 寄存器2值低字节 (假设值分别是 0x1234 和 0x5678)CRC: (计算前面 01 03 04 12 34 56 78 这7个字节的CRC)完整RTU响应帧 (十六进制): `01 03 04 12 34 56 78 [CRC Hi] [CRC Lo]`异常响应: 如果出错(比如寄存器地址不存在),功能码最高位置 1 (即原功能码 + 0x80),数据区只有一个字节的错误码。
地址: 0x01功能码: 0x83 (0x03 + 0x80)数据: 异常码 (1 Byte) 例如 0x02 (非法数据地址)CRC: (计算 01 83 02 的CRC)完整RTU异常响应帧: `01 83 02 [CRC Hi] [CRC Lo]`Modbus TCP 报文 (基于以太网):
[ MBAP 头 (7 Bytes) ] [ 从站地址 (1 Byte) ] [ 功能码 (1 Byte) ] [ 数据 (N Bytes) ]事务处理标识 (Transaction Identifier) (2 Bytes): 由主站生成,用于匹配请求和响应。同一个事务ID的请求响应是一对。非常重要!
协议标识符 (Protocol Identifier) (2 Bytes): 固定为 0x00 0x00,表示 Modbus 协议。
长度 (Length) (2 Bytes): 指示后面还有多少个字节(从站地址 + 功能码 + 数据)。注意:不包括 MBAP 头自身的长度。
单元标识符 (Unit Identifier) (1 Byte):这就是 RTU 报文里的“从站地址”! 在 TCP 下,它通常用于标识连接在网关、网桥后面的串行网络上的具体 Modbus 从站设备。如果直接连接 TCP 设备,这个字段可能由设备定义其用途(有时也直接当从站地址用)。
特点: 运行在 TCP/IP 网络上(通常是端口 502)。去掉了 CRC 校验,增加了 MBAP 头 (Modbus Application Protocol Header)。
MBAP 头详解 (7字节):
示例 (同样的读请求 - Modbus TCP): 主站请求(事务ID假设为 0x0001)读取单元标识符(相当于从站地址)为 1 的设备,同样读 2 个保持寄存器 (0x0000 开始)。
MBAP头: 事务ID: 0x00 0x01 协议ID: 0x00 0x00 长度: 0x00 0x06 (后面有 01(地址) + 03(功能码) + 00 00(起始地址) + 00 02(数量) = 6字节) 单元ID: 0x01应用数据单元: 地址/单元ID: 0x01 (这里和MBAP里的单元ID一致) 功能码: 0x03 数据: 起始地址 0x00 0x00, 数量 0x00 0x02完整TCP请求帧 (十六进制): `00 01 00 00 00 06 01 03 00 00 00 02`正常响应:
MBAP头: 事务ID: 0x00 0x01 (和请求匹配!) 协议ID: 0x00 0x00 长度: 0x00 0x05 (后面有 01(地址) + 03(功能码) + 04(字节计数) + 4字节数据 = 5字节? 等等... 计算:01+03+04+[4字节数据]=8字节? 这里有个关键点!) **修正:** MBAP 头里的 `长度` = 单元标识符(1) + 功能码(1) + 数据区长度。 数据区长度 = 字节计数(1) + 实际数据字节数(4) = 5字节。 所以整个 PDU 长度 = 1(单元ID) + 1(功能码) + 5(数据区) = 7字节? 不对。 **关键:** MBAP 长度字段 = 后面跟随的字节数,即 **从 `单元标识符` 字节开始,一直到报文结束的所有字节数**。 响应帧结构: [MBAP] + [单元ID 1B] + [功能码 1B] + [字节计数 1B] + [寄存器数据 4B] -> 后面部分共 1+1+1+4=7 字节。 所以长度字段应该是 `00 07` (0x0007)。MBAP头 (修正后): `00 01 00 00 00 07 01`应用数据单元: 单元ID: 0x01 功能码: 0x03 数据: 字节计数 0x04, 寄存器1值 0x12 0x34, 寄存器2值 0x56 0x78完整TCP响应帧 (十六进制): `00 01 00 00 00 07 01 03 04 12 34 56 78`异常响应: 功能码+0x80,数据区一个字节错误码。长度字段计算类似。
M: 哇!这下清楚多了!TCP 那个 MBAP 头,特别是事务 ID 和长度,原来是干这个用的。单元标识符其实就是串行网络里的从站地址换了个地方。那... 我最初的问题,设备不响应,可能出在哪里?
W: 根据报文结构,结合你的情况,重点排查:
地址: 你请求报文里的从站地址 (RTU) 或单元标识符 (TCP) 真的是目标设备的地址吗?确认设备设置的地址。
功能码: 你读的线圈/寄存器,设备支持这个功能码吗?比如你试图用 0x03 去读只允许 0x04 读的输入寄存器,设备会报错(异常响应)。查设备手册!
数据区 - 起始地址: Modbus 协议地址通常是 0-based(从0开始)。但很多软件/手册用 1-based(如 40001 对应协议地址 0x0000)。务必搞清楚你用的软件和设备的约定! 这是超级大坑!你发的是 40001 的地址还是 0x0000?
数据区 - 数量: 你请求的数量超过设备允许的范围了吗?比如设备只有10个寄存器,你请求从地址0开始读11个,也会出错。
错误校验 (RTU): 你计算(或者软件自动计算)的 CRC 正确吗?用在线CRC计算器验证下你的请求报文。设备端校验失败会直接丢弃报文,不会有任何响应。
格式 (RTU): 波特率、数据位、停止位、校验位(None/Even/Odd)主站和从站设置完全一致吗?特别是校验位。RTU 报文之间是否有足够的 3.5 字符静默时间?
连接 (TCP): TCP 连接建立成功了吗?能 Ping 通设备 IP 吗?防火墙(尤其是工控机上的)是否放行了端口 502?设备作为 TCP Server 在监听吗?主站作为 TCP Client 连接上了吗?
MBAP 长度 (TCP): 你的 TCP 请求帧,MBAP 头里的长度字段计算正确吗?后面跟的字节数算对了吗?常见的错误来源。
M: 明白了王工!太感谢了!我这就拿报文分析软件(或者串口助手/Wireshark)抓一下实际收发的数据,对着报文结构一条条核对,重点看地址、功能码、起始地址、数量、CRC(RTU)/MBAP长度(TCP)。有这结构图在手,心里有底多了!
W: 这就对了!记住几个关键点:
主从问答: 主问,从答(或执行)。
核心四件套 (应用层): 地址、功能码、数据、校验(MBRTU)/MBAP头(MBTCP)。
地址陷阱: 0-based vs 1-based (40001 vs 0x0000)。
功能码: 读 (01,02,03,04) vs 写 (05,06,0F,10)。
RTU 要点: 二进制、紧凑、CRC、3.5字符间隔。
TCP 要点:MBAP头 (事务ID匹配、长度计算、单元ID)、无CRC、走网络 (IP:Port 502)。
异常响应: 功能码 + 0x80 + 错误码。
M: 记下了!王工,下次调试我请您吃饭!
W: 哈哈,吃饭就免了,把设备调通了就是最好的谢礼。去吧,按这个思路查,准能搞定!遇到具体抓到的报文看不懂再随时来问。
---
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!