Modbus是一种串行通信协议,最初由Modicon公司(现为施耐德电气的一部分)于1979年开发,用于其可编程逻辑控制器(PLC)。由于其简单性、开放性和易于实现的特点,Modbus已成为工业领域最流行的通信协议之一。
Modbus的核心特点:
Modbus协议有多种变体,适用于不同的物理层:
在实际应用中,Modbus RTU和Modbus TCP/IP是最常用的两种变体。
Modbus采用简单的请求-响应模型:
一个Modbus网络中通常有:
Modbus定义了四种不同的数据区域,每种区域有特定的访问权限:
数据类型 | 访问权限 | 地址范围 | 说明 |
---|---|---|---|
线圈(Coils) | 读写 | 0xxxx | 1位,布尔值(ON/OFF) |
离散输入 | 只读 | 1xxxx | 1位,布尔值 |
输入寄存器 | 只读 | 3xxxx | 16位,模拟量输入 |
保持寄存器 | 读写 | 4xxxx | 16位,模拟量输出 |
注意:这里的"x"表示数字,实际地址从0开始,但在协议中通常使用偏移量(如线圈地址0对应协议中的000001)。
Modbus RTU帧结构如下:
字段 | 长度 | 说明 |
---|---|---|
从站地址 | 1字节 | 1-247 (0为广播地址) |
功能码 | 1字节 | 指示要执行的操作类型 |
数据 | N字节 | 取决于功能码 |
CRC校验 | 2字节 | 循环冗余校验 |
RTU帧特点:
Modbus TCP/IP在RTU基础上增加了MBAP头:
字段 | 长度 | 说明 |
---|---|---|
事务标识符 | 2字节 | 用于请求/响应匹配 |
协议标识符 | 2字节 | 0表示Modbus协议 |
长度字段 | 2字节 | 后续字节数 |
单元标识符 | 1字节 | 通常与RTU从站地址相同 |
功能码 | 1字节 | 同RTU |
数据 | N字节 | 同RTU |
Modbus定义了多种功能码,主要分为三类:
常用功能码:
代码 | 名称 | 作用 |
---|---|---|
01 | 读线圈状态 | 读取一个或多个线圈的ON/OFF状态 |
02 | 读离散输入 | 读取离散输入的状态 |
03 | 读保持寄存器 | 读取保持寄存器的内容 |
04 | 读输入寄存器 | 读取输入寄存器的内容 |
05 | 写单个线圈 | 强制单个线圈ON或OFF |
06 | 写单个寄存器 | 写入单个保持寄存器 |
15 | 写多个线圈 | 强制多个线圈ON或OFF |
16 | 写多个寄存器 | 写入多个保持寄存器 |
异常响应:
当从设备检测到错误时,会返回异常响应,将功能码的最高位置1(即原功能码+0x80),并附加异常码。
Modbus使用大端字节序(Big-Endian)存储多字节数据。对于32位浮点数,通常有两种排列方式:
在开发时需要注意设备使用的具体格式。
所需工具:
NuGet包:
对于Modbus开发,推荐使用以下库:
安装命令:
Install-Package NModbus
Install-Package EasyModbusTCP
NModbus是一个开源的Modbus实现,支持:
核心类:
ModbusFactory
- 创建主站/从站实例的工厂类IModbusMaster
- 主站接口IModbusSlave
- 从站接口ModbusSerialMaster
- 串行主站实现ModbusTcpMaster
- TCP主站实现using System;
using System.Net.Sockets;
using Modbus.Device;
class ModbusTcpMasterExample
{
public static void Main()
{
// 创建TCP客户端连接
TcpClient tcpClient = new TcpClient("127.0.0.1", 502);
// 创建Modbus TCP主站
IModbusMaster master = ModbusIpMaster.CreateIp(tcpClient);
try
{
// 读取保持寄存器 (功能码03)
ushort startAddress = 0;
ushort numRegisters = 10;
ushort[] registers = master.ReadHoldingRegisters(1, startAddress, numRegisters);
Console.WriteLine("读取到的寄存器值:");
for (int i = 0; i < registers.Length; i++)
{
Console.WriteLine($"寄存器 {startAddress + i}: {registers[i]}");
}
// 写入单个寄存器 (功能码06)
ushort registerAddress = 5;
ushort value = 12345;
master.WriteSingleRegister(1, registerAddress, value);
Console.WriteLine($"已写入寄存器 {registerAddress} 值为 {value}");
}
finally
{
// 清理资源
master.Dispose();
tcpClient.Close();
}
}
}
using System;
using System.IO.Ports;
using Modbus.Device;
class ModbusRtuMasterExample
{
public static void Main()
{
// 配置串口
SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
try
{
// 打开串口
serialPort.Open();
// 创建Modbus RTU主站
IModbusSerialMaster master = ModbusSerialMaster.CreateRtu(serialPort);
// 设置超时
master.Transport.ReadTimeout = 1000;
master.Transport.WriteTimeout = 1000;
// 读取输入寄存器 (功能码04)
byte slaveId = 1;
ushort startAddress = 0;
ushort numRegisters = 5;
ushort[] inputRegisters = master.ReadInputRegisters(slaveId, startAddress, numRegisters);
Console.WriteLine("读取到的输入寄存器值:");
for (int i = 0; i < inputRegisters.Length; i++)
{
Console.WriteLine($"输入寄存器 {startAddress + i}: {inputRegisters[i]}");
}
// 写入多个线圈 (功能码15)
ushort coilAddress = 10;
bool[] coilValues = { true, false, true, true, false };
master.WriteMultipleCoils(slaveId, coilAddress, coilValues);
Console.WriteLine("已写入多个线圈状态");
}
catch (Exception ex)
{
Console.WriteLine($"发生错误: {ex.Message}");
}
finally
{
// 清理资源
serialPort?.Close();
}
}
}
Modbus设备可能返回异常响应,我们需要正确处理这些异常:
try
{
// 尝试读取不存在的寄存器
ushort[] registers = master.ReadHoldingRegisters(1, 10000, 10);
}
catch (Modbus.SlaveException ex)
{
Console.WriteLine($"Modbus异常: {ex.Message}");
Console.WriteLine($"功能码: {ex.FunctionCode}");
Console.WriteLine($"异常码: {ex.SlaveExceptionCode}");
// 常见异常码
switch (ex.SlaveExceptionCode)
{
case 1:
Console.WriteLine("非法功能码");
break;
case 2:
Console.WriteLine("非法数据地址");
break;
case 3:
Console.WriteLine("非法数据值");
break;
case 4:
Console.WriteLine("从站设备故障");
break;
default:
Console.WriteLine("未知异常");
break;
}
}
当需要读取大量数据时,Modbus的单个请求限制(通常最多125个寄存器)可能导致效率低下。我们可以实现分段读取:
public static ushort[] ReadLargeRegisters(IModbusMaster master, byte slaveId,
ushort startAddress, ushort numberOfPoints, ushort maxBatchSize = 125)
{
List<ushort> results = new List<ushort>();
ushort remaining = numberOfPoints;
ushort currentAddress = startAddress;
while (remaining > 0)
{
ushort batchSize = (remaining > maxBatchSize) ? maxBatchSize : remaining;
try
{
ushort[] batch = master.ReadHoldingRegisters(slaveId, currentAddress, batchSize);
results.AddRange(batch);
currentAddress += batchSize;
remaining -= batchSize;
}
catch (Exception ex)
{
Console.WriteLine($"读取地址 {currentAddress} 失败: {ex.Message}");
throw;
}
}
return results.ToArray();
}
Modbus寄存器存储的是16位无符号整数,但实际数据可能是其他类型:
// 将两个寄存器转换为32位整数
public static int ConvertToInt32(ushort highRegister, ushort lowRegister, bool isBigEndian = true)
{
byte[] bytes = new byte[4];
if (isBigEndian)
{
bytes[0] = (byte)(highRegister >> 8);
bytes[1] = (byte)highRegister;
bytes[2] = (byte)(lowRegister >> 8);
bytes[3] = (byte)lowRegister;
}
else
{
bytes[0] = (byte)(lowRegister >> 8);
bytes[1] = (byte)lowRegister;
bytes[2] = (byte)(highRegister >> 8);
bytes[3] = (byte)highRegister;
}
return BitConverter.ToInt32(bytes, 0);
}
// 将两个寄存器转换为IEEE 754浮点数
public static float ConvertToFloat(ushort highRegister, ushort lowRegister, bool isBigEndian = true)
{
byte[] bytes = new byte[4];
if (isBigEndian)
{
bytes[0] = (byte)(highRegister >> 8);
bytes[1] = (byte)highRegister;
bytes[2] = (byte)(lowRegister >> 8);
bytes[3] = (byte)lowRegister;
}
else
{
bytes[0] = (byte)(lowRegister >> 8);
bytes[1] = (byte)lowRegister;
bytes[2] = (byte)(highRegister >> 8);
bytes[3] = (byte)highRegister;
}
return BitConverter.ToSingle(bytes, 0);
}
using System;
using System.Net;
using System.Net.Sockets;
using Modbus.Device;
using Modbus.Data;
class ModbusTcpSlaveExample
{
private static ModbusSlave slave;
private static TcpListener listener;
private static bool isRunning = true;
public static void Main()
{
Console.WriteLine("Modbus TCP从站示例");
Console.WriteLine("按Ctrl+C停止服务");
// 设置数据存储
DataStore dataStore = DataStoreFactory.CreateDefaultDataStore();
// 初始化一些测试数据
dataStore.HoldingRegisters[0] = 1234;
dataStore.HoldingRegisters[1] = 5678;
dataStore.CoilDiscretes[0] = true;
dataStore.CoilDiscretes[1] = false;
// 创建TCP监听器
listener = new TcpListener(IPAddress.Any, 502);
listener.Start();
// 创建Modbus从站
slave = ModbusTcpSlave.CreateTcp(1, listener);
slave.DataStore = dataStore;
// 处理控制台中断
Console.CancelKeyPress += (sender, e) =>
{
isRunning = false;
e.Cancel = true;
};
// 启动从站
Console.WriteLine("从站已启动,等待请求...");
slave.ListenAsync().GetAwaiter().GetResult();
// 主循环
while (isRunning)
{
// 可以在这里更新数据存储或执行其他任务
System.Threading.Thread.Sleep(100);
}
// 清理资源
listener.Stop();
Console.WriteLine("从站已停止");
}
}
开发一个Modbus数据监控系统,具有以下功能:
ModbusMonitor
├── Core
│ ├── ModbusService (封装Modbus操作)
│ ├── DataRepository (数据存储)
│ └── AlarmService (报警管理)
├── Models
│ ├── DeviceConfig
│ ├── DataPoint
│ └── AlarmSetting
├── Services
│ ├── IModbusService
│ └── IDataLogger
└── UI (WPF或WinForms)
设备配置类:
public class DeviceConfig
{
public string Name { get; set; }
public byte SlaveId { get; set; }
public ProtocolType Protocol { get; set; } // TCP, RTU
public string ConnectionString { get; set; } // "127.0.0.1:502" 或 "COM3,9600,None,8,One"
public List<DataPointConfig> DataPoints { get; set; } = new List<DataPointConfig>();
}
public class DataPointConfig
{
public string Name { get; set; }
public PointType PointType { get; set; } // Coil, Input, HoldingRegister, etc.
public ushort Address { get; set; }
public DataType DataType { get; set; } // UInt16, Int32, Float, etc.
public int Length { get; set; } = 1; // 对于数组类型
public float ScalingFactor { get; set; } = 1.0f;
public float Offset { get; set; } = 0.0f;
public int PollingInterval { get; set; } = 1000; // ms
}
Modbus服务封装:
public interface IModbusService : IDisposable
{
bool IsConnected { get; }
Task<bool> ConnectAsync(DeviceConfig config);
Task DisconnectAsync();
Task<object> ReadDataPointAsync(DataPointConfig point);
Task<bool> WriteDataPointAsync(DataPointConfig point, object value);
event EventHandler<DataReceivedEventArgs> DataReceived;
event EventHandler<ErrorEventArgs> ErrorOccurred;
}
public class ModbusService : IModbusService
{
private IModbusMaster _master;
private DeviceConfig _currentConfig;
private readonly ILogger _logger;
public bool IsConnected => _master != null;
public ModbusService(ILogger logger)
{
_logger = logger;
}
public async Task<bool> ConnectAsync(DeviceConfig config)
{
try
{
if (IsConnected)
await DisconnectAsync();
_currentConfig = config;
switch (config.Protocol)
{
case ProtocolType.TCP:
var parts = config.ConnectionString.Split(':');
string ip = parts[0];
int port = parts.Length > 1 ? int.Parse(parts[1]) : 502;
var tcpClient = new TcpClient();
await tcpClient.ConnectAsync(ip, port);
_master = ModbusIpMaster.CreateIp(tcpClient);
break;
case ProtocolType.RTU:
var serialParams = config.ConnectionString.Split(',');
string portName = serialParams[0];
int baudRate = serialParams.Length > 1 ? int.Parse(serialParams[1]) : 9600;
Parity parity = serialParams.Length > 2 ? (Parity)Enum.Parse(typeof(Parity), serialParams[2]) : Parity.None;
int dataBits = serialParams.Length > 3 ? int.Parse(serialParams[3]) : 8;
StopBits stopBits = serialParams.Length > 4 ? (StopBits)Enum.Parse(typeof(StopBits), serialParams[4]) : StopBits.One;
var serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
serialPort.Open();
_master = ModbusSerialMaster.CreateRtu(serialPort);
break;
}
_master.Transport.ReadTimeout = 2000;
_master.Transport.WriteTimeout = 2000;
_logger.LogInformation($"成功连接到设备 {config.Name}");
return true;
}
catch (Exception ex)
{
_logger.LogError($"连接设备 {config.Name} 失败: {ex.Message}");
return false;
}
}
public async Task DisconnectAsync()
{
if (_master != null)
{
try
{
if (_master is ModbusIpMaster ipMaster)
{
ipMaster.Dispose();
}
else if (_master is ModbusSerialMaster serialMaster)
{
serialMaster.Dispose();
}
_logger.LogInformation($"已断开与设备 {_currentConfig?.Name} 的连接");
}
catch (Exception ex)
{
_logger.LogError($"断开连接时出错: {ex.Message}");
}
finally
{
_master = null;
_currentConfig = null;
}
}
}
public async Task<object> ReadDataPointAsync(DataPointConfig point)
{
if (!IsConnected)
throw new InvalidOperationException("未连接到设备");
try
{
object rawValue = null;
object scaledValue = null;
switch (point.PointType)
{
case PointType.Coil:
bool[] coils = await Task.Run(() =>
_master.ReadCoils(point.SlaveId, point.Address, (ushort)point.Length));
rawValue = coils[0];
scaledValue = (bool)rawValue;
break;
case PointType.HoldingRegister:
ushort[] registers = await Task.Run(() =>
_master.ReadHoldingRegisters(point.SlaveId, point.Address, (ushort)point.Length));
// 根据数据类型转换
switch (point.DataType)
{
case DataType.UInt16:
rawValue = registers[0];
scaledValue = (ushort)rawValue * point.ScalingFactor + point.Offset;
break;
case DataType.Int16:
rawValue = (short)registers[0];
scaledValue = (short)rawValue * point.ScalingFactor + point.Offset;
break;
case DataType.UInt32:
rawValue = (uint)(registers[0] << 16 | registers[1]);
scaledValue = (uint)rawValue * point.ScalingFactor + point.Offset;
break;
case DataType.Int32:
rawValue = (int)(registers[0] << 16 | registers[1]);
scaledValue = (int)rawValue * point.ScalingFactor + point.Offset;
break;
case DataType.Float:
byte[] bytes = new byte[4];
bytes[0] = (byte)(registers[0] >> 8);
bytes[1] = (byte)registers[0];
bytes[2] = (byte)(registers[1] >> 8);
bytes[3] = (byte)registers[1];
rawValue = BitConverter.ToSingle(bytes, 0);
scaledValue = (float)rawValue * point.ScalingFactor + point.Offset;
break;
}
break;
// 其他数据类型处理...
}
// 触发数据接收事件
DataReceived?.Invoke(this, new DataReceivedEventArgs
{
Point = point,
RawValue = rawValue,
ScaledValue = scaledValue,
Timestamp = DateTime.Now
});
return scaledValue;
}
catch (Exception ex)
{
_logger.LogError($"读取数据点 {point.Name} 失败: {ex.Message}");
ErrorOccurred?.Invoke(this, new ErrorEventArgs(ex));
throw;
}
}
// 其他方法实现...
}
数据轮询服务:
public class DataPollingService
{
private readonly IModbusService _modbusService;
private readonly IDataRepository _repository;
private readonly ILogger _logger;
private readonly Dictionary<DataPointConfig, Timer> _pollingTimers = new Dictionary<DataPointConfig, Timer>();
public DataPollingService(IModbusService modbusService, IDataRepository repository, ILogger logger)
{
_modbusService = modbusService;
_repository = repository;
_logger = logger;
_modbusService.DataReceived += OnDataReceived;
_modbusService.ErrorOccurred += OnErrorOccurred;
}
public void StartPolling(DeviceConfig device)
{
foreach (var point in device.DataPoints)
{
var timer = new Timer(point.PollingInterval);
timer.Elapsed += async (sender, e) =>
{
try
{
await _modbusService.ReadDataPointAsync(point);
}
catch (Exception ex)
{
_logger.LogError($"轮询数据点 {point.Name} 时出错: {ex.Message}");
}
};
timer.AutoReset = true;
timer.Enabled = true;
_pollingTimers[point] = timer;
}
}
public void StopPolling()
{
foreach (var timer in _pollingTimers.Values)
{
timer.Stop();
timer.Dispose();
}
_pollingTimers.Clear();
}
private void OnDataReceived(object sender, DataReceivedEventArgs e)
{
// 存储数据到数据库
_repository.SaveDataPoint(e.Point, e.RawValue, e.ScaledValue, e.Timestamp);
// 检查报警条件
CheckAlarmConditions(e.Point, e.ScaledValue);
}
private void OnErrorOccurred(object sender, ErrorEventArgs e)
{
_logger.LogError($"Modbus错误: {e.Error.Message}");
// 可以在这里实现重连逻辑
}
private void CheckAlarmConditions(DataPointConfig point, object value)
{
// 实现报警检查逻辑
// 如果value超过设定的阈值,触发报警
}
}
public async Task<object> RobustReadDataPoint(DataPointConfig point, int maxRetries = 3)
{
int retryCount = 0;
Exception lastError = null;
while (retryCount < maxRetries)
{
try
{
return await _modbusService.ReadDataPointAsync(point);
}
catch (IOException ex)
{
lastError = ex;
_logger.LogWarning($"IO异常,尝试重新连接 (尝试 {retryCount + 1}/{maxRetries})");
await Reconnect();
}
catch (SlaveException ex)
{
lastError = ex;
_logger.LogError($"从站异常: {ex.Message}");
break; // Modbus协议错误通常不需要重试
}
catch (Exception ex)
{
lastError = ex;
_logger.LogWarning($"读取失败,重试中 (尝试 {retryCount + 1}/{maxRetries}): {ex.Message}");
}
retryCount++;
await Task.Delay(1000 * retryCount); // 指数退避
}
throw new Exception($"读取数据点 {point.Name} 失败,达到最大重试次数", lastError);
}
private async Task Reconnect()
{
try
{
await _modbusService.DisconnectAsync();
await Task.Delay(1000);
await _modbusService.ConnectAsync(_currentConfig);
}
catch (Exception ex)
{
_logger.LogError($"重新连接失败: {ex.Message}");
throw;
}
}
Modbus通信通常涉及多线程操作,需要注意:
// 线程安全的Modbus操作包装器
public class ThreadSafeModbusMaster
{
private readonly IModbusMaster _master;
private readonly object _lock = new object();
public ThreadSafeModbusMaster(IModbusMaster master)
{
_master = master;
}
public ushort[] ReadHoldingRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
lock (_lock)
{
return _master.ReadHoldingRegisters(slaveAddress, startAddress, numberOfPoints);
}
}
// 包装其他需要的方法...
}
完善的日志记录对于Modbus应用至关重要:
public class ModbusLogger : ILogger
{
private readonly string _logFilePath;
public ModbusLogger(string logFilePath)
{
_logFilePath = logFilePath;
}
public void LogInformation(string message)
{
Log("INFO", message);
}
public void LogWarning(string message)
{
Log("WARN", message);
}
public void LogError(string message)
{
Log("ERROR", message);
}
public void LogDebug(string message)
{
Log("DEBUG", message);
}
private void Log(string level, string message)
{
string logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}";
// 控制台输出
Console.WriteLine(logEntry);
// 文件记录
try
{
File.AppendAllText(_logFilePath, logEntry + Environment.NewLine);
}
catch (Exception ex)
{
Console.WriteLine($"无法写入日志文件: {ex.Message}");
}
}
// 可以添加Modbus特定的日志方法,如记录原始帧数据
public void LogFrame(byte[] frame, bool isRequest)
{
string direction = isRequest ? "TX" : "RX";
string hex = BitConverter.ToString(frame).Replace("-", " ");
LogDebug($"{direction} Frame: {hex}");
}
}
现代工业系统中,Modbus常与OPC UA一起使用:
// 示例:将Modbus数据发布为OPC UA节点
public class ModbusOpcUaPublisher
{
private readonly IModbusService _modbusService;
private readonly ApplicationConfiguration _opcConfig;
private ApplicationInstance _application;
public ModbusOpcUaPublisher(IModbusService modbusService, string opcServerUri)
{
_modbusService = modbusService;
// 配置OPC UA应用
_opcConfig = new ApplicationConfiguration
{
ApplicationName = "Modbus OPC UA Server",
ApplicationUri = opcServerUri,
ApplicationType = ApplicationType.Server,
ServerConfiguration = new ServerConfiguration
{
BaseAddresses = { $"opc.tcp://localhost:62541/ModbusServer" },
SecurityPolicies = new ServerSecurityPolicyCollection(),
UserTokenPolicies = new UserTokenPolicyCollection()
},
SecurityConfiguration = new SecurityConfiguration(),
TransportConfigurations = new TransportConfigurationCollection(),
TransportQuotas = new TransportQuotas { OperationTimeout = 10000 },
ClientConfiguration = new ClientConfiguration()
};
_application = new ApplicationInstance(_opcConfig);
}
public async Task StartAsync()
{
// 初始化OPC UA服务器
await _application.CheckApplicationInstanceCertificate(false, 0);
var server = new StandardServer();
await _application.Start(server);
// 创建地址空间
var namespaceManager = new NamespaceManager(server.DefaultNamespace);
var objectsFolder = namespaceManager.GetObjectsFolder();
// 添加Modbus数据点
foreach (var point in _modbusService.GetDataPoints())
{
var variable = new DataVariableState(objectsFolder);
variable.NodeId = new NodeId(point.Name, namespaceManager.DefaultNamespaceIndex);
variable.BrowseName = new QualifiedName(point.Name);
variable.DisplayName = new LocalizedText(point.Name);
variable.DataType = GetOpcDataType(point.DataType);
variable.ValueRank = ValueRank.Scalar;
variable.AccessLevel = AccessLevels.CurrentRead;
variable.UserAccessLevel = AccessLevels.CurrentRead;
variable.Historizing = false;
// 添加节点
objectsFolder.AddChild(variable);
// 设置值更新回调
_modbusService.DataReceived += (sender, e) =>
{
if (e.Point.Name == point.Name)
{
variable.Value = e.ScaledValue;
variable.Timestamp = DateTime.Now;
variable.ClearChangeMasks(server.SystemContext, false);
}
};
}
}
private NodeId GetOpcDataType(DataType dataType)
{
switch (dataType)
{
case DataType.Boolean: return DataTypeIds.Boolean;
case DataType.Int16: return DataTypeIds.Int16;
case DataType.UInt16: return DataTypeIds.UInt16;
case DataType.Int32: return DataTypeIds.Int32;
case DataType.UInt32: return DataTypeIds.UInt32;
case DataType.Float: return DataTypeIds.Float;
default: return DataTypeIds.BaseDataType;
}
}
}
Modbus网关可以在不同协议间转换数据:
public class ModbusGateway
{
private readonly IModbusMaster _sourceMaster;
private readonly IModbusSlave _targetSlave;
private readonly List<PointMapping> _mappings;
private readonly Timer _pollingTimer;
public ModbusGateway(IModbusMaster sourceMaster, IModbusSlave targetSlave,
List<PointMapping> mappings, int pollingInterval = 1000)
{
_sourceMaster = sourceMaster;
_targetSlave = targetSlave;
_mappings = mappings;
_pollingTimer = new Timer(pollingInterval);
_pollingTimer.Elapsed += async (s, e) => await PollAndUpdate();
}
public void Start()
{
_pollingTimer.Start();
}
public void Stop()
{
_pollingTimer.Stop();
}
private async Task PollAndUpdate()
{
foreach (var mapping in _mappings)
{
try
{
object value = await ReadFromSource(mapping.Source);
await WriteToTarget(mapping.Target, value);
}
catch (Exception ex)
{
// 处理错误
}
}
}
private async Task<object> ReadFromSource(PointAddress source)
{
switch (source.PointType)
{
case PointType.Coil:
bool[] coils = await Task.Run(() =>
_sourceMaster.ReadCoils(source.SlaveId, source.Address, 1));
return coils[0];
case PointType.HoldingRegister:
ushort[] registers = await Task.Run(() =>
_sourceMaster.ReadHoldingRegisters(source.SlaveId, source.Address, 1));
return registers[0];
// 其他类型...
default:
throw new NotSupportedException($"不支持的源点类型: {source.PointType}");
}
}
private async Task WriteToTarget(PointAddress target, object value)
{
switch (target.PointType)
{
case PointType.Coil:
bool coilValue = (bool)value;
await Task.Run(() =>
_targetSlave.DataStore.CoilDiscretes[target.Address] = coilValue);
break;
case PointType.HoldingRegister:
ushort registerValue = Convert.ToUInt16(value);
await Task.Run(() =>
_targetSlave.DataStore.HoldingRegisters[target.Address] = registerValue);
break;
// 其他类型...
default:
throw new NotSupportedException($"不支持的目标点类型: {target.PointType}");
}
}
}
public class PointMapping
{
public PointAddress Source { get; set; }
public PointAddress Target { get; set; }
}
public class PointAddress
{
public byte SlaveId { get; set; }
public PointType PointType { get; set; }
public ushort Address { get; set; }
}
虽然传统Modbus缺乏内置安全机制,但我们可以实现一些保护措施:
// 示例:使用TLS包装Modbus TCP
public class SecureModbusTcpMaster : IModbusMaster
{
private readonly SslStream _sslStream;
private readonly ModbusIpMaster _innerMaster;
public SecureModbusTcpMaster(string host, int port, string serverCertName)
{
var tcpClient = new TcpClient(host, port);
_sslStream = new SslStream(tcpClient.GetStream(), false,
(sender, certificate, chain, errors) =>
{
if (errors != SslPolicyErrors.None)
return false;
var serverCertificate = (X509Certificate2)certificate;
return serverCertificate.GetNameInfo(X509NameType.SimpleName, false) == serverCertName;
});
_sslStream.AuthenticateAsClient(serverCertName);
_innerMaster = ModbusIpMaster.CreateIp(_sslStream);
}
// 实现IModbusMaster接口,委托给_innerMaster
public ushort[] ReadHoldingRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints)
{
return _innerMaster.ReadHoldingRegisters(slaveAddress, startAddress, numberOfPoints);
}
// 其他方法...
public void Dispose()
{
_innerMaster?.Dispose();
_sslStream?.Dispose();
}
}
问题1:无响应或超时
问题2:CRC校验错误
问题3:非法数据地址错误
问题4:响应延迟
使用Modbus嗅探工具:
记录原始帧数据:
public class ModbusFrameLogger
{
private readonly Stream _stream;
private readonly ILogger _logger;
public ModbusFrameLogger(Stream stream, ILogger logger)
{
_stream = stream;
_logger = logger;
}
public async Task<byte[]> ReadFrameAsync()
{
byte[] buffer = new byte[256];
int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
byte[] frame = new byte[bytesRead];
Array.Copy(buffer, frame, bytesRead);
_logger.LogDebug($"RX: {BitConverter.ToString(frame)}");
return frame;
}
return null;
}
public async Task WriteFrameAsync(byte[] frame)
{
_logger.LogDebug($"TX: {BitConverter.ToString(frame)}");
await _stream.WriteAsync(frame, 0, frame.Length);
}
}
// 不好的做法 - 单独读取每个寄存器
for (ushort i = 0; i < 10; i++)
{
ushort[] value = master.ReadHoldingRegisters(slaveId, i, 1);
// 处理value
}
// 好的做法 - 批量读取
ushort[] values = master.ReadHoldingRegisters(slaveId, 0, 10);
for (ushort i = 0; i < values.Length; i++)
{
// 处理values[i]
}
public async Task<Dictionary<string, object>> ReadMultiplePointsAsync(
List<DataPointConfig> points, int batchSize = 10)
{
var results = new Dictionary<string, object>();
var tasks = new List<Task>();
// 按从站地址分组
var groups = points.GroupBy(p => p.SlaveId);
foreach (var group in groups)
{
// 按批量大小分块
var chunks = group.Batch(batchSize);
foreach (var chunk in chunks)
{
// 为每个块创建并行任务
var chunkTasks = chunk.Select(async point =>
{
try
{
object value = await ReadDataPointAsync(point);
lock (results)
{
results[point.Name] = value;
}
}
catch (Exception ex)
{
// 处理错误
}
});
tasks.AddRange(chunkTasks);
}
}
await Task.WhenAll(tasks);
return results;
}
public class ModbusDataCache
{
private readonly ConcurrentDictionary<string, CacheItem> _cache;
private readonly TimeSpan _defaultExpiration;
public ModbusDataCache(TimeSpan defaultExpiration)
{
_cache = new ConcurrentDictionary<string, CacheItem>();
_defaultExpiration = defaultExpiration;
}
public async Task<object> GetOrAddAsync(string key, Func<Task<object>> valueFactory,
TimeSpan? expiration = null)
{
if (_cache.TryGetValue(key, out var item) && !item.IsExpired)
{
return item.Value;
}
object value = await valueFactory();
var newItem = new CacheItem(value, expiration ?? _defaultExpiration);
_cache.AddOrUpdate(key, newItem, (k, oldItem) => newItem);
return value;
}
private class CacheItem
{
public object Value { get; }
public DateTimeOffset Expiration { get; }
public bool IsExpired => DateTimeOffset.Now >= Expiration;
public CacheItem(object value, TimeSpan lifetime)
{
Value = value;
Expiration = DateTimeOffset.Now.Add(lifetime);
}
}
}
官方文档:
开源库:
测试工具:
Modbus RTU设备:
Modbus TCP设备:
开发板:
书籍:
在线课程:
社区:
优势:
局限:
尽管Modbus已有40多年历史,但它仍在工业领域广泛使用。未来的发展趋势包括:
适合使用Modbus的场景:
不适合的场景:
Modbus作为一种简单可靠的工业通信协议,仍然是工业自动化领域的重要组成部分。通过本指南,您应该已经掌握了使用C#进行Modbus开发的核心知识和技能。无论是连接传统设备还是开发现代工业应用,Modbus都是一个值得掌握的协议。
随着工业物联网(IIoT)的发展,Modbus可能会逐渐被更现代的协议所补充或替代,但由于其简单性和广泛部署,Modbus仍将在未来许多年继续发挥重要作用。掌握Modbus开发不仅有助于解决当前的工业通信需求,也为理解更复杂的工业协议奠定了基础。