C# Modbus RTU通讯实现方案

C# Modbus RTU通讯实现方案,包含CRC16校验计算。这个实现可直接用于工业设备通讯。

核心实现方案

1. CRC16校验计算(Modbus标准)

这是Modbus RTU协议中最关键的部分,确保数据传输的完整性:

using System;

public static class Crc16
{
    /// <summary>
    /// Modbus CRC16计算
    /// </summary>
    /// <param name="data">需要计算的数据字节数组</param>
    /// <returns>2字节的CRC校验值</returns>
    public static byte[] ComputeCrc(byte[] data)
    {
        ushort crc = 0xFFFF; // CRC初始值
        
        foreach (byte b in data)
        {
            crc ^= b; // 与数据字节进行异或
            
            for (int i = 0; i < 8; i++)
            {
                // 判断最低位是否为1
                if ((crc & 0x0001) != 0)
                {
                    crc >>= 1; // 右移一位
                    crc ^= 0xA001; // 与多项式0xA001进行异或
                }
                else
                {
                    crc >>= 1; // 右移一位
                }
            }
        }
        
        // 返回CRC值(低字节在前,高字节在后 - Modbus字节序)
        return new byte[] { (byte)(crc & 0xFF), (byte)((crc >> 8) & 0xFF) };
    }
    
    /// <summary>
    /// 验证CRC校验码
    /// </summary>
    public static bool ValidateCrc(byte[] dataWithCrc)
    {
        if (dataWithCrc.Length < 2) return false;
        
        // 分离数据和CRC
        int dataLength = dataWithCrc.Length - 2;
        byte[] data = new byte[dataLength];
        Array.Copy(dataWithCrc, 0, data, 0, dataLength);
        
        // 计算数据的CRC
        byte[] calculatedCrc = ComputeCrc(data);
        
        // 比较计算的CRC与接收的CRC
        return calculatedCrc[0] == dataWithCrc[dataLength] && 
               calculatedCrc[1] == dataWithCrc[dataLength + 1];
    }
}

2. Modbus RTU通讯完整实现

using System;
using System.IO.Ports;
using System.Threading;

public class ModbusRtuMaster
{
    private SerialPort _serialPort;
    private readonly object _lockObject = new object();
    
    // 响应超时时间(毫秒)
    public int Timeout { get; set; } = 1000;
    
    /// <summary>
    /// 打开串口连接
    /// </summary>
    public bool Open(string portName, int baudRate = 9600, Parity parity = Parity.None, 
                     int dataBits = 8, StopBits stopBits = StopBits.One)
    {
        try
        {
            _serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
            _serialPort.ReadTimeout = Timeout;
            _serialPort.WriteTimeout = Timeout;
            _serialPort.Open();
            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"打开串口失败: {ex.Message}");
            return false;
        }
    }
    
    /// <summary>
    /// 读取保持寄存器(功能码0x03)
    /// </summary>
    /// <param name="slaveAddress">从站地址</param>
    /// <param name="startAddress">起始地址</param>
    /// <param name="registerCount">寄存器数量</param>
    public byte[] ReadHoldingRegisters(byte slaveAddress, ushort startAddress, ushort registerCount)
    {
        lock (_lockObject)
        {
            try
            {
                // 1. 构建请求帧
                byte[] request = BuildReadHoldingRegistersRequest(slaveAddress, startAddress, registerCount);
                
                // 2. 发送请求
                _serialPort.Write(request, 0, request.Length);
                
                // 3. 等待并接收响应(根据Modbus RTU规范,需要等待至少3.5个字符时间)
                Thread.Sleep(CalculateInterFrameDelay());
                
                // 4. 读取响应
                int bytesToRead = 5 + registerCount * 2; // 固定5字节 + 数据字节
                byte[] response = new byte[bytesToRead];
                int bytesRead = 0;
                int totalBytesRead = 0;
                
                // 读取完整的响应帧
                while (totalBytesRead < bytesToRead)
                {
                    bytesRead = _serialPort.Read(response, totalBytesRead, bytesToRead - totalBytesRead);
                    totalBytesRead += bytesRead;
                }
                
                // 5. 验证响应
                if (!ValidateResponse(response, slaveAddress, 0x03))
                {
                    throw new InvalidOperationException("响应验证失败");
                }
                
                // 6. 提取并返回寄存器数据
                return ExtractRegisterData(response, registerCount);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"读取寄存器失败: {ex.Message}");
                return null;
            }
        }
    }
    
    /// <summary>
    /// 构建读取保持寄存器的请求帧
    /// </summary>
    private byte[] BuildReadHoldingRegistersRequest(byte slaveAddress, ushort startAddress, ushort registerCount)
    {
        // 请求帧结构:[地址][功能码][起始地址高][起始地址低][寄存器数量高][寄存器数量低][CRC低][CRC高]
        byte[] request = new byte[8];
        
        request[0] = slaveAddress;          // 从站地址
        request[1] = 0x03;                  // 功能码:读取保持寄存器
        request[2] = (byte)(startAddress >> 8);    // 起始地址高字节
        request[3] = (byte)(startAddress & 0xFF);  // 起始地址低字节
        request[4] = (byte)(registerCount >> 8);   // 寄存器数量高字节
        request[5] = (byte)(registerCount & 0xFF); // 寄存器数量低字节
        
        // 计算CRC并添加到请求帧末尾
        byte[] crc = Crc16.ComputeCrc(request.Take(6).ToArray());
        request[6] = crc[0]; // CRC低字节
        request[7] = crc[1]; // CRC高字节
        
        return request;
    }
    
    /// <summary>
    /// 计算帧间延迟(3.5个字符时间)
    /// </summary>
    private int CalculateInterFrameDelay()
    {
        // 3.5个字符时间 = 3.5 * (1 + 数据位 + 停止位 + 奇偶位) / 波特率
        int charBits = 1 + _serialPort.DataBits + 
                      (_serialPort.StopBits == StopBits.One ? 1 : 2) + 
                      (_serialPort.Parity == Parity.None ? 0 : 1);
        
        return (int)(3500.0 * charBits / _serialPort.BaudRate);
    }
    
    /// <summary>
    /// 验证响应帧
    /// </summary>
    private bool ValidateResponse(byte[] response, byte expectedAddress, byte expectedFunctionCode)
    {
        // 检查长度
        if (response.Length < 5) return false;
        
        // 检查地址和功能码
        if (response[0] != expectedAddress) return false;
        
        // 错误响应检查(功能码最高位为1表示错误)
        if ((response[1] & 0x80) != 0)
        {
            Console.WriteLine($"Modbus错误响应,异常码: {response[2]}");
            return false;
        }
        
        if (response[1] != expectedFunctionCode) return false;
        
        // 验证CRC
        return Crc16.ValidateCrc(response);
    }
    
    /// <summary>
    /// 从响应中提取寄存器数据
    /// </summary>
    private byte[] ExtractRegisterData(byte[] response, ushort registerCount)
    {
        int dataLength = response[2]; // 第三个字节是数据字节数
        byte[] registerData = new byte[dataLength];
        Array.Copy(response, 3, registerData, 0, dataLength);
        return registerData;
    }
    
    /// <summary>
    /// 写入单个寄存器(功能码0x06)
    /// </summary>
    public bool WriteSingleRegister(byte slaveAddress, ushort registerAddress, ushort value)
    {
        lock (_lockObject)
        {
            try
            {
                // 构建请求帧:[地址][功能码][寄存器地址高][寄存器地址低][值高][值低][CRC低][CRC高]
                byte[] request = new byte[8];
                request[0] = slaveAddress;
                request[1] = 0x06; // 功能码:写单个寄存器
                request[2] = (byte)(registerAddress >> 8);
                request[3] = (byte)(registerAddress & 0xFF);
                request[4] = (byte)(value >> 8);
                request[5] = (byte)(value & 0xFF);
                
                byte[] crc = Crc16.ComputeCrc(request.Take(6).ToArray());
                request[6] = crc[0];
                request[7] = crc[1];
                
                // 发送请求
                _serialPort.Write(request, 0, request.Length);
                
                // 等待并读取响应
                Thread.Sleep(CalculateInterFrameDelay());
                byte[] response = new byte[8];
                _serialPort.Read(response, 0, 8);
                
                // 验证响应(写操作会回显相同的请求帧)
                return ValidateResponse(response, slaveAddress, 0x06);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"写入寄存器失败: {ex.Message}");
                return false;
            }
        }
    }
    
    public void Close()
    {
        if (_serialPort != null && _serialPort.IsOpen)
        {
            _serialPort.Close();
        }
    }
}

通讯流程说明

Modbus RTU通讯的完整流程图:

sequenceDiagram participant 上位机 as 上位机(C#程序) participant 串口 as 串口设备 participant 下位机 as 下位机(PLC/仪表) 上位机->>+串口: 1. 打开串口连接 串口-->>-上位机: 连接成功 上位机->>上位机: 2. 构建请求帧 Note over 上位机: [地址][功能码][数据][CRC] 上位机->>+串口: 3. 发送请求帧 串口->>+下位机: 4. 转发数据 Note over 下位机: 等待3.5字符时间 下位机->>下位机: 5. 验证CRC校验 alt CRC校验成功 下位机->>下位机: 6. 处理请求 下位机->>串口: 7. 返回响应帧 else CRC校验失败 下位机->>串口: 丢弃数据,不响应 end 串口->>-上位机: 8. 接收响应 上位机->>上位机: 9. 验证响应CRC alt 响应有效 上位机->>上位机: 10. 解析数据 else 响应无效 上位机->>上位机: 记录错误,可重试 end

使用示例

class Program
{
    static void Main()
    {
        // 创建Modbus主站实例
        var modbus = new ModbusRtuMaster();
        
        // 打开串口连接
        if (modbus.Open("COM3", 9600))
        {
            try
            {
                // 示例1:读取保持寄存器
                // 从地址1的从站,读取起始地址为0的2个寄存器
                byte[] registerData = modbus.ReadHoldingRegisters(0x01, 0x0000, 2);
                
                if (registerData != null && registerData.Length == 4)
                {
                    // 将字节数据转换为实际值(注意字节序)
                    ushort value1 = (ushort)((registerData[0] << 8) | registerData[1]);
                    ushort value2 = (ushort)((registerData[2] << 8) | registerData[3]);
                    
                    Console.WriteLine($"寄存器0: {value1}");
                    Console.WriteLine($"寄存器1: {value2}");
                }
                
                // 示例2:写入单个寄存器
                // 向地址1的从站,写入值1234到地址2的寄存器
                bool writeSuccess = modbus.WriteSingleRegister(0x01, 0x0002, 1234);
                Console.WriteLine($"写入操作: {(writeSuccess ? "成功" : "失败")}");
                
                // 示例3:直接使用CRC计算
                byte[] testData = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x02 };
                byte[] crcValue = Crc16.ComputeCrc(testData);
                Console.WriteLine($"CRC16校验值: 0x{crcValue[0]:X2} 0x{crcValue[1]:X2}");
            }
            finally
            {
                modbus.Close();
            }
        }
    }
}

关键要点说明

项目 说明
CRC16算法 Modbus使用CRC-16-IBM(多项式0x8005),计算时初始值为0xFFFF,结果低字节在前
字节序 Modbus使用大端序(高字节在前),注意与主机字节序的转换
帧间延迟 RTU模式要求帧间至少有3.5个字符时间的空闲,否则会视为同一帧
异常处理 从站可能返回异常响应(功能码最高位置1),需要正确处理
线程安全 串口操作需要加锁,避免多线程同时访问

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

调试建议

  1. 使用串口调试助手(如友善串口调试助手)先测试设备通信
  2. 开启日志记录,记录完整的请求和响应字节,方便排查问题
  3. 注意超时设置,根据实际网络环境调整合适的超时时间
  4. 验证设备地址和寄存器地址,确保与实际设备配置一致
posted @ 2025-12-14 11:25  u95900090  阅读(0)  评论(0)    收藏  举报