C#与.NET实现光伏逆变器Modbus数据采集与设备模型构建
在物联网(IoT)领域,尤其是在光伏(Photovoltaic, PV)电站中,集成各种现场设备(如逆变器、传感器、电表等)是构建智能监控和管理系统的关键。这些设备通常采用工业通信协议,其中Modbus TCP因其简单和广泛支持而成为主流选择。本文将详细介绍如何利用C#和.NET平台,设计并实现一套用于光伏逆变器Modbus TCP数据采集、解析和设备模型构建的解决方案。
Modbus TCP 协议基础
Modbus TCP报文由两大部分构成:MBAP Header(Modbus Application Protocol Header)和PDU(Protocol Data Unit)。
一个标准的Modbus TCP请求报文示例:00 01 00 00 00 06 01 03 75 30 00 06
此报文的含义是:
- MBAP Header (7字节):
- 事务标识符(Transaction Identifier):
00 01,用于匹配请求与响应。 - 协议标识符(Protocol Identifier):
00 00,对于Modbus TCP固定为0。 - 长度(Length):
00 06,PDU部分的字节长度(单元标识符 + 功能码 + 数据)。
- 事务标识符(Transaction Identifier):
- PDU (功能码 + 数据):
- 单元标识符(Unit Identifier):
01,指定Modbus从站的地址。 - 功能码(Function Code):
03,表示"读取保持寄存器"(Read Holding Registers)。 - 起始地址(Starting Address):
75 30,对应十进制的30000,表示要读取的第一个寄存器地址。 - 寄存器数量(Quantity of Registers):
00 06,表示从起始地址开始读取6个寄存器。
- 单元标识符(Unit Identifier):
下图展示了Modbus TCP请求报文的结构:



对应的Modbus TCP响应报文结构示例:00 01 00 00 00 0F 01 03 0C 09 C4 00 64 13 88 00 32
此响应报文的含义是:
- MBAP Header (7字节): 与请求类似。
- PDU (功能码 + 数据):
- 单元标识符(Unit Identifier):
01。 - 功能码(Function Code):
03。 - 字节计数(Byte Count):
0C,表示后续数据字段的字节数,这里是12字节。 - 数据(Data):
09 C4 00 64 13 88 00 32,这12个字节是6个寄存器的原始值,每个寄存器2字节。
- 单元标识符(Unit Identifier):
下图展示了Modbus TCP响应报文的结构:

PDU数据解析示例:

需要注意的是,Modbus协议中的数据通常以大端字节序(Big-Endian)存储。此外,许多光伏设备协议会定义"倍率"(Scale),原始寄存器值需要除以该倍率才能得到实际的物理量。

设计理念与代码结构
为了提高系统的灵活性、可维护性和可扩展性,我们将解决方案设计为分层架构。这有助于实现协议层、数据解析层和业务模型层的解耦,方便未来支持多品牌设备、实现配置化映射以及协议扩展。
核心设计目标:
- 协议抽象:封装Modbus TCP报文的构造与解析细节。
- 数据解码:独立处理寄存器数据的字节序转换和倍率计算。
- 业务建模:将原始数据映射到有意义的、面向业务的设备模型。
代码结构示意图:

C# 实现细节
1. Modbus TCP 请求消息封装
我们定义一个类来表示Modbus TCP请求,并提供序列化方法将其转换为字节数组。
public class ModbusTcpRequestMessage
{
public ushort TransactionIdentifier { get; set; }
public byte UnitAddress { get; set; }
public byte FunctionCode { get; set; } = 0x03; // 默认读取保持寄存器
public ushort StartingRegisterAddress { get; set; }
public ushort RegisterQuantity { get; set; }
/// <summary>
/// 将Modbus请求对象序列化为Modbus TCP报文的字节数组。
/// </summary>
public byte[] GeneratePayload()
{
var dataSegments = new List<byte>();
// MBAP Header
// 事务标识符 (2字节,大端)
dataSegments.AddRange(BitConverter.GetBytes(TransactionIdentifier).Reverse());
// 协议标识符 (2字节,Modbus TCP固定为0x0000)
dataSegments.AddRange(new byte[] { 0x00, 0x00 });
// PDU长度计算: Unit ID (1) + 功能码 (1) + 起始地址 (2) + 寄存器数量 (2) = 6字节
ushort pduLength = (ushort)(1 + 1 + 2 + 2);
dataSegments.AddRange(BitConverter.GetBytes(pduLength).Reverse()); // 长度字段 (2字节,大端)
// PDU Body
dataSegments.Add(UnitAddress);
dataSegments.Add(FunctionCode);
dataSegments.AddRange(BitConverter.GetBytes(StartingRegisterAddress).Reverse()); // 起始地址 (2字节,大端)
dataSegments.AddRange(BitConverter.GetBytes(RegisterQuantity).Reverse()); // 寄存器数量 (2字节,大端)
return dataSegments.ToArray();
}
}
2. Modbus TCP 响应消息解析
定义响应报文的结构,并提供一个静态解析器来处理接收到的字节数组。
public class ModbusTcpResponseMessage
{
public ushort TransactionIdentifier { get; set; }
public byte UnitAddress { get; set; }
public byte FunctionCode { get; set; }
public byte DataByteCount { get; set; } // 实际数据字节数
public byte[] RawDataPayload { get; set; } // 原始数据部分
}
public static class ModbusTcpResponseParser
{
/// <summary>
/// 解析Modbus TCP响应报文的字节数组。
/// </summary>
/// <param name="responseBuffer">完整的Modbus TCP响应字节数组。</param>
/// <returns>解析后的ModbusTcpResponseMessage对象。</returns>
public static ModbusTcpResponseMessage Parse(byte[] responseBuffer)
{
if (responseBuffer == null || responseBuffer.Length < 9)
throw new ArgumentException("Modbus TCP响应报文长度不足或为空", nameof(responseBuffer));
var parsedMessage = new ModbusTcpResponseMessage();
parsedMessage.TransactionIdentifier = ReadBigEndianUInt16(responseBuffer, 0);
ushort protocolId = ReadBigEndianUInt16(responseBuffer, 2);
ushort length = ReadBigEndianUInt16(responseBuffer, 4); // MBAP Header中的Length字段
if (protocolId != 0x0000)
throw new InvalidOperationException("Modbus TCP协议ID不匹配 (预期0x0000)");
parsedMessage.UnitAddress = responseBuffer[6];
parsedMessage.FunctionCode = responseBuffer[7];
parsedMessage.DataByteCount = responseBuffer[8];
// 提取数据部分
if (responseBuffer.Length < 9 + parsedMessage.DataByteCount)
throw new ArgumentException("Modbus TCP响应报文数据部分长度与字节计数不匹配");
parsedMessage.RawDataPayload = new byte[parsedMessage.DataByteCount];
// 从索引9开始,复制DataByteCount字节到RawDataPayload
Buffer.BlockCopy(responseBuffer, 9, parsedMessage.RawDataPayload, 0, parsedMessage.DataByteCount);
return parsedMessage;
}
/// <summary>
/// 从字节数组中读取一个大端序的USHORT值。
/// </summary>
private static ushort ReadBigEndianUInt16(byte[] buffer, int offset)
{
// Modbus通常使用大端字节序:高位字节在前,低位字节在后
return (ushort)((buffer[offset] << 8) | buffer[offset + 1]);
}
}
3. 寄存器数据解码(倍率 + 字节序)
这个静态类负责将原始的寄存器字节数据转换为有意义的数值,包括处理字节序和应用倍率。
public static class ModbusRegisterDecoder
{
/// <summary>
/// 从原始数据负载中读取指定索引的USHORT寄存器值。
/// </summary>
/// <param name="registerBytes">包含多个寄存器原始数据的字节数组。</param>
/// <param name="registerIndex">要读取的寄存器在数据序列中的0基索引。</param>
/// <returns>解析出的USHORT值。</returns>
public static ushort GetUInt16Value(byte[] registerBytes, int registerIndex)
{
// 每个寄存器占用2个字节
int byteOffset = registerIndex * 2;
if (byteOffset + 1 >= registerBytes.Length)
throw new ArgumentOutOfRangeException(nameof(registerIndex), "寄存器索引超出数据范围");
// Modbus寄存器为大端字节序
return (ushort)((registerBytes[byteOffset] << 8) | registerBytes[byteOffset + 1]);
}
/// <summary>
/// 从原始数据负载中读取指定索引的寄存器值,并应用倍率。
/// </summary&m
/// <param name="registerBytes">包含多个寄存器原始数据的字节数组。</param>
/// <param name="registerIndex">要读取的寄存器在数据序列中的0基索引。</param>
/// <param name="scaleFactor">应用到原始值的倍率。</param>
/// <returns>经过倍率调整后的双精度浮点数。</returns>
public static double GetScaledValue(byte[] registerBytes, int registerIndex, double scaleFactor)
{
ushort rawValue = GetUInt16Value(registerBytes, registerIndex);
return rawValue / scaleFactor;
}
}
4. 光伏逆变器业务模型定义
定义一个C#类,表示光伏逆变器的核心运行数据,作为物模型的具体实现。
public class PhotovoltaicInverterModel
{
public double DcInputVoltage { get; set; } // 直流输入电压 (V)
public double DcInputCurrent { get; set; } // 直流输入电流 (A)
public double AcOutputPower { get; set; } // 交流输出功率 (kW)
public double InternalTemperatureC { get; set; } // 内部温度 (℃)
public DateTime ObservationTimestamp { get; set; } = DateTime.UtcNow; // 数据采集时间戳
}
5. 从响应数据到业务模型的映射
这个静态类负责将已解析的Modbus响应数据,根据预设的寄存器地址和倍率配置,填充到光伏逆变器业务模型中。
public static class InverterModelMapper
{
/// <summary>
/// 将Modbus TCP响应数据映射到光伏逆变器业务模型。
/// </summary>
/// <param name="response">已解析的ModbusTcpResponseMessage对象。</param>
/// <returns>填充了数据的PhotovoltaicInverterModel对象。</returns>
public static PhotovoltaicInverterModel MapFromModbusResponse(ModbusTcpResponseMessage response)
{
if (response == null || response.RawDataPayload == null)
throw new ArgumentNullException(nameof(response), "Modbus响应或其数据为空");
var dataRegisters = response.RawDataPayload;
// 假设寄存器30000, 30001, 30002, 30003分别对应数据负载中的0, 1, 2, 3索引
// 具体的倍率和寄存器地址映射应参照设备厂商的通信协议文档
return new PhotovoltaicInverterModel
{
// 例如: 寄存器30000代表直流电压,倍率为10
DcInputVoltage = ModbusRegisterDecoder.GetScaledValue(dataRegisters, 0, 10.0),
// 例如: 寄存器30001代表直流电流,倍率为10
DcInputCurrent = ModbusRegisterDecoder.GetScaledValue(dataRegisters, 1, 10.0),
// 例如: 寄存器30002代表交流功率,倍率为1000
AcOutputPower = ModbusRegisterDecoder.GetScaledValue(dataRegisters, 2, 1000.0),
// 例如: 寄存器30003代表内部温度,假设不需要倍率
InternalTemperatureC = ModbusRegisterDecoder.GetUInt16Value(dataRegisters, 3)
};
}
}
6. 端到端示例调用
通过一个完整的模拟示例,展示上述组件如何协同工作,完成Modbus数据的采集到业务模型构建的整个流程。
public class InverterDataSimulation
{
public static void ExecuteSimulation()
{
Console.WriteLine("--- 光伏逆变器Modbus数据采集模拟 ---");
// 1. 构建Modbus TCP请求:读取从站ID为1,起始地址30000,数量为6个寄存器
var modbusRequest = new ModbusTcpRequestMessage
{
TransactionIdentifier = 1001, // 事务标识符
UnitAddress = 1, // 从站单元ID
StartingRegisterAddress = 30000, // 起始寄存器地址 (例如:对应华为逆变器直流输入电压)
RegisterQuantity = 6 // 读取寄存器数量
};
byte[] requestBytes = modbusRequest.GeneratePayload();
Console.WriteLine($"生成的Modbus请求报文: {BitConverter.ToString(requestBytes).Replace("-", " ")}");
// --- 在实际应用中,这里会通过TCP Socket将 requestBytes 发送给物理逆变器 ---
// --- 并等待接收其响应报文 ---
// 2. 模拟接收到的Modbus TCP响应报文
// 假设响应数据依次是:
// 寄存器0 (30000) = 0x09C4 (2500) -> 电压 250.0V (2500 / 10)
// 寄存器1 (30001) = 0x0064 (100) -> 电流 10.0A (100 / 10)
// 寄存器2 (30002) = 0x1388 (5000) -> 功率 5.0kW (5000 / 1000)
// 寄存器3 (30003) = 0x0032 (50) -> 温度 50℃ (不需要倍率)
byte[] simulatedResponseBytes =
{
0x00, 0x01, // 事务ID (与请求匹配)
0x00, 0x00, // 协议ID
0x00, 0x0F, // 长度 (15 bytes following)
0x01, // 单元ID
0x03, // 功能码
0x0C, // 字节计数 (12字节数据)
0x09, 0xC4, // 寄存器数据 0 (2500)
0x00, 0x64, // 寄存器数据 1 (100)
0x13, 0x88, // 寄存器数据 2 (5000)
0x00, 0x32 // 寄存器数据 3 (50)
};
Console.WriteLine($"模拟接收到的Modbus响应报文: {BitConverter.ToString(simulatedResponseBytes).Replace("-", " ")}");
// 3. 解析Modbus响应报文
ModbusTcpResponseMessage parsedResponse = ModbusTcpResponseParser.Parse(simulatedResponseBytes);
Console.WriteLine($"\n解析成功 - 事务ID: {parsedResponse.TransactionIdentifier}, 功能码: {parsedResponse.FunctionCode}, 数据长度: {parsedResponse.DataByteCount} 字节");
Console.WriteLine($"原始数据Payload: {BitConverter.ToString(parsedResponse.RawDataPayload).Replace("-", " ")}");
// 4. 将解析出的数据映射到光伏逆变器业务模型
PhotovoltaicInverterModel inverterData = InverterModelMapper.MapFromModbusResponse(parsedResponse);
Console.WriteLine("\n--- 光伏逆变器数据模型 ---");
Console.WriteLine($"直流输入电压: {inverterData.DcInputVoltage} V");
Console.WriteLine($"直流输入电流: {inverterData.DcInputCurrent} A");
Console.WriteLine($"交流输出功率: {inverterData.AcOutputPower} kW");
Console.WriteLine($"内部温度: {inverterData.InternalTemperatureC} ℃");
Console.WriteLine($"数据采集时间: {inverterData.ObservationTimestamp:yyyy-MM-dd HH:mm:ss UTC}");
}
}