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协议的核心脉络串起来了。从上到下看:先确定用RTU还是TCP,然后按照帧结构组包,选择正确的功能码,最后把寄存器地址映射对。每一步都有坑,但只要你按这个流程走,基本不会出大问题。
我的调试习惯:先用Modbus Poll或ModScan这类工具模拟主站,发几个简单的读请求,看看从站能不能正常响应。能响应了,再用代码实现。这样可以把协议问题跟代码问题分开排查,效率高很多。
好了,Modbus协议基础就讲到这里。记住:帧结构是骨架,功能码是动作,地址映射是灵魂。三者缺一不可。下一节咱们会讲具体的通信故障排查方法,到时候这些基础知识都会用上。
公众号:蓝海资料掘金营,微信deep3321