简介面向工业自动化与嵌入式开发初学者的Modbus协议入门资料聚焦功能码含义与报文实例帮助读者理解主从设备间数字量、模拟量的读写流程。包内为1个PDF文档压缩包约10KB篇幅精炼适合作为通信调试时的速查手册。内容系统梳理01至19号功能码的作用重点展开01读取线圈、02读取输入状态、03读取保持寄存器、04读取输入寄存器、05强置单线圈、06预置单寄存器等常用功能码并给出完整报文示例逐段解析设备地址、命令号、起始地址、寄存器数量与CRC校验各字段含义同时附设备响应格式说明。已有2330人学习适合需要快速掌握Modbus报文构造与解析、排查通信问题的工程师对照查阅。1. 一份把 Modbus 功能码讲透的 PDF从报文到字节序的落地手册调试现场最怕的不是设备不响应而是响应了但数据对不上。某次产线联调PLC 读回来的温度值始终是 65535查了两小时才发现是功能码 03 的寄存器数量字段填成了字节数。这类问题翻遍协议文档也未必能找到答案但一份把功能码和实例报文逐字节拆开的资料能省下大量时间。这份《Modbus通讯功能码及实例.pdf》就是干这个的它把 01 到 19 号功能码的作用列成表再对最常用的 01、02、03、04、05、06 六个功能码给出完整的请求与响应报文示例每个字节标出含义。适合刚接触 Modbus 的嵌入式开发者、做上位机对接的软件工程师以及需要排查 485 总线通信问题的现场调试人员。它不是协议规范的替代品而是一份能对着报文逐字节核对的速查手册。2. 六个核心功能码的报文结构逐字节拆开看2.1 读线圈与读输入状态01 和 02 的位打包逻辑功能码 01 读取可读写线圈状态02 读取只读输入状态两者报文结构完全一致只是命令号不同。请求帧固定 8 字节设备地址、功能码、起始地址高字节、起始地址低字节、寄存器数量高字节、寄存器数量低字节、CRC 低字节、CRC 高字节。资料里给的例子是[11][01][00][13][00][25][CRC低][CRC高]设备地址 0x11 即十进制 17起始地址 0x0013 即 19读取数量 0x0025 即 37 个线圈。响应帧的结构值得单独说因为它是新手最容易翻车的地方。响应格式为设备地址、功能码、返回字节数、数据 1 到数据 n、CRC 低、CRC 高。返回字节数等于(读取数量 7) // 8因为每个字节打包 8 个线圈状态不足 8 的倍数时高位补 0。资料例子中读 37 个线圈返回 5 个字节对应 40 个位最后 3 位无意义。数据字节内低位对应低地址线圈比如[CD]展开成二进制1100 1101bit0 对应起始地址那个线圈bit1 对应下一个依此类推。这里有个实操细节很多国产仪表或变频器的线圈地址从 1 开始编号而报文里的起始地址从 0 开始。资料明确写了“起始地址为 0”但现场手册往往标“线圈 1 对应地址 0”。对接时如果读不到预期线圈先把手册地址减 1 再试这是血泪经验里排第一位的检查项。# 构造功能码 01 请求帧读取从站 0x11 起始地址 0x0013 的 37 个线圈 import struct def build_read_coils(slave_addr, start_addr, count): frame struct.pack(BBHH, slave_addr, 0x01, start_addr, count) crc calc_crc16(frame) # CRC 计算见 2.4 节 return frame struct.pack(H, crc) # CRC 低字节在前 req build_read_coils(0x11, 0x0013, 37) # 期望输出: 11 01 00 13 00 25 crc_lo crc_hi上面代码里struct.pack(BBHH, ...)的大端格式对应 Modbus 报文的高字节在前规则而 CRC 用H小端打包因为 CRC 在报文中低字节先发。这个大小端混用是 Modbus 的固定规则不是笔误写代码时如果统一用大端打包 CRC从站会直接丢弃帧。2.2 读保持寄存器与输入寄存器03 和 04 的双字节拼接功能码 03 读保持寄存器、04 读输入寄存器请求帧结构和 01/02 相同但“寄存器数量”字段的含义从线圈个数变成了寄存器个数。资料例子[11][03][00][6B][00][03][CRC低][CRC高]表示读起始地址 0x006B 即 107 开始的 3 个保持寄存器。响应帧的关键差异在于每个寄存器占 2 字节返回字节数等于寄存器数量乘 2。例子中返回[11][03][06][02][2B][00][00][00][64][CRC低][CRC高]字节数 0x06 对应 3 个寄存器数据依次为 0x022B555、0x00000、0x0064100。资料特意点明“一个模拟量需要返回两个字节”这句话背后是无数人踩过的坑把两个字节当成两个独立寄存器解析结果数值全错。拼接时高字节在前、低字节在后即value (data[0] 8) | data[1]。但要注意有些设备厂商在寄存器内部又做了一次字交换比如把 32 位浮点数拆成两个寄存器时低字在前。这种情况协议本身不规定只能查设备手册。我一般会先用 03 读一个已知值的寄存器比如设备铭牌上的量程上限确认字节序后再批量解析。def parse_read_holding_response(resp): # resp 为完整响应帧含地址、功能码、字节数、数据、CRC byte_count resp[2] data resp[3:3 byte_count] values [] for i in range(0, byte_count, 2): values.append((data[i] 8) | data[i 1]) # 高字节在前 return values # 对资料例子解析应得到 [555, 0, 100]2.3 写单线圈与写单寄存器05 和 06 的“原样返回”约定功能码 05 写单个线圈请求帧里“下置数据”字段只有两个合法值FF 00表示闭合00 00表示断开其他值从站应视为非法。资料例子[11][05][00][AC][FF][00][CRC低][CRC高]就是把地址 0x00AC 的线圈置为闭合。功能码 06 写单个保持寄存器数据字段就是目标值例子[11][06][00][01][00][03][CRC低][CRC高]把 1 号寄存器写成 3。这两个功能码的响应规则很特别成功时从站原样返回请求帧失败时不响应。这意味着上位机不能靠“收到响应”判断成功还要比对响应帧和请求帧是否逐字节一致。实际项目里我见过从站返回了功能码但数据字段被改的情况虽然少见但校验时加上帧比对不亏。另外 05 和 06 一次只能操作一个对象要批量写必须用 15 和 16资料在功能码表里列了但没给实例需要自己按 01/03 的结构类推。2.4 CRC16 校验低字节在前的唯一例外资料在 01 功能码部分专门提醒“CRC 校验在命令中的高低字节的顺序和其他的相反”。Modbus RTU 的 CRC16 多项式为 0xA001反向初值 0xFFFF计算范围从帧头到 CRC 之前的所有字节。算出的 16 位结果在报文中低字节先发、高字节后发这和地址、数据字段的高字节在前规则相反。def calc_crc16(data: bytes) - int: crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc # 校验资料中 01 请求帧前 6 字节 frame bytes([0x11, 0x01, 0x00, 0x13, 0x00, 0x25]) crc calc_crc16(frame) print(fCRC低0x{crc 0xFF:02X} CRC高0x{crc 8:02X})这段代码可以直接抄进任何 Python 上位机项目。参数上唯一要改的是data的切片范围发送前对“帧头到数据末尾”算 CRC接收后对“帧头到 CRC 前”算 CRC 再和收到的两个字节比对。如果比对失败先检查是不是把 CRC 两个字节也算进去了这是最常见的翻车点。3. 从 PDF 到可运行代码搭一套最小验证环境3.1 用虚拟串口对跑通 03 功能码拿到资料后别急着接真实设备先用虚拟串口把请求响应链路跑通。Linux 下用socat创建一对虚拟串口Windows 下可以用 com0com原理一样一端发另一端收。下面以 Linux 为例开两个终端一个模拟主站发请求一个模拟从站回响应。# 终端 1创建虚拟串口对 socat -d -d pty,raw,echo0 pty,raw,echo0 # 输出会显示两个 /dev/pts/N比如 /dev/pts/3 和 /dev/pts/4# 终端 2主站侧向 /dev/pts/3 发 03 请求 import serial, struct from crc_util import calc_crc16 # 用 2.4 节的函数 ser serial.Serial(/dev/pts/3, 9600, timeout1) req_body struct.pack(BBHH, 0x11, 0x03, 0x006B, 0x0003) req req_body struct.pack(H, calc_crc16(req_body)) ser.write(req) print(sent:, req.hex()) resp ser.read(64) print(recv:, resp.hex())从站侧在/dev/pts/4上读请求、解析、拼响应。这一步的价值在于你能在不碰硬件的情况下确认自己的 CRC 算对了、字节序拼对了、响应解析逻辑没写反。等虚拟环境跑通再把串口名换成/dev/ttyUSB0接真实 485 转换器问题范围就缩小到接线和从站参数上了。3.2 串口参数与 485 方向控制Modbus RTU 的串口参数在资料里没展开但现场九成通信失败都出在这里。常见配置是 9600 波特率、8 数据位、无校验、1 停止位但这不是唯一答案必须和从站手册一致。校验位尤其容易错有些设备默认偶校验主站配成无校验后能发不能收因为从站回帧的校验位对不上。485 总线还需要方向控制。USB 转 485 模块一般自动切换收发方向但如果是自己画板子用 MAX485 这类芯片需要用一个 GPIO 控制 DE/RE 引脚发送前拉高发完等最后一个字节移出移位寄存器再拉低。拉早了下半帧被截断拉晚了总线冲突。我一般会在write()之后加一个按波特率估算的延时9600 下每字节约 1.04ms8 字节帧留 2ms 余量比较稳。提示虚拟串口测试通过不代表真实总线通过。接线时 A 接 A、B 接 B但不同厂商的 A/B 标法可能相反读不到就先把两根线对调试一次这是成本最低的排查动作。3.3 用资料里的实例帧做回归用例资料给了六个功能码的完整示例帧这些帧本身就是现成的测试用例。把每个请求帧的期望响应帧写成断言每次改完代码跑一遍能挡住大部分回归问题。比如 03 的例子输入11 03 00 6B 00 03加正确 CRC断言解析结果为[555, 0, 100]。01 的例子断言返回字节数为 5、第一个数据字节为 0xCD。这种用例不需要真实设备跑在 CI 里也很快。功能码请求关键字段期望响应要点01起始 0x0013数量 0x0025返回 5 字节首字节 0xCD03起始 0x006B数量 0x0003返回 6 字节值 555/0/10005地址 0x00AC数据 FF 00原样返回请求帧06地址 0x0001数据 00 03原样返回请求帧这张表可以直接转成参数化测试的数据源。注意 05 和 06 的断言是帧相等而不是字段相等因为“原样返回”是协议约定任何字节差异都值得警惕。4. 避坑与排查五条现场踩出来的记录4.1 现象读 03 返回异常码 0x83原因寄存器数量超限请求功能码 03 时从站回[地址][83][02][CRC]0x83 是 03 的最高位置 1 表示异常异常码 02 表示非法数据地址。常见原因是请求的起始地址加数量超出了从站支持的寄存器范围或者数量字段填了 0。资料里没写数量上限但多数从站限制单次读 125 个寄存器以内。解决方法是把数量降到 100 以内分段读并核对从站手册的寄存器映射表确认地址区间真实存在。4.2 现象数据能读但值明显偏大或偏小原因字节序或数据类型误判用 03 读一个温度寄存器返回01 F4按无符号解析是 500但实际温度是 5.00。这是典型的定点数缩放问题不是通信错误。另一种情况是 32 位数据跨两个寄存器高字低字顺序和预期相反读出来是个天文数字。解决方法是先确认寄存器数据类型16 位整型、32 位浮点、定点缩放再确认字序。我一般会写一个探测脚本把同一地址按几种组合都解析一遍和已知物理量比对。4.3 现象05 写线圈成功但设备没动作原因地址偏移或线圈类型不匹配05 请求原样返回了说明从站接受了帧但继电器没吸合。先查地址手册标的“线圈 1”在报文里可能是地址 0也可能是地址 1取决于厂商是否做了 1-based 偏移。再查类型有些点位是只读输入状态用 05 写虽然从站可能不报错但不会生效应该确认该点位是否支持写操作。解决方法是先用 01 读回该线圈状态确认写入前后的变化再对照手册确认可写属性。4.4 现象总线挂多台设备后通信时好时坏原因终端电阻与共地缺失单台设备调试正常挂上第三台后开始丢帧。485 总线是差分信号长距离或高波特率下需要在总线两端各加 120 欧终端电阻中间节点不加。另外所有设备的 GND 要共地否则共模电压漂移会导致误码。解决方法是先降波特率到 4800 验证是否改善再检查终端电阻和屏蔽线接地。这类问题没有软件层的后悔药只能从物理层查。4.5 现象CRC 校验偶尔失败但帧内容看起来正确原因帧间隔不足或半双工冲突从站回帧的 CRC 偶尔对不上但把收到的字节打印出来看内容又是对的。常见原因是主站发完请求后立刻切到接收但 485 芯片的方向切换有延时截掉了响应帧的头几个字节或者总线上有多个主站同时发送造成冲突。解决方法是发送后留足 3.5 个字符时间的静默间隔再切接收并确认总线上只有一个主站。用示波器看 A/B 线波形是最直接的确认手段。5. 把功能码表变成调试工具一个地址扫描脚本的写法资料里的功能码表最有价值的用法不是背而是驱动一个扫描脚本。现场拿到一台陌生从站不知道它支持哪些寄存器、哪些可读与其翻手册不如扫一遍。下面这个脚本按 03 功能码从地址 0 开始逐段读把有响应的地址区间和值打印出来遇到异常码就跳过。参数上把起始地址、结束地址、步长做成命令行参数步长默认 10因为大多数设备的寄存器是连续块状分布的。import serial, struct, sys from crc_util import calc_crc16 def read_holding(ser, slave, addr, count): body struct.pack(BBHH, slave, 0x03, addr, count) ser.write(body struct.pack(H, calc_crc16(body))) resp ser.read(5 count * 2) if len(resp) 5 or resp[1] 0x80: return None n resp[2] return [(resp[3i] 8) | resp[4i] for i in range(0, n, 2)] def scan(port, slave, start, end, step10): ser serial.Serial(port, 9600, timeout0.3) for addr in range(start, end, step): vals read_holding(ser, slave, addr, step) if vals: print(f0x{addr:04X}: {vals}) ser.close() if __name__ __main__: scan(sys.argv[1], int(sys.argv[2], 0), int(sys.argv[3], 0), int(sys.argv[4], 0))用法是python scan.py /dev/ttyUSB0 0x11 0 200从站地址 0x11扫 0 到 200。脚本里timeout0.3是关键参数太短会把慢响应的从站误判为无响应太长会让扫描变慢。扫出来的地址区间再对照资料里 03 的报文结构手工构造单次请求验证比盲猜快得多。这个脚本我只在实验室和授权设备上跑生产总线上乱扫可能触发从站的异常处理逻辑这一点要有数。从那以后我每次接手新从站都先用这个脚本扫一遍地址空间再拿资料里的实例帧做交叉验证最后才写业务代码。这套顺序帮我省掉了大量“以为是协议问题其实是地址问题”的来回。希望帮到你。本文还有配套的精品资源点击获取