首页 / 资讯中心 / 文章详情

Modbus功能码报文逐字节拆解:01/02/03/04/05/06实例与避坑指南

Modbus功能码报文逐字节拆解:01/02/03/04/05/06实例与避坑指南 ★ FEATURED ARTICLE
简介这份PDF资料面向工业自动化、嵌入式开发及现场调试人员系统梳理Modbus通信协议的功能码体系与报文实例帮助读者理解主从设备间的数据交互逻辑解决协议报文构造与解析中的常见困惑。资源包内含1个PDF文件大小约10KB篇幅精炼便于快速查阅与随身携带。内容以功能码为主线逐一说明01至19号功能码的作用涵盖读取线圈状态、读取输入状态、读取保持寄存器、读取输入寄存器、强置单线圈、预置单寄存器、强置多线圈、预置多寄存器、报告从机标识等常用操作并给出读可读写数字量、读只可读数字量、写数字量、读可读写模拟量、读只可读模拟量、写单个模拟量等典型场景的完整命令帧与响应帧示例逐字段解释设备地址、命令号、起始地址、寄存器数量及CRC校验的排列规则。目前已有2326人学习下载适合需要快速掌握Modbus报文格式、排查通信异常或开展设备联调的工程师与学习者参考。1. 从一份 PDF 说起为什么 Modbus 功能码值得逐字节拆开看手头这份《Modbus通讯功能码及实例.pdf》第一次翻的时候我以为是又一份抄来抄去的协议速查表直到看到里面用[11][01][00][13][00][25][CRC低][CRC高]这种逐字节展开的报文例子才意识到它把「功能码怎么用」这件事讲到了能直接对着串口调试助手敲的程度。Modbus 协议本身不复杂复杂的是现场同样是读保持寄存器有人用 03 有人用 04有人把起始地址写成 1 有人写成 0有人 CRC 高低字节顺序搞反结果就是设备一声不吭。这份资料的价值在于它把 01 到 19 号功能码的作用列全了又把最常用的 01、02、03、04、05、06 六个功能码的请求帧、响应帧、每个字节的含义拆开讲等于把 Modbus 报文这个黑匣子掀开给你看。适合谁做 PLC 数据采集、组态对接、嵌入式从站开发、LabVIEW 或 Python 上位机联调的工程师尤其是那种「协议文档看懂了但报文对不上」的阶段。下面我按「先立住原理、再动手复现、最后说坑」的顺序把这份资料里能直接抄作业的部分整理出来。2. 功能码分类与报文结构先搞清楚线圈和寄存器的边界2.1 四类数据区与功能码的对应关系Modbus 把从站的数据分成四个区这个划分是所有功能码的根。搞混这四个区后面地址和功能码一定对不上。数据区读写属性位/字常用功能码线圈Coils可读写位01 读、05 写单个、15 写多个离散输入Discrete Inputs只读位02 读保持寄存器Holding Registers可读写字16位03 读、06 写单个、16 写多个输入寄存器Input Registers只读字16位04 读资料里把 01 叫「读取线圈状态」、02 叫「读取输入状态」、03 叫「读取保持寄存器」、04 叫「读取输入寄存器」命名和上表一致。这里有个反直觉的点线圈和离散输入都是「位」但线圈可写、离散输入只读保持寄存器和输入寄存器都是「字」但保持可写、输入只读。现场最常见的错误就是拿 03 去读只读的输入寄存器或者拿 01 去读离散输入从站要么返回异常码要么干脆不回。选型逻辑很简单要控制的东西继电器、指示灯走线圈要采集的开关量限位、按钮走离散输入要改的参数设定值、阈值走保持寄存器要读的测量值温度、电压走输入寄存器。2.2 通用报文骨架地址、功能码、数据、CRC不管哪个功能码报文骨架都是四段[设备地址][功能码][数据段][CRC校验]。资料里反复出现的[设备地址]是 1 字节范围 1 到 247例子里的[11]是十六进制对应十进制 17 号设备。功能码 1 字节。数据段长度随功能码变化。CRC 是 2 字节注意资料里特别强调的一点CRC 校验在命令中的高低字节顺序和其他的相反也就是发送时先发 CRC 低字节、再发 CRC 高字节。这个细节坑了无数人后面避坑章节会专门说。请求帧和响应帧的结构不一样。以读类功能码为例请求里数据段是「起始地址 数量」响应里数据段是「字节个数 数据」。写类功能码请求里是「地址 要写的值」响应通常是原样返回请求回显。理解这个「请求问什么、响应答什么」的差异是能看懂后面实例的前提。2.3 地址从 0 开始协议地址和组态地址差 1资料里读线圈的例子起始地址写的是[00][13]十六进制 0x0013 等于十进制 19但文字说明里写「起始地址为 19」同时又说「起始地址为 0」。这里其实藏着一个行业惯例Modbus 协议报文里的地址是从 0 开始编号的而很多组态软件、PLC 编程软件里显示的地址是从 1 开始的。所以你在组态软件里看到「40001」对应报文里的保持寄存器地址是 0x0000看到「40002」报文地址是 0x0001。资料里例子说「想和 17 号通讯」设备地址写[11]这是十六进制和十进制的换算不是地址偏移。这两件事别混设备地址是站号寄存器地址是数据位置各有各的编号规则。搞不清这个联调时会出现「明明按文档写的地址就是读不到」的经典翻车。3. 六个常用功能码的报文实例逐字节拆开对着敲3.1 功能码 01 读线圈请求与响应逐字段解析资料给的请求例子是[11][01][00][13][00][25][CRC低][CRC高]。拆开看[11]设备地址十进制 17[01]功能码读线圈[00][13]起始地址0x0013 19[00][25]读取数量0x0025 37 个线圈[CRC低][CRC高]校验响应例子是[11][01][05][CD][6B][B2][0E][1B][CRC低][CRC高]。拆开看[11]设备地址[01]功能码[05]返回字节个数5 个字节[CD][6B][B2][0E][1B]数据共 5 字节 40 位覆盖请求的 37 个线圈[CRC低][CRC高]校验资料对数据的解释很关键每个字节表示 8 个开关量每一位 0 表示断开、1 表示闭合。例子中「20 号开关闭合21 号断开22 闭合23 闭合24 断开25 断开26 闭合27 闭合」。这里要注意如果请求的线圈数不是 8 的整倍数最后一个字节的高位部分无意义置 0。37 个线圈需要 5 个字节40 位最后 3 位是填充的 0解析时不能当成真实状态。用 Python 手工构造一帧读线圈请求可以这样写import struct def build_read_coils(slave_id, start_addr, count): # 功能码 01读线圈 frame struct.pack(BBHH, slave_id, 0x01, start_addr, count) crc calc_crc(frame) # 注意CRC 低字节在前高字节在后 frame struct.pack(H, crc) return frame def calc_crc(data): crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc # 复现资料里的例子17 号设备起始 19读 37 个线圈 print(build_read_coils(17, 19, 37).hex())这段代码里struct.pack(BBHH, ...)的表示大端字节序Modbus 报文字段都是大端。CRC 计算用标准多项式 0xA001算完后用H小端打包正好实现「低字节在前」。参数说明slave_id是站号start_addr是协议地址从 0 开始count是线圈数量。跑出来和资料例子对一下如果 CRC 对得上说明你的构造逻辑没问题。3.2 功能码 02 与 04只读区的读法差异资料对 02 和 04 的描述很简短「和读取线圈状态类似只是第二个字节的命令号不再是 1 而是 2」「和读取保存寄存器类似只是第二个字节的命令号不再是 2 而是 4」。这里资料原文有个笔误04 应该是「不再是 3 而是 4」因为读保持寄存器是 03。这个笔误不影响理解但提醒我们02 和 01 的报文结构完全一样04 和 03 的报文结构完全一样唯一区别就是功能码字节。所以实际开发中读线圈和读离散输入可以共用一个函数读保持寄存器和读输入寄存器也可以共用一个函数只把功能码当参数传进去。常见做法是封装一个通用读函数def build_read(slave_id, func_code, start_addr, count): # func_code: 0x01 线圈, 0x02 离散输入, 0x03 保持寄存器, 0x04 输入寄存器 frame struct.pack(BBHH, slave_id, func_code, start_addr, count) frame struct.pack(H, calc_crc(frame)) return frame参数上唯一要盯的是count的范围读位类01/02一次最多 2000 个读字类03/04一次最多 125 个。超了从站会返回异常码 03非法数据值。这个上限不是资料里写的是 Modbus 规范里的现场如果一次读太多寄存器失败先查数量有没有超。3.3 功能码 05 写单线圈FF00 和 0000 的强制约定资料给的写线圈例子是[11][05][00][AC][FF][00][CRC低][CRC高]。拆开[11]设备地址[05]功能码写单线圈[00][AC]线圈地址0x00AC 172[FF][00]写入数据表示闭合[CRC低][CRC高]校验资料特别强调此处只可以是 [FF][00] 表示闭合[00][00] 表示断开其他数值非法。这是 05 功能码最容易被忽略的约束。有人想当然地写[00][01]表示闭合从站直接拒绝或忽略。响应方面资料说「如果成功把计算机发送的命令原样返回否则不响应」。注意是「原样返回」不是返回一个字节的状态所以写单线圈的成功判断就是收到一模一样的 8 字节。def build_write_single_coil(slave_id, addr, on): # onTrue 写 FF00 闭合onFalse 写 0000 断开 value 0xFF00 if on else 0x0000 frame struct.pack(BBHH, slave_id, 0x05, addr, value) frame struct.pack(H, calc_crc(frame)) return frame参数on是布尔值映射到 FF00/0000 两个合法值避免手滑写出非法数据。这个封装在现场很实用因为写线圈的操作往往来自界面按钮布尔值最自然。3.4 功能码 03 读保持寄存器一个寄存器两个字节资料给的读保持寄存器例子是[11][03][00][6B][00][03][CRC低][CRC高]。起始地址 0x006B 107读 3 个寄存器。响应是[11][03][06][02][2B][00][00][00][64][CRC低][CRC高]。拆开[06]返回字节个数6 字节[02][2B]第 1 个寄存器0x022B 555[00][00]第 2 个寄存器0[00][64]第 3 个寄存器0x0064 100资料明确说「一个模拟量需要返回两个字节」所以返回字节数 寄存器数 × 2。解析时按两个字节一组高字节在前。这里有个现场高频坑有些设备的寄存器是 32 位浮点占两个寄存器字节序还有 ABCD、CDAB、BADC、DCBA 四种排列资料没涉及这块但如果你读出来是乱码数值先怀疑字节序别怀疑功能码。def parse_read_holding(response): # response 是完整响应帧去掉地址、功能码、字节数、CRC byte_count response[2] data response[3:3byte_count] regs [] for i in range(0, byte_count, 2): regs.append((data[i] 8) | data[i1]) return regs(data[i] 8) | data[i1]就是高字节左移 8 位再或上低字节还原 16 位无符号整数。如果设备是有符号数还要判断最高位做补码转换。3.5 功能码 06 写单寄存器回显即成功资料给的写单寄存器例子是[11][06][00][01][00][03][CRC低][CRC高]。地址 0x0001写入值 0x0003。资料说「如果成功把计算机发送的命令原样返回否则不响应」。和 05 一样06 的成功判断也是回显比对。这里注意06 写的是保持寄存器值可以是 0x0000 到 0xFFFF 任意 16 位值不像 05 只有两个合法值。def build_write_single_register(slave_id, addr, value): frame struct.pack(BBHH, slave_id, 0x06, addr, value) frame struct.pack(H, calc_crc(frame)) return frame参数value范围 0 到 65535。如果要写负数先转成补码value 0xFFFF。3.6 15 和 16 号功能码批量写的报文长度计算资料对 15强置多线圈和 16预置多寄存器只给了名称没给实例。但这两个在现场批量下发时很常用补一下。15 的请求结构是[地址][0F][起始地址高][低][数量高][低][字节数][数据...][CRC]其中字节数 (数量 7) // 8。16 的请求结构是[地址][10][起始地址高][低][数量高][低][字节数][数据...][CRC]字节数 数量 × 2。响应都是返回[地址][功能码][起始地址高][低][数量高][低][CRC]共 8 字节。批量写的好处是一次事务改多个点减少轮询周期但要注意从站对单帧最大长度的限制别一次写几百个寄存器把缓冲区撑爆。4. 联调避坑CRC、地址、字节序和异常码的排查清单4.1 坑一CRC 高低字节顺序反了从站完全不响应现象报文用在线 CRC 计算器算出来是对的但发出去从站一点反应没有连异常码都不回。原因Modbus RTU 的 CRC 在报文里是低字节在前、高字节在后而很多计算器或库默认输出高字节在前。资料里专门写了「CRC 校验在命令中的高低字节的顺序和其他的相反」就是这个意思。解决算完 CRC 后发送时先发crc 0xFF再发(crc 8) 0xFF。用 Python 的struct.pack(H, crc)就是小端正好。收到响应校验时把最后两字节按小端还原成 16 位再和计算值比。4.2 坑二起始地址 0 和 1 混用读到的数据整体偏移现象读出来的第一个值总是上一个寄存器的值或者读到的数据整体错位一格。原因协议地址从 0 开始组态软件地址从 1 开始两边没换算。资料里例子说「起始地址为 19」但报文写[00][13]如果直接拿组态软件里的 19 去填报文实际读的是第 20 个。解决统一在代码里用协议地址从 0 开始界面显示时再加 1。或者反过来界面用 1 起始构造报文前减 1。关键是全项目统一别一半加一半不加。4.3 坑三05 功能码写了非 FF00/0000 的值从站静默丢弃现象写线圈命令发出去从站不返回也不动作。原因05 功能码的数据段只有 FF00 和 0000 两个合法值写其他值从站按非法数据处理但很多从站不返回异常码直接丢弃。解决封装写线圈函数时强制布尔映射杜绝手写十六进制。资料里那句「其他数值非法」是血泪经验别挑战。4.4 坑四读数量超上限返回异常码 03现象读少量寄存器正常读多了返回[地址][功能码|0x80][03][CRC]。原因功能码最高位被置 1 表示异常异常码 03 是非法数据值通常是读取数量超过规范上限位类 2000、字类 125。解决分批读每批不超过上限或者查设备手册看它支持的单帧最大寄存器数。异常码解析时功能码字节与 0x80 做与运算非零就是异常帧。4.5 坑五字节序没确认32 位数据读出来是天文数字现象读两个连续寄存器拼 32 位浮点结果是 1.5e38 这种离谱值。原因32 位数据跨两个寄存器字节序有 ABCD、CDAB、BADC、DCBA 四种设备手册没写清楚或写错了。解决拿一个已知值比如设定 25.0去读四种排列都试一遍哪个出来是 25.0 就用哪个。这个没有捷径只能试。常见做法是在代码里留一个字节序配置项现场调。5. 从报文到工具链用 Python 脚本验证功能码并固化调试习惯把上面这些拼起来写一个能实际跑的最小验证脚本对着串口或 TCP 网关发一帧读保持寄存器看返回对不对。这是我认为这份资料最该配的「后悔药」——光看报文例子不够得让代码跑通一次后面所有功能码都是同一个套路。import serial import struct import time def calc_crc(data): crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc def build_frame(slave_id, func_code, start_addr, value_or_count): frame struct.pack(BBHH, slave_id, func_code, start_addr, value_or_count) frame struct.pack(H, calc_crc(frame)) return frame def transact(ser, frame, expected_len): ser.reset_input_buffer() ser.write(frame) time.sleep(0.1) resp ser.read(expected_len) if len(resp) 3: return None # 校验 CRC body, crc_recv resp[:-2], struct.unpack(H, resp[-2:])[0] if calc_crc(body) ! crc_recv: return None # 判断异常帧 if resp[1] 0x80: return (exception, resp[2]) return resp # 打开串口参数按现场改 ser serial.Serial(COM3, baudrate9600, bytesize8, parityN, stopbits1, timeout1) # 读 17 号设备起始 1073 个保持寄存器 req build_frame(17, 0x03, 107, 3) resp transact(ser, req, 6 5) # 地址功能码字节数6数据2CRC print(resp.hex() if resp else no response)这段脚本的关键点serial.Serial的参数要和从站一致波特率、数据位、校验位、停止位任何一个不对都通不了transact里先清空接收缓冲再发避免上一帧残留CRC 校验用struct.unpack(H, ...)按小端还原异常帧判断用resp[1] 0x80。参数expected_len要按功能码算读保持寄存器 3 个是1116211字节读线圈 37 个是1115210字节。算错长度会导致读不全或读多。验证顺序建议这样走先用 03 读一个已知寄存器确认通信链路和 CRC 都对再用 06 写一个值确认回显一致然后用 01 读线圈、05 写线圈确认位操作最后试 02 和 04 只读区。每步通了再下一步别一上来就批量读写混着来。从那以后我每次接手新的 Modbus 设备都强制先跑一遍这个最小脚本把站号、波特率、地址基准、字节序四个参数钉死再写业务逻辑。这套习惯帮我省掉了至少一半的现场返工。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站