2. Modbus协议详解(上):Modbus RTU帧结构、地址映射、功能码(03/06/16)、CRC校验实战
各位同行,今天我们来聊聊Modbus RTU。说实话,这个协议在储能系统里太常见了。BMS、PCS、EMS之间通信,十有八九都在用Modbus。我最早接触它是在一个光伏电站项目上,当时被CRC校验折腾了一整天——嗯,今天咱们就把这些坑都填上。
2.1 Modbus RTU帧结构——说白了就是数据怎么打包
Modbus RTU的帧结构其实很简单。我习惯把它想象成一个快递包裹:有地址、有指令、有数据、最后还有个校验码。具体长这样:
| 地址域 | 功能码 | 数据域 | CRC校验 |
|---|---|---|---|
| 1字节 | 1字节 | N字节 | 2字节 |
举个例子,你要读取BMS的电池电压,发送的报文可能是这样的:
01 03 00 00 00 02 C4 0B
拆开来看:
- 01 —— 从机地址,也就是BMS的ID
- 03 —— 功能码,表示我要读保持寄存器
- 00 00 —— 起始地址,从0号寄存器开始
- 00 02 —— 读取2个寄存器(4字节数据)
- C4 0B —— CRC校验,低字节在前
2.2 地址映射——寄存器到底怎么分配?
地址映射是Modbus协议里最容易出问题的地方。我见过太多项目因为地址对不上而联调失败。
Modbus定义了四种数据对象,但在储能系统里,我们最常用的是保持寄存器(Holding Register),也就是功能码03和06/16操作的对象。
| 数据对象 | 访问类型 | 对应功能码 | 地址范围 |
|---|---|---|---|
| 线圈(Coil) | 读写 | 01/05/15 | 00001-09999 |
| 离散输入(Discrete Input) | 只读 | 02 | 10001-19999 |
| 输入寄存器(Input Register) | 只读 | 04 | 30001-39999 |
| 保持寄存器(Holding Register) | 读写 | 03/06/16 | 40001-49999 |
注意,这里有个坑:协议地址和物理地址的区别。Modbus协议里,地址是从0开始的,但很多设备厂家文档里写的是40001这种PLC地址。我曾经因为没搞清楚这个,浪费了整整一个下午。
2.3 功能码03/06/16——读写操作的核心
这三个功能码,基本覆盖了储能系统90%的通信需求。咱们一个一个说。
2.3.1 功能码03:读保持寄存器
这是最常用的读操作。请求帧和响应帧的结构如下:
请求帧(主机→从机):
从机地址 + 0x03 + 起始地址(2字节) + 寄存器数量(2字节) + CRC(2字节)
响应帧(从机→主机):
从机地址 + 0x03 + 数据字节数 + 数据(N字节) + CRC(2字节)
举个例子,读取BMS的SOC(假设在寄存器0x0010):
// 请求:读取地址0x0010,1个寄存器
发送:01 03 00 10 00 01 85 CF
// 响应:SOC值为60.0%(假设缩放系数10)
接收:01 03 02 02 58 B8 3A
// 数据0x0258 = 600,除以10得60.0%
2.3.2 功能码06:写单个寄存器
这个简单,就是写一个寄存器。常用于设置参数或下发指令。
请求帧:
从机地址 + 0x06 + 寄存器地址(2字节) + 数据值(2字节) + CRC(2字节)
比如设置PCS的功率目标值为500kW(假设缩放系数100):
// 写入0x01F4(500)到地址0x0020
发送:01 06 00 20 01 F4 89 AC
响应帧和请求帧完全一样,表示写入成功。
2.3.3 功能码16:写多个寄存器
这个功能码在储能系统里特别有用。比如你要同时设置充放电功率、运行模式、启停状态,用16一次搞定。
请求帧:
从机地址 + 0x10 + 起始地址(2字节) + 寄存器数量(2字节) + 字节数 + 数据(N字节) + CRC(2字节)
举个例子,同时设置PCS的运行模式(地址0x0030)和功率目标(地址0x0031):
// 模式=1(充电),功率=300kW(0x012C)
发送:01 10 00 30 00 02 04 00 01 01 2C 7A 9B
2.4 CRC校验实战——别让校验码坑了你
CRC校验是Modbus RTU的"守门员"。计算不对,整个报文都会被丢弃。我刚开始做的时候,经常因为CRC算错而抓狂。
Modbus RTU用的是CRC-16(Modbus),多项式是0x8005,初始值0xFFFF。计算步骤:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC低字节异或
- 右移1位,如果移出的位是1,则与0xA001异或
- 重复8次
- 所有字节处理完后,得到CRC值
- 低字节在前,高字节在后
这里有个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 >>= 1;
}
}
}
return crc; // 注意:返回的是小端格式
}
2.5 实战案例——读取BMS数据
咱们来个完整的例子。假设BMS从机地址0x01,要读取以下数据:
| 参数 | 寄存器地址 | 数据类型 | 缩放系数 |
|---|---|---|---|
| 总电压 | 0x0000 | uint16 | 0.1V |
| 总电流 | 0x0001 | int16 | 0.1A |
| SOC | 0x0002 | uint16 | 0.1% |
| SOH | 0x0003 | uint16 | 0.1% |
请求报文:
01 03 00 00 00 04 44 0C
响应报文:
01 03 08 0B B8 00 64 02 58 03 E8 7A 1B
解析一下:
- 0x0BB8 = 3000 → 总电压300.0V
- 0x0064 = 100 → 电流10.0A(正数表示充电)
- 0x0258 = 600 → SOC 60.0%
- 0x03E8 = 1000 → SOH 100.0%
- 帧结构:地址+功能码+数据+CRC,缺一不可
- 地址映射:搞清楚协议地址和PLC地址的区别
- 功能码:03读、06写单个、16写多个,够用了
- CRC校验:低字节在前,别搞反了
好了,Modbus RTU的核心内容就这些。说白了就是"怎么问、怎么答、怎么校验"。下节课咱们聊聊Modbus TCP,看看它和RTU有什么不同。
公众号:蓝海资料掘金营,微信deep3321