当前位置:首页 > 技术 > 正文内容

C#与.NET实现光伏逆变器Modbus数据采集与设备模型构建

访客 技术 2026年7月11日 4

在物联网(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部分的字节长度(单元标识符 + 功能码 + 数据)。
  • PDU (功能码 + 数据):
    • 单元标识符(Unit Identifier):01,指定Modbus从站的地址。
    • 功能码(Function Code):03,表示"读取保持寄存器"(Read Holding Registers)。
    • 起始地址(Starting Address):75 30,对应十进制的30000,表示要读取的第一个寄存器地址。
    • 寄存器数量(Quantity of Registers):00 06,表示从起始地址开始读取6个寄存器。

下图展示了Modbus TCP请求报文的结构:

Modbus TCP请求报文MBAP Header解析

Modbus TCP请求报文PDU解析

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字节。

下图展示了Modbus TCP响应报文的结构:

Modbus TCP响应报文结构

PDU数据解析示例:

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}");
    }
}

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。