资讯动态

Modbus功能码与CRC16实战:RS-485调试、批量读写与异常排查

发布时间:2026/10/27 4:53:40 来源:尧图企业网站定制
简介这份PDF面向工业自动化、嵌入式开发与PLC通信的初学者及现场调试人员围绕Modbus功能码的含义与报文实例展开帮助读者解决功能码混用、请求帧构造、从站地址设置和响应解析等常见问题。文档共1个PDF文件约10KB体量轻便适合在设备联调时随查随用。内容系统梳理0106等常用功能码结合线圈、输入状态、保持寄存器与输入寄存器的读写场景逐项给出设备地址、功能码、起始地址、寄存器数量及CRC校验的报文示例并说明返回字节个数、数据位与模拟量高低字节的解析方式同时兼及15、16号批量写入、17号报告从机标识以及0712号异常状态、诊断校验和通信事件记录等功能便于建立完整的报文认知。已有2329人学习适合希望快速掌握Modbus数字量与模拟量读写、提升PLC与仪表联调效率的读者参考。1. 手边那份 Modbus 功能码 PDF先把八条常用码吃透调试 485 设备最耗时间的环节往往不是写代码而是对着一串十六进制猜对面到底回了什么。手边这份《Modbus通讯功能码及实例.pdf》解决的正是这个问题它把 Modbus 功能码从 01 排到 21把保留区间也交代清楚再用 01、02、03、04、05、06 六个最常用的码给出完整的请求帧和响应帧逐字节解释。它不讨论协议是怎么演化来的只告诉你第 5 个字节该填什么、CRC 为什么低字节在前、返回的 5 个数据字节怎么拆成 37 个开关量。适合两类人刚接手 485 总线、连起始地址填 0 还是 1 都要犹豫半天的新手以及写了半年采集程序、仍然靠抓包工具反推报文的老手。后者翻它主要是为了核对 15、16 这两个批量写功能码的字段长度以及异常应答里那个 0x80 到底怎么来的。2. 四类数据区与 0106 功能码请求帧和响应帧逐字节拆拿到一份功能码表绝大多数人第一反应是背。背不下来也不用慌真正需要刻进脑子的是「功能码和它操作的数据区是一一绑定的」这件事。PDF 开篇那张表列了二十多条落到日常采集和控制的其实只有前六条加上批量写的 15、16。这一章按数据区把这六条拆开每一段都对着 PDF 里的示例帧核一遍长度长度对不上后面的解析全是白费。2.1 四类数据区先分清为什么 01 和 03 不能混用Modbus 把从站里的数据分成四个互相独立的区每个区有自己的地址空间都从 0 开始编号。线圈是可读可写的单 bit物理上一般对应继电器输出离散输入是只读的单 bit对应限位、按钮这类干接点输入寄存器是只读的 16 位字通常是传感器测量值保持寄存器是可读可写的 16 位字放设定值和累计量。关键点在「互相独立」四个字地址 0 在线圈区、离散输入区、输入寄存器区、保持寄存器区里是四个完全不同的东西。设备手册写「40001 是设定值」你发 01 功能码去读 0 号地址读回来的是继电器状态不是设定值。同样地用 03 去读一个只读的输入寄存器地址运气好返回异常码 02运气不好返回一堆看起来合理但其实错位的数这种错最难查。所以选功能码的顺序永远是先确定这个量在哪个区再选功能码最后才去查地址。PDF 那张表把 01 和 02、03 和 04 并排放就是这个意思——它们的报文结构完全一样只有第二个字节不同。2.2 01/02 读位位序、字节数与补齐规则读线圈的请求固定 8 字节结构如下。字节位置内容示例PDF 帧说明0从站地址0x11十进制 17 号站1功能码0x01读线圈固定 012-3起始地址0x0013十进制 190 基4-5读取数量0x0025十进制 37 个线圈6-7CRC低字节在前从字节 0 算到字节 5响应侧多一个「字节数」字段后面跟着数据最后两字节 CRC。数据部分按位打包第一个数据字节的 bit0 对应起始地址那一位bit1 对应起始地址加 1依次往后跨字节继续。PDF 例子里返回 5 个数据字节CD 6B B2 0E 1B第一个字节 0xCD 展开成二进制1100 1101最低位为 1 表示起始地址 19 这一路闭合次低位为 0 表示 20 断开再往上是 1、1、0、0、1、1正好对上 PDF 文字里描述的 20 号闭合、21 号断开那串状态。字节数的算法是ceil(数量 / 8)。37 个线圈算出来是 4.625向上取整得 5 字节响应帧总长就是 11152 10 字节。PDF 给的示例帧去掉 CRC 是 8 字节加上 2 字节正好 10长度自洽。如果数量不是 8 的整数倍最后一个字节的高位没有实际意义规范要求填 0——PDF 里 0x1B 是0001 1011最高三位确实是 0这条不是废话有些从站会在这几位塞随机值解析时务必只取有效的低若干位。单次读取上限 2000 个线圈。超过这个数规范的从站会直接回异常码 03不规范的从站可能截断返回帧长对不上你还在猜是不是干扰。2.3 03/04 读字数量字段与每个寄存器两字节读保持寄存器的请求结构和 01 一模一样只把功能码换成 03。区别在响应每个寄存器占两个字节高字节在前所以数据字节数恒等于数量 × 2一定是个偶数。PDF 的例子请求是11 03 00 6B 00 03起始地址 0x006B 即 107读 3 个寄存器响应是11 03 06 02 2B 00 00 00 64字节数字段为 06正好是 3×2。三个值分别是 0x022B、0x0000、0x0064换算成十进制是 555、0、100。响应帧总长 11 字节和「11162」对得上。这里有个高频误判设备的说明书写的地址是 40001、40002而报文里要填 0、1。换算规则后面 3.3 节单独讲先记住一条——PDF 里所有示例的起始地址都是协议地址0 基不是 PLC 地址。04 功能码读输入寄存器报文结构与 03 完全一致只换功能码。很多仪表把测量值放在输入寄存器设定值放在保持寄存器两个区地址都从 0 编读错区就会得到一组恒定不变或者明显离谱的数。判断方法很简单你改一次设定值读数跟着变的那个才是保持寄存器。单次读取上限 125 个寄存器。这个数字不是随便定的响应帧最大 256 字节扣掉地址、功能码、字节数、CRC 一共 5 字节剩下 251 字节取偶数就是 250除 2 得 125。写代码时把它做成常量比事后看异常码省事。2.4 05/06 写单点回显本身就是确认写单个线圈用 05数据域只有两个合法值FF 00表示闭合00 00表示断开其他任何组合都是非法值规范的从站应当返回异常码 03。PDF 例子里11 05 00 AC FF 00就是把地址 0x00AC 那一路闭合。写单个保持寄存器用 06数据域是任意 16 位值PDF 例子11 06 00 01 00 03把 1 号寄存器写成 3。这两条码的响应规则要特别记住成功时从站把请求帧原样回显 8 个字节失败时不响应或者回异常帧。没有「OK」这种字段。很多新手写完就往下走不看回显结果设备因为地址越界静默丢弃了请求程序以为写成功第二天现场发现参数根本没生效。正确做法是把回显的 8 个字节和发出的 8 个字节做逐字节比较任何一个字节不同都当失败处理。还有一个边界写单个线圈一条命令只能改一路要连续改 8 路就得发 8 帧。这时候换 15 功能码更划算相关内容在第 4 章。2.5 从示例反推设备地址、起始地址、数量的三个坑把 PDF 的示例帧当成练习题来读能提前暴露三个典型的理解偏差。第一是设备地址。示例里 0x11 是十七进制写的 17不是十进制的 11。地址范围 1 到 2470 是广播地址写操作可以用广播但从站不会回应读操作基本别碰。地址 248 到 255 保留。现场最常见的翻车是把拨码开关按二进制拨出来的 17 当成十进制 17 直接填进代码其实拨码多半是二进制加权拨出来 0x11 就是十进制 17直接填 17 即可。第二是起始地址。示例的 0x0013 是十进制的 19PDF 特意注明「起始地址为 0」意思是第一路开关的地址是 0 而不是 1。如果设备手册写「第 20 路」报文里就该填 19。这个减一是新手最容易漏的一步。第三是数量字段的含义。01 里这个字段是线圈个数03 里是寄存器个数单位完全不同。别把「读 37 个线圈」的 37 直接套到 03 上那会读回 74 个字节。3. CRC16 与地址偏移把示例帧变成能真正发出去的帧PDF 里的示例帧大多把 CRC 两个字节写成占位符这很合理——CRC 是算出来的不是查出来的。但从「看懂帧」到「发出帧」中间隔着两件事一是把 CRC 算对并且按低字节在前的顺序发出去二是把设备手册上的 4 万字头地址换算成报文里的 0 基地址。这两步任何一步出错现象都是一样的设备不响应或者回异常码。所以别急着写业务逻辑先拿一个已知正确的帧做自检。3.1 CRC16/Modbus 的四个参数与发送顺序Modbus RTU 用的 CRC 是 CRC-16/IBM 的一个变种参数固定宽度 16 位多项式 0x8005反向表示为 0xA001初值 0xFFFF输入和输出都反射结果不异或。算法从帧的第一个字节从站地址开始一直算到最后一个数据字节CRC 本身不参与计算。比参数更容易错的是发送顺序算出来的 16 位结果低字节先发高字节后发。而帧里其他所有多字节字段起始地址、寄存器数量、寄存器数值都是高字节在前。同一帧里两种字节序并存这是 Modbus 最容易被吐槽的设计之一也确实是新手翻车的重灾区。参数项取值常见错误多项式0xA001反向误用 0x1021CRC-CCITT初值0xFFFF误用 0x0000结果异或0x0000误加 0xFFFF发送顺序低字节在前按大端发从站全静默3.2 用代码算 CRC 并拼出一帧 03 请求先把 CRC 函数写死再拼帧最后拿一个已知答案回验。下面这段可以直接跑输出里01 03 00 00 00 0a c5 cd是业界流传很广的一个自检样例算出来对得上说明你的实现没问题。def crc16_modbus(data: bytes) - int: 标准 Modbus RTU CRC16返回 16 位整数 crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 0x0001: # 最低位为 1 才异或多项式 crc (crc 1) ^ 0xA001 else: crc 1 return crc frame bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x0A]) # 读 1 号站 0 起 10 个保持寄存器 crc crc16_modbus(frame) print(hex(crc)) # 0xcdc5 print((frame crc.to_bytes(2, little)).hex( )) # 01 03 00 00 00 0a c5 cdcrc.to_bytes(2, little)这一句就是「低字节在前」的落地别写成big。函数内部的0xA001是0x8005的位反转形式配合右移实现比正向实现少一次位反转速度也够用——9600 波特率下一帧才几十字节Python 跑这个循环毫无压力。拼完整请求时把起始地址和数量按大端打包再整体算 CRCimport struct def build(addr: int, func: int, payload: bytes) - bytes: body bytes([addr, func]) payload # 地址 功能码 数据域 return body crc16_modbus(body).to_bytes(2, little) # 对应 PDF 里读 3 个保持寄存器那一帧17 号站起始 107数量 3 req build(0x11, 0x03, struct.pack(HH, 107, 3)) print(req.hex( )) # 11 03 00 6b 00 03 xx xxstruct.pack(HH, ...)里的是大端两个H是两个无符号 16 位正好对应起始地址和数量两个字段。参数改起来很直观换站号改addr换功能码改func读寄存器就把payload换成struct.pack(HH, 起始, 数量)写单寄存器就换成struct.pack(HH, 地址, 数值)。注意 05 功能码的数值域必须手动写成b\xff\x00或b\x00\x00不能用struct.pack随便塞。3.3 0 基地址与 1 基地址40001 到底对应谁设备手册几乎不会直接写协议地址而是写 40001、30001、00001、10001 这种带区号的编号。换算规则是去掉最高位数字剩下的减 1。手册地址段数据区可用功能码报文里填的协议地址0000109999线圈01 / 05 / 15编号减 11000119999离散输入02编号减 100013000139999输入寄存器04编号减 300014000149999保持寄存器03 / 06 / 16编号减 40001所以「40001 是设定值」意味着报文里起始地址填 0。有些设备厂商偷懒手册直接写协议地址这时候就不能再减。判断办法是看地址范围出现 40001 这种五位数一律按带区号处理如果手册写的是 0 到 200 这种小数字多半已经是协议地址。现场还有一个变种部分设备手册写的是「寄存器编号 1」但实际协议地址是 1 不是 0。这种情况只能试——先按减 1 发一帧读回全 0 或者异常码 02再按不减发一帧。把两种可能都写进调试脚本比在文档里找依据快。3.4 串口参数与帧间隔9600 8N1 下的时间预算RTU 帧的结束不靠结束符靠静默时间。规范要求帧内字符间隔不超过 1.5 个字符时间帧与帧之间至少 3.5 个字符时间。9600 波特率、8 数据位、无校验、1 停止位8N1下一个字符占 10 位约 1.04ms3.5 个字符就是 3.6ms 左右。这个数在 Windows 上根本不是你能精确控制的所以实际做法是保守一点帧间隔给 5 到 10ms在高负载下也别低于 3ms。参数常用值说明波特率9600 / 19200 / 38400长距离优先低波特率数据位 / 校验 / 停止位8 / N / 1少数设备用 8E1必须对齐帧内字符间隔≤1.5 字符由串口硬件决定一般不用管帧间间隔510ms半双工切换要留时间响应超时3001000ms从站忙时链路层可长达数秒还有个常被忽略的点485 是半双工发送完最后一字节到切换成接收中间有硬件延时。USB 转 485 的转换器延时从几百微秒到几毫秒不等便宜的转换器能到 10ms 以上。如果收不到响应先把帧间隔和收发切换延时都调大试试别一上来就怀疑设备坏了。4. 批量读写与异常应答15/16 功能码和 0x80 的处理单点读写跑通之后真实项目马上会撞上批量场景一次要下发 16 路输出、一次要写 20 个参数。逐条发 05/06 当然能work但一轮下来几百毫秒就没了碰上轮询周期紧的场合直接超时。15 和 16 就是为这个准备的代价是字段变多、长度换算容易错。这一章把两个批量码的字段拆清楚再把异常应答的识别方法补上——不会读异常码前面所有的排查都只能靠猜。4.1 15 写多线圈字节数怎么算、不足 8 位怎么补写多个线圈的请求比 05 多三个字段结构是从站地址、功能码 0x0F、起始地址高低、线圈数量高低、字节数、数据、CRC。字段长度说明起始地址2第一路线圈的协议地址线圈数量2要写多少路最大 1968字节数1(数量 7) // 8数据字节数按位打包LSB 对应起始地址举例从地址 0 开始写 10 路线圈状态依次是 1、0、1、1、0、0、1、0、1、1那么字节数 (107)//8 2第一个字节是0100 1101即 0x4Dbit0 到 bit7 分别对应第 0 到第 7 路第二个字节是0000 0011即 0x03bit0、bit1 对应第 8、9 路高位补 0。请求帧完整写法是11 0F 00 00 00 0A 02 4D 03 CRC低 CRC高。从站成功时的响应只回显前 6 个字节加 CRC共 8 字节不含数据域11 0F 00 00 00 0A CRC低 CRC高。这里有两个易错点。一是字节数字段很容易算错尤其数量是 8 的整数倍时(87)//8 1看起来像是少算了一个字节其实是对的。二是最后一个字节的高位必须补 0否则从站可能把多出来的位当成有效数据把不该动的线圈给动了。我一般会在代码里显式做一次掩码data[-1] (1 (n % 8)) - 1当n % 8为 0 时这条语句要跳过否则会全清零。4.2 16 写多寄存器数量×2 的字节数与 123 的上限写多个保持寄存器的结构地址、功能码 0x10、起始地址高低、寄存器数量高低、字节数、数据、CRC。字节数恒等于数量 × 2数据区每个寄存器还是高字节在前。单次最多写 123 个寄存器。这个上限同样来自 256 字节的帧长限制扣掉 9 字节固定开销剩 247除以 2 取整就是 123。超了会收到异常码 03。操作功能码单次上限上限来源读线圈012000响应位打包后的字节数读离散输入022000同上读保持寄存器03125响应 250 数据字节读输入寄存器04125同上写多线圈151968请求 246 数据字节 × 8写多寄存器16123请求 246 数据字节 ÷ 2写多寄存器的响应也是回显前 6 字节11 10 起始高 起始低 数量高 数量低 CRC低 CRC高。很多人以为会回显数据等不到就以为失败其实回显里根本没有数据域。4.3 异常应答的识别功能码最高位置 1从站处理不了请求时会返回一个异常帧长度固定 5 字节从站地址、功能码、异常码、CRC 两字节。识别方法只有一个——功能码的最高位是 1也就是「原功能码 0x80」。看到第 2 个字节 ≥ 0x80就该去查第 3 个字节。异常码含义多数情况下的真实原因01非法功能设备不支持该功能码或该区只读被当可写02非法数据地址起始地址越界或数量超出设备实际范围03非法数据值数量超上限05 的数值域不是 FF00/000004从站设备故障设备内部异常需要断电重启或查设备日志05确认设备已接受但需要较长时间处理06从站设备忙上一条命令还没处理完需要加大间隔实测里 02 和 03 占绝大多数。02 基本就是地址换算错了或者设备实际只有几十个寄存器你读了第 200 个03 基本就是数量超限或者 05 写了一个非法状态值。代码里不要用「响应长度对不对」来判断成败要用功能码。异常帧也是合法帧长度也对CRC 也对只有功能码变了。4.4 分片与轮询节奏一次读多少寄存器不超时上限是规范给的实际能用多少取决于从站的实现质量。有些小设备响应帧能拼到 256 字节但内部处理要 200ms有些设备一次读 100 个寄存器就开始丢帧。所以分片大小做成可配置项默认给保守值。我的默认配置读保持寄存器一次 32 个读线圈一次 64 个写多寄存器一次 16 个。这个量级下响应帧长度在 100 字节以内大多数从站都能在 50ms 内处理完。轮询间隔按「响应超时 × 2 10ms」给别把间隔压到极限485 总线上多个站点的轮询最容易在间隔过短时丢帧。分片还要注意边界起始地址 0 读 100 个寄存器分成 031、3263、6495、9699 四片最后一片只有 4 个数量字段填 4字节数字段填 8别偷懒填成 32。5. 常见翻车点排查报文发对了却读不到数的五种现场帧算对了、CRC 也对、串口助手回显正常设备就是不动。这种场景我遇到过不止一次每回都要从物理层往上重查一遍。下面五条是按现场出现频率排的每条都按「看到什么现象、根子在哪、怎么处理」写遇到问题按顺序对照就行。5.1 现象串口助手有回显设备毫无反应现象用串口调试助手发帧助手自己不回显设备也不回但示波器或者转换器上的收发指示灯在闪。原因八成是把 485 的 A、B 两根线接反了。RS-485 的 A 是正端、B 是负端但不同厂商的标记习惯不一样有的标 A/B有的标 /−有的标 D/D−。接反之后差分信号极性颠倒接收端解出来的全是乱码CRC 必然不对从站直接丢弃。另一种可能是总线两端没有 120Ω 终端电阻长线反射导致误码。解决先断电把 A、B 对调试一帧这一步零成本。然后检查终端电阻总线长度超过 50 米或者波特率高于 19200 时两端各接一个 120Ω中间节点不要接。再量一下总线静态电压A-B 之间应该在 200mV 以上才算偏置正常。5.2 现象数值恒为 0 或整体翻倍现象读回来的数据一直是 0或者读出来 250 实际是 500所有值都差一个比例。原因两个方向。恒定 0 多数是地址换算错了40001 你填了 40001 而不是 0从站越界后有些实现会返回全 0 而不是异常码 02。整体翻倍或者差 10 倍的是设备用整数表示小数手册里写了「单位 0.1」读回 250 实际是 25.0你没做换算。解决先确认手册地址是协议地址还是 PLC 地址按 3.3 节的表换算一遍。再做单点读取验证只读 1 个寄存器手动改一次设备上的值看读数跟不跟着变跟着变说明地址对不跟着变就是地址错区。最后翻手册的「数据格式」一节找比例因子和单位把它写进代码的解析函数里别在业务层临时乘。5.3 现象偶发 CRC 错误重发就好现象大部分帧正常每隔几十帧出现一次 CRC 错或者无响应重发一次就成功。原因帧间隔不够。半双工的 485 在发送转接收的时候有硬件延时上一站的应答还没完全发完主机就开始发下一帧或者上一帧的最后一个字节被下一帧的起始位吃掉。转换器质量差的话这个延时能到 10ms。解决把帧间间隔从 3.5 字符时间放大到 10 到 20ms收发切换的延时单独加。轮询周期本来给 20ms 的加到 50ms 试。另外检查是不是多个站点的地址有重复重复地址也会造成应答冲突现象和干扰很像。5.4 现象写完立刻读回是旧值现象用 06 写了一个寄存器回显正确马上用 03 读回得到的还是旧值过 100ms 再读就是新值了。原因从站在接收到写命令之后需要把值落到 EEPROM 或者内部逻辑里这个过程要几毫秒到几十毫秒。回显只表示「命令收到了」不表示「值已经生效」。写后立即读读到的可能是处理前的缓存。解决写完之后固定延时再读延时按设备手册给的处理时间没写就给 50ms 起步。更稳的做法是写后读改写成「轮询直到读回值等于写入值或超时 500ms」别用固定延时硬扛。频繁写 EEPROM 还有寿命问题能放到 RAM 的参数就别每轮都写。5.5 现象一次读 100 个寄存器直接回异常码 03现象读 10 个寄存器正常改成 100 个就返回功能码 0x83、异常码 03。原因数量超了设备支持的上限。规范允许 125 个但设备内部的寄存器可能只有几十个或者固件把响应缓冲做得很小超出就报非法数据值。这里报 03 而不是 02是因为地址本身合法是数量不合法。解决把读取数量做成二分探测先按 32 发成功再试 64、96第一次失败就退回上一个成功值。探测结果记到设备配置文件里别每次启动都试一遍。顺带检查起始地址加数量有没有越界越界时报的是 02两个码别混。6. 把功能码表做成回归清单每次上线前跑一遍PDF 那张表的价值不在于背而在于它可以被翻译成一份可执行的冒烟用例。我的习惯是每接一个新设备先照着下面这张清单跑一轮全过了再接业务逻辑。清单里的地址和数量都是占位按设备手册改。编号动作期望结果主要验证点101 读 8 个线圈回 1 字节数据位序、字节数202 读 8 个离散输入回 1 字节数据区选对没303 读 1 个保持寄存器回 2 字节数据地址换算404 读 1 个输入寄存器回 2 字节数据区选对没505 写单线圈再读回回显 8 字节读回一致回显校验606 写单寄存器再读回回显 8 字节读回一致写后延时715 写 10 路线圈回显 8 字节逐位读回字节数与补齐816 写 4 个寄存器回显 8 字节逐个读回数量×2903 读 125 个寄存器正常或异常码 03上限探测10发一个错误地址异常码 02异常识别11发一个错误功能码异常码 010x80 识别12连续发 200 帧零 CRC 错帧间隔与稳定性第 12 条最容易被跳过但它恰恰是最有价值的一条。前面十一条验证的是「协议对不对」第十二条验证的是「链路稳不稳」。很多现场问题不是协议写错了是时序在特定负载下崩了短时间测不出来跑一晚上就能暴露。写这套用例的时候我习惯把每帧的原始十六进制和解析结果一起打到日志里格式是「时间戳 方向 原始帧 解析值」。出问题的时候直接拿日志里那一帧去比对 PDF 的示例比在现场重新抓包快得多。还有一个可以省很多事的技巧把 CRC 校验做成双向的——既检查应答帧的 CRC也在发送前用同一个函数回验一遍自己拼的帧。有些拼帧函数的 bug 只在特定数量下触发比如字节数为 0 的时候回验一次就能当场发现。从那以后我每次接新设备前两小时一定先跑完这张清单再接业务代码宁可慢半天也不等到现场联调时才发现地址减了两次 1。希望这份清单和上面几节的拆帧方法能帮你在下一次 485 调试里少走两趟现场。功能码表就那几页真正值钱的是把它变成自己的验证流程。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑