先决条件:先凑齐「可复现」的四件事
Modbus RTU 的测试之所以经常变成「换了三根线还是不通」,是因为参数、接线、报文和判据四件事没有同时固定下来。一次能复现的测试,至少需要:一条确定的物理链路、一组主从完全一致的串口参数、一条能逐字节核对的请求报文,以及一套不依赖「感觉」的判据。下面四步按这个顺序展开,每一步都能独立验证。
| 准备项 | 具体内容 | 不确定时的后果 |
|---|---|---|
| 从站参数 | 从站地址、波特率、校验方式、数据位、停止位、寄存器地址表 | 参数不一致时表现为完全无响应,最容易被误判为硬件坏 |
| 物理链路 | 两线(或四线)双绞线、信号地、两端终端电阻 | 时通时不通,长线或高波特率下先出问题 |
| 主站工具 | 命令行主站(如开源的 mbpoll)、串口调试助手或自写脚本 | 看不到原始字节,只能看到「读失败」 |
| 从站替代品 | 开源仿真器(如 pymodbus 自带的仿真器)或一台已知正常的从站 | 无法判断问题在主站侧还是在现场设备侧 |
先把从站参数问清楚
Modbus 串行链路是主从结构:同一时刻总线上只有一个主站,通信永远由主站发起,从站之间不互相通信。地址 1–247 分配给从站,地址 0 是广播地址,248–255 保留。需要注意,247 是协议地址空间的上限;单段总线实际能挂多少设备还受收发器负载能力与线缆条件约束,工程上常见的规划是把一段总线控制在 32 个标准单位负载以内,超出时用中继器或低单位负载收发器。因此第一步不是接线,而是拿到从站设备说明里的这几项:地址、波特率、校验方式、数据位、停止位,以及寄存器表的地址范围与数据类型。
寄存器表要特别注意「地址口径」。协议规定 PDU 内的数据引用从 0 开始编号,而不少设备说明书按 1 开始标注(例如写「40001」表示第一个保持寄存器)。两种口径差 1,是现场「读到了值但明显不对」和「直接返回异常」的最常见原因之一。
第一步:接线与终端电阻
两线制 RS-485 是最常见的接法:一对双绞线走 D0/D1,信号地单独一根,总线按「手拉手」串下去,不要做成星形。规范对长度和支线有明确约束:在 9600 bit/s、线径 AWG26 或更粗的条件下,主干总长上限为 1000 m;支线必须很短,单根不超过 20 m;若使用多口分线器分出 n 根支线,每根支线的长度上限为 40 m 除以 n,等效于所有支线加起来的长度控制在 40 m 以内。四线制系统的主干长度上限按规范为两线制的一半,即在同样 9600 bit/s、AWG26 条件下约为 500 m。
终端电阻的取值容易被各家说法带偏。规范给出的做法是:在总线两端各接一个线路终端,取值可用 150 Ω/0.5 W;如果这对线还需要做失效保护偏置,则更推荐 120 Ω/0.25 W 串一只 1 nF、耐压 10 V 以上的电容。三个硬性约束:被动双绞线上最多只能有两个终端;终端绝不能接在支线上;RS-232 点对点连接不需要任何终端。
偏置(极化)电阻则是另一件东西:当总线空闲、没有设备驱动时,接收器可能停在不确定状态,需要把 D1 上拉到 5 V、D0 下拉到公共端,阻值取 450–650 Ω 之间。整条总线只在一点做偏置,通常放在主站或其分支处,其他设备不得重复实现。代价是:带偏置的总线,可挂设备数量要比不带偏置时少 4 个。信号地建议整条总线只在一点接保护地,避免形成地环路。
接线完成后先做一次静态检查:断电用万用表量 A/B 之间、A/地之间的电阻,确认没有短路;上电后分别量各节点的 D0–D1 电压差,空闲时应停在一个确定电平上。这一步花两分钟,能省掉后面大量「到底是不是线的问题」的争论。
第二步:统一串口参数与帧间隔
RTU 模式下每个字符固定 11 位:1 个起始位、8 个数据位(低位先发)、1 个校验位、1 个停止位,也就是常说的 8E1。规范要求默认使用偶校验,同时建议兼容无校验;若选择无校验,则改为 0 个校验位加 2 个停止位,总位数仍然是 11 位。实现类的默认参数是:偶校验、RTU 模式、两线 RS-485;基本类必须支持 9600 bit/s(19200 也推荐),而规范给出的默认波特率是 19200 bit/s,未实现 19200 时取 9600。主站和每一个从站必须完全一致,包括校验方式和停止位。
RTU 靠时间而不是靠分隔符切帧:帧与帧之间至少要有 3.5 个字符时间的静默(t3.5),同一帧内两个字符之间若出现超过 1.5 个字符时间的间隔,接收方会把这一帧判为不完整并丢弃。字符时间随波特率变化,所以在低波特率时这两个定时器必须严格按字符时间计算;当波特率高于 19200 bit/s 时,规范建议直接改用固定值:字符间超时 750 µs、帧间间隔 1.750 ms。
| 波特率(8E1,11 位/字符) | 单字符时间 | t1.5 | t3.5 |
|---|---|---|---|
| 9600 bit/s | 1.146 ms | 1.719 ms | 4.010 ms |
| 19200 bit/s | 0.573 ms | 0.859 ms | 2.005 ms |
| 高于 19200 bit/s | — | 固定 0.750 ms | 固定 1.750 ms |
表中前两行是按 11 位字符时间推算的定时值,用于理解字符时间随波特率缩短的关系;波特率一旦超过 19200 bit/s,规范就不再按字符时间推算,而是直接采用固定值这一行。
工程上还有两个参数要一起定:响应超时和轮询间隔。规范没有规定响应超时的具体数值,只说它由应用决定,并给出「9600 bit/s 下通常为 1 s 到数秒」的参考;广播后主站需要等待的周转延迟通常取 100–200 ms,且应短于响应超时。用 USB 转串口转换器时,还要留意芯片驱动的接收延迟(latency timer)设置,这类默认值会在高速率下放大帧内间隔,必要时调小。
第三步:构造并发送第一条请求报文
RTU 帧由四段组成:从站地址 1 字节、功能码 1 字节、数据 0–252 字节、CRC 2 字节,整帧最大 256 字节;对应地,串行链路上的 PDU 最大 253 字节。CRC 用 16 位寄存器实现,初值 0xFFFF,每移出一位为 1 时与多项式 0xA001 异或,算完后低字节先发、高字节后发。这一点与「寄存器数值按大端发送」正好相反,是最容易写错的地方。
读 10 个保持寄存器(从站 1、起始地址 0)的请求如下,最后一行的两个字节是算出来的 CRC:
| 字段 | 取值 | 说明 |
|---|---|---|
| 从站地址 | 01 | 单播请求,只有地址为 1 的从站应答 |
| 功能码 | 03 | 读保持寄存器 |
| 起始地址 | 00 00 | PDU 口径,从 0 开始 |
| 寄存器数量 | 00 0A | 十进制 10,上限 125 |
| CRC | C5 CD | 低字节在前 |
从站正常应答会回显功能码,并带一个字节的字节数(10 个寄存器为 0x14)和 20 字节数据;若只读 1 个寄存器且值为 100,应答就是 01 03 02 00 64 B9 AF——其中 00 64 是寄存器值 100,按大端发送,CRC 为 B9 AF。如果从站认为请求有问题,则把功能码最高位置 1,用「0x80 + 原功能码」作答,紧跟一个字节的异常码,例如 01 83 02 C0 F1 表示「非法数据地址」。
| 异常码 | 含义 | 排查方向 |
|---|---|---|
| 01 | 非法功能 | 从站不支持该功能码,或设备处于不允许该操作的状态 |
| 02 | 非法数据地址 | 起始地址加数量超出该从站实际存在的寄存器范围 |
| 03 | 非法数据值 | 数量超出功能码上限,或请求结构本身不合法 |
| 04 | 从站设备故障 | 从站执行动作时出现不可恢复错误 |
| 06 | 从站设备忙 | 从站正在处理长任务,稍后重试 |
各功能码单次能操作的条目数有硬上限:读线圈 01 与读离散输入 02 最多 2000 个;读保持寄存器 03 与读输入寄存器 04 最多 125 个;写单个线圈 05、写单个寄存器 06 只有 1 个;写多个线圈 15 最多 1968 个;写多个寄存器 16 最多 123 个。超过上限,从站回的就是异常码 03 而不是数据。
第四步:先用仿真从站对测,再接现场设备
把主站工具直接对现场设备,一旦不通就同时有两个未知量。更快的路径是先做一个仿真从站,把主站侧的参数、报文和解析链路验证干净,再换到真实设备上。
以 pymodbus 自带的仿真器为例,它用一个 JSON 文件描述服务器与设备,命令行通过 --json_file 指定配置,用 --modbus_server 和 --modbus_device 选择要加载的服务器与设备;把服务器的 comm 设为 serial、framer 设为 rtu,并给出 port、baudrate、parity、bytesize、stopbits,就得到一个走串口的 RTU 从站;再按所用版本的文档在设备条目里设定从站地址字段(新版文档中为 slave),即可让它只应答某一个地址。设备侧的 setup 定义寄存器规模,再用 uint16、bits 等段落定义各区块的取值范围(不同版本的字段命名有差异,以所用版本的文档为准)。
主站侧用开源的 mbpoll 就够。它的默认值本身就是一套可用的起点:地址 1、波特率 19200、偶校验、8 数据位、1 停止位、起始引用 1、超时 1.00 s。读 10 个保持寄存器的命令形如:
mbpoll -m rtu -a 1 -b 9600 -P even -d 8 -s 1 -r 1 -c 10 -t 4 -1 -v /dev/ttyUSB0
其中 -t 4 指保持寄存器(-t 3 是输入寄存器,-t 0 是线圈,-t 1 是离散输入),-1 表示只轮询一次,-v 打开调试输出以便看原始字节,-R/-F 用于让工具自己控制 RTS 方向脚(半双工两线时必需,否则方向切换要靠转换器硬件完成)。-r 的默认口径是「数据模型第 1 个」,要按 PDU 的 0 起口径寻址时加 -0。
这一轮通过后,把命令里的设备换成现场从站、把地址与参数改成说明书里的值,重跑同一条命令即可。参数、通道、寄存器地址这三项建议当场记进测试记录,后面每次复测都从这份记录出发。
验证方法:用四项证据代替「感觉通了」
「读到数了」不等于通信正确。至少凑齐下面四项证据,才能判定测试通过:
- 字节级比对:请求与应答逐字节对照——地址、功能码、字节数、CRC 是否正确。CRC 错、地址错、字节数不匹配,都属于协议层错误,而不是数值问题。
- 工具统计:主站工具在停止轮询后会给出统计,例如「发送 4 帧、接收 4 帧、0 错误、0.0% 丢帧」。丢帧率与错误计数比单次读数更有说服力,连续轮询几十次仍为 0,才算链路稳定。
- 写入后回读:用功能码 06 写入一个特征值(例如把某个保持寄存器写成 100),再用 03 读回同一地址,值一致说明「读—写—回读」这条路径都通。这一步能发现「只有部分寄存器可写」的现场问题。
- 长时观察:让轮询连续跑 10–30 分钟,观察超时与错误计数是否增长。Modbus RTU 在从站检测到校验错误或帧错误时不会回任何东西,主站只会看到超时;因此「偶发超时」几乎总是指向电气层或参数层,而不是应用层。
把三类典型结果直接对应到下一步动作,比逐项猜要快:
| 观察到的现象 | 最可能的原因 | 下一步 |
|---|---|---|
| 完全没有应答,主站超时 | 地址不符、A/B 接反、参数(校验/停止位)不一致、从站未上电 | 先用仿真从站确认主站参数,再核对现场设备说明书 |
| 有应答但是 0x80 类异常帧 | 功能码不被支持、地址越界、数量超限 | 按异常码定位:01 查功能码,02 查地址范围,03 查数量 |
| 时通时断、偶发超时 | 终端电阻缺失或多于两个、支线过长、地环路、屏蔽层处理不当 | 按第一步的电气约束逐项复核,先量电阻再动线 |
| 数据能读但值明显不对 | 地址口径差 1、数据类型(16 位/32 位、有无符号)理解错、字序不同 | 用 06 写入特征值后回读,确认口径后再谈数值换算 |
常见坑:避开这九个,多数测试一次就能过
| 坑 | 表现 | 处理 |
|---|---|---|
| 地址口径差 1 | 读到相邻寄存器的值,或直接回异常码 02 | 确认说明书是 PDU 0 起还是数据模型 1 起;命令行工具按需加 0 起开关 |
| CRC 字节序写反 | 从站完全不回,主站超时 | CRC 低字节先发;寄存器数值才是大端 |
| 校验/停止位不一致 | 无响应或大量帧错误 | 统一为偶校验 8E1;选无校验时必须用 2 个停止位 |
| A/B 接反 | 万用表看有电平、通信完全不通 | 断电对调 D0/D1 再试;不同厂家标注并不统一,以实测为准 |
| 终端电阻缺失或过多 | 短线正常、加长后丢帧 | 只在总线两端各一个,绝不能接在支线;被动线上不超过两个 |
| 偏置重复实现 | 挂的设备越多越不稳定 | 整条总线只在一点偏置;带偏置时设备数量少 4 个 |
| 地址重复或多主站 | 应答混杂、偶发冲突 | 逐台断开法定位重复地址;同一总线只保留一个主站 |
| 把广播当单播用 | 发出去没有应答 | 地址 0 的广播只用于写命令,且从站一律不应答 |
| 半双工方向不切换 | 请求发得出去、收不回来 | 确认转换器自动切换方向,或由主站工具控制 RTS 方向脚 |
还有一类坑不在报文里,而在时序里:高波特率下用默认的接收延迟设置,会让帧内字符间隔被拉长到超过 t1.5,接收方按「帧不完整」丢弃。遇到「低波特率正常、提高波特率就丢帧」,优先调整转换器的接收延迟,而不是继续加大重试次数。
小结:把参数、报文、统计留下来
一次合格的 Modbus RTU 主从测试,最终留下的应该是一份可复现的记录:串口参数(波特率、校验、数据位、停止位)、物理参数(终端电阻位置与阻值、是否偏置、接地方式)、一条能对上号的特征报文(含 CRC),以及一段轮询统计。下次现场再出问题,用同一份记录重跑,就能把「设备坏了」和「参数或接线变了」区分开——这比任何一次性的「通/不通」结论都有价值。