Modbus报文详解:RTU与TCP从协议结构到现场排错实战
发布时间:2026/10/14 4:14:59 锦皓数字建站

干工业自动化这一行早晚都要跟 Modbus 报文打交道。不管是 PLC 去读一台仪表的数据还是上位机跟变频器通信Modbus 始终是那个最基础却又绕不开的协议。这期工控小知识我就把平时调试中关于 Modbus 报文的逻辑和细节一次性倒干净针对 RTU 和 TCP 两种最常见形态从报文结构讲到现场排错既照顾刚开始看报文一头雾水的朋友也给常年在现场折腾的同行当一份可查的备忘录。1. 先弄明白 Modbus 报文到底在传什么1.1 一主多从的问答游戏Modbus 本质上是一套谁会问、谁会答的规矩。总线上只允许有一个主站通常是 PLC、工控机或者触摸屏剩下的都是从站比如温度变送器、压力表、电机保护器。通信流程极其简单主站发出请求帧从站收到帧、判断地址是不是自己是就处理不是就忽略处理完后从站针对这一请求作出响应。整个过程就是一次对话一问一答不会出现两个从站同时往外发数据的情况。很多人第一次接触 Modbus 会觉得它土毕竟没有复杂的加密、没有握手协商帧结构就那么几个字节。但你反过来想工业现场要的就是确定性和可维护性。一个泵站的转速、一个罐子的液位这些数据要是因为协议太复杂传错了那可不是重新发一次包那么简单。Modbus 报文之所以这么多年还在到处服役恰恰因为它的设计把所有复杂度都留给了开发者传输层则做到了极致的简单可靠。1.2 RTU、ASCII 和 TCP三种报文形态Modbus 并不是只有一种帧格式最经典的是串行链路上跑的 RTU 和 ASCII 两种还有挂载在以太网上的 Modbus TCP。RTU传输时每个字节是 8 位二进制数连续发送帧紧凑效率高是现场绝对的主流。绝大多数仪表、变频器的出厂协议都选 RTU。ASCII每个字节拆成两个 ASCII 字符传输帧里还要加开始和结束标记传输效率比 RTU 低一半但胜在肉眼可读调试时方便观察。现在用得很少只有老设备或者某些特殊无线模块还在坚持。Modbus TCP以太网版本在报文里增加了一个 MBAP 头去掉了 CRC 校验。因为以太网链路本身有可靠传输机制没必要在应用层再做一层 CRC。后面所有拆解优先讲 RTU因为它的字节最紧凑逻辑最完整TCP 只需要在 RTU 的基础上理解一个头和少了校验即可一通百通。2. 逐字节拆解 RTU 报文2.1 设备地址一个字节的门牌号RTU 请求帧的第一条字段是设备地址占 1 个字节范围 1 到 247。这里有个现场常见误区很多人写程序时把地址填成 0还觉得 0 是不是也能用、更省事。其实 0 是广播地址主站发的请求所有从站都会接收执行但从站不会返回任何响应。广播一般只用于主站要求所有设备同时复位这类场景日常采集千万别用不然你会发现设备明明执行了却一点回音都没有。还有一个容易踩坑的地方修改设备地址的时候很多从站要求用广播帧结合串口命令来改比如有的仪表支持发送一条地址修改命令给广播地址 0设备收到后既执行又返回响应。但更多设备在广播模式下就是不理你这时候只能用拨码或者按键先把设备地址改好再接到总线上调试。我见过好几个现场设备地址出厂都是 1结果一台从站设备忘了改跟 PLC 通信时 CRC、波特率都对就是响应错乱查了半天才发现两台设备撞地址了。2.2 功能码决定了主站让从站干什么第二个字节是功能码。这是整个报文里最直观的语义部分看到功能码你基本就知道这条请求要干什么。常用功能码就那么几个功能码含义操作对象读写方向01 (0x01)读线圈状态线圈 (DO)只读02 (0x02)读离散输入状态离散输入 (DI)只读03 (0x03)读保持寄存器保持寄存器 (AI/AO 参数区)只读04 (0x04)读输入寄存器输入寄存器 (AI 采集区)只读05 (0x05)写单个线圈线圈 (DO)写06 (0x06)写单个保持寄存器保持寄存器写15 (0x0F)写多个线圈线圈 (DO)写16 (0x10)写多个保持寄存器保持寄存器写很多新手搞不清保持寄存器和输入寄存器的区别。简单说保持寄存器既能读又能写存放的大多是可修改的参数比如设定温度、PID 参数、变频器频率给定值。输入寄存器只能读不能写存放的是从传感器或设备内部直接采集来的值比如当前温度、电流、电压。线圈和离散输入的区分也类似线圈是输出可写离散输入是开关量状态输入只读。调功能码之前一定要先翻设备手册确认支持的功能码和寄存器范围。有的设备所有参数都放在保持寄存器里但偏偏有厂商的说明书把寄存器区段标注得云里雾里看错了功能码轻则返回不支持重则直接拉低设备里的参数。建议任何新设备第一次通信时先发一条功能码 03 读几个已知地址确认响应跟预期一致再往下做其他操作。2.3 数据字段寄存器地址和数量的组织从第三个字节开始是数据字段内容根据功能码不同而不同。读操作的数据段固定由两个要素组成起始寄存器地址2 字节和寄存器数量2 字节。以功能码 03 为例请求地址 功能码 起始地址高字节 起始地址低字节 数量高字节 数量低字节 CRC 高字节 CRC 低字节这里有两个极其容易出现理解偏差的细节。第一个是寄存器地址的偏移。Modbus 协议里设备内部的寄存器地址是从 0 开始的也就是 0x0000。但人机界面、组态软件、PLC 的 Modbus 地址映射却往往从 40001 开始比如 40001 对应的就是协议地址 0x000040002 对应 0x0001依此类推。你读协议地址 0x0000从软件界面看到的就是 40001。要是对着文档去填文档告诉你读取 40010你不能直接发 0x0A得先用 40010 减 40001得到 9再发 0x0009。这个差值坑过的人不在少数。第二个细节是数据的字节序。寄存器都是 16 位宽一个寄存器要占 2 字节。Modbus 协议本身规定大端序发送也就是高字节在前、低字节在后。但要注意这个前和后只是在帧里的存放顺序不同的从站设备里数据实际解释顺序仍然可能不同。后面我会专门讲大小端现场问题先记住一个准则读一个 16 位整数先看协议栈能不能组出预期的值再怀疑大小端。2.4 CRC 校验两字节的强大指纹RTU 帧的最后两个字节是 CRC-16 校验但是注意存放方式是低字节在前高字节在后。这是 Modbus RTU 特有的一点很多人在解析或者组帧时容易把这两个字节搞反然后一直收到从站的异常响应甚至设备完全不回应。CRC 校验计算覆盖的数据范围是从设备地址开始一直到数据字段的最后一个字节不包括 CRC 本身。算法用的多项式是 0x8005经过反转后实际计算时用的多项式值是 0xA001初始值为 0xFFFF。具体步骤预置一个 16 位寄存器为 0xFFFF。取出报文第一个字节与 CRC 寄存器的低 8 位异或。将 CRC 寄存器右移 1 位最高位补 0。如果右移出去的位是 1则将 CRC 寄存器与 0xA001 异或。重复第 3、4 步 8 次。取下一个字节重复上述步骤直到所有字节处理完。最后得到的 CRC 值低字节在前发送出去。举一个完整的示例请求帧 01 03 00 00 00 0A即读地址为 1 的从站从起始寄存器 0x0000 开始读取 10 个保持寄存器。经过该算法得到的 CRC 是 0xCDC5那么 RTU 帧追加的低字节是 0xC5高字节是 0xCD。什么时候需要考虑算 CRC如果是用现成的组态软件、仪表配置工具它们会帮你算好如果你是自己用单片机、Python、C# 写通信程序底层串口程序就得自己组帧。我强烈建议初期不要在项目里自己一遍遍较验一定难啃的 CRC 细节先把这部分封装成公共函数调成熟了就不用再碰。否则后面每次调试都要担心是不是我 CRC 写错了。3. 从请求到响应一次完整读操作的报文之旅3.1 读保持寄存器功能码 03的请求帧假设现场有一台温控器从站地址是 1我们需要读取从协议地址 0x0000对应组态地址 40001开始的 10 个保持寄存器那请求帧组装出来是01 03 00 00 00 0A C5 CD各字节含义字节位置值说明101从站地址203功能码读保持寄存器3-400 00起始寄存器地址 0x00005-600 0A寄存器数量 107-8C5 CDCRC 校验低字节在前这里提醒一个实战细节发送前主站必须保证波特率、数据位、停止位、校验位与从站一致。现场最常见的组合是 9600、8 位数据位、无校验、1 位停止位简写为 9600-8-N-1。如果设备手册上标注的是 Even偶校验那就不是 8-N-1 了。不少现场仪表支持自动适应波特率但也有老式设备必须手动拨码设置可千万别默认全是一样的。说完参数再说时序。RTU 帧是一帧一帧连续字节之间没有固定起始和结束字符。接收方靠什么来判断一帧结束呢靠静默时间。Modbus 官方规定帧内两个字节之间间隔不能超过 1.5 个字符时间一帧结束后至少要静默 3.5 个字符时间接收方才认为一帧结束了。以 9600 波特率来算3.5 个字符时间大约是 4ms 左右。如果你写串口程序时把读超时设得太短或者太长就会出现读半帧或两帧黏在一起的现象。最简单的处理是使用支持帧间隔判断的驱动或库别用固定超时就硬闯。3.2 响应帧怎么看从站处理完请求后正常情况会返回一帧响应。还是以上面的请求举例假设从站返回了 20 字节的数据10 个寄存器每个 2 字节响应帧结构如下01 03 14 [数据1高] [数据1低] [数据2高] [数据2低] ... [数据10高] [数据10低] [CRC低] [CRC高]其中第三个字节 0x14 是十进制 20表示后面跟着的数据字节数。这里有个容易忽略的地方读请求里你告诉从站的是我要读多少个寄存器而响应里从站告诉你的却是我要返回多少字节。为什么是 20 而不是 10因为每个寄存器占 2 字节。很多人在上位机解析时直接按请求时的数量去截响应遇到数量不匹配就被从站识别为错误流程实际只要记住响应数据字节数 寄存器数量 × 2。在实际操作中我更建议直接抓完整帧拿上位机解析。用串口调试助手把原始报文一帧一帧拉出来先用肉眼把地址、功能码、字节数这些固定字段对上再拿计算器把寄存器值换算成实际工程量基本能确认通信是否正常。这一步别偷懒你越早能读懂光秃秃的十六进制后面查起问题来就越快。依赖图形化软件一键显示数值当然是方便但一旦数值不对你得有能力回到原报文层面去定位。3.3 写操作功能码 05、06、16 的报文差异写操作和读操作的报文明细差异很大尤其写单个和写多个之间格式完全不同。功能码 05 写单个线圈。请求帧是从站地址 05 线圈地址(2 字节) 写入值(2 字节) CRC。写入值只有两种合法情况0xFF00 表示线圈为 ON0x0000 表示线圈为 OFF其他任何值都被视为非法。比如让 1 号从站的线圈 0x0000 输出 ON报文是01 05 00 00 FF 00 8C 3A这里要注意05 功能码响应帧和请求帧通常是完全一样的从站把请求原样回给主站表示我已经执行完了。如果主站程序里没写这个逻辑看到响应长度对不上可能会误判成错误。功能码 06 写单个保持寄存器。请求帧是从站地址 06 寄存器地址(2 字节) 写入值(2 字节) CRC。比如把 1 号从站保持寄存器 0x0000 的值写成 0x0100报文是01 06 00 00 01 00 CRC低 CRC高功能码 16 写多个寄存器。请求帧格式差异最大后面要附加要写字节数字段。比如从站地址为 2向保持寄存器 0x000A 开始连续写两个寄存器值分别为 0x1122、0x3344报文是02 10 00 0A 00 02 04 11 22 33 44 CRC低 CRC高其中第四个位置 0x04 是数据总字节数2 个寄存器 × 2 字节。和 05 一样16 的响应帧里也不带数据只返回起始地址和数量用来说明我写了哪个区域、写了多少个。我遇到过不止一次程序里要求写多个寄存器、又期望把写入值返回出来结果每个从站响应都对不上然后才意识到对 16 功能码的响应理解错了。4. Modbus TCP 报文不过是少了个 CRC 的兄弟4.1 MBAP 头部六字节拆解Modbus TCP 是在以太网链路层之上直接承载 Modbus 应用层报文。相比 RTU它最大的两个区别一是加了 6 字节的 MBAP 报文头二是不需要 CRC 校验。CRC 由下层的 TCP/IP 协议栈保证数据完整性应用层就不用再重复计算。MBAP 头从前往后拆开来看事务标识符2 字节每次请求分配一个编号用来把请求和响应配对。比如这次请求写 1响应回来也必须是 1下一次请求写 2响应就得是 2。上位机多任务并发时这个编号非常重要。协议标识符2 字节Modbus 协议规定必须写 0x0000。这个字段是为了兼容以后可能出现的其他协议扩展而保留的现在一律为 0。长度2 字节表示后面所有字节数即单元标识符 功能码 数据的总长度不包括长度自己前面的 6 个字节。MBAP 头之后还有一个单元标识符对应 RTU 里的从站地址。在 Modbus TCP 里网关可以把多个串口从站映射到一个 IP 地址后面这时的单元标识符就用来区分这些串口从站。如果现场只有一个设备直接连着网口一般填 0x00 或 0xFF 就能通具体看网关配置。4.2 一个 TCP 读请求报文实例解析还是上面读 10 个寄存器的场景改成 Modbus TCP从 IP 地址为 192.168.1.10 的设备上读取寄存器事务标识符设为 100 01 00 00 00 06 01 03 00 00 00 0A逐个字段看位置值含义1-200 01事务标识符13-400 00协议标识符05-600 06长度6701单元标识符1803功能码读保持寄存器9-1000 00起始地址11-1200 0A寄存器数量长度为什么是 6计算一下单元标识符 1 字节 功能码 1 字节 数据格式里“起始地址”2 字节和“寄存器数量”2 字节加起来正好 6 字节。所有网络字节序都按大端也就是高字节在前。相比 RTU这一帧少了尾部 CRC其他数据字段格式完全一致所以在串口上学会的寄存器地址、功能码等内容搬到 TCP 上可以无缝迁移。响应帧也类似比如响应返回 20 字节数据结构是00 01 00 00 00 17 01 03 14 [20字节数据]0x17 是十进制 23代表从单元标识符后的 1 字节标识 1 字节功能码 1 字节字节计数 20 字节数据总共 23 字节。如果你抓包看到长度字段与实际长度不匹配那么这帧基本已经被截断或者拼错了包通信不可能正常。5. 现场调试中常见的几个大坑5.1 CRC 校验错误现象很典型主站请求发出去从站没有回应或者从站回应了但主站校验报错。用串口抓包助手看帧结构和波特率都对就是 CRC 不符。排查思路分两步。第一步确认帧里 CRC 的高低字节顺序。Modbus RTU 是低字节在前也就是算出来的 0xCDC5发送时先发 0xC5 再发 0xCD。很多把 CRC 当成“标准大端”的程序员在这里反着拼一遍从站就判定校验错误直接丢弃帧。第二步检查计算范围。不少人是把从站地址一路算到了最后一个数据字节这个范围是对的但如果你把 CRC 本身也算进去或者漏掉前面的地址字节校验值怎么算都对不上。想快速验证是不是 CRC 写错了一个很笨但有效的办法是把抓到的完整 RTU 帧包括结尾的带 CRC逐字节丢给一个标准 CRC 计算工具如果得到的 CRC 为 0说明帧本身正确。如果不为 0那基本就是封装或计算范围有问题再逐段排查。5.2 请求发出去石沉大海这种问题占现场排查量的一半以上尤其是第一次接某个新设备时。设备地址、功能码、数据格式全都没毛病发送也正常可从站就是不回。第一优先检查物理层。RS-485 是差分信号A/B或者标 D/D-两根线有没有接反屏蔽双绞线有没有把屏蔽层可靠接地终端电阻是不是两端都并了 120 欧姆别忘了有些设备的端子标注的是 TX/TX-那就对应的是 RS-485 的 A/B别跟其他总线的正负极搞混。第二检查从站是否进入了“只听模式”或者上位机把接口占用调成了别的模式。很多仪表支持 Modbus RTU 和 Ethernet 双通信默认停在 Modbus TCP结果你从串口发 RTU设备根本不闻不问。真遇到这种先把设备拨码或软件设置切回到 RTU 模式。第三才轮到报文层面先用最简单的请求往下试比如只读一个寄存器固定功能码 03。不少新人一开始就按手册读一段很长的地址区间恰好地址越界从站返回异常或者干脆不响应你自己还会摸不着头脑。把范围缩小一隔离问题就清楚了。还有一个隐蔽的点是帧间间隔。有些上位机软件发送频率太快帧间隔小于 3.5 字符时间从站会把两帧数据当成一帧来解析当然不可能正常响应。真遇到这种情况把发送间隔调大一点比如从 10ms 调到 50ms 或 100ms往往直接就好了。5.3 数据读回来了但总觉得不对最常见的是 16 位整数高低字节反了。比如你读一个 16 位温度寄存器协议栈返回 0x01 0x2C其实 0x012C 是 300也就是 300 度但界面上显示 0x2C01 变成了 11265。这种问题极容易出现在 PLC 组态和品牌触摸屏的场景里因为它们的数据区经常默认按大端解释。解决办法就是根据设备手册里定义的寄存器字节序调整上位机的字节交换设置或程序里的高低字节交换。第二个常见问题是寄存器地址偏移。设备手册告诉你“温度在 40001”你就在组态软件里填 40001看起来没问题。但如果你直接用自定义协议去读起始地址得填 0x0000 而不是 1。反过来有些新设备手册直接就写“寄存器地址 0x0000 表示 40001”你得仔细分辨它到底用的哪种编号体系。建议在项目文档里留一个“协议地址 ↔ 组态地址”对照表现场调试时能省不少力气。第三个容易被忽略的是数据换算系数。Modbus 报文里发来的永远是原始整数比如电流变送器量程 0-100A输出 4-20mA对应寄存器值是 0-4095显示时要做一次线性变换。如果你直接拿原始整数当工程量就会得到数值怎么都对不上的诡异现象。这里没有捷径量程、满量程、偏移量都得对照说明书一项一项算清楚。5.4 异常码速查从站收到请求但如果功能码不支持、地址越界或者数据不合格它会返回一帧异常响应。异常的标志是功能码的最高位置 1也就是在原始功能码基础上加 0x80。例如请求功能码 03异常响应返回 0x83。异常响应帧一般是这样从站地址 0x83 异常码 CRC异常码这一字节就把问题指向了具体方向异常码含义常见触发原因01非法功能从站不支持该功能码02非法数据地址寄存器地址越界了03非法数据值写入值不在允许范围04从站设备故障从站内部出错多检查设备状态我调设备的第一反应不是看波形抓统计而是先看返回的异常码。返回 02那就在地址范围上做排除返回 03那就检查写入值的格式看看是不是写成负数、浮点数转整数溢出了。异常码能把排查范围缩小很多但很多资料里只写异常码 01、02……没有解释触发场景所以我把频发原因也列进来。6. 调试工具与最后几条建议6.1 常用工具组合先备好一个 USB 转 RS-485 适配器大概是现场出现频率最高的调试工具千万别拿普通 USB 转串口直接接到 RS-485 总线上电平不匹配很容易把设备口搞坏。选适配器的时候注意看它是否自带隔离、是否支持自动收发切换。自动收发切换的型号在调试时非常省心不然还得控制 RTS 信号线。调试软件我一般配两套。一套是通用的串口调试助手用来发裸报文和抓原始十六进制数据比如把01 03 00 00 00 0A C5 CD一行代码发出去看设备回什么。另一套是设备厂商自带的配置软件通常能直接显示寄存器数值适合先确认设备和上位机的通信链路是否通。两套结合起来既能看到图形化的中文参数也能回归到最原始的报文层面定位问题。如果你用的是 TCP 网关Wireshark 抓包很值得一试。在过滤条件里输入modbus就能看到完整的事务标识符、长度、功能码、寄存器地址。TCP 报文里没有 CRC定位问题基本就集中在 MBAP 头的长度字段和字节序上过滤很快。6.2 物理层布线建议报文只是逻辑层它最后都得在线上跑物理层一乱协议分析得再明白也没用。RS-485 布线有几点我吃了亏才认真对待第一必须用屏蔽双绞线A、B 一对线第二屏蔽层只能单点接地别两头都接不然会形成地环路第三总线两端接 120 欧姆终端电阻这个电阻的作用是消除信号反射几百米的距离不接电阻可能也能通信但一长距离、高速率时误码率就上来了第四从站走线的分支越短越好尽量手拉手别搞成大星形。调试时如果主站和从站本机都能通信但挂到总线上就不正常我第一时间就是查终端电阻和屏蔽层接地。写在最后再分享一点个人的习惯我在现场养成了一个习惯设备第一次上电通信成功之后马上存一份通信参数截图包括从站地址、波特率、校验方式、寄存器地址映射表。这东西平时看着不起眼但半年后再去改造这个站翻旧文档找参数时它就是最快的救命稻草。另外无论调试哪个协议都建议从最小闭环开始。先让设备单独通信成功一次再去加画面、加报警、加多设备轮询。别一上来就把一整张仪表台账全部挂进采集脚本里出了问题报文和数据混在一起看神仙也难分。Modbus 报文说穿了也就是“地址 功能码 数据 校验”这个框架抓稳了这个底子再往上去搞 RS-485 多子站轮询、Modbus TCP 跨网关采集都会顺很多。希望这篇对正在啃报文的同行有点用下次再遇到整个站通信趴窝先别急着重启设备把报文抓住一步步拆基本上都能找到答案。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。