通信协议基础:Modbus RTU/TCP协议详解

各位同学,咱们今天聊点实在的。做储能设备远程监控,通信协议是绕不开的坎。我最早接触Modbus是在一个光伏电站项目里,当时设备死活连不上,折腾了两天,最后发现是寄存器地址写错了。嗯,从那以后,我对协议细节再也不敢马虎。

说白了,Modbus就是工业设备之间的“普通话”。不管你是PLC、逆变器还是BMS(电池管理系统),大家约定好格式,数据就能流通。今天咱们重点拆解Modbus RTU和Modbus TCP,把数据帧结构、寄存器映射、CRC校验这些硬骨头啃下来。

核心要点:Modbus是应用层协议,RTU走串口(RS-232/485),TCP走以太网。底层不同,但数据模型完全一样。

一、Modbus RTU vs Modbus TCP:到底差在哪?

我经常被问到:“老师,RTU和TCP选哪个?”我的回答是:看现场环境。RTU适合长距离、抗干扰要求高的场景,比如储能站里电表、温控器这些老设备。TCP则适合局域网内高速通信,比如你用一个上位机同时采集几十台设备的数据。

咱们直接看对比表,一目了然:

特性 Modbus RTU Modbus TCP
物理层 RS-232 / RS-485 以太网(RJ45)
传输速率 9600~115200 bps 10/100 Mbps
最大节点数 32(RS-485) 理论上无限制
数据帧格式 地址+功能码+数据+CRC MBAP头+功能码+数据
校验方式 CRC-16(必须) TCP/IP协议栈保证
典型应用 电表、温控器、老旧PLC 上位机、网关、新式控制器

你想想看,RTU多了一个CRC校验,因为串口通信容易受干扰。TCP呢,底层有TCP/IP协议栈帮你保证数据完整性,所以MBAP头里没有CRC字段。这个区别,很多新手会忽略。

二、数据帧结构:手把手拆解

咱们先看RTU帧。我习惯把它分成四段:

| 地址码(1字节) | 功能码(1字节) | 数据区(N字节) | CRC校验(2字节) |

举个例子,你要读取储能逆变器的直流电压(寄存器地址0x3100):

发送:01 03 31 00 00 01 84 0A
解析:
01    → 设备地址(逆变器)
03    → 功能码(读保持寄存器)
31 00 → 起始寄存器地址(高字节在前)
00 01 → 读取1个寄存器
84 0A → CRC校验(低字节在前)

收到响应:

01 03 02 0B B8 7C 8D
解析:
01    → 设备地址
03    → 功能码
02    → 数据字节数(2字节)
0B B8 → 寄存器值(0x0BB8 = 3000,表示300.0V)
7C 8D → CRC校验

我的小技巧:调试时先用串口助手抓包,看CRC对不对。我曾经遇到一个设备,CRC计算方式是大端模式,结果我按小端算,死活对不上。后来翻手册才发现,有些厂家会魔改CRC顺序。

再来看TCP帧。它比RTU多了个MBAP头:

| 事务ID(2字节) | 协议ID(2字节) | 长度(2字节) | 单元ID(1字节) | 功能码(1字节) | 数据区(N字节) |

事务ID用于匹配请求和响应,协议ID固定为0x0000。长度字段表示从单元ID开始到帧尾的字节数。单元ID其实就是RTU里的设备地址,为了兼容而保留。

同样的读电压操作,TCP帧长这样:

00 01 00 00 00 06 01 03 31 00 00 01
解析:
00 01 → 事务ID
00 00 → 协议ID
00 06 → 后面还有6字节
01    → 单元ID
03    → 功能码
31 00 → 起始地址
00 01 → 读取数量

看到了吗?TCP帧里没有CRC,因为TCP/IP协议栈已经做了校验。但代价是帧头多了7个字节,传输效率略低。

二、寄存器映射表设计:储能系统的“地图”

寄存器映射表,说白了就是一张“地址-含义”对照表。没有它,你读回来的数据就是一堆乱码。我在设计储能系统时,通常把寄存器分成四类:

  • 线圈(Coil):可读可写,1位。用于控制继电器、开关。功能码01/05/15。
  • 离散输入(Discrete Input):只读,1位。用于读取状态,比如断路器分合。功能码02。
  • 输入寄存器(Input Register):只读,16位。用于读取模拟量,比如温度、电压。功能码04。
  • 保持寄存器(Holding Register):可读可写,16位。用于设置参数,比如充放电功率。功能码03/06/16。

下面是我给一个储能BMS设计的寄存器映射表示例:

寄存器地址 数据类型 读写属性 描述 单位/范围
0x0000 Uint16 只读 电池组总电压 0.1V,如3000=300.0V
0x0001 Uint16 只读 电池组总电流 0.1A,如500=50.0A
0x0002 Uint16 只读 SOC(荷电状态) 0~1000,对应0.0%~100.0%
0x0010 Uint16 读写 充放电功率设定值 1W,如5000=5000W
0x0011 Uint16 读写 运行模式 0=待机,1=充电,2=放电
0x0020 Uint16 只读 最高单体电压 0.001V,如4200=4.200V
0x0021 Uint16 只读 最低单体电压 0.001V,如4100=4.100V
0x0030 Uint16 只读 最高温度 0.1℃,如350=35.0℃
0x0031 Uint16 只读 最低温度 0.1℃,如280=28.0℃

注意:寄存器地址通常从0开始,但有些设备手册会写“40001”这种PLC地址。那是Modicon的遗留习惯,实际通信时要把40001减去40001得到0x0000。别搞混了,我见过有人因为这个错位,读回来的数据全是0。

三、CRC校验原理:别被它吓到

CRC(循环冗余校验)是RTU协议的“守护神”。它的原理不复杂,就是多项式除法。咱们用标准CRC-16(多项式0x8005)来算。

我直接给一个C语言实现,你在单片机上也能跑:

uint16_t crc16_modbus(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc = crc >> 1;
            }
        }
    }
    return crc;
}

注意,Modbus的CRC是低字节在前。比如计算结果是0x0A84,发送时先发0x84,再发0x0A。这个顺序搞反了,设备就不认你。

避坑指南:我曾经在一个项目中,用上位机给设备写参数,CRC算对了,但设备就是不响应。后来发现,设备要求CRC高字节在前。嗯,有些非标设备会改CRC顺序。所以拿到新设备,第一件事就是看手册里的CRC示例。

四、知识体系总览

为了让你对本章内容有个整体印象,我画了一张图。它把Modbus RTU/TCP的核心知识点串起来了:

Modbus通信协议知识体系 Modbus协议 Modbus RTU Modbus TCP 串口通信(RS-232/485) CRC-16校验 地址+功能码+数据+CRC 以太网通信 MBAP头+功能码+数据 TCP/IP协议栈保证 寄存器映射表设计 线圈 | 离散输入 | 输入寄存器 | 保持寄存器 地址映射 → 数据类型 → 单位/缩放 → 读写属性

这张图把RTU和TCP的差异、寄存器分类都标出来了。你把它存下来,以后做项目时对照着看,思路会清晰很多。

五、实战中的几个坑

最后,我分享几个实战中容易踩的坑:

  1. 波特率不匹配:RTU通信时,所有设备的波特率、数据位、停止位、校验位必须一致。我遇到过现场有人把9600和19200混用,结果数据全是乱码。
  2. 地址冲突:同一个总线上,不能有两个相同地址的设备。否则数据会碰撞,谁也读不到正确值。
  3. 寄存器地址偏移:有些设备手册写的是“40001”,实际通信地址是0x0000。记得做减法。
  4. CRC计算错误:尤其是大小端顺序,一定要看手册示例。
  5. TCP连接超时:Modbus TCP默认端口是502。如果设备没响应,先ping一下IP,再检查防火墙是不是把502端口封了。

总结一下:Modbus协议不难,但细节决定成败。寄存器映射表是系统的“地图”,CRC是数据的“保险”。把这两样搞明白,你的储能监控系统就稳了一半。

好了,这一章就到这里。下一章咱们会讲如何用Python写一个Modbus采集程序,到时候我会带着你一行一行敲代码。

专注资料整理