基于C#实现Modbus通信及CRC校验

一、Modbus通信实现

1. 使用NModbus库
csharp 复制代码
// 安装NuGet包
Install-Package NModbus4

// 基本通信类
public class ModbusClient
{
    private IModbusMaster _master;
    private SerialPort _serialPort;

    public void ConnectRtu(string portName, int baudRate = 9600)
    {
        _serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        _serialPort.Open();
        
        _master = ModbusSerialMaster.CreateRtu(_serialPort);
        _master.Transport.ReadTimeout = 3000;
        _master.Transport.WriteTimeout = 3000;
    }

    public ushort[] ReadHoldingRegisters(byte slaveId, ushort startAddr, ushort count)
    {
        return _master.ReadHoldingRegisters(slaveId, startAddr, count);
    }

    public void WriteSingleRegister(byte slaveId, ushort addr, ushort value)
    {
        _master.WriteSingleRegister(slaveId, addr, value);
    }

    public void Disconnect()
    {
        _master?.Close();
        _serialPort?.Close();
    }
}

二、CRC校验实现

1. CRC16算法实现
csharp 复制代码
public static class ModbusCrc16
{
    private const ushort POLYNOMIAL = 0xA001; // 0x8005反转后的值
    private const ushort INITIAL_VALUE = 0xFFFF;

    public static byte[] Calculate(byte[] data)
    {
        ushort crc = INITIAL_VALUE;
        
        foreach (byte b in data)
        {
            crc ^= b;
            for (int i = 0; i < 8; i++)
            {
                if ((crc & 0x0001) != 0)
                {
                    crc >>= 1;
                    crc ^= POLYNOMIAL;
                }
                else
                {
                    crc >>= 1;
                }
            }
        }
        
        return new byte[] { (byte)(crc & 0xFF), (byte)(crc >> 8) };
    }

    public static bool Verify(byte[] frame)
    {
        if (frame.Length < 2) return false;
        
        byte[] data = frame.Take(frame.Length - 2).ToArray();
        byte[] receivedCrc = frame.Skip(frame.Length - 2).ToArray();
        byte[] calculatedCrc = Calculate(data);
        
        return receivedCrc.SequenceEqual(calculatedCrc);
    }
}
2. 带CRC的完整帧构造
csharp 复制代码
// 构造Modbus RTU请求帧
byte[] BuildRequestFrame(byte slaveId, byte functionCode, ushort startAddr, ushort count)
{
    var frame = new List<byte>();
    
    frame.Add(slaveId);        // 从站地址
    frame.Add(functionCode);   // 功能码
    frame.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)startAddr))); // 起始地址
    frame.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)count)));   // 寄存器数量
    
    byte[] crc = ModbusCrc16.Calculate(frame.ToArray());
    frame.Add(crc[0]);         // CRC低字节
    frame.Add(crc[1]);         // CRC高字节
    
    return frame.ToArray();
}

三、完整通信流程示例

1. 读取保持寄存器
csharp 复制代码
using (var client = new ModbusClient())
{
    try
    {
        client.ConnectRtu("COM3");
        
        // 构造请求帧
        byte[] request = BuildRequestFrame(
            slaveId: 1,
            functionCode: 0x03, // 读保持寄存器
            startAddr: 0x0000,
            count: 10
        );

        // 发送请求并接收响应
        byte[] response = client.SendRequest(request);
        
        // 验证CRC
        if (!ModbusCrc16.Verify(response))
            throw new InvalidDataException("CRC校验失败");

        // 解析数据
        ushort[] registers = response.Skip(3).Take(20).ToArray()
            .Select((b, i) => (ushort)(b << 8 | response[3 + i + 1]))
            .ToArray();
        
        Console.WriteLine($"读取到寄存器值: {string.Join(", ", registers)}");
    }
    finally
    {
        client.Disconnect();
    }
}

四、关键功能扩展

1. 异常处理机制
csharp 复制代码
public static class ModbusExceptionHandler
{
    public static void HandleError(Exception ex)
    {
        if (ex is TimeoutException)
            LogError("通信超时,请检查设备连接");
        else if (ex is InvalidDataException)
            LogError("数据校验失败,可能设备响应异常");
        else
            LogError($"未知错误: {ex.Message}");
    }

    private static void LogError(string message)
    {
        File.AppendAllText("modbus_errors.log", 
            $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}\n");
    }
}
2. 性能优化(查表法)
csharp 复制代码
public static class ModbusCrc16Optimized
{
    private static readonly ushort[] CrcTable = new ushort[256];

    static ModbusCrc16Optimized()
    {
        for (ushort i = 0; i < 256; i++)
        {
            ushort crc = i;
            for (byte j = 0; j < 8; j++)
            {
                crc = (crc & 1) != 0 
                    ? (ushort)((crc >> 1) ^ 0xA001) 
                    : crc >> 1;
            }
            CrcTable[i] = crc;
        }
    }

    public static ushort Calculate(byte[] data)
    {
        ushort crc = 0xFFFF;
        foreach (byte b in data)
        {
            crc = (ushort)((crc >> 8) ^ CrcTable[(crc ^ b) & 0xFF]);
        }
        return crc;
    }
}

五、工程实践建议

1. 配置管理
csharp 复制代码
{
  "ModbusConfig": {
    "PortName": "COM3",
    "BaudRate": 9600,
    "Parity": "None",
    "DataBits": 8,
    "StopBits": 1,
    "Timeout": 3000,
    "RetryCount": 3
  }
}
2. 多线程处理
csharp 复制代码
public class ModbusPoller
{
    private Timer _pollTimer;
    private readonly ModbusClient _client;

    public ModbusPoller(ModbusClient client)
    {
        _client = client;
        _pollTimer = new Timer(5000);
        _pollTimer.Elapsed += async (s, e) => await PollData();
    }

    private async Task PollData()
    {
        try
        {
            var data = await _client.ReadHoldingRegistersAsync(1, 0, 10);
            UpdateUI(data);
        }
        catch (Exception ex)
        {
            ModbusExceptionHandler.HandleError(ex);
        }
    }
}

六、调试与验证

1. 使用Wireshark抓包分析
  1. 过滤Modbus协议:tcp.port == 502

  2. 验证帧结构:

    复制代码
    | 地址 | 功能码 | 数据 | CRC低 | CRC高 |
2. 单元测试示例
csharp 复制代码
[TestFixture]
public class ModbusTests
{
    [Test]
    public void TestCRC16()
    {
        byte[] testData = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 };
        byte[] expectedCrc = { 0x84, 0x0A };
        
        Assert.AreEqual(expectedCrc, ModbusCrc16.Calculate(testData));
    }
}

参考代码 C# Modbus通讯及CRC校验计算 www.youwenfan.com/contentcsp/93611.html

七、扩展功能实现

1. 异步通信
csharp 复制代码
public async Task<ushort[]> ReadHoldingRegistersAsync(byte slaveId, ushort startAddr, ushort count)
{
    return await Task.Run(() => 
        _master.ReadHoldingRegisters(slaveId, startAddr, count));
}
2. 批量操作
csharp 复制代码
public void WriteMultipleRegisters(byte slaveId, ushort startAddr, ushort[] values)
{
    _master.WriteMultipleRegisters(slaveId, startAddr, values);
}

八、常见问题解决

  1. CRC校验失败 检查字节序是否正确(Modbus要求小端序) 验证数据帧完整性(是否包含完整的2字节CRC)
  2. 通信超时 调整ReadTimeoutWriteTimeout参数 检查物理连接(串口线/TCP网络)
  3. 数据解析错误 确认寄存器地址和数据类型匹配 使用十六进制工具验证原始数据
相关推荐
前端 贾公子10 小时前
v-if 与 v-for 的优先级对比
开发语言·前端·javascript
嗯嗯=12 小时前
python学习篇
开发语言·python·学习
全靠bug跑14 小时前
Spring Cache 实战:核心注解详解与缓存过期时间配置
java·redis·springcache
不会c嘎嘎14 小时前
QT中的常用控件 (二)
开发语言·qt
聆风吟º14 小时前
【数据结构手札】空间复杂度详解:概念 | 习题
java·数据结构·算法
计算机程序设计小李同学14 小时前
基于SpringBoot的个性化穿搭推荐及交流平台
java·spring boot·后端
是一个Bug14 小时前
50道核心JVM面试题
java·开发语言·面试
朱朱没烦恼yeye14 小时前
java基础学习
java·python·学习
烛阴15 小时前
C# 正则表达式(5):前瞻/后顾(Lookaround)——零宽断言做“条件校验”和“精确提取”
前端·正则表达式·c#
她和夏天一样热15 小时前
【观后感】Java线程池实现原理及其在美团业务中的实践
java·开发语言·jvm