Modbus功能码与报文解析:PDU/ADU、异常响应与位字打包 简介《Modbus通讯功能码及实例.pdf》面向工业自动化、嵌入式开发、PLC编程及现场调试人员围绕Modbus通用工业通信协议整理常用功能码、报文结构与典型实例帮助读者理解主从设备之间线圈、输入状态、保持寄存器、输入寄存器的读写过程以及异常诊断思路。压缩包内仅含1个PDF文件大小约10KB内容篇幅精炼便于打印携带和随查随用文档以功能码01至06为主线同时覆盖15、16、17等批量操作与从机标识并配以设备地址、命令号、起始地址、寄存器数量、数据字节及CRC校验等字段说明还原请求帧与响应帧的组成。已有2332人学习适合作为协议速查手册、通信调试入门参考和报文分析辅助材料可帮助读者对照实例梳理读写流程、排查通信异常并加深对功能码用途的理解。1. 功能码不是命令表一条 8 字节报文如何决定工业现场的一次读写很多人第一次看 Modbus会把功能码当成一张命令表01 读线圈、03 读寄存器背下来就行。真正上现场会发现功能码只是 PDU 的第一个字节决定一次读写能否成立的还有地址基准、数量上限、字节计数和异常应答。Modbus 把“请求什么数据”和“数据怎么编进帧”分开定义串口 RTU 与以太网 TCP 只是外面的 ADU 不同里面的 PDU 语义一致。这份资料里列出的 01 到 06 六个高频码正好覆盖数字量与模拟量的读写把这六个码的请求和响应拆开就能理解功能码表背后的数据模型。先给结论读懂功能码不是背名称而是能根据请求数据域反推响应长度并根据异常码判断失败发生在哪一层。2. PDU 与 ADU功能码在报文里的位置为什么先定地址再定语义2.1 从串口帧到 PDU地址域、功能码域、数据域、CRC 的分层Modbus RTU 的一帧由三部分组成从站地址、PDU、CRC。PDU 又是“功能码 数据域”功能码占一个字节数据域长度由功能码和请求参数决定。从站地址放在 PDU 外面这不是随意排的一条 485 总线上可以挂多个设备主站必须先声明“我在跟谁说话”从站才会去解释后面的功能码。地址 0 是广播1 到 247 是从站地址248 到 255 保留。TCP 那边把地址和 CRC 换成了 MBAP 头PDU 原样保留所以功能码的语义跨传输层不变。资源里的例子[11][01][00][13][00][25][CRC低][CRC高]可以按字段拆开看0x11是 17 号从站0x01是读线圈0x0013是起始地址 190x0025是读 37 个开关量。这里有两个容易看漏的点起始地址在 PDU 里从 0 开始算不是从 1 开始多字节数值按高字节在前编码所以00 13拼出来是 19而不是 0x1300。CRC 两个字节则是低字节在前和前面数据域的高字节在前相反后面单独讲。字段长度资源例子含义从站地址1 字节0x1117 号从站功能码1 字节0x01读线圈状态起始地址2 字节0x00130-based 地址 19读取数量2 字节0x002537 个线圈CRC2 字节低字节在前从地址到数据域末字节设备响应[11][01][05][CD][6B][B2][0E][1B][CRC低][CRC高]里0x05是返回的字节个数后面 5 个字节是位图。37 个线圈需要 5 个字节因为 5×840 位最后 3 位没有实际意义按约定补 0。这个“字节个数”字段很关键RTU 没有总长度字段响应变长时主站要靠它知道后面该取多少字节。功能码 03 的响应也是同样思路只是字节个数等于寄存器数量乘 2。2.2 01/03 的请求与响应长度为什么能对上字节计数与寄存器数功能码 01 和 03 的请求都是定长 8 字节地址 1 字节、功能码 1 字节、起始地址 2 字节、数量 2 字节、CRC 2 字节。响应则变长靠第二个字节后面的“字节个数”把长度撑开。01 的字节个数是ceil(线圈数量 / 8)03 的字节个数是寄存器数量 × 2。主站解析时先读字节个数再按这个数取数据最后两个字节才是 CRC。这个设计让功能码本身不需要携带“响应有多长”长度信息被放进了数据域。用代码构造一个 03 请求能把地址和数量的编码看得很清楚import struct def build_read_holding(slave, start, qty): # 功能码 03起始地址和寄存器数量都按 16 位大端编码 pdu struct.pack(BHH, 0x03, start, qty) return pdu pdu build_read_holding(0x11, 0x006B, 0x0003) print(pdu.hex( )) # 03 00 6b 00 03BHH里的表示大端B是功能码两个H分别是起始地址和数量。slave没有进 PDU它要加在 PDU 前面CRC 也要在整帧拼好后追加。这个函数只负责 PDU方便在 TCP 和 RTU 两种 ADU 里复用。起始地址 0x006B 是 107数量 3对应资源里“从 107 号寄存器开始读 3 个模拟量”的例子。响应解析要先把异常响应拦下来def parse_read_holding_response(frame): slave, func, byte_count frame[0], frame[1], frame[2] if func 0x80: raise RuntimeError(f异常响应异常码0x{frame[2]:02X}) values [] for i in range(0, byte_count, 2): values.append(int.from_bytes(frame[3 i:5 i], big)) return slave, values frame bytes.fromhex(11 03 06 02 2B 00 00 00 64) print(parse_read_holding_response(frame)) # (17, [555, 0, 100])这段代码假设frame已经去掉了 CRC只保留地址、功能码、字节个数和数据。func 0x80用来判断最高位如果为 1说明从站返回的是异常响应此时frame[2]不是字节个数而是异常码。正常响应里byte_count6循环每次取两个字节按大端拼成 16 位整数得到 0x022B555、0x00000、0x0064100和资源例子一致。真实串口帧末尾还有两个 CRC 字节解析数据时不能把它们算进byte_count。2.3 异常响应功能码最高位置 1 之后错误码说明了什么异常响应的格式是[从站地址][功能码 | 0x80][异常码][CRC低][CRC高]。功能码最高位置 1表示“这个功能码的请求失败了”后面的异常码才说明原因。比如请求 03 读保持寄存器从站返回0x83主站就知道这是 03 的异常如果返回0x03那还是正常功能码不能混。功能码正常范围是 1 到 127异常标记用 0x80 不会和正常功能码冲突资源里把 128 到 255 保留给异常应答也是同一个空间划分思路。异常码含义常见触发场景0x01非法功能从站不支持该功能码0x02非法数据地址起始地址或寄存器范围越界0x03非法数据值数量超过上限、写线圈值不是 FF00/00000x04从站设备故障从站内部执行失败0x05确认从站已接受但需要长时间处理0x06从站设备忙从站正在执行长任务0x08存储奇偶校验错误从站存储校验失败0x0A网关路径不可用网关找不到目标设备0x0B网关目标设备无响应网关转发后目标从站未回异常码 02 和 03 最容易在联调时遇到。02 多半是地址基准错了比如手册写 40001报文里发了 0x000103 常见于数量超限比如 03 一次读 200 个寄存器超过 125 的上限。异常码 06 可以退避重试02 重试没有意义先把地址和数量改对。异常响应只有 5 个字节比正常响应短主站如果按正常响应的字节个数去等就会一直等到超时所以解析入口先看功能码最高位是比较稳妥的顺序。3. 位与字的分界01/02/05/15 为什么按位打包03/04/06/16 为什么按字拆高字节3.1 线圈与离散输入LSB 先行、末字节高位补零的打包规则功能码 01 读线圈、02 读离散输入数据域都是位图。一个字节装 8 个开关量字节的最低位对应该字节里地址最小的线圈也就是 LSB 先行。资源例子返回CD 6B B2 0E 1B第一个字节 0xCD 展开成二进制是1100 1101从低位往高位读bit01、bit10、bit21、bit31、bit40、bit50、bit61、bit71。对应起始地址 19 开始的 8 个线圈就是 19 号闭合、20 号断开、21 号闭合、22 号闭合、23 号断开、24 号断开、25 号闭合、26 号闭合。原文例子里说的 20 号到 27 号是 1-based 编号和 PDU 的 0-based 地址差 1。解析位图时不能按“高位先”去读否则开关顺序会整个反掉def unpack_bits(data, qty): bits [] for byte in data: for i in range(8): bits.append((byte i) 1) return bits[:qty] data bytes.fromhex(CD 6B B2 0E 1B) print(unpack_bits(data, 37)[:8]) # [1, 0, 1, 1, 0, 0, 1, 1]data是响应里的位图字节qty是请求里要读的线圈数量。每个字节从 bit0 到 bit7 依次追加最后按qty截断。37 个线圈会得到 40 位最后 3 位是补零的高位不能当成真实开关状态。02 的打包规则和 01 完全一样只是数据来源是只读的离散输入从站不会因为 02 改变输入状态。写多线圈 15 的请求数据也按同样规则打包所以 15 和 01 的位图可以共用一套打包函数本文还有配套的精品资源点击获取