3、数据采集基础:Modbus TCP协议解析、使用pymodbus读取BMS数据、数据校验与异常处理

好,咱们今天来啃一块硬骨头——Modbus TCP协议。说实话,做储能系统这么多年,我见过太多人在这个环节翻车了。BMS(电池管理系统)的数据读不上来,整个监控系统就是个摆设。你想想看,一个储能电站,几十个电池簇,几百个从站,要是通讯断了,那画面太美我不敢看。

我个人习惯,不管项目多急,先把协议吃透再动手。磨刀不误砍柴工嘛。

3.1 Modbus TCP协议,说白了就是“主从问答”

Modbus TCP本质上是一个请求/响应协议。主站(咱们的监控系统)发一个请求帧,从站(BMS)回一个响应帧。就这么简单。

但简单归简单,里面的门道可不少。我刚开始做的时候,以为就是个socket通讯,结果被坑得够呛。

一个标准的Modbus TCP帧长这样:

| 事务标识符(2B) | 协议标识符(2B) | 长度(2B) | 单元标识符(1B) | 功能码(1B) | 数据(nB) |

咱们拆开来看:

  • 事务标识符:用来匹配请求和响应。我一般用递增的计数器,简单粗暴。
  • 协议标识符:固定为0x0000,表示Modbus协议。
  • 长度:后面所有字节的总长度。
  • 单元标识符:也就是从站地址。BMS通常从1开始编号。
  • 功能码:读线圈用0x01,读保持寄存器用0x03。读BMS数据,99%的情况用0x03。
  • 数据:起始地址和读取数量。

核心要点:Modbus TCP的报文结构是固定的,但数据内容完全取决于BMS厂商的寄存器映射表。没有这张表,你啥也干不了。

3.2 使用pymodbus读取BMS数据

Python里操作Modbus TCP,我首选pymodbus。这个库封装得很好,几行代码就能搞定通讯。

先安装:

pip install pymodbus

然后写个简单的读取示例:

from pymodbus.client import ModbusTcpClient

# 连接BMS
client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# 读取保持寄存器,从地址0开始,读10个寄存器
result = client.read_holding_registers(0, 10, unit=1)

if not result.isError():
    # 数据在result.registers里
    print(f"读取成功: {result.registers}")
else:
    print(f"读取失败: {result}")

client.close()

嗯,这里要注意。很多BMS的寄存器地址是从0开始的,但有些厂商从1开始。我遇到过一家厂商,文档写的是地址1,实际通讯时要传0。你说坑不坑?

所以我的建议是:先读一个已知值验证一下。比如BMS的型号寄存器,读出来跟文档对一下,确认地址映射没问题。

3.3 数据校验,别信BMS发来的原始数据

BMS发来的数据,你敢直接用吗?反正我不敢。

我在项目里遇到过,BMS的电压数据突然跳变,从3.2V直接蹦到4.5V。要是直接拿这个数据去做告警,那整个电站都得跳闸。

所以数据校验这一步,必须做。我一般做三层校验:

  1. 范围校验:每个数据都有合理的物理范围。比如单体电压,正常在2.5V~4.2V之间。超出这个范围,直接丢弃。
  2. 变化率校验:相邻两次采集的数据,变化不能太离谱。比如电压1秒内变化超过0.5V,那肯定有问题。
  3. CRC校验:Modbus TCP本身没有CRC,但有些BMS会在数据域里加校验。这个要看厂商文档。

代码实现起来也不复杂:

def validate_voltage(voltage):
    """单体电压校验"""
    if voltage < 2.5 or voltage > 4.2:
        return False
    return True

def validate_change_rate(old_val, new_val, max_rate=0.5):
    """变化率校验"""
    if abs(new_val - old_val) > max_rate:
        return False
    return True

避坑指南:我曾经遇到过BMS返回的数据字节序是反的。Modbus默认是大端模式,但有些BMS用的小端。pymodbus里可以用word_order参数调整,或者自己手动交换高低字节。

3.4 异常处理,通讯断了怎么办?

通讯异常是常态,不是异常。你想想看,储能电站环境那么恶劣,电磁干扰、网线松动、BMS死机,什么情况都可能发生。

我的异常处理策略是:

  • 超时重试:默认超时3秒,重试3次。每次重试间隔递增,避免雪崩。
  • 断线重连:如果连续3次都失败,主动断开连接再重连。
  • 数据保持:通讯中断期间,用最后一次有效数据填充,并打上“陈旧”标记。
  • 告警上报:通讯中断超过30秒,必须上报告警。

代码示例:

import time
from pymodbus.client import ModbusTcpClient

class BMSReader:
    def __init__(self, host, port=502, timeout=3):
        self.host = host
        self.port = port
        self.timeout = timeout
        self.client = None
        self.retry_count = 0
        self.max_retry = 3

    def read_with_retry(self, address, count, unit=1):
        """带重试的读取"""
        for i in range(self.max_retry):
            try:
                if not self.client or not self.client.is_socket_open():
                    self.connect()
                result = self.client.read_holding_registers(
                    address, count, unit=unit
                )
                if not result.isError():
                    self.retry_count = 0
                    return result.registers
            except Exception as e:
                print(f"读取失败,第{i+1}次重试: {e}")
                time.sleep(2 ** i)  # 指数退避
        self.retry_count += 1
        return None

    def connect(self):
        """建立连接"""
        self.client = ModbusTcpClient(self.host, port=self.port, timeout=self.timeout)
        self.client.connect()

重要提醒:不要在主线程里做同步读取。Modbus通讯是IO操作,会阻塞主线程。一定要用异步或者多线程。我一般用asyncio配合pymodbus的异步客户端,效果很好。

3.5 知识体系总览

说了这么多,咱们用一张图把整个流程串起来。这张图是我做项目时自己画的,每次带新人都会拿出来讲一遍。

Modbus TCP数据采集流程 监控系统(主站) BMS(从站) 请求帧(功能码0x03) 响应帧(寄存器数据) 数据校验三层过滤 范围校验 变化率校验 CRC校验(可选) 异常处理策略 • 超时重试(指数退避) • 断线重连 • 数据保持与陈旧标记 • 告警上报(30秒阈值) • 异步非阻塞读取

这张图把整个流程分成了四个阶段:请求、响应、校验、异常处理。每个阶段都有对应的代码实现。我个人建议,刚开始做的时候,先把校验和异常处理写好,再去调通讯。顺序别搞反了。

好了,这一节的内容就到这儿。记住一句话:Modbus TCP不难,难的是把数据采准、采稳。下一节咱们会深入BMS的寄存器映射表,看看那些电压、温度、SOC数据到底藏在哪儿。


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