说起工业自动化领域的通信协议,Modbus 绝对是绑不开的存在。这玩意儿诞生于1979年,比咱们很多开发者年龄都大,但至今仍活跃在全球超过70%的工业设备中。
我去年接手一个智能工厂项目,需要对接12台不同厂商的PLC设备。客户一开始说:"用现成的组态软件就行",结果发现授权费要小20万,而且扩展性极差。最后我们用C#从零实现了Modbus TCP主站,不仅省下大笔费用,还把数据采集周期从500ms压缩到了50ms以内。
读完这篇文章,你将收获:
很多开发者对Modbus的理解仅限于"读写寄存器",但实际项目中遇到的问题往往出在细节:
我见过最离谱的案例:某团队调试了两周,最后发现是把保持寄存器(Holding Register)和输入寄存器(Input Register)搞混了。
工业现场的网络环境跟办公室可不一样。电磁干扰、线缆老化、交换机过热……各种幺蛾子层出不穷。
| 问题类型 | 发生频率 | 平均恢复时间 |
|---|---|---|
| 连接超时 | 15次/天 | 2–5秒 |
| 响应数据不完整 | 8次/天 | 需重试 |
| 设备主动断开 | 3次/天 | 需重连 |
| CRC/协议校验失败 | 5次/天 | 需重试 |
如果你的代码里只有简单的try-catch,那基本上线就等着被叫去"救火"吧。
当设备数量超过10台,采集点位超过1000个时,同步阻塞的方式就会暴露问题:
Modbus TCP的报文结构其实挺简洁的,我给你画个图:

几个关键点:
csharp/// <summary>
/// Modbus功能码定义 - 这几个够应付90%的场景了
/// </summary>
public static class ModbusFunctionCodes
{
public const byte ReadCoils = 0x01; // 读线圈(离散输出)
public const byte ReadDiscreteInputs = 0x02; // 读离散输入
public const byte ReadHoldingRegisters = 0x03; // 读保持寄存器(最常用!)
public const byte ReadInputRegisters = 0x04; // 读输入寄存器
public const byte WriteSingleCoil = 0x05; // 写单个线圈
public const byte WriteSingleRegister = 0x06; // 写单个寄存器
public const byte WriteMultipleCoils = 0x0F; // 写多个线圈
public const byte WriteMultipleRegisters = 0x10;// 写多个寄存器(批量写入必备)
}
💡 经验之谈:实际项目中,0x03(读保持寄存器)和0x10(写多个寄存器)这两个功能码能覆盖80%以上的需求。先把这俩吃透再说别的。
咱们先来个能用的版本,理解核心流程,实际业务有标准的三方包,不用自己写:
csharpusing System.Net.Sockets;
/// <summary>
/// Modbus TCP 主站基础实现
/// 适用场景:单设备调试、协议学习、概念验证
/// </summary>
public class BasicModbusMaster : IDisposable
{
private TcpClient _tcpClient;
private NetworkStream _stream;
private ushort _transactionId;
private readonly object _lock = new object();
public string Host { get; }
public int Port { get; }
public int Timeout { get; set; } = 3000; // 默认3秒超时
public BasicModbusMaster(string host, int port = 502)
{
Host = host;
Port = port;
}
/// <summary>
/// 建立TCP连接
/// </summary>
public void Connect()
{
_tcpClient = new TcpClient();
// 这里用IAsyncResult实现超时控制,比直接Connect靠谱
var result = _tcpClient.BeginConnect(Host, Port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(Timeout));
if (!success)
{
throw new TimeoutException($"连接设备 {Host}:{Port} 超时");
}
_tcpClient.EndConnect(result);
_stream = _tcpClient.GetStream();
_stream.ReadTimeout = Timeout;
_stream.WriteTimeout = Timeout;
}
/// <summary>
/// 读取保持寄存器 - 功能码0x03
/// </summary>
/// <param name="unitId">从站地址</param>
/// <param name="startAddress">起始地址(从0开始)</param>
/// <param name="quantity">读取数量</param>
/// <returns>寄存器值数组</returns>
public ushort[] ReadHoldingRegisters(byte unitId, ushort startAddress, ushort quantity)
{
lock (_lock) // 确保线程安全
{
// 1. 构建请求报文
var request = BuildReadRequest(unitId, 0x03, startAddress, quantity);
// 2. 发送请求
_stream.Write(request, 0, request.Length);
// 3. 读取响应头(MBAP Header: 7字节)
var header = new byte[7];
int bytesRead = 0;
while (bytesRead < 7)
{
int read = _stream.Read(header, bytesRead, 7 - bytesRead);
if (read == 0) throw new IOException("连接被远程主机关闭");
bytesRead += read;
}
// 4. 解析响应长度,读取剩余数据
int remainingLength = (header[4] << 8) | header[5] - 1; // 减去UnitId
var data = new byte[remainingLength];
bytesRead = 0;
while (bytesRead < remainingLength)
{
int read = _stream.Read(data, bytesRead, remainingLength - bytesRead);
if (read == 0) throw new IOException("数据接收不完整");
bytesRead += read;
}
// 5. 检查异常响应
if ((data[0] & 0x80) != 0)
{
throw new ModbusException($"设备返回异常码: 0x{data[1]:X2}");
}
// 6. 解析寄存器数据(注意:大端序!)
int byteCount = data[1];
var registers = new ushort[quantity];
for (int i = 0; i < quantity; i++)
{
// 大端序转换:高字节在前
registers[i] = (ushort)((data[2 + i * 2] << 8) | data[3 + i * 2]);
}
return registers;
}
}
/// <summary>
/// 构建读取请求报文
/// </summary>
private byte[] BuildReadRequest(byte unitId, byte functionCode,
ushort startAddress, ushort quantity)
{
_transactionId++;
var request = new byte[12];
// MBAP Header
request[0] = (byte)(_transactionId >> 8); // 事务ID高字节
request[1] = (byte)(_transactionId & 0xFF); // 事务ID低字节
request[2] = 0x00; // 协议ID高字节
request[3] = 0x00; // 协议ID低字节
request[4] = 0x00; // 长度高字节
request[5] = 0x06; // 长度低字节(UnitId + FC + Addr + Qty = 6)
request[6] = unitId; // 单元标识符
// PDU
request[7] = functionCode; // 功能码
request[8] = (byte)(startAddress >> 8); // 起始地址高字节
request[9] = (byte)(startAddress & 0xFF); // 起始地址低字节
request[10] = (byte)(quantity >> 8); // 数量高字节
request[11] = (byte)(quantity & 0xFF); // 数量低字节
return request;
}
public void Dispose()
{
_stream?.Dispose();
_tcpClient?.Dispose();
}
}
/// <summary>
/// Modbus异常类
/// </summary>
public class ModbusException : Exception
{
public ModbusException(string message) : base(message) { }
}
使用示例:
csharp// 连接到PLC,读取10个保持寄存器
using var master = new BasicModbusMaster("127.0.0.1", 502);
master.Connect();
ushort[] values = master.ReadHoldingRegisters(
unitId: 1,
startAddress: 0, // 对应PLC地址40001
quantity: 10
);
Console.WriteLine($"读取到 {values.Length} 个寄存器:");
for (int i = 0; i < values.Length; i++)
{
Console.WriteLine($" 地址 {i}: {values[i]}");
}

⚠️ 踩坑预警:很多PLC厂商的地址是从1开始的(如40001),但Modbus协议层是从0开始。所以读40001时,实际传入的startAddress应该是0。这是一个大坑,不过测试几次也能发现规则。
基础版本在实验室能跑,但一到现场就各种崩。咱们加上重试机制和完善的异常处理:
csharpusing Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace AppModbusMaster
{
/// <summary>
/// 带重试机制的Modbus TCP主站
/// 适用场景:生产环境、网络不稳定的工业现场
/// </summary>
public class RobustModbusMaster : IDisposable
{
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private ushort _transactionId;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly ILogger<RobustModbusMaster>? _logger;
// 配置项
public string Host { get; }
public int Port { get; }
public int Timeout { get; set; } = 3000;
public int MaxRetries { get; set; } = 3;
public int RetryDelayMs { get; set; } = 500;
// 连接状态
public bool IsConnected => _tcpClient?.Connected == true;
private DateTime _lastActivityTime = DateTime.MinValue;
public RobustModbusMaster(string host, int port = 502,
ILogger<RobustModbusMaster>? logger = null)
{
Host = host;
Port = port;
_logger = logger;
}
/// <summary>
/// 确保连接可用,支持自动重连
/// </summary>
private async Task EnsureConnectedAsync()
{
if (IsConnected && (DateTime.Now - _lastActivityTime).TotalMinutes < 5)
{
return; // 连接正常且活跃
}
// 关闭旧连接
CloseConnection();
_tcpClient = new TcpClient();
try
{
using var cts = new CancellationTokenSource(Timeout);
await _tcpClient.ConnectAsync(Host, Port, cts.Token);
_stream = _tcpClient.GetStream();
_stream.ReadTimeout = Timeout;
_stream.WriteTimeout = Timeout;
_logger?.LogInformation("成功连接到 {Host}:{Port}", Host, Port);
}
catch (OperationCanceledException)
{
throw new TimeoutException($"连接 {Host}:{Port} 超时");
}
}
/// <summary>
/// 带重试的读取保持寄存器
/// </summary>
public async Task<ushort[]> ReadHoldingRegistersAsync(
byte unitId, ushort startAddress, ushort quantity)
{
Exception? lastException = null;
for (int attempt = 1; attempt <= MaxRetries; attempt++)
{
await _semaphore.WaitAsync();
try
{
await EnsureConnectedAsync();
var result = await ExecuteReadAsync(unitId, 0x03, startAddress, quantity);
_lastActivityTime = DateTime.Now;
if (attempt > 1)
{
_logger?.LogInformation(
"第 {Attempt} 次重试成功,地址范围: {Start}-{End}",
attempt, startAddress, startAddress + quantity - 1);
}
return result;
}
catch (Exception ex) when (ex is IOException or SocketException or TimeoutException)
{
lastException = ex;
_logger?.LogWarning(
"读取失败 (尝试 {Attempt}/{Max}): {Message}",
attempt, MaxRetries, ex.Message);
CloseConnection(); // 出错后关闭连接,下次重新建立
if (attempt < MaxRetries)
{
await Task.Delay(RetryDelayMs * attempt); // 递增延迟
}
}
finally
{
_semaphore.Release();
}
}
throw new ModbusExceptionEx(
$"读取寄存器失败,已重试 {MaxRetries} 次: {lastException?.Message}",
lastException);
}
/// <summary>
/// 执行实际的读取操作
/// </summary>
private async Task<ushort[]> ExecuteReadAsync(
byte unitId, byte functionCode, ushort startAddress, ushort quantity)
{
var request = BuildReadRequest(unitId, functionCode, startAddress, quantity);
// 发送请求
await _stream!.WriteAsync(request);
await _stream.FlushAsync();
// 读取响应(带超时)
using var cts = new CancellationTokenSource(Timeout);
// 读取MBAP头
var header = new byte[7];
await ReadExactlyAsync(_stream, header, 7, cts.Token);
// 验证事务ID
ushort responseTransactionId = (ushort)((header[0] << 8) | header[1]);
if (responseTransactionId != _transactionId)
{
throw new ModbusException(
$"事务ID不匹配: 期望 {_transactionId}, 收到 {responseTransactionId}");
}
// 读取PDU
int pduLength = ((header[4] << 8) | header[5]) - 1;
var pdu = new byte[pduLength];
await ReadExactlyAsync(_stream, pdu, pduLength, cts.Token);
// 检查异常响应
if ((pdu[0] & 0x80) != 0)
{
throw new ModbusException(GetExceptionMessage(pdu[1]));
}
// 解析数据
var registers = new ushort[quantity];
for (int i = 0; i < quantity; i++)
{
registers[i] = (ushort)((pdu[2 + i * 2] << 8) | pdu[3 + i * 2]);
}
return registers;
}
/// <summary>
/// 确保读取指定字节数
/// </summary>
private static async Task ReadExactlyAsync(
NetworkStream stream, byte[] buffer, int count, CancellationToken ct)
{
int offset = 0;
while (offset < count)
{
int read = await stream.ReadAsync(
buffer.AsMemory(offset, count - offset), ct);
if (read == 0)
{
throw new IOException("连接被远程主机关闭");
}
offset += read;
}
}
/// <summary>
/// 获取异常码的友好描述
/// </summary>
private static string GetExceptionMessage(byte exceptionCode)
{
return exceptionCode switch
{
0x01 => "非法功能码 - 设备不支持该操作",
0x02 => "非法数据地址 - 请检查寄存器地址范围",
0x03 => "非法数据值 - 写入的值超出允许范围",
0x04 => "从站设备故障 - 设备内部错误",
0x05 => "确认 - 请求已接受但处理中",
0x06 => "从站设备忙 - 请稍后重试",
0x0A => "网关路径不可用",
0x0B => "网关目标设备无响应",
_ => $"未知异常码: 0x{exceptionCode:X2}"
};
}
private byte[] BuildReadRequest(byte unitId, byte functionCode,
ushort startAddress, ushort quantity)
{
_transactionId++;
var request = new byte[12];
request[0] = (byte)(_transactionId >> 8);
request[1] = (byte)(_transactionId & 0xFF);
request[2] = 0x00;
request[3] = 0x00;
request[4] = 0x00;
request[5] = 0x06;
request[6] = unitId;
request[7] = functionCode;
request[8] = (byte)(startAddress >> 8);
request[9] = (byte)(startAddress & 0xFF);
request[10] = (byte)(quantity >> 8);
request[11] = (byte)(quantity & 0xFF);
return request;
}
private void CloseConnection()
{
try
{
_stream?.Dispose();
_tcpClient?.Dispose();
}
catch { /* 忽略关闭时的异常 */ }
finally
{
_stream = null;
_tcpClient = null;
}
}
public void Dispose()
{
_semaphore.Dispose();
CloseConnection();
}
}
public class ModbusExceptionEx : Exception
{
public ModbusExceptionEx(string message, Exception? inner = null)
: base(message, inner) { }
}
}
当设备多了,咱们得上并发。但这里有个关键问题:每个Modbus设备同一时刻只能处理一个请求。所以不能简单地多线程狂发,得用连接池 + 请求队列的方式:
csharpusing Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace AppModbusMaster
{
/// <summary>
/// 高性能Modbus设备管理器
/// 特点:连接池、请求队列、批量采集优化
/// </summary>
public class ModbusDeviceManager : IAsyncDisposable
{
private readonly ConcurrentDictionary<string, DeviceConnection> _connections = new();
private readonly Channel<DataPoint> _dataChannel;
private readonly ILogger<ModbusDeviceManager>? _logger;
public int MaxConcurrentDevices { get; set; } = 50;
public int BatchSize { get; set; } = 100; // 单次最大读取寄存器数
public ModbusDeviceManager(ILogger<ModbusDeviceManager>? logger = null)
{
_logger = logger;
_dataChannel = Channel.CreateBounded<DataPoint>(
new BoundedChannelOptions(10000)
{
FullMode = BoundedChannelFullMode.DropOldest
});
}
/// <summary>
/// 注册设备
/// </summary>
public void RegisterDevice(string deviceId, string host, int port = 502, byte unitId = 1)
{
var connection = new DeviceConnection
{
DeviceId = deviceId,
Host = host,
Port = port,
UnitId = unitId,
Master = new RobustModbusMaster(host, port, null)
};
_connections.TryAdd(deviceId, connection);
_logger?.LogInformation("注册设备: {DeviceId} -> {Host}:{Port}", deviceId, host, port);
}
/// <summary>
/// 并发采集所有设备
/// </summary>
public async Task<Dictionary<string, DeviceData>> CollectAllAsync(
IEnumerable<CollectTask> tasks,
CancellationToken ct = default)
{
var results = new ConcurrentDictionary<string, DeviceData>();
// 按设备分组任务
var tasksByDevice = tasks.GroupBy(t => t.DeviceId);
// 使用SemaphoreSlim控制并发数
using var semaphore = new SemaphoreSlim(MaxConcurrentDevices);
var collectTasks = tasksByDevice.Select(async group =>
{
await semaphore.WaitAsync(ct);
try
{
var deviceId = group.Key;
if (!_connections.TryGetValue(deviceId, out var connection))
{
_logger?.LogWarning("设备未注册: {DeviceId}", deviceId);
return;
}
var deviceData = new DeviceData { DeviceId = deviceId };
var sw = System.Diagnostics.Stopwatch.StartNew();
// 合并相邻地址,减少请求次数
var mergedRequests = MergeRequests(group.ToList());
foreach (var request in mergedRequests)
{
try
{
var values = await connection.Master.ReadHoldingRegistersAsync(
connection.UnitId,
request.StartAddress,
request.Quantity);
// 拆分回原始的采集点
foreach (var task in request.OriginalTasks)
{
int offset = task.Address - request.StartAddress;
var pointValues = new ushort[task.Count];
Array.Copy(values, offset, pointValues, 0, task.Count);
deviceData.Points[task.PointName] = new PointData
{
Values = pointValues,
Timestamp = DateTime.Now,
Quality = Quality.Good
};
}
}
catch (Exception ex)
{
_logger?.LogError(ex,
"采集失败: {DeviceId}, 地址: {Addr}",
deviceId, request.StartAddress);
// 标记这批点位为坏质量
foreach (var task in request.OriginalTasks)
{
deviceData.Points[task.PointName] = new PointData
{
Quality = Quality.Bad,
ErrorMessage = ex.Message
};
}
}
}
deviceData.CollectTime = sw.ElapsedMilliseconds;
results.TryAdd(deviceId, deviceData);
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(collectTasks);
return new Dictionary<string, DeviceData>(results);
}
/// <summary>
/// 合并相邻地址请求,优化通信效率
/// 这是性能提升的关键所在!
/// </summary>
private List<MergedRequest> MergeRequests(List<CollectTask> tasks)
{
var sorted = tasks.OrderBy(t => t.Address).ToList();
var merged = new List<MergedRequest>();
MergedRequest? current = null;
foreach (var task in sorted)
{
if (current == null)
{
current = new MergedRequest
{
StartAddress = task.Address,
Quantity = task.Count
};
current.OriginalTasks.Add(task);
}
else
{
// 检查是否可以合并(地址连续且不超过最大批量)
int newEnd = task.Address + task.Count;
int currentEnd = current.StartAddress + current.Quantity;
int gap = task.Address - currentEnd;
// 允许最多10个寄存器的间隙(读取一些无用数据换取更少的请求次数)
if (gap <= 10 && (newEnd - current.StartAddress) <= BatchSize)
{
current.Quantity = (ushort)(newEnd - current.StartAddress);
current.OriginalTasks.Add(task);
}
else
{
merged.Add(current);
current = new MergedRequest
{
StartAddress = task.Address,
Quantity = task.Count
};
current.OriginalTasks.Add(task);
}
}
}
if (current != null)
{
merged.Add(current);
}
return merged;
}
public async ValueTask DisposeAsync()
{
foreach (var connection in _connections.Values)
{
connection.Master.Dispose();
}
_connections.Clear();
}
}
// 辅助类定义
public class DeviceConnection
{
public string DeviceId { get; set; } = "";
public string Host { get; set; } = "";
public int Port { get; set; }
public byte UnitId { get; set; }
public RobustModbusMaster Master { get; set; } = null!;
}
public class CollectTask
{
public string DeviceId { get; set; } = "";
public string PointName { get; set; } = "";
public ushort Address { get; set; }
public ushort Count { get; set; } = 1;
}
public class MergedRequest
{
public ushort StartAddress { get; set; }
public ushort Quantity { get; set; }
public List<CollectTask> OriginalTasks { get; } = new();
}
public class DeviceData
{
public string DeviceId { get; set; } = "";
public Dictionary<string, PointData> Points { get; } = new();
public long CollectTime { get; set; }
}
public class PointData
{
public ushort[]? Values { get; set; }
public DateTime Timestamp { get; set; }
public Quality Quality { get; set; }
public string? ErrorMessage { get; set; }
}
public enum Quality { Good, Bad, Uncertain }
public class DataPoint
{
public string DeviceId { get; set; } = "";
public string PointName { get; set; } = "";
public ushort Address { get; set; }
public ushort[] Values { get; set; } = Array.Empty<ushort>();
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public Quality Quality { get; set; } = Quality.Uncertain;
public string? ErrorMessage { get; set; }
public int Count => Values.Length;
public bool HasError => Quality != Quality.Good || !string.IsNullOrEmpty(ErrorMessage);
}
}
完整使用示例:
csharpinternal class Program
{
static async Task Main(string[] args)
{
// 配置并注册设备
await using var manager = new ModbusDeviceManager();
// 批量注册生产线上的PLC
for (int i = 1; i <= 1; i++)
{
manager.RegisterDevice(
deviceId: $"PLC-{i:D2}",
host: $"127.0.0.{1 + i}",
port: 502,
unitId: 1
);
}
// 定义采集任务
var tasks = new List<CollectTask>();
for (int plc = 1; plc <= 20; plc++)
{
// 每台PLC采集50个点位
for (int point = 0; point < 50; point++)
{
tasks.Add(new CollectTask
{
DeviceId = $"PLC-{plc:D2}",
PointName = $"Temperature_{point}",
Address = (ushort)(0 + point),
Count = 1
});
}
}
// 执行并发采集
var sw = Stopwatch.StartNew();
var results = await manager.CollectAllAsync(tasks);
sw.Stop();
Console.WriteLine($"采集完成: {results.Count} 台设备, 耗时 {sw.ElapsedMilliseconds}ms");
// 统计采集质量
int goodCount = results.Values.SelectMany(d => d.Points.Values).Count(p => p.Quality == Quality.Good);
int totalCount = results.Values.SelectMany(d => d.Points.Values).Count();
Console.WriteLine($"采集成功率: {goodCount * 100.0 / totalCount:F1}%");
}
}

光读不行,还得能写。而且实际项目中,寄存器里存的可不光是整数:
csharpusing Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace AppModbusMaster
{
/// <summary>
/// 数据类型转换工具 - 处理Modbus中的各种数据格式
/// </summary>
public static class ModbusDataConverter
{
/// <summary>
/// 32位浮点数转换(占用2个寄存器)
/// 注意:不同厂商的字序可能不同!
/// </summary>
public static float ToFloat(ushort highWord, ushort lowWord, bool swapWords = false)
{
if (swapWords)
{
(highWord, lowWord) = (lowWord, highWord);
}
byte[] bytes = new byte[4];
bytes[0] = (byte)(lowWord & 0xFF);
bytes[1] = (byte)(lowWord >> 8);
bytes[2] = (byte)(highWord & 0xFF);
bytes[3] = (byte)(highWord >> 8);
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// 浮点数转为寄存器值
/// </summary>
public static (ushort High, ushort Low) FromFloat(float value, bool swapWords = false)
{
byte[] bytes = BitConverter.GetBytes(value);
ushort low = (ushort)(bytes[0] | (bytes[1] << 8));
ushort high = (ushort)(bytes[2] | (bytes[3] << 8));
return swapWords ? (low, high) : (high, low);
}
/// <summary>
/// 32位有符号整数
/// </summary>
public static int ToInt32(ushort highWord, ushort lowWord, bool swapWords = false)
{
if (swapWords)
{
(highWord, lowWord) = (lowWord, highWord);
}
return (highWord << 16) | lowWord;
}
/// <summary>
/// 字符串转换(ASCII编码,每个寄存器存2个字符)
/// </summary>
public static string ToString(ushort[] registers, bool swapBytes = true)
{
var chars = new List<char>();
foreach (var reg in registers)
{
char high = (char)(reg >> 8);
char low = (char)(reg & 0xFF);
if (swapBytes)
{
if (low != 0) chars.Add(low);
if (high != 0) chars.Add(high);
}
else
{
if (high != 0) chars.Add(high);
if (low != 0) chars.Add(low);
}
}
return new string(chars.ToArray()).TrimEnd('\0');
}
}
// 在RobustModbusMaster中添加写入方法
public partial class RobustModbusMaster
{
/// <summary>
/// 写入多个保持寄存器 - 功能码0x10
/// </summary>
public async Task WriteMultipleRegistersAsync(
byte unitId, ushort startAddress, ushort[] values)
{
for (int attempt = 1; attempt <= MaxRetries; attempt++)
{
await _semaphore.WaitAsync();
try
{
await EnsureConnectedAsync();
await ExecuteWriteAsync(unitId, startAddress, values);
_lastActivityTime = DateTime.Now;
return;
}
catch (Exception ex) when (ex is IOException or SocketException or TimeoutException)
{
_logger?.LogWarning("写入失败 (尝试 {Attempt}/{Max}): {Message}",
attempt, MaxRetries, ex.Message);
CloseConnection();
if (attempt < MaxRetries)
{
await Task.Delay(RetryDelayMs * attempt);
}
else
{
throw new ModbusExceptionEx($"写入失败,已重试 {MaxRetries} 次", ex);
}
}
finally
{
_semaphore.Release();
}
}
}
private async Task ExecuteWriteAsync(byte unitId, ushort startAddress, ushort[] values)
{
_transactionId++;
int byteCount = values.Length * 2;
// PDU = UnitID(1) + FC(1) + Addr(2) + Qty(2) + ByteCount(1) + Data
int requestLength = 7 + 6 + byteCount; // MBAP(7) + PDU头(5) + ByteCount(1) + Data
var request = new byte[requestLength];
// MBAP Header
request[0] = (byte)(_transactionId >> 8);
request[1] = (byte)(_transactionId & 0xFF);
request[2] = 0x00; // Protocol ID
request[3] = 0x00;
request[4] = (byte)((6 + byteCount + 1) >> 8); // 修正:Length字段
request[5] = (byte)((6 + byteCount + 1) & 0xFF);
request[6] = unitId;
// PDU
request[7] = 0x10; // 功能码 Write Multiple Registers
request[8] = (byte)(startAddress >> 8);
request[9] = (byte)(startAddress & 0xFF);
request[10] = (byte)(values.Length >> 8);
request[11] = (byte)(values.Length & 0xFF);
request[12] = (byte)byteCount;
// 数据(大端序)
for (int i = 0; i < values.Length; i++)
{
request[13 + i * 2] = (byte)(values[i] >> 8);
request[14 + i * 2] = (byte)(values[i] & 0xFF);
}
await _stream!.WriteAsync(request);
await _stream.FlushAsync();
// 读取响应 - 修正:先读MBAP头,再根据长度读取PDU
using var cts = new CancellationTokenSource(Timeout);
// 1. 读取MBAP头(7字节)
var header = new byte[7];
await ReadExactlyAsync(_stream, header, 7, cts.Token);
// 2. 验证事务ID
ushort responseTransactionId = (ushort)((header[0] << 8) | header[1]);
if (responseTransactionId != _transactionId)
{
throw new ModbusException(
$"事务ID不匹配: 期望 {_transactionId}, 收到 {responseTransactionId}");
}
// 3. 读取PDU(长度在MBAP头中)
int pduLength = ((header[4] << 8) | header[5]) - 1; // 减去Unit ID
var pdu = new byte[pduLength];
await ReadExactlyAsync(_stream, pdu, pduLength, cts.Token);
// 4. 检查异常响应
if ((pdu[0] & 0x80) != 0)
{
throw new ModbusException(GetExceptionMessage(pdu[1]));
}
// 5. 验证写入响应(正常响应:FC + StartAddr(2) + Quantity(2) = 5字节)
if (pdu.Length < 5)
{
throw new ModbusException("写入响应格式错误");
}
ushort responseAddress = (ushort)((pdu[1] << 8) | pdu[2]);
ushort responseQuantity = (ushort)((pdu[3] << 8) | pdu[4]);
_logger?.LogDebug(
"写入成功: 地址={Address}, 数量={Quantity}",
responseAddress, responseQuantity);
}
}
}
写入操作示例:
csharp// 写入单个浮点数(如设定温度)
float setpoint = 75.5f;
var (high, low) = ModbusDataConverter.FromFloat(setpoint);
await master.WriteMultipleRegistersAsync(1, 200, new ushort[] { high, low });
Console.WriteLine("参数写入成功!");
⚠️ 踩坑预警:写入操作一定要谨慎!最好加上参数范围校验,避免误写入导致设备故障。我们项目组曾因为写入了超出范围的值,导致一台变频器报警停机,整条产线停了2小时。
csharp// ❌ 错误写法:直接用BitConverter,在x86上是小端序
ushort[] regs = { 0x4248, 0x0000 }; // 期望得到50.0f
float wrong = BitConverter.ToSingle(
BitConverter.GetBytes(regs[0]).Concat(BitConverter.GetBytes(regs[1])).ToArray(), 0);
// 结果:1.58819e-41(完全错误)
// ✅ 正确写法:手动处理字节序
float correct = ModbusDataConverter.ToFloat(regs[0], regs[1]);
// 结果:50.0(正确)
设备手册地址 Modbus协议地址 说明 40001 0 保持寄存器,偏移1 40100 99 保持寄存器,偏移1 30001 0 输入寄存器,偏移1 00001 0 线圈,偏移1
建议:封装地址转换函数,统一处理:
csharppublic static ushort ConvertPlcAddress(int plcAddress)
{
// 40001-49999 -> 保持寄存器,地址-40001
// 30001-39999 -> 输入寄存器��地址-30001
// 00001-09999 -> 线圈,地址-1
return plcAddress switch
{
>= 40001 and <= 49999 => (ushort)(plcAddress - 40001),
>= 30001 and <= 39999 => (ushort)(plcAddress - 30001),
>= 1 and <= 9999 => (ushort)(plcAddress - 1),
_ => throw new ArgumentException($"无效的PLC地址: {plcAddress}")
};
}
高频采集时,多个响应可能粘在一起。解决方案:严格按长度字段读取,并校验事务ID。
| 品牌 | 差异 |
|---|---|
| 西门子S7 | 浮点数字序可能需要交换高低字 |
| 施耐德 | 单次最大读取量可能限制为100 |
| 三菱 | 某些型号不支持0x10批量写入 |
| 欧姆龙 | 地址映射规则可能与标准不同 |
大多数PLC同时只支持4-8个TCP连接。解决方案:使用连接池,复用连接,避免频繁断开重连。
协议先行:Modbus TCP的报文结构(MBAP + PDU)是一切的基础,字节序、地址偏移这些细节搞不清,代码写得再漂亮也是白搭
健壮性第一:工业现场不是实验室,重试机制、异常处理、自动重连是生产环境的标配,别等上线了再补
性能靠设计:地址合并策略能让请求次数降低90%+,这种优化比调参数有效得多
入门 → 基础协议实现 → 异常处理与重试 ↓ 进阶 → 并发采集优化 → 连接池设计 → 数据类型处理 ↓ 高级 → 与OPC UA集成 → 边缘计算网关 → 工业物联网平台
如果这篇文章对你有帮助,别忘了点赞、收藏、转发三连,让更多工业软件开发者少走弯路!
推荐标签:#C#开发 #工业通信 #Modbus #物联网 #性能优化
金句提炼:
相关信息
通过网盘分享的文件:AppModbusMaster.zip 链接: https://pan.baidu.com/s/1BP6TsTTgSyFfHfIsEJKeuw?pwd=u6cv 提取码: u6cv --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!