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
调试建议
- 使用串口调试助手(如友善串口调试助手)先测试设备通信
- 开启日志记录,记录完整的请求和响应字节,方便排查问题
- 注意超时设置,根据实际网络环境调整合适的超时时间
- 验证设备地址和寄存器地址,确保与实际设备配置一致

浙公网安备 33010602011771号