3. Modbus协议基础:Modbus RTU与TCP帧结构、功能码详解、寄存器地址映射

好,咱们今天聊聊Modbus协议。说实话,在储能系统里,Modbus算是老熟人了。不管是BMS跟PCS通信,还是EMS采集各个设备的数据,十有八九都跑的是Modbus。我最早接触它是在一个光伏电站项目上,当时逆变器死活连不上,折腾了两天,最后发现是寄存器地址算错了——嗯,这种坑咱们后面会细说。

3.1 Modbus RTU与TCP:两种传输方式的区别

Modbus协议有两种常见的物理层实现:RTU和TCP。说白了,RTU走串口(RS-485/232),TCP走以太网。我个人的习惯是:距离短、设备少、现场环境恶劣的,优先用RTU;需要远程监控、数据量大的,用TCP。

核心区别一句话总结:RTU是串行通信,帧结构里带CRC校验;TCP是网络通信,帧结构里带MBAP报文头,校验交给TCP/IP协议栈处理。

3.1.1 Modbus RTU帧结构

RTU的帧结构其实挺简单的,就四个部分:

字段 长度 说明
地址码 1字节 从站地址,范围1-247
功能码 1字节 指示操作类型(读/写)
数据区 N字节 寄存器地址、数据内容等
CRC校验 2字节 循环冗余校验,低字节在前

举个例子,读取从站地址为1的设备的保持寄存器,起始地址0x0000,读取2个寄存器,RTU报文长这样:

01 03 00 00 00 02 C4 0B
│  │  │     │     └── CRC校验
│  │  │     └──────── 读取数量(2个寄存器)
│  │  └────────────── 起始地址(0x0000)
│  └───────────────── 功能码(03:读保持寄存器)
└──────────────────── 从站地址(1号设备)

这里有个细节要注意:CRC校验是低字节在前。我见过不少新手把CRC高低字节搞反了,结果设备就是不响应。你想想看,报文发出去石沉大海,排查半天才发现是校验顺序错了。

3.1.2 Modbus TCP帧结构

TCP的帧结构比RTU多了一个MBAP报文头,但去掉了CRC校验。为什么?因为TCP/IP协议栈自己会做校验,没必要重复。

字段 长度 说明
事务标识符 2字节 用于匹配请求和响应
协议标识符 2字节 固定为0x0000
长度字段 2字节 后续数据的字节数
单元标识符 1字节 相当于RTU的地址码
功能码 1字节 同RTU
数据区 N字节 寄存器地址、数据等

同样的操作,TCP报文长这样:

00 01 00 00 00 06 01 03 00 00 00 02
│    │    │     │  │  │     └── 读取数量
│    │    │     │  │  └──────── 起始地址
│    │    │     │  └─────────── 功能码
│    │    │     └────────────── 单元标识符
│    │    └─────────────────── 长度(后续6字节)
│    └─────────────────────── 协议标识符(固定0x0000)
└─────────────────────────── 事务标识符(本次会话ID)

我的小技巧:调试TCP通信时,事务标识符每次请求最好递增。有些设备会检查这个字段,如果重复可能会丢弃报文。我在一个储能柜项目里就遇到过,EMS每隔1秒发同样的事务ID,结果PCS直接不理了。

3.2 功能码详解:你必须要掌握的五个功能码

Modbus功能码有很多,但储能系统里常用的就五个。我列个表,你一看就明白:

功能码 名称 操作对象 常用场景
0x01 读线圈状态 DO(数字量输出) 读取继电器状态、开关信号
0x02 读离散输入 DI(数字量输入) 读取急停按钮、门禁信号
0x03 读保持寄存器 保持寄存器 读取电压、电流、SOC等模拟量
0x04 读输入寄存器 输入寄存器 读取传感器采集的原始值
0x06 写单个寄存器 保持寄存器 设置参数、下发指令
0x10 写多个寄存器 保持寄存器 批量设置参数、下发曲线

这里我重点说说0x03和0x06,因为储能系统里90%的通信都在用这两个功能码。

3.2.1 功能码0x03:读保持寄存器

这是最常用的功能码。请求报文里要告诉从站:起始地址和读取数量。响应报文里返回的是字节数+数据。

举个例子,读取起始地址0x0100开始的3个寄存器:

请求:01 03 01 00 00 03 (CRC省略)
响应:01 03 06 00 0A 00 14 00 1E (CRC省略)
      │  │  │  └── 第3个寄存器值:0x001E(30)
      │  │  │  └──── 第2个寄存器值:0x0014(20)
      │  │  └─────── 第1个寄存器值:0x000A(10)
      │  └────────── 数据字节数(6字节)
      └───────────── 功能码(0x03)

注意:一次读取的寄存器数量有限制。RTU模式下一般不超过125个寄存器,TCP模式下可以多一些,但最好别超过240个。我曾经在调试时一次性读了200个寄存器,结果从站直接超时了——它内部缓冲区不够大。

3.2.2 功能码0x06:写单个寄存器

写单个寄存器相对简单。请求报文里包含寄存器地址和要写入的值。响应报文是请求报文的回显(成功时)。

请求:01 06 00 01 00 64 (CRC省略)
      │  │  │     └── 写入值:0x0064(100)
      │  │  └──────── 寄存器地址:0x0001
      │  └─────────── 功能码(0x06)
      └────────────── 从站地址

响应:01 06 00 01 00 64 (与请求一致,表示写入成功)

如果写入失败,从站会返回异常响应。异常响应的功能码是请求功能码+0x80,后面跟一个异常码。比如:

异常响应:01 86 02 (CRC省略)
          │  │  └── 异常码:0x02(非法数据地址)
          │  └───── 功能码+0x80
          └──────── 从站地址

常见的异常码有:

  • 0x01:非法功能码(从站不支持这个功能)
  • 0x02:非法数据地址(寄存器地址不存在)
  • 0x03:非法数据值(写入的值超出范围)
  • 0x04:从站设备故障(硬件问题)

3.3 寄存器地址映射:最容易踩坑的地方

好了,终于到了重点。寄存器地址映射,说白了就是「哪个地址对应哪个数据」。但这里有个大坑——地址偏移。

很多设备厂商的说明书上写的是「40001对应SOC」,但实际通信时你填的地址是0x0000。为什么?因为Modbus协议里,保持寄存器的起始地址是0x0000,而PLC的地址是从40001开始的。这个偏移量是1,不是0。

记住这个公式:

协议地址 = 说明书地址 - 地址偏移量

对于保持寄存器:协议地址 = 说明书地址 - 40001

对于输入寄存器:协议地址 = 说明书地址 - 30001

举个例子,说明书上写「保持寄存器40010是电池总电压」,那么协议地址就是:

40010 - 40001 = 9 → 十六进制 0x0009

所以发送的报文应该是:

01 03 00 09 00 01 (读取地址0x0009的1个寄存器)

而不是:

01 03 00 0A 00 01 (这是错误的!0x000A对应的是40011)

避坑指南:我曾经在一个储能项目里,BMS厂商给的地址表是「40001~40050」,我直接用0x0000~0x0031去读,结果读回来的数据全是乱的。后来发现他们实际用的是0x0001~0x0032——偏移量是0而不是1。所以拿到地址表后,第一件事就是确认偏移量。

3.3.1 数据格式:不只是整数

寄存器里存的不一定是整数。储能系统里常见的数据格式有:

  • 16位无符号整数:0~65535,用于状态标志、小范围数值
  • 16位有符号整数:-32768~32767,用于温度、功率等
  • 32位浮点数(IEEE 754):占用2个寄存器,用于电压、电流、SOC等
  • 32位整数:占用2个寄存器,用于累计电量等大数值

这里有个容易搞混的地方:32位数据的字节序。有的设备是大端模式(高字节在前),有的是小端模式(低字节在前)。我建议你在调试时先用一个已知值测试一下。

比如SOC是50.0%,用浮点数表示是0x42280000(大端)。如果读回来的两个寄存器是0x0000和0x4228,那就是小端模式;如果是0x4228和0x0000,那就是大端模式。

3.4 知识体系总览

说了这么多,我画个图帮你理一理思路:

Modbus协议知识体系 Modbus RTU 串口通信 · CRC校验 Modbus TCP 以太网通信 · MBAP报文头 帧结构组成 地址码/单元标识符 → 功能码 → 数据区(寄存器地址+数据) → CRC校验(RTU) 核心功能码 0x03 读保持寄存器 · 0x06 写单个寄存器 · 0x10 写多个寄存器 0x01 读线圈 · 0x02 读离散输入 · 0x04 读输入寄存器 寄存器地址映射 协议地址 = 说明书地址 - 偏移量 · 注意字节序(大端/小端)

这张图把Modbus协议的核心脉络串起来了。从上到下看:先确定用RTU还是TCP,然后按照帧结构组包,选择正确的功能码,最后把寄存器地址映射对。每一步都有坑,但只要你按这个流程走,基本不会出大问题。

我的调试习惯:先用Modbus Poll或ModScan这类工具模拟主站,发几个简单的读请求,看看从站能不能正常响应。能响应了,再用代码实现。这样可以把协议问题跟代码问题分开排查,效率高很多。

好了,Modbus协议基础就讲到这里。记住:帧结构是骨架,功能码是动作,地址映射是灵魂。三者缺一不可。下一节咱们会讲具体的通信故障排查方法,到时候这些基础知识都会用上。


公众号:蓝海资料掘金营,微信deep3321