Modbus 是工业自动化领域应用最广泛的通信协议之一,由 Modicon 公司(现施耐德电气)于 1979 年发布。它是一种主从(Master/Slave)架构的应用层协议,广泛应用于 PLC、传感器、变频器、仪表等工业设备之间的数据交换。在 .NET/WPF 开发中,通过串口(Serial Port)与 Modbus 设备通信是工业软件开发的核心技能。
| 传输模式 | 物理层 | 数据格式 | 传输距离 | 典型应用 |
|---|
| Modbus RTU | RS-485/RS-232 | 二进制数据,CRC校验 | 最长1200米 | 工业现场设备通信 |
| Modbus ASCII | RS-485/RS-232 | ASCII字符,LRC校验 | 最长1200米 | 调试和简单场景 |
| Modbus TCP | 以太网 | TCP/IP协议 | 网络范围内 | 车间级/厂级通信 |
| 组成部分 | 长度 | 说明 |
|---|
| 设备地址 (Slave Address) | 1字节 | 从站地址,0-247 |
| 功能码 (Function Code) | 1字节 | 指定操作类型 |
| 数据 (Data) | N字节 | 请求/响应数据 |
| CRC校验 (CRC) | 2字节 | 循环冗余校验,低字节在前 |
| 功能码 | 说明 | 操作对象 |
|---|
| 01 | 读线圈状态 | Coil(可读写位) |
| 02 | 读离散输入 | Discrete Input(只读位) |
| 03 | 读保持寄存器 | Holding Register(可读写字) |
| 04 | 读输入寄存器 | Input Register(只读字) |
| 05 | 写单个线圈 | 单个 Coil |
| 06 | 写单个寄存器 | 单个 Holding Register |
| 15 | 写多个线圈 | 多个 Coil |
| 16 | 写多个寄存器 | 多个 Holding Register |
Install-Package System.IO.Ports
Install-Package NModbus
/// <summary>
/// 串口通信参数配置
/// </summary>
public class SerialPortConfig
{
/// <summary>端口名称,如 COM3、/dev/ttyUSB0</summary>
public string PortName { get; set; } = "COM3";
/// <summary>波特率,常见值:9600、19200、38400、115200</summary>
public int BaudRate { get; set; } = 9600;
/// <summary>数据位,通常为8</summary>
public int DataBits { get; set; } = 8;
/// <summary>停止位</summary>
public StopBits StopBits { get; set; } = StopBits.One;
/// <summary>校验位</summary>
public Parity Parity { get; set; } = Parity.None;
/// <summary>读超时(毫秒)</summary>
public int ReadTimeout { get; set; } = 1000;
/// <summary>写超时(毫秒)</summary>
public int WriteTimeout { get; set; } = 1000;
}
| 设备类型 | 波特率 | 数据位 | 停止位 | 校验位 |
|---|
| 标准Modbus设备 | 9600 | 8 | 1 | None |
| 部分施耐德PLC | 19200 | 8 | 1 | Even |
| 部分西门子变频器 | 38400 | 8 | 1 | Even |
| 部分ABB设备 | 9600 | 8 | 1 | None |
/// <summary>
/// Modbus RTU 主站管理器
/// </summary>
public class ModbusRtuMaster : IDisposable
{
private SerialPort _serialPort;
private IModbusMaster _master;
private readonly SerialPortConfig _config;
private readonly ILogger<ModbusRtuMaster> _logger;
public bool IsConnected => _serialPort?.IsOpen ?? false;
public ModbusRtuMaster(SerialPortConfig config, ILogger<ModbusRtuMaster> logger)
{
_config = config;
_logger = logger;
}
/// <summary>
/// 打开串口连接
/// </summary>
public bool Connect()
{
try
{
_serialPort = new SerialPort(_config.PortName)
{
BaudRate = _config.BaudRate,
DataBits = _config.DataBits,
StopBits = _config.StopBits,
Parity = _config.Parity,
ReadTimeout = _config.ReadTimeout,
WriteTimeout = _config.WriteTimeout
};
_serialPort.Open();
// 创建 Modbus RTU 主站
var factory = new ModbusFactory();
_master = factory.CreateRtuMaster(_serialPort);
_master.Transport.ReadTimeout = _config.ReadTimeout;
_master.Transport.WriteTimeout = _config.WriteTimeout;
_master.Transport.Retries = 3;
_master.Transport.WaitToRetryMilliseconds = 200;
_logger.LogInformation("串口 {Port} 连接成功,波特率 {Baud}",
_config.PortName, _config.BaudRate);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "串口连接失败:{Port}", _config.PortName);
return false;
}
}
/// <summary>
/// 断开连接
/// </summary>
public void Disconnect()
{
if (_serialPort?.IsOpen == true)
{
_serialPort.Close();
_serialPort.Dispose();
_serialPort = null;
_master = null;
}
}
public void Dispose()
{
Disconnect();
}
}
/// <summary>
/// 读取线圈状态 — 功能码 01
/// 从地址 startAddress 开始读取 numberOfPoints 个线圈
/// </summary>
public async Task<bool[]> ReadCoilsAsync(byte slaveId, ushort startAddress, ushort numberOfPoints)
{
try
{
var coils = await _master.ReadCoilsAsync(slaveId, startAddress, numberOfPoints);
_logger.LogDebug("读取线圈:Slave={Slave}, Addr={Addr}, Count={Count}",
slaveId, startAddress, numberOfPoints);
return coils;
}
catch (TimeoutException ex)
{
_logger.LogError(ex, "读取线圈超时:Slave={Slave}, Addr={Addr}", slaveId, startAddress);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "读取线圈异常:Slave={Slave}", slaveId);
throw;
}
}
/// <summary>
/// 读取保持寄存器 — 功能码 03
/// 最常用的读取操作,用于读取温度、压力、频率等模拟量
/// </summary>
public async Task<ushort[]> ReadHoldingRegistersAsync(byte slaveId, ushort startAddress, ushort numberOfPoints)
{
try
{
var registers = await _master.ReadHoldingRegistersAsync(slaveId, startAddress, numberOfPoints);
_logger.LogDebug("读保持寄存器:Slave={Slave}, Addr={Addr}, Count={Count}, Data=[{Data}]",
slaveId, startAddress, numberOfPoints, string.Join(", ", registers));
return registers;
}
catch (Exception ex)
{
_logger.LogError(ex, "读保持寄存器失败:Slave={Slave}", slaveId);
throw;
}
}
/// <summary>
/// 读取输入寄存器 — 功能码 04
/// </summary>
public async Task<ushort[]> ReadInputRegistersAsync(byte slaveId, ushort startAddress, ushort numberOfPoints)
{
try
{
return await _master.ReadInputRegistersAsync(slaveId, startAddress, numberOfPoints);
}
catch (Exception ex)
{
_logger.LogError(ex, "读输入寄存器失败:Slave={Slave}", slaveId);
throw;
}
}
/// <summary>
/// 读取离散输入 — 功能码 02
/// </summary>
public async Task<bool[]> ReadDiscreteInputsAsync(byte slaveId, ushort startAddress, ushort numberOfPoints)
{
try
{
return await _master.ReadInputsAsync(slaveId, startAddress, numberOfPoints);
}
catch (Exception ex)
{
_logger.LogError(ex, "读离散输入失败:Slave={Slave}", slaveId);
throw;
}
}
/// <summary>
/// 写单个线圈 — 功能码 05
/// 用于控制继电器、阀门等开关量
/// </summary>
public async Task WriteSingleCoilAsync(byte slaveId, ushort address, bool value)
{
try
{
await _master.WriteSingleCoilAsync(slaveId, address, value);
_logger.LogInformation("写线圈:Slave={Slave}, Addr={Addr}, Value={Value}",
slaveId, address, value);
}
catch (Exception ex)
{
_logger.LogError(ex, "写线圈失败:Slave={Slave}, Addr={Addr}", slaveId, address);
throw;
}
}
/// <summary>
/// 写单个寄存器 — 功能码 06
/// 用于设置频率、阈值等参数
/// </summary>
public async Task WriteSingleRegisterAsync(byte slaveId, ushort address, ushort value)
{
try
{
await _master.WriteSingleRegisterAsync(slaveId, address, value);
_logger.LogInformation("写寄存器:Slave={Slave}, Addr={Addr}, Value={Value}",
slaveId, address, value);
}
catch (Exception ex)
{
_logger.LogError(ex, "写寄存器失败:Slave={Slave}, Addr={Addr}", slaveId, address);
throw;
}
}
/// <summary>
/// 写多个寄存器 — 功能码 16
/// 用于批量写入参数
/// </summary>
public async Task WriteMultipleRegistersAsync(byte slaveId, ushort startAddress, ushort[] values)
{
try
{
await _master.WriteMultipleRegistersAsync(slaveId, startAddress, values);
_logger.LogInformation("写多个寄存器:Slave={Slave}, Addr={Addr}, Count={Count}",
slaveId, startAddress, values.Length);
}
catch (Exception ex)
{
_logger.LogError(ex, "写多个寄存器失败:Slave={Slave}", slaveId);
throw;
}
}
/// <summary>
/// 写多个线圈 — 功能码 15
/// </summary>
public async Task WriteMultipleCoilsAsync(byte slaveId, ushort startAddress, bool[] values)
{
try
{
await _master.WriteMultipleCoilsAsync(slaveId, startAddress, values);
_logger.LogInformation("写多个线圈:Slave={Slave}, Addr={Addr}, Count={Count}",
slaveId, startAddress, values.Length);
}
catch (Exception ex)
{
_logger.LogError(ex, "写多个线圈失败:Slave={Slave}", slaveId);
throw;
}
}
/// <summary>
/// Modbus CRC16 校验算法
/// RTU 模式使用 CRC-16/MODBUS 多项式 0xA001
/// </summary>
public static class ModbusCrc
{
/// <summary>
/// 计算 CRC16 校验码
/// </summary>
/// <param name="data">需要校验的数据</param>
/// <returns>CRC校验码(低字节在前,高字节在后)</returns>
public static ushort ComputeCrc(byte[] data)
{
ushort crc = 0xFFFF;
foreach (byte b in data)
{
crc ^= b;
for (int i = 0; i < 8; i++)
{
if ((crc & 0x0001) != 0)
{
crc >>= 1;
crc ^= 0xA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
/// <summary>
/// 验证接收数据的 CRC 校验码
/// </summary>
public static bool ValidateCrc(byte[] data, ushort expectedCrc)
{
var computed = ComputeCrc(data);
return computed == expectedCrc;
}
/// <summary>
/// 将 CRC 追加到数据末尾(低字节在前)
/// </summary>
public static byte[] AppendCrc(byte[] data)
{
var crc = ComputeCrc(data);
var result = new byte[data.Length + 2];
Array.Copy(data, result, data.Length);
result[data.Length] = (byte)(crc & 0xFF); // CRC低字节
result[data.Length + 1] = (byte)((crc >> 8) & 0xFF); // CRC高字节
return result;
}
}
/// <summary>
/// 手动构建和解析 Modbus RTU 帧 — 理解协议细节
/// </summary>
public class ModbusRtuFrame
{
/// <summary>
/// 构建读保持寄存器请求帧(FC03)
/// </summary>
public static byte[] BuildReadHoldingRegistersRequest(byte slaveId, ushort startAddress, ushort count)
{
// 帧结构:[从站地址][功能码][起始地址高][起始地址低][数量高][数量低][CRC低][CRC高]
var frame = new byte[6];
frame[0] = slaveId;
frame[1] = 0x03; // 功能码
frame[2] = (byte)((startAddress >> 8) & 0xFF); // 起始地址高字节
frame[3] = (byte)(startAddress & 0xFF); // 起始地址低字节
frame[4] = (byte)((count >> 8) & 0xFF); // 数量高字节
frame[5] = (byte)(count & 0xFF); // 数量低字节
return ModbusCrc.AppendCrc(frame);
}
/// <summary>
/// 解析读保持寄存器响应帧(FC03)
/// </summary>
public static ushort[] ParseReadHoldingRegistersResponse(byte[] response)
{
// 响应结构:[从站地址][功能码][字节数][数据...][CRC低][CRC高]
byte slaveId = response[0];
byte functionCode = response[1];
byte byteCount = response[2];
// 检查异常响应(功能码最高位为1表示异常)
if ((functionCode & 0x80) != 0)
{
byte exceptionCode = response[2];
throw new InvalidOperationException($"Modbus异常响应,异常码:{exceptionCode}");
}
// 提取寄存器值
int registerCount = byteCount / 2;
var registers = new ushort[registerCount];
for (int i = 0; i < registerCount; i++)
{
registers[i] = (ushort)((response[3 + i * 2] << 8) | response[3 + i * 2 + 1]);
}
return registers;
}
}
/// <summary>
/// Modbus 寄存器数据转换工具
/// 一个寄存器为16位(2字节),需要根据设备文档解析实际值
/// </summary>
public static class ModbusDataConverter
{
/// <summary>
/// 16位无符号整数 → 直接就是寄存器值
/// </summary>
public static ushort ToUInt16(ushort register) => register;
/// <summary>
/// 16位有符号整数(补码)
/// </summary>
public static short ToInt16(ushort register) => (short)register;
/// <summary>
/// 两个寄存器 → 32位浮点数(IEEE 754)
/// 典型应用:温度、压力、流量等模拟量
/// </summary>
public static float ToFloat32(ushort highRegister, ushort lowRegister)
{
// 将两个16位寄存器合并为32位
uint combined = ((uint)highRegister << 16) | lowRegister;
byte[] bytes = BitConverter.GetBytes(combined);
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// 浮点数 → 两个寄存器
/// </summary>
public static (ushort High, ushort Low) FromFloat32(float value)
{
byte[] bytes = BitConverter.GetBytes(value);
uint combined = BitConverter.ToUInt32(bytes, 0);
ushort high = (ushort)((combined >> 16) & 0xFFFF);
ushort low = (ushort)(combined & 0xFFFF);
return (high, low);
}
/// <summary>
/// 两个寄存器 → 32位有符号整数
/// </summary>
public static int ToInt32(ushort highRegister, ushort lowRegister)
{
uint combined = ((uint)highRegister << 16) | lowRegister;
return (int)combined;
}
/// <summary>
/// 寄存器值 → 缩放后的实际值
/// 很多设备用整数存储,需要乘以精度因子
/// 例如:温度 250 表示 25.0°C
/// </summary>
public static double ToScaledValue(ushort register, double scaleFactor = 0.1, double offset = 0)
{
return register * scaleFactor + offset;
}
/// <summary>
/// 多个连续寄存器 → 字符串(每个寄存器存2个ASCII字符)
/// </summary>
public static string ToString(ushort[] registers)
{
var sb = new StringBuilder();
foreach (var reg in registers)
{
sb.Append((char)(reg >> 8));
sb.Append((char)(reg & 0xFF));
}
return sb.ToString().TrimEnd('\0', ' ');
}
}
/// <summary>
/// Modbus 设备监控 ViewModel
/// </summary>
public class ModbusMonitorViewModel : ObservableObject, IDisposable
{
private readonly ModbusRtuMaster _modbusMaster;
private readonly ILogger<ModbusMonitorViewModel> _logger;
private CancellationTokenSource _cts;
private bool _isMonitoring;
// 设备参数
public byte SlaveId { get; set; } = 1;
// 监控数据
private float _temperature;
public float Temperature
{
get => _temperature;
set => SetProperty(ref _temperature, value);
}
private float _pressure;
public float Pressure
{
get => _pressure;
set => SetProperty(ref _pressure, value);
}
private bool _pumpRunning;
public bool PumpRunning
{
get => _pumpRunning;
set => SetProperty(ref _pumpRunning, value);
}
private bool _valveOpen;
public bool ValveOpen
{
get => _valveOpen;
set => SetProperty(ref _valveOpen, value);
}
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
// 历史数据记录
public ObservableCollection<DataRecord> HistoryData { get; } = new();
// 命令
public IAsyncRelayCommand ConnectCommand { get; }
public IAsyncRelayCommand DisconnectCommand { get; }
public IAsyncRelayCommand StartMonitorCommand { get; }
public IAsyncRelayCommand StopMonitorCommand { get; }
public IAsyncRelayCommand TogglePumpCommand { get; }
public IAsyncRelayCommand ToggleValveCommand { get; }
public ModbusMonitorViewModel(ModbusRtuMaster modbusMaster, ILogger<ModbusMonitorViewModel> logger)
{
_modbusMaster = modbusMaster;
_logger = logger;
ConnectCommand = new AsyncRelayCommand(ConnectAsync, () => !IsConnected);
DisconnectCommand = new AsyncRelayCommand(DisconnectAsync, () => IsConnected);
StartMonitorCommand = new AsyncRelayCommand(StartMonitoringAsync, () => IsConnected && !_isMonitoring);
StopMonitorCommand = new AsyncRelayCommand(StopMonitoringAsync, () => _isMonitoring);
TogglePumpCommand = new AsyncRelayCommand(TogglePumpAsync, () => IsConnected);
ToggleValveCommand = new AsyncRelayCommand(ToggleValveAsync, () => IsConnected);
}
/// <summary>
/// 启动周期性轮询
/// </summary>
private async Task StartMonitoringAsync()
{
_cts = new CancellationTokenSource();
_isMonitoring = true;
StatusMessage = "监控运行中...";
try
{
while (!_cts.Token.IsCancellationRequested)
{
await ReadDeviceDataAsync();
await Task.Delay(1000, _cts.Token); // 1秒轮询间隔
}
}
catch (OperationCanceledException)
{
// 正常取消
}
finally
{
_isMonitoring = false;
StatusMessage = "监控已停止";
}
}
/// <summary>
/// 读取设备所有数据
/// </summary>
private async Task ReadDeviceDataAsync()
{
try
{
// 读取温度和压力(保持寄存器地址0-1,两个寄存器组成一个Float)
var registers = await _modbusMaster.ReadHoldingRegistersAsync(SlaveId, 0, 4);
Temperature = ModbusDataConverter.ToFloat32(registers[0], registers[1]);
Pressure = ModbusDataConverter.ToFloat32(registers[2], registers[3]);
// 读取设备状态(线圈地址0-7)
var coils = await _modbusMaster.ReadCoilsAsync(SlaveId, 0, 8);
PumpRunning = coils[0];
ValveOpen = coils[1];
// 记录历史数据
HistoryData.Insert(0, new DataRecord
{
Timestamp = DateTime.Now,
Temperature = Temperature,
Pressure = Pressure
});
// 保留最近100条记录
while (HistoryData.Count > 100)
{
HistoryData.RemoveAt(HistoryData.Count - 1);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "读取设备数据失败");
StatusMessage = $"通信异常:{ex.Message}";
}
}
/// <summary>
/// 切换水泵状态
/// </summary>
private async Task TogglePumpAsync()
{
await _modbusMaster.WriteSingleCoilAsync(SlaveId, 0, !PumpRunning);
}
/// <summary>
/// 切换阀门状态
/// </summary>
private async Task ToggleValveAsync()
{
await _modbusMaster.WriteSingleCoilAsync(SlaveId, 1, !ValveOpen);
}
private async Task ConnectAsync()
{
IsConnected = _modbusMaster.Connect();
StatusMessage = IsConnected ? "已连接" : "连接失败";
}
private async Task DisconnectAsync()
{
if (_isMonitoring) await StopMonitoringAsync();
_modbusMaster.Disconnect();
IsConnected = false;
StatusMessage = "已断开";
}
public void Dispose()
{
_cts?.Cancel();
_modbusMaster?.Dispose();
}
}
public class DataRecord
{
public DateTime Timestamp { get; set; }
public float Temperature { get; set; }
public float Pressure { get; set; }
}
<!-- Modbus 设备监控界面 -->
<Window x:Class="ModbusDemo.MonitorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Modbus 设备监控" Width="800" Height="600">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 连接控制区 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="连接" Command="{Binding ConnectCommand}" Margin="0,0,5,0"
Padding="15,5" Background="#4CAF50" Foreground="White"/>
<Button Content="断开" Command="{Binding DisconnectCommand}" Margin="0,0,5,0"
Padding="15,5" Background="#f44336" Foreground="White"/>
<Button Content="开始监控" Command="{Binding StartMonitorCommand}" Margin="0,0,5,0"
Padding="15,5"/>
<Button Content="停止监控" Command="{Binding StopMonitorCommand}" Margin="0,0,5,0"
Padding="15,5"/>
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Margin="10,0,0,0"/>
</StackPanel>
<!-- 实时数据显示区 -->
<UniformGrid Grid.Row="1" Columns="4" Margin="0,0,0,10">
<Border Margin="5" Padding="10" Background="#E3F2FD" CornerRadius="5">
<StackPanel>
<TextBlock Text="温度" FontSize="14" Foreground="Gray"/>
<TextBlock Text="{Binding Temperature, StringFormat={}{0:F1}℃}"
FontSize="28" FontWeight="Bold" Foreground="#1976D2"/>
</StackPanel>
</Border>
<Border Margin="5" Padding="10" Background="#FFF3E0" CornerRadius="5">
<StackPanel>
<TextBlock Text="压力" FontSize="14" Foreground="Gray"/>
<TextBlock Text="{Binding Pressure, StringFormat={}{0:F2}MPa}"
FontSize="28" FontWeight="Bold" Foreground="#F57C00"/>
</StackPanel>
</Border>
<Border Margin="5" Padding="10" Background="#E8F5E9" CornerRadius="5">
<StackPanel>
<TextBlock Text="水泵" FontSize="14" Foreground="Gray"/>
<TextBlock FontSize="28" FontWeight="Bold"
Foreground="{Binding PumpRunning, Converter={StaticResource BoolToColorConverter}}">
<TextBlock.Text>
<Binding Path="PumpRunning" Converter="{StaticResource BoolToStringConverter}"/>
</TextBlock.Text>
</TextBlock>
<Button Content="切换" Command="{Binding TogglePumpCommand}" Margin="0,5,0,0"/>
</StackPanel>
</Border>
<Border Margin="5" Padding="10" Background="#FCE4EC" CornerRadius="5">
<StackPanel>
<TextBlock Text="阀门" FontSize="14" Foreground="Gray"/>
<TextBlock FontSize="28" FontWeight="Bold"
Foreground="{Binding ValveOpen, Converter={StaticResource BoolToColorConverter}}">
<TextBlock.Text>
<Binding Path="ValveOpen" Converter="{StaticResource BoolToStringConverter}"/>
</TextBlock.Text>
</TextBlock>
<Button Content="切换" Command="{Binding ToggleValveCommand}" Margin="0,5,0,0"/>
</StackPanel>
</Border>
</UniformGrid>
<!-- 历史数据列表 -->
<DataGrid Grid.Row="2" ItemsSource="{Binding HistoryData}" AutoGenerateColumns="False"
IsReadOnly="True" AlternatingRowBackground="#F5F5F5">
<DataGrid.Columns>
<DataGridTextColumn Header="时间" Binding="{Binding Timestamp, StringFormat={}{0:HH:mm:ss}}"/>
<DataGridTextColumn Header="温度(℃)" Binding="{Binding Temperature, StringFormat={}{0:F1}}"/>
<DataGridTextColumn Header="压力(MPa)" Binding="{Binding Pressure, StringFormat={}{0:F2}}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
/// <summary>
/// Modbus RTU 从站模拟器 — 用于开发调试
/// 在没有真实设备时模拟设备响应
/// </summary>
public class ModbusSlaveSimulator : IDisposable
{
private SerialPort _serialPort;
private IModbusSlave _slave;
private Task _listenTask;
private CancellationTokenSource _cts;
/// <summary>
/// 启动从站模拟器
/// </summary>
public void Start(string portName, byte slaveId)
{
_serialPort = new SerialPort(portName)
{
BaudRate = 9600,
DataBits = 8,
Parity = Parity.None,
StopBits = StopBits.One
};
_serialPort.Open();
var factory = new ModbusFactory();
_slave = factory.CreateSlave(slaveId);
// 初始化模拟数据
InitializeSimulatedData();
_cts = new CancellationTokenSource();
_listenTask = _slave.ListenAsync(_cts.Token);
}
/// <summary>
/// 初始化模拟数据
/// </summary>
private void InitializeSimulatedData()
{
var dataStore = _slave.DataStore;
// 模拟线圈状态(地址1-8)
dataStore.CoilDiscretes[1] = true; // 水泵运行
dataStore.CoilDiscretes[2] = false; // 阀门关闭
// 模拟保持寄存器
// 温度 = 25.5°C,存为两个寄存器的Float
var (tempHigh, tempLow) = ModbusDataConverter.FromFloat32(25.5f);
dataStore.HoldingRegisters[1] = tempHigh;
dataStore.HoldingRegisters[2] = tempLow;
// 压力 = 1.024MPa
var (pressHigh, pressLow) = ModbusDataConverter.FromFloat32(1.024f);
dataStore.HoldingRegisters[3] = pressHigh;
dataStore.HoldingRegisters[4] = pressLow;
// 模拟输入寄存器 — 模拟量输入
dataStore.InputRegisters[1] = 1024; // 原始AD值
dataStore.InputRegisters[2] = 2048;
}
public void Dispose()
{
_cts?.Cancel();
_serialPort?.Close();
_serialPort?.Dispose();
}
}
/// <summary>
/// Modbus 通信管理器 — 支持自动重连和心跳检测
/// </summary>
public class ModbusCommunicationManager : IDisposable
{
private readonly ModbusRtuMaster _master;
private readonly ILogger _logger;
private CancellationTokenSource _cts;
private bool _autoReconnect = true;
private int _reconnectInterval = 5000; // 重连间隔(毫秒)
private int _failedCount;
private const int MaxFailedCount = 3;
public event EventHandler<bool> ConnectionStateChanged;
public ModbusCommunicationManager(SerialPortConfig config, ILogger logger)
{
_master = new ModbusRtuMaster(config, logger);
_logger = logger;
}
/// <summary>
/// 启动通信管理(含自动重连)
/// </summary>
public async Task StartAsync(CancellationToken cancellationToken = default)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
while (!_cts.Token.IsCancellationRequested)
{
try
{
if (!_master.IsConnected)
{
_logger.LogInformation("尝试连接Modbus设备...");
var connected = _master.Connect();
ConnectionStateChanged?.Invoke(this, connected);
if (!connected)
{
await Task.Delay(_reconnectInterval, _cts.Token);
continue;
}
}
// 心跳检测 — 读取一个寄存器
var data = await _master.ReadHoldingRegistersAsync(1, 0, 1);
_failedCount = 0; // 重置失败计数
}
catch (Exception ex)
{
_failedCount++;
_logger.LogWarning(ex, "通信失败({Count}/{Max})", _failedCount, MaxFailedCount);
if (_failedCount >= MaxFailedCount)
{
_logger.LogWarning("连续失败次数达到上限,断开连接并准备重连");
_master.Disconnect();
ConnectionStateChanged?.Invoke(this, false);
_failedCount = 0;
}
}
await Task.Delay(1000, _cts.Token);
}
}
public void Dispose()
{
_cts?.Cancel();
_master?.Dispose();
}
}
/// <summary>
/// 变频器 Modbus RTU 控制示例
/// 以 Modbus RTU 控制变频器启停和频率为例
/// 寄存器地址参考具体变频器的 Modbus 通信手册
/// </summary>
public class VfdController
{
private readonly ModbusRtuMaster _master;
private readonly byte _slaveId;
// 变频器寄存器地址定义(需参考实际设备手册)
private const ushort RegControlCommand = 0x2000; // 控制命令寄存器
private const ushort RegFrequencySetting = 0x2001; // 频率设定寄存器
private const ushort RegOutputFrequency = 0x2100; // 输出频率
private const ushort RegOutputCurrent = 0x2101; // 输出电流
private const ushort RegOutputVoltage = 0x2102; // 输出电压
private const ushort RegFaultCode = 0x2103; // 故障代码
// 控制命令
private const ushort CmdStart = 0x0001;
private const ushort CmdStop = 0x0002;
private const ushort CmdJog = 0x0003;
private const ushort CmdResetFault = 0x0004;
public VfdController(ModbusRtuMaster master, byte slaveId = 1)
{
_master = master;
_slaveId = slaveId;
}
/// <summary>启动变频器</summary>
public async Task StartAsync() =>
await _master.WriteSingleRegisterAsync(_slaveId, RegControlCommand, CmdStart);
/// <summary>停止变频器</summary>
public async Task StopAsync() =>
await _master.WriteSingleRegisterAsync(_slaveId, RegControlCommand, CmdStop);
/// <summary>设置运行频率(0.01Hz精度,如5000表示50.00Hz)</summary>
public async Task SetFrequencyAsync(double frequencyHz) =>
await _master.WriteSingleRegisterAsync(_slaveId, RegFrequencySetting, (ushort)(frequencyHz * 100));
/// <summary>故障复位</summary>
public async Task ResetFaultAsync() =>
await _master.WriteSingleRegisterAsync(_slaveId, RegControlCommand, CmdResetFault);
/// <summary>读取变频器运行状态</summary>
public async Task<VfdStatus> ReadStatusAsync()
{
var registers = await _master.ReadHoldingRegistersAsync(_slaveId, RegOutputFrequency, 4);
return new VfdStatus
{
OutputFrequency = registers[0] / 100.0, // 转换为Hz
OutputCurrent = registers[1] / 10.0, // 转换为A
OutputVoltage = registers[2] / 10.0, // 转换为V
FaultCode = registers[3]
};
}
}
public class VfdStatus
{
public double OutputFrequency { get; set; } // Hz
public double OutputCurrent { get; set; } // A
public double OutputVoltage { get; set; } // V
public ushort FaultCode { get; set; }
}
Modbus RTU 是工业通信的基础协议。掌握协议帧结构、功能码、CRC 校验,熟练使用 NModbus 库进行读写操作,以及数据类型转换(特别是 Float32 的处理),是工业软件开发的核心能力。结合 WPF 可以快速构建实时监控界面,配合自动重连机制保障系统稳定性。
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- 设备接入类主题通常同时涉及协议、线程、实时刷新和异常恢复。
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 明确采集周期、重连策略、数据缓存和状态同步方式。
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 只验证通信成功,不验证断线、抖动和异常包。
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐设备模拟、离线回放、现场诊断和配置中心能力。
- 当你准备把《Modbus RTU串口通信实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
- 如果把《Modbus RTU串口通信实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Modbus RTU串口通信实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Modbus RTU串口通信实战》最大的收益和代价分别是什么?