C# Modbus RTU 多从站控制全攻略:一端口,双轴控制
前言:当 Modbus 遇上多从站
在工业自动化领域,Modbus RTU 是一种常见的通信协议,但当多个设备共享同一个 COM 口时,就像让多个哑巴通过同一个话筒说话。本文将详细介绍如何使用 C# 实现一个 COM 口控制多个 AZ 驱动器。

一、项目架构设计
1.1 技术栈选择
csharp
// 项目使用的技术栈
- .NET 6.0/8.0 // 跨平台,高性能
- NModbus // Modbus 通信库(关键)
- WPF + MVVM // 桌面应用架构
- CommunityToolkit.Mvvm // MVVM 开发工具
- Dependency Injection // 依赖注入管理
- ILogger // 日志记录
1.2 项目结构
ModbusMultiSlave/
├── Controllers/ // Modbus 通信控制器
│ ├── AzDriverController.cs
│ └── AzDriverAddresses.cs
├── Services/ // 业务服务层
│ ├── IAxisService.cs
│ ├── AxisService.cs
│ └── PlcAddressService.cs
├── Models/ // 数据模型
│ ├── AzDriverStatus.cs
│ └── Alarm.cs
├── ViewModels/ // 视图模型
│ ├── TestViewModel.cs
│ └── MainViewModel.cs
├── Views/ // 视图
│ └── TestPage.xaml
├── Interfaces/ // 接口定义
│ └── IAxisService.cs
└── App.xaml.cs // 应用入口
二、核心代码实现
2.1 Modbus RTU 控制器类
AzDriverController.cs
csharp
using System;
using System.Collections.Concurrent;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NModbus;
using NModbus.Serial;
namespace ModbusMultiSlave.Controllers
{
/// <summary>
/// AZ 驱动器控制器 - 支持多个从站共享串口
/// </summary>
public class AzDriverController : IDisposable
{
#region 私有字段
private SerialPort _serialPort;
private IModbusSerialMaster _master;
private readonly object _lock = new object();
private bool _isConnected = false;
private string _currentPortName;
private int _currentBaudRate;
private bool _isDisposed = false;
private readonly ILogger<AzDriverController> _logger;
// 记录每个从站是否在线
private readonly ConcurrentDictionary<byte, bool> _slaveOnlineStatus = new ConcurrentDictionary<byte, bool>();
// 通信统计和控制
private int _communicationCount = 0;
private byte _lastSlaveId = 0;
private DateTime _lastCommunicationTime = DateTime.MinValue;
private const int MIN_COMM_INTERVAL_MS = 50; // 最小通信间隔
#endregion
#region 属性
public bool IsConnected => _isConnected && _serialPort?.IsOpen == true;
public string PortName => _currentPortName;
public int BaudRate => _currentBaudRate;
public int CommunicationCount => _communicationCount;
#endregion
#region 构造函数
public AzDriverController(ILogger<AzDriverController> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_logger.LogInformation("AzDriverController 实例已创建");
}
#endregion
#region 连接管理
/// <summary>
/// 连接到串口
/// </summary>
public bool Connect(string portName = "COM11", int baudRate = 115200)
{
lock (_lock)
{
try
{
_logger.LogInformation("尝试连接串口: {PortName}@{BaudRate}bps", portName, baudRate);
// 如果已经连接并且参数相同,直接返回成功
if (_isConnected && _currentPortName == portName && _currentBaudRate == baudRate)
{
_logger.LogInformation("串口已连接相同参数,直接返回成功");
return true;
}
// 如果已有连接但参数不同,先断开
if (_isConnected)
{
_logger.LogInformation("已有连接但参数不同,先断开连接");
DisconnectInternal();
}
_currentPortName = portName;
_currentBaudRate = baudRate;
// 创建并配置串口
_serialPort = new SerialPort(portName, baudRate, Parity.Even, 8, StopBits.One)
{
Handshake = Handshake.None,
ReadTimeout = 2000, // 适当增加超时时间
WriteTimeout = 2000,
RtsEnable = true, // RS485 需要 RTS 控制
DtrEnable = true
};
// 打开串口
_serialPort.Open();
_logger.LogInformation("串口打开成功");
// 创建 Modbus RTU 主站
var factory = new ModbusFactory();
_master = factory.CreateRtuMaster(_serialPort);
// 配置 Modbus 参数
_master.Transport.Retries = 2;
_master.Transport.ReadTimeout = 2000;
_master.Transport.WriteTimeout = 2000;
_master.Transport.SlaveBusyUsesRetryCount = false;
_isConnected = true;
_slaveOnlineStatus.Clear();
_logger.LogInformation("✅ AZ 驱动器连接成功: {PortName}@{BaudRate}bps", portName, baudRate);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ AZ 驱动器连接失败: {PortName}@{BaudRate}bps", portName, baudRate);
DisconnectInternal();
return false;
}
}
}
/// <summary>
/// 断开连接
/// </summary>
public void Disconnect()
{
lock (_lock)
{
DisconnectInternal();
}
}
private void DisconnectInternal()
{
if (!_isConnected) return;
_logger.LogInformation("正在断开串口连接");
_isConnected = false;
_slaveOnlineStatus.Clear();
try
{
_master?.Dispose();
_master = null;
_logger.LogInformation("Modbus 主站已释放");
}
catch (Exception ex)
{
_logger.LogError(ex, "释放 Modbus 主站时发生错误");
}
try
{
if (_serialPort?.IsOpen == true)
{
_serialPort.Close();
_logger.LogInformation("串口已关闭");
}
_serialPort?.Dispose();
_serialPort = null;
}
catch (Exception ex)
{
_logger.LogError(ex, "关闭串口时发生错误");
}
_logger.LogInformation("AZ 驱动器已断开连接");
}
#endregion
#region 通信辅助方法
/// <summary>
/// 确保通信间隔,避免冲突
/// </summary>
private void EnsureCommunicationInterval(byte slaveId)
{
if (_lastSlaveId == 0 || _lastSlaveId == slaveId)
{
// 相同从站,不需要额外间隔
return;
}
// 不同从站之间需要间隔
var elapsed = DateTime.Now - _lastCommunicationTime;
if (elapsed.TotalMilliseconds < MIN_COMM_INTERVAL_MS)
{
var waitTime = (int)(MIN_COMM_INTERVAL_MS - elapsed.TotalMilliseconds);
_logger.LogDebug("等待通信间隔: {WaitTime}ms (从站 {From} -> {To})",
waitTime, _lastSlaveId, slaveId);
Thread.Sleep(waitTime);
}
_lastSlaveId = slaveId;
}
/// <summary>
/// 检查从站是否在线
/// </summary>
public bool CheckSlaveOnline(byte slaveId)
{
lock (_lock)
{
if (!_isConnected)
{
_logger.LogWarning("检查从站 {SlaveId} 在线状态失败: 未连接", slaveId);
return false;
}
try
{
_logger.LogDebug("检查从站 {SlaveId} 是否在线", slaveId);
// 确保通信间隔
EnsureCommunicationInterval(slaveId);
// 尝试读取状态字来判断从站是否在线
var registers = _master.ReadHoldingRegisters(slaveId,
(ushort)(AzDriverAddresses.StatusWord - 1), 1);
bool online = registers != null && registers.Length > 0;
_slaveOnlineStatus[slaveId] = online;
_logger.LogInformation("从站 {SlaveId} 在线状态: {Online}",
slaveId, online ? "在线" : "离线");
_lastCommunicationTime = DateTime.Now;
return online;
}
catch (Exception ex)
{
_logger.LogWarning("检查从站 {SlaveId} 在线状态失败: {Message}", slaveId, ex.Message);
_slaveOnlineStatus[slaveId] = false;
return false;
}
}
}
/// <summary>
/// 获取从站在线状态
/// </summary>
public bool IsSlaveOnline(byte slaveId)
{
return _slaveOnlineStatus.TryGetValue(slaveId, out bool online) && online;
}
#endregion
#region 基本读写操作
/// <summary>
/// 读取单个寄存器
/// </summary>
public ushort ReadRegister(byte slaveId, ushort address)
{
lock (_lock)
{
try
{
_communicationCount++;
_logger.LogDebug("#{Count} 读取寄存器 [Slave:{SlaveId}, Addr:{Address}(0x{Address:X4})]",
_communicationCount, slaveId, address, address);
if (!_isConnected)
{
_logger.LogWarning("读取寄存器失败:未连接");
return 0;
}
// 确保通信间隔
EnsureCommunicationInterval(slaveId);
var registers = _master.ReadHoldingRegisters(slaveId, (ushort)(address - 1), 1);
var result = registers?.Length > 0 ? registers[0] : (ushort)0;
_logger.LogDebug("寄存器读取结果 [Slave:{SlaveId}]: 0x{Result:X4} ({Result})",
slaveId, result, result);
_slaveOnlineStatus[slaveId] = true;
_lastCommunicationTime = DateTime.Now;
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "读取寄存器失败 [Slave:{SlaveId}, Addr:{Address}]", slaveId, address);
_slaveOnlineStatus[slaveId] = false;
return 0;
}
}
}
/// <summary>
/// 写入单个寄存器
/// </summary>
public bool WriteRegister(byte slaveId, ushort address, ushort value)
{
lock (_lock)
{
try
{
_communicationCount++;
_logger.LogDebug("#{Count} 写入寄存器 [Slave:{SlaveId}, Addr:{Address}, Value:{Value}(0x{Value:X4})]",
_communicationCount, slaveId, address, value, value);
if (!_isConnected)
{
_logger.LogWarning("写入寄存器失败:未连接");
return false;
}
// 确保通信间隔
EnsureCommunicationInterval(slaveId);
_master.WriteSingleRegister(slaveId, (ushort)(address - 1), value);
_logger.LogDebug("寄存器写入成功 [Slave:{SlaveId}]", slaveId);
_slaveOnlineStatus[slaveId] = true;
_lastCommunicationTime = DateTime.Now;
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "写入寄存器失败 [Slave:{SlaveId}, Addr:{Address}]", slaveId, address);
_slaveOnlineStatus[slaveId] = false;
return false;
}
}
}
/// <summary>
/// 读取 32 位数据(两个寄存器)
/// </summary>
public int Read32BitData(byte slaveId, ushort startAddress)
{
lock (_lock)
{
try
{
_communicationCount++;
_logger.LogDebug("#{Count} 读取32位数据 [Slave:{SlaveId}, Addr:{Address}(0x{Address:X4})]",
_communicationCount, slaveId, startAddress, startAddress);
if (!_isConnected)
{
_logger.LogWarning("读取32位数据失败:未连接");
return 0;
}
// 确保通信间隔
EnsureCommunicationInterval(slaveId);
var registers = _master.ReadHoldingRegisters(slaveId, (ushort)(startAddress - 1), 2);
if (registers.Length != 2)
{
_logger.LogWarning("读取32位数据失败:返回数据长度错误");
return 0;
}
int high = registers[0]; // 高位寄存器
int low = registers[1]; // 低位寄存器
_logger.LogDebug("寄存器原始值:高位={High}(0x{High:X4}), 低位={Low}(0x{Low:X4})",
high, high, low, low);
// 组合成 32 位整数(高位在前,标准 Modbus)
int value = (high << 16) | low;
// 处理有符号整数(二进制补码)
if ((value & 0x80000000) != 0) // 最高位为 1,表示负数
{
value = (int)(value - 0x100000000);
_logger.LogDebug("有符号负数转换:原始值=0x{Value:X8}, 转换后={Result}",
(uint)value + 0x100000000, value);
}
else
{
_logger.LogDebug("有符号正数:值={Value}", value);
}
_slaveOnlineStatus[slaveId] = true;
_lastCommunicationTime = DateTime.Now;
return value;
}
catch (Exception ex)
{
_logger.LogError(ex, "读取32位数据失败 [Slave:{SlaveId}, Addr:{Address}]",
slaveId, startAddress);
_slaveOnlineStatus[slaveId] = false;
return 0;
}
}
}
/// <summary>
/// 写入 32 位数据(两个寄存器)
/// </summary>
public bool Write32BitData(byte slaveId, ushort startAddress, int value)
{
lock (_lock)
{
try
{
_communicationCount++;
_logger.LogDebug("#{Count} 写入32位数据 [Slave:{SlaveId}, Addr:{Address}, 值:{Value}]",
_communicationCount, slaveId, startAddress, value);
if (!_isConnected)
{
_logger.LogWarning("写入32位数据失败:未连接");
return false;
}
// 确保通信间隔
EnsureCommunicationInterval(slaveId);
// 处理有符号整数(二进制补码)
uint unsignedValue;
if (value < 0)
{
// 负数:转换为补码
unsignedValue = (uint)(0x100000000 + value);
_logger.LogDebug("负数{Value}的补码:0x{UnsignedValue:X8}",
value, unsignedValue);
}
else
{
// 正数:直接转换
unsignedValue = (uint)value;
}
// 高位在前(标准 Modbus)
ushort high = (ushort)((unsignedValue >> 16) & 0xFFFF);
ushort low = (ushort)(unsignedValue & 0xFFFF);
_logger.LogDebug("分解为寄存器:高位={High}(0x{High:X4}), 低位={Low}(0x{Low:X4})",
high, high, low, low);
// 写入寄存器
var values = new ushort[] { high, low };
_master.WriteMultipleRegisters(slaveId, (ushort)(startAddress - 1), values);
_logger.LogDebug("✅ 32位数据写入成功");
_slaveOnlineStatus[slaveId] = true;
_lastCommunicationTime = DateTime.Now;
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "写入32位数据失败");
_slaveOnlineStatus[slaveId] = false;
return false;
}
}
}
#endregion
#region 核心控制功能
/// <summary>
/// 发送控制命令(关键:发送后自动清零)
/// </summary>
public bool SendControlCommand(byte slaveId, ushort command)
{
lock (_lock)
{
try
{
_communicationCount++;
_logger.LogDebug("#{Count} 发送控制命令 [Slave:{SlaveId}, Cmd:0x{Command:X4}]",
_communicationCount, slaveId, command);
if (!_isConnected)
{
_logger.LogWarning("发送控制命令失败:未连接");
return false;
}
// 确保通信间隔
EnsureCommunicationInterval(slaveId);
// 1. 写入控制命令
_master.WriteSingleRegister(slaveId,
(ushort)(AzDriverAddresses.ControlWord - 1), command);
// 2. 短暂延迟确保命令被接收
Thread.Sleep(50);
// 3. 重要:立即将控制字清零
_master.WriteSingleRegister(slaveId,
(ushort)(AzDriverAddresses.ControlWord - 1), 0);
_logger.LogDebug("控制命令发送完成 [Slave:{SlaveId}]", slaveId);
_slaveOnlineStatus[slaveId] = true;
_lastCommunicationTime = DateTime.Now;
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "发送控制命令失败 [Slave:{SlaveId}]", slaveId);
_slaveOnlineStatus[slaveId] = false;
return false;
}
}
}
/// <summary>
/// 设置绝对定位模式
/// </summary>
public bool SetAbsoluteMode(byte slaveId)
{
_logger.LogInformation("设置从站 {SlaveId} 为绝对定位模式", slaveId);
return WriteRegister(slaveId, AzDriverAddresses.OperationMode, 1);
}
/// <summary>
/// 设置原点位置
/// </summary>
public bool SetOrigin(byte slaveId)
{
_logger.LogInformation("从站 {SlaveId} 设置原点", slaveId);
// 写入位置预置命令 ON
if (!WriteRegister(slaveId, AzDriverAddresses.PositionPreset, 1))
return false;
// 短暂延迟
Thread.Sleep(100);
// 写入位置预置命令 OFF
return WriteRegister(slaveId, AzDriverAddresses.PositionPreset, 0);
}
/// <summary>
/// 执行绝对定位
/// </summary>
public bool MoveAbsolute(byte slaveId, int position, int speed)
{
lock (_lock)
{
_logger.LogInformation("从站 {SlaveId} 开始绝对定位: 位置={Position}, 速度={Speed}",
slaveId, position, speed);
if (!_isConnected)
{
_logger.LogWarning("从站 {SlaveId} 绝对定位失败: 未连接", slaveId);
return false;
}
try
{
// 1. 确保为绝对定位模式
if (!SetAbsoluteMode(slaveId))
{
_logger.LogWarning("从站 {SlaveId} 设置绝对定位模式失败", slaveId);
return false;
}
// 2. 设置目标位置
if (!Write32BitData(slaveId, AzDriverAddresses.TargetPosition, position))
{
_logger.LogWarning("从站 {SlaveId} 设置目标位置失败", slaveId);
return false;
}
// 3. 设置运行速度
if (!Write32BitData(slaveId, AzDriverAddresses.RunSpeed, speed))
{
_logger.LogWarning("从站 {SlaveId} 设置运行速度失败", slaveId);
return false;
}
// 4. 发送启动命令
var result = SendControlCommand(slaveId, AzDriverAddresses.ControlCommands.START);
if (result)
_logger.LogInformation("从站 {SlaveId} 绝对定位命令已发送", slaveId);
else
_logger.LogWarning("从站 {SlaveId} 绝对定位命令发送失败", slaveId);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "从站 {SlaveId} 绝对定位失败", slaveId);
_slaveOnlineStatus[slaveId] = false;
return false;
}
}
}
/// <summary>
/// 执行原点复归
/// </summary>
public bool GoHome(byte slaveId)
{
_logger.LogInformation("从站 {SlaveId} 执行原点复归", slaveId);
return SendControlCommand(slaveId, AzDriverAddresses.ControlCommands.ZHOME);
}
/// <summary>
/// 停止运动
/// </summary>
public bool Stop(byte slaveId)
{
_logger.LogInformation("从站 {SlaveId} 停止运动", slaveId);
return SendControlCommand(slaveId, AzDriverAddresses.ControlCommands.STOP);
}
/// <summary>
/// 复位报警
/// </summary>
public bool ResetAlarm(byte slaveId)
{
_logger.LogInformation("从站 {SlaveId} 复位报警", slaveId);
return SendControlCommand(slaveId, AzDriverAddresses.ControlCommands.ALM_RST);
}
/// <summary>
/// 急停
/// </summary>
public bool EmergencyStop(byte slaveId)
{
_logger.LogInformation("从站 {SlaveId} 急停", slaveId);
return SendControlCommand(slaveId, AzDriverAddresses.ControlCommands.ALL_OFF);
}
/// <summary>
/// 设置运行速度
/// </summary>
public bool SetSpeed(byte slaveId, int speed)
{
_logger.LogInformation("从站 {SlaveId} 设置运行速度: {Speed}", slaveId, speed);
return Write32BitData(slaveId, AzDriverAddresses.RunSpeed, speed);
}
/// <summary>
/// 读取驱动器状态
/// </summary>
public AzDriverStatus ReadStatus(byte slaveId)
{
var status = new AzDriverStatus();
lock (_lock)
{
_logger.LogDebug("读取从站 {SlaveId} 状态", slaveId);
if (!_isConnected)
{
_logger.LogWarning("读取从站 {SlaveId} 状态失败: 未连接", slaveId);
return status;
}
try
{
// 读取状态字
ushort statusWord = ReadRegister(slaveId, AzDriverAddresses.StatusWord);
status.IsReady = (statusWord & AzDriverAddresses.StatusBits.READY) != 0;
status.IsMoving = (statusWord & AzDriverAddresses.StatusBits.MOVE) != 0;
status.InPosition = (statusWord & AzDriverAddresses.StatusBits.IN_POS) != 0;
_logger.LogDebug("从站 {SlaveId} 状态字: 0x{StatusWord:X4}, 就绪={IsReady}, 移动={IsMoving}, 到位={InPosition}",
slaveId, statusWord, status.IsReady, status.IsMoving, status.InPosition);
// 读取报警代码
status.AlarmCode = ReadRegister(slaveId, AzDriverAddresses.AlarmCode);
_logger.LogDebug("从站 {SlaveId} 报警代码: 0x{AlarmCode:X4}", slaveId, status.AlarmCode);
// 读取实际位置
status.ActualPosition = Read32BitData(slaveId, AzDriverAddresses.ActualPosition);
_logger.LogDebug("从站 {SlaveId} 实际位置: {ActualPosition}", slaveId, status.ActualPosition);
// 读取实际速度
status.ActualSpeed = Read32BitData(slaveId, AzDriverAddresses.ActualSpeed);
_logger.LogDebug("从站 {SlaveId} 实际速度: {ActualSpeed}", slaveId, status.ActualSpeed);
// 读取指令位置
status.CommandPosition = Read32BitData(slaveId, AzDriverAddresses.CommandPosition);
_logger.LogDebug("从站 {SlaveId} 指令位置: {CommandPosition}", slaveId, status.CommandPosition);
_slaveOnlineStatus[slaveId] = true;
_logger.LogDebug("从站 {SlaveId} 状态读取完成", slaveId);
return status;
}
catch (Exception ex)
{
_logger.LogError(ex, "读取从站 {SlaveId} 状态失败", slaveId);
_slaveOnlineStatus[slaveId] = false;
return status;
}
}
}
#endregion
#region 诊断工具
/// <summary>
/// 诊断 32 位数据读取问题
/// </summary>
public void Diagnose32BitData(byte slaveId, ushort startAddress, int expectedValue = 0)
{
lock (_lock)
{
try
{
_logger.LogInformation("=== 32位数据诊断 [Slave:{SlaveId}, Addr:{Address}] ===",
slaveId, startAddress);
if (!_isConnected)
{
_logger.LogWarning("未连接,无法诊断");
return;
}
// 读取两个寄存器的原始值
var registers = _master.ReadHoldingRegisters(slaveId, (ushort)(startAddress - 1), 2);
if (registers.Length == 2)
{
ushort reg1 = registers[0]; // 第一个寄存器
ushort reg2 = registers[1]; // 第二个寄存器
_logger.LogInformation("寄存器原始值:");
_logger.LogInformation(" 寄存器1({Address}) = {Reg1} (0x{Reg1:X4})",
startAddress, reg1, reg1);
_logger.LogInformation(" 寄存器2({Address}) = {Reg2} (0x{Reg2:X4})",
(ushort)(startAddress + 1), reg2, reg2);
// 不同解析方式
_logger.LogInformation("不同解析方式:");
// 1. 高位在前有符号(标准 Modbus)
int valueHighFirstSigned = (reg1 << 16) | reg2;
if ((valueHighFirstSigned & 0x80000000) != 0)
valueHighFirstSigned = (int)(valueHighFirstSigned - 0x100000000);
_logger.LogInformation(" 1. 高位在前有符号:{Value}", valueHighFirstSigned);
// 2. 低位在前有符号
int valueLowFirstSigned = (reg2 << 16) | reg1;
if ((valueLowFirstSigned & 0x80000000) != 0)
valueLowFirstSigned = (int)(valueLowFirstSigned - 0x100000000);
_logger.LogInformation(" 2. 低位在前有符号:{Value}", valueLowFirstSigned);
// 3. 高位在前无符号
uint valueHighFirstUnsigned = (uint)((reg1 << 16) | reg2);
_logger.LogInformation(" 3. 高位在前无符号:{Value}", valueHighFirstUnsigned);
// 4. 低位在前无符号
uint valueLowFirstUnsigned = (uint)((reg2 << 16) | reg1);
_logger.LogInformation(" 4. 低位在前无符号:{Value}", valueLowFirstUnsigned);
// 如果有期望值,显示期望的寄存器值
if (expectedValue != 0)
{
_logger.LogInformation("期望值 {Expected} 对应的寄存器值:", expectedValue);
if (expectedValue >= 0)
{
// 正数
ushort expectedHigh = (ushort)((expectedValue >> 16) & 0xFFFF);
ushort expectedLow = (ushort)(expectedValue & 0xFFFF);
_logger.LogInformation(" 高位在前:高位=0x{High:X4}({High}), 低位=0x{Low:X4}({Low})",
expectedHigh, expectedHigh, expectedLow, expectedLow);
}
else
{
// 负数(使用二进制补码)
uint negativeValue = (uint)(0x100000000 + expectedValue);
ushort expectedHigh = (ushort)((negativeValue >> 16) & 0xFFFF);
ushort expectedLow = (ushort)(negativeValue & 0xFFFF);
_logger.LogInformation(" 负数{Expected}的补码:0x{NegValue:X8}",
expectedValue, negativeValue);
_logger.LogInformation(" 高位在前:高位=0x{High:X4}({High}), 低位=0x{Low:X4}({Low})",
expectedHigh, expectedHigh, expectedLow, expectedLow);
}
}
// 特殊值分析
if (reg1 == 0xFFFF && reg2 == 0xC180)
{
_logger.LogInformation("⚠️ 检测到特殊值组合:0xFFFF, 0xC180");
_logger.LogInformation(" 这个组合表示有符号32位整数:-16000");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "诊断32位数据失败");
}
_logger.LogInformation("=== 诊断结束 ===");
}
}
#endregion
#region 资源清理
public void Dispose()
{
if (_isDisposed) return;
_logger.LogInformation("开始释放 AzDriverController 资源");
DisconnectInternal();
_isDisposed = true;
_logger.LogInformation("AzDriverController 资源已释放");
GC.SuppressFinalize(this);
}
~AzDriverController()
{
Dispose();
}
#endregion
}
}
2.2 驱动器地址定义
AzDriverAddresses.cs
csharp
using System;
namespace ModbusMultiSlave.Controllers
{
/// <summary>
/// AZ 驱动器寄存器地址定义
/// </summary>
public static class AzDriverAddresses
{
// 寄存器地址(基于 1 的地址)
public const ushort StatusWord = 0x0001; // 状态字
public const ushort AlarmCode = 0x0002; // 报警代码
public const ushort ActualPosition = 0x0003; // 实际位置(32位,起始地址)
public const ushort ActualSpeed = 0x0005; // 实际速度(32位)
public const ushort CommandPosition = 0x0007; // 指令位置(32位)
public const ushort TargetPosition = 0x0009; // 目标位置(32位)
public const ushort RunSpeed = 0x000B; // 运行速度(32位)
public const ushort ControlWord = 0x000D; // 控制字
public const ushort OperationMode = 0x000E; // 操作模式
public const ushort PositionPreset = 0x000F; // 位置预置
// 状态字位定义
public static class StatusBits
{
public const ushort READY = 0x0001; // 位0:就绪状态
public const ushort MOVE = 0x0002; // 位1:移动状态
public const ushort IN_POS = 0x0004; // 位2:到位标志
public const ushort ALARM = 0x0008; // 位3:报警状态
public const ushort ENABLED = 0x0010; // 位4:使能状态
public const ushort HOME_COMPLETE = 0x0020; // 位5:回零完成
}
// 控制命令
public static class ControlCommands
{
public const ushort START = 0x0001; // 启动
public const ushort STOP = 0x0002; // 停止
public const ushort ZHOME = 0x0004; // 原点复归
public const ushort ALM_RST = 0x0008; // 报警复位
public const ushort ALL_OFF = 0x0010; // 全部关闭(急停)
}
// 操作模式
public static class OperationModes
{
public const ushort ABSOLUTE = 0x0001; // 绝对定位模式
public const ushort RELATIVE = 0x0002; // 相对定位模式
public const ushort JOG = 0x0003; // 点动模式
}
}
}
2.3 驱动器状态模型
AzDriverStatus.cs
csharp
using System;
namespace ModbusMultiSlave.Models
{
/// <summary>
/// AZ 驱动器状态信息
/// </summary>
public class AzDriverStatus
{
// 驱动器状态
public bool IsReady { get; set; } = false;
public bool IsMoving { get; set; } = false;
public bool InPosition { get; set; } = false;
public bool IsEnabled { get; set; } = false;
public bool HomeComplete { get; set; } = false;
// 报警和故障信息
public ushort AlarmCode { get; set; } = 0;
public bool HasAlarm => AlarmCode != 0;
// 位置信息
public int ActualPosition { get; set; } = 0; // 实际位置
public int CommandPosition { get; set; } = 0; // 指令位置
public int TargetPosition { get; set; } = 0; // 目标位置
// 速度信息
public int ActualSpeed { get; set; } = 0; // 实际速度
public int RunSpeed { get; set; } = 0; // 运行速度
// 状态显示
public string StatusText
{
get
{
if (AlarmCode != 0) return $"报警: {GetAlarmDescription()}";
if (IsMoving) return "运行中";
if (IsReady) return "就绪";
if (HomeComplete) return "回零完成";
return "未知";
}
}
/// <summary>
/// 获取报警描述
/// </summary>
public string GetAlarmDescription()
{
return AlarmCode switch
{
0 => "正常",
1 => "过电流",
2 => "过电压",
3 => "欠电压",
4 => "电机过热",
5 => "驱动器过热",
6 => "过载",
7 => "编码器异常",
8 => "位置误差过大",
9 => "通信异常",
10 => "参数错误",
11 => "硬件故障",
12 => "软件故障",
13 => "电源故障",
14 => "紧急停止",
15 => "限位开关触发",
_ => $"未知报警 (0x{AlarmCode:X4})"
};
}
/// <summary>
/// 转换为字符串表示
/// </summary>
public override string ToString()
{
return $"状态: {StatusText}, " +
$"位置: {ActualPosition}, " +
$"速度: {ActualSpeed}, " +
$"报警: {GetAlarmDescription()}";
}
}
}
2.4 轴服务接口
IAxisService.cs
csharp
using System;
using System.Threading.Tasks;
namespace ModbusMultiSlave.Services
{
/// <summary>
/// 轴服务接口
/// </summary>
public interface IAxisService : IDisposable
{
// 轴基本信息
string AxisName { get; }
byte SlaveId { get; }
// 目标参数
int TargetPosition { get; set; }
int TargetSpeed { get; set; }
// 状态信息
int ActualPosition { get; }
int CommandPosition { get; }
int ActualSpeed { get; }
string AlarmDescription { get; }
bool IsReady { get; }
bool IsMoving { get; }
bool IsConnected { get; }
string Status { get; }
ushort AlarmCode { get; }
// 连接管理
Task<bool> ConnectAsync(string portName = "COM11", int baudRate = 115200);
Task DisconnectAsync();
// 控制命令
Task<bool> SetOriginAsync();
Task<bool> GoHomeAsync();
Task<bool> MoveAbsoluteAsync();
Task<bool> StopAsync();
Task<bool> EmergencyStopAsync();
Task<bool> ResetAlarmAsync();
Task<bool> SetSpeedAsync(int speed);
// 状态监控
Task UpdateStatusAsync();
void StartMonitoring();
void StopMonitoring();
// 获取底层驱动控制器
AzDriverController GetDriver();
}
}
2.5 轴服务实现
AxisService.cs
csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using ModbusMultiSlave.Controllers;
using Microsoft.Extensions.Logging;
namespace ModbusMultiSlave.Services
{
/// <summary>
/// 轴服务实现
/// </summary>
public class AxisService : IAxisService
{
#region 私有字段
private readonly AzDriverController _driver;
private readonly byte _slaveId;
private readonly ILogger<AxisService> _logger;
private CancellationTokenSource _monitoringCts;
private bool _isMonitoring = false;
private bool _isDisposed = false;
private bool _hasSetAbsoluteMode = false;
#endregion
#region 属性
public string AxisName { get; }
public byte SlaveId => _slaveId;
public int TargetPosition { get; set; } = 0;
public int TargetSpeed { get; set; } = 1000;
public int ActualPosition { get; private set; }
public int CommandPosition { get; private set; }
public int ActualSpeed { get; private set; }
public string AlarmDescription { get; private set; } = "正常";
public bool IsReady { get; private set; }
public bool IsMoving { get; private set; }
public bool IsConnected => _driver?.IsConnected == true && _driver.IsSlaveOnline(_slaveId);
public string Status { get; private set; } = "未连接";
public ushort AlarmCode { get; private set; }
#endregion
#region 构造函数
public AxisService(byte slaveId, string axisName, ILogger<AxisService> logger, AzDriverController driver)
{
_slaveId = slaveId;
AxisName = axisName;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_driver = driver ?? throw new ArgumentNullException(nameof(driver));
_logger.LogInformation("创建轴服务: {AxisName} (从站号: {SlaveId})", axisName, slaveId);
}
#endregion
#region 连接管理
public async Task<bool> ConnectAsync(string portName = "COM11", int baudRate = 115200)
{
try
{
Status = "连接中...";
_logger.LogInformation("{AxisName} 开始连接: {PortName}@{BaudRate}bps",
AxisName, portName, baudRate);
// 验证连接参数
if (!ValidateConnectionParameters(portName, baudRate))
{
Status = "参数错误";
return false;
}
// 连接到驱动器
var result = await Task.Run(() => _driver.Connect(portName, baudRate));
if (result)
{
// 检查从站是否在线
_logger.LogInformation("{AxisName} 正在检查从站 {SlaveId} 是否在线", AxisName, _slaveId);
var isOnline = await Task.Run(() => _driver.CheckSlaveOnline(_slaveId));
if (isOnline)
{
// 设置绝对定位模式(只在首次连接时设置)
if (!_hasSetAbsoluteMode)
{
_logger.LogInformation("{AxisName} 设置绝对定位模式", AxisName);
await Task.Run(() => _driver.SetAbsoluteMode(_slaveId));
_hasSetAbsoluteMode = true;
}
Status = "已连接";
_logger.LogInformation("✅ {AxisName} 连接成功: 从站 {SlaveId} 在线", AxisName, _slaveId);
}
else
{
Status = "从站无响应";
_logger.LogWarning("❌ {AxisName} 连接失败: 从站 {SlaveId} 无响应", AxisName, _slaveId);
return false;
}
}
else
{
Status = "连接失败";
_logger.LogWarning("❌ {AxisName} 连接失败", AxisName);
}
return result;
}
catch (Exception ex)
{
Status = "连接异常";
_logger.LogError(ex, "⚠️ {AxisName} 连接异常", AxisName);
return false;
}
}
private bool ValidateConnectionParameters(string portName, int baudRate)
{
// 检查端口是否存在
var ports = System.IO.Ports.SerialPort.GetPortNames();
if (!ports.Contains(portName))
{
_logger.LogError("串口 {PortName} 不存在,可用端口: {AvailablePorts}",
portName, string.Join(", ", ports));
return false;
}
// 检查波特率是否有效
var validBaudRates = new[] { 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600 };
if (!validBaudRates.Contains(baudRate))
{
_logger.LogError("无效的波特率 {BaudRate},有效值: {ValidBaudRates}",
baudRate, string.Join(", ", validBaudRates));
return false;
}
_logger.LogInformation("连接参数验证通过: {PortName}@{BaudRate}bps", portName, baudRate);
return true;
}
public async Task DisconnectAsync()
{
try
{
StopMonitoring();
_hasSetAbsoluteMode = false;
Status = "未连接";
_logger.LogInformation("{AxisName} 已断开连接", AxisName);
await Task.CompletedTask;
}
catch (Exception ex)
{
_logger.LogError(ex, "{AxisName} 断开连接异常", AxisName);
}
}
#endregion
#region 核心控制命令
public async Task<bool> SetOriginAsync()
{
try
{
_logger.LogInformation("{AxisName} 设置原点", AxisName);
var result = await Task.Run(() => _driver.SetOrigin(_slaveId));
if (result)
_logger.LogInformation("✅ {AxisName} 设置原点成功", AxisName);
else
_logger.LogWarning("❌ {AxisName} 设置原点失败", AxisName);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "⚠️ {AxisName} 设置原点失败", AxisName);
return false;
}
}
public async Task<bool> GoHomeAsync()
{
try
{
_logger.LogInformation("{AxisName} 回原点", AxisName);
var result = await Task.Run(() => _driver.GoHome(_slaveId));
if (result)
_logger.LogInformation("✅ {AxisName} 回原点命令已发送", AxisName);
else
_logger.LogWarning("❌ {AxisName} 回原点命令发送失败", AxisName);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "⚠️ {AxisName} 回原点失败", AxisName);
return false;
}
}
public async Task<bool> MoveAbsoluteAsync()
{
try
{
_logger.LogInformation("{AxisName} 开始绝对定位: 位置={TargetPosition}, 速度={TargetSpeed}",
AxisName, TargetPosition, TargetSpeed);
// 确保已设置绝对定位模式
if (!_hasSetAbsoluteMode)
{
_logger.LogInformation("{AxisName} 自动设置绝对定位模式", AxisName);
await Task.Run(() => _driver.SetAbsoluteMode(_slaveId));
_hasSetAbsoluteMode = true;
}
var result = await Task.Run(() => _driver.MoveAbsolute(_slaveId, TargetPosition, TargetSpeed));
if (result)
_logger.LogInformation("✅ {AxisName} 绝对定位命令已发送", AxisName);
else
_logger.LogWarning("❌ {AxisName} 绝对定位命令发送失败", AxisName);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "⚠️ {AxisName} 绝对定位失败", AxisName);
return false;
}
}
public async Task<bool> StopAsync()
{
try
{
_logger.LogInformation("{AxisName} 停止", AxisName);
var result = await Task.Run(() => _driver.Stop(_slaveId));
if (result)
_logger.LogInformation("✅ {AxisName} 停止命令已发送", AxisName);
else
_logger.LogWarning("❌ {AxisName} 停止命令发送失败", AxisName);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "⚠️ {AxisName} 停止失败", AxisName);
return false;
}
}
public async Task<bool> EmergencyStopAsync()
{
try
{
_logger.LogInformation("{AxisName} 急停", AxisName);
var result = await Task.Run(() => _driver.EmergencyStop(_slaveId));
if (result)
_logger.LogInformation("✅ {AxisName} 急停命令已发送", AxisName);
else
_logger.LogWarning("❌ {AxisName} 急停命令发送失败", AxisName);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "⚠️ {AxisName} 急停失败", AxisName);
return false;
}
}
public async Task<bool> ResetAlarmAsync()
{
try
{
_logger.LogInformation("{AxisName} 清理报警", AxisName);
var result = await Task.Run(() => _driver.ResetAlarm(_slaveId));
if (result)
_logger.LogInformation("✅ {AxisName} 清理报警命令已发送", AxisName);
else
_logger.LogWarning("❌ {AxisName} 清理报警命令发送失败", AxisName);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "⚠️ {AxisName} 清理报警失败", AxisName);
return false;
}
}
public async Task<bool> SetSpeedAsync(int speed)
{
try
{
TargetSpeed = speed;
_logger.LogInformation("{AxisName} 设置速度: {Speed}", AxisName, speed);
var result = await Task.Run(() => _driver.SetSpeed(_slaveId, speed));
if (result)
_logger.LogInformation("✅ {AxisName} 设置速度成功", AxisName);
else
_logger.LogWarning("❌ {AxisName} 设置速度失败", AxisName);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "⚠️ {AxisName} 设置速度失败", AxisName);
return false;
}
}
#endregion
#region 状态监控
public async Task UpdateStatusAsync()
{
try
{
_logger.LogDebug("{AxisName} 开始更新状态", AxisName);
var status = await Task.Run(() => _driver.ReadStatus(_slaveId));
ActualPosition = status.ActualPosition;
CommandPosition = status.CommandPosition;
ActualSpeed = status.ActualSpeed;
AlarmCode = status.AlarmCode;
AlarmDescription = status.GetAlarmDescription();
IsReady = status.IsReady;
IsMoving = status.IsMoving;
UpdateStatusText();
_logger.LogDebug("{AxisName} 状态更新完成: 位置={ActualPosition}, 速度={ActualSpeed}, 报警={AlarmDescription}",
AxisName, ActualPosition, ActualSpeed, AlarmDescription);
}
catch (Exception ex)
{
_logger.LogError(ex, "⚠️ {AxisName} 更新状态失败", AxisName);
Status = "状态更新失败";
}
}
private void UpdateStatusText()
{
if (AlarmCode != 0)
{
Status = $"报警: {AlarmDescription}";
_logger.LogWarning("{AxisName} 状态: {Status}", AxisName, Status);
}
else if (IsMoving)
{
Status = "运行中";
_logger.LogInformation("{AxisName} 状态: {Status}", AxisName, Status);
}
else if (IsReady)
{
Status = "就绪";
_logger.LogInformation("{AxisName} 状态: {Status}", AxisName, Status);
}
else if (IsConnected)
{
Status = "已连接";
_logger.LogInformation("{AxisName} 状态: {Status}", AxisName, Status);
}
else
{
Status = "未连接";
_logger.LogInformation("{AxisName} 状态: {Status}", AxisName, Status);
}
}
public void StartMonitoring()
{
if (_isMonitoring)
{
_logger.LogInformation("{AxisName} 监控已在运行", AxisName);
return;
}
_isMonitoring = true;
_monitoringCts = new CancellationTokenSource();
_logger.LogInformation("📡 {AxisName} 启动状态监控", AxisName);
Task.Run(async () =>
{
while (!_monitoringCts.Token.IsCancellationRequested && _isMonitoring)
{
try
{
if (IsConnected)
{
await UpdateStatusAsync();
}
else
{
_logger.LogDebug("{AxisName} 未连接,跳过状态更新", AxisName);
}
await Task.Delay(500, _monitoringCts.Token); // 500ms 更新一次
}
catch (OperationCanceledException)
{
_logger.LogInformation("{AxisName} 监控被取消", AxisName);
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "{AxisName} 监控出错", AxisName);
await Task.Delay(1000);
}
}
_isMonitoring = false;
_logger.LogInformation("📡 {AxisName} 状态监控已停止", AxisName);
}, _monitoringCts.Token);
}
public void StopMonitoring()
{
_logger.LogInformation("📡 {AxisName} 停止状态监控", AxisName);
_monitoringCts?.Cancel();
_isMonitoring = false;
}
#endregion
#region 辅助方法
public AzDriverController GetDriver()
{
return _driver;
}
#endregion
#region 清理资源
public void Dispose()
{
if (_isDisposed)
{
_logger.LogDebug("{AxisName} 已释放,跳过", AxisName);
return;
}
_logger.LogInformation("{AxisName} 开始释放资源", AxisName);
StopMonitoring();
_monitoringCts?.Dispose();
_isDisposed = true;
GC.SuppressFinalize(this);
_logger.LogInformation("{AxisName} 资源已释放", AxisName);
}
~AxisService()
{
Dispose();
}
#endregion
}
}
三、视图模型实现
TestViewModel.cs
csharp
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using ModbusMultiSlave.Controllers;
using ModbusMultiSlave.Services;
namespace ModbusMultiSlave.ViewModels
{
/// <summary>
/// 测试视图模型 - 控制两个轴
/// </summary>
public partial class TestViewModel : ObservableObject, IDisposable
{
#region 私有字段
private readonly ILogger<TestViewModel> _logger;
private readonly IAxisService _xAxisService;
private readonly IAxisService _rzAxisService;
private readonly SemaphoreSlim _commandLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _monitoringCts;
private bool _isMonitoring = false;
private bool _isDisposed = false;
#endregion
#region 轴控制属性
// X轴属性
[ObservableProperty]
private string _xAxisStatus = "未连接";
[ObservableProperty]
private bool _xAxisIsConnected = false;
[ObservableProperty]
private int _xAxisActualPosition;
[ObservableProperty]
private int _xAxisCommandPosition;
[ObservableProperty]
private int _xAxisActualSpeed;
[ObservableProperty]
private string _xAxisAlarm = "正常";
[ObservableProperty]
private bool _xAxisIsReady;
[ObservableProperty]
private bool _xAxisIsMoving;
[ObservableProperty]
private int _xAxisTargetPosition = 0;
[ObservableProperty]
private int _xAxisTargetSpeed = 1000;
[ObservableProperty]
private int _xAxisAcceleration = 1500;
[ObservableProperty]
private int _xAxisDeceleration = 1500;
// Rz轴属性
[ObservableProperty]
private string _rzAxisStatus = "未连接";
[ObservableProperty]
private bool _rzAxisIsConnected = false;
[ObservableProperty]
private int _rzAxisActualPosition;
[ObservableProperty]
private int _rzAxisCommandPosition;
[ObservableProperty]
private int _rzAxisActualSpeed;
[ObservableProperty]
private string _rzAxisAlarm = "正常";
[ObservableProperty]
private bool _rzAxisIsReady;
[ObservableProperty]
private bool _rzAxisIsMoving;
[ObservableProperty]
private int _rzAxisTargetPosition = 0;
[ObservableProperty]
private int _rzAxisTargetSpeed = 1000;
[ObservableProperty]
private int _rzAxisAcceleration = 1500;
[ObservableProperty]
private int _rzAxisDeceleration = 1500;
// 连接参数
[ObservableProperty]
private string _comPort = "COM11";
[ObservableProperty]
private int _baudRate = 115200;
[ObservableProperty]
private bool _isConnecting = false;
#endregion
#region 构造函数
public TestViewModel(
ILogger<TestViewModel> logger,
IAxisService xAxisService,
IAxisService rzAxisService)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_xAxisService = xAxisService ?? throw new ArgumentNullException(nameof(xAxisService));
_rzAxisService = rzAxisService ?? throw new ArgumentNullException(nameof(rzAxisService));
_logger.LogInformation("TestViewModel 初始化完成");
// 初始化连接
InitializeConnection();
}
#endregion
#region 初始化
private async void InitializeConnection()
{
try
{
_logger.LogInformation("开始初始化 AZ 驱动器连接");
// 异步连接两个轴
await ConnectAllAxesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "初始化连接失败");
}
}
private async Task ConnectAllAxesAsync()
{
try
{
IsConnecting = true;
_logger.LogInformation("=== 第一阶段: 连接 X 轴 ===");
// 连接 X 轴
bool xConnected = await _xAxisService.ConnectAsync(ComPort, BaudRate);
XAxisIsConnected = xConnected;
XAxisStatus = xConnected ? "已连接" : "连接失败";
if (xConnected)
{
await Task.Delay(1000);
_logger.LogInformation("=== 第二阶段: 连接 Rz 轴 ===");
// 连接 Rz 轴
bool rzConnected = await _rzAxisService.ConnectAsync(ComPort, BaudRate);
RzAxisIsConnected = rzConnected;
RzAxisStatus = rzConnected ? "已连接" : "连接失败";
if (rzConnected)
{
_logger.LogInformation("=== 第三阶段: 启动状态监控 ===");
// 启动状态监控
_xAxisService.StartMonitoring();
_rzAxisService.StartMonitoring();
// 启动自己的状态更新循环
StartStatusMonitoring();
_logger.LogInformation("✅ 所有轴连接成功并启动监控");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "连接轴失败");
}
finally
{
IsConnecting = false;
}
}
#endregion
#region 状态监控
private void StartStatusMonitoring()
{
if (_isMonitoring) return;
_isMonitoring = true;
_monitoringCts = new CancellationTokenSource();
Task.Run(async () =>
{
while (!_monitoringCts.Token.IsCancellationRequested && _isMonitoring)
{
try
{
await UpdateAxesStatusAsync();
await Task.Delay(500, _monitoringCts.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "状态监控出错");
await Task.Delay(1000);
}
}
}, _monitoringCts.Token);
}
private void StopStatusMonitoring()
{
_monitoringCts?.Cancel();
_isMonitoring = false;
}
private async Task UpdateAxesStatusAsync()
{
// 使用信号量确保串行更新
await _commandLock.WaitAsync();
try
{
// 更新 X 轴状态
if (XAxisIsConnected)
{
await _xAxisService.UpdateStatusAsync();
XAxisActualPosition = _xAxisService.ActualPosition;
XAxisCommandPosition = _xAxisService.CommandPosition;
XAxisActualSpeed = _xAxisService.ActualSpeed;
XAxisAlarm = _xAxisService.AlarmDescription;
XAxisIsReady = _xAxisService.IsReady;
XAxisIsMoving = _xAxisService.IsMoving;
XAxisStatus = _xAxisService.Status;
}
// 短暂延迟,避免通信冲突
await Task.Delay(50);
// 更新 Rz 轴状态
if (RzAxisIsConnected)
{
await _rzAxisService.UpdateStatusAsync();
RzAxisActualPosition = _rzAxisService.ActualPosition;
RzAxisCommandPosition = _rzAxisService.CommandPosition;
RzAxisActualSpeed = _rzAxisService.ActualSpeed;
RzAxisAlarm = _rzAxisService.AlarmDescription;
RzAxisIsReady = _rzAxisService.IsReady;
RzAxisIsMoving = _rzAxisService.IsMoving;
RzAxisStatus = _rzAxisService.Status;
}
}
finally
{
_commandLock.Release();
}
}
#endregion
#region X轴命令
[RelayCommand]
private async Task XAxisConnectAsync()
{
try
{
XAxisStatus = "连接中...";
var result = await _xAxisService.ConnectAsync(ComPort, BaudRate);
XAxisIsConnected = result;
XAxisStatus = result ? "已连接" : "连接失败";
if (result)
{
_xAxisService.StartMonitoring();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴连接失败");
XAxisStatus = "连接异常";
}
}
[RelayCommand]
private async Task XAxisDisconnectAsync()
{
try
{
await _xAxisService.DisconnectAsync();
XAxisIsConnected = false;
XAxisStatus = "未连接";
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴断开失败");
}
}
[RelayCommand]
private async Task XAxisSetOriginAsync()
{
try
{
if (!XAxisIsConnected) return;
var result = await _xAxisService.SetOriginAsync();
if (!result)
{
await ShowErrorAsync("X轴原点设置", "设置失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴原点设置失败");
await ShowErrorAsync("X轴原点设置", ex.Message);
}
}
[RelayCommand]
private async Task XAxisGoHomeAsync()
{
try
{
if (!XAxisIsConnected) return;
var result = await _xAxisService.GoHomeAsync();
if (!result)
{
await ShowErrorAsync("X轴回原点", "执行失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴回原点失败");
await ShowErrorAsync("X轴回原点", ex.Message);
}
}
[RelayCommand]
private async Task XAxisStartAsync()
{
try
{
if (!XAxisIsConnected) return;
// 更新驱动器参数
await _xAxisService.SetSpeedAsync(XAxisTargetSpeed);
_xAxisService.TargetPosition = XAxisTargetPosition;
// 执行绝对定位
var result = await _xAxisService.MoveAbsoluteAsync();
if (!result)
{
await ShowErrorAsync("X轴启动", "启动失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴启动失败");
await ShowErrorAsync("X轴启动", ex.Message);
}
}
[RelayCommand]
private async Task XAxisStopAsync()
{
try
{
if (!XAxisIsConnected) return;
var result = await _xAxisService.StopAsync();
if (!result)
{
await ShowErrorAsync("X轴停止", "停止失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴停止失败");
await ShowErrorAsync("X轴停止", ex.Message);
}
}
[RelayCommand]
private async Task XAxisResetAlarmAsync()
{
try
{
if (!XAxisIsConnected) return;
var result = await _xAxisService.ResetAlarmAsync();
if (!result)
{
await ShowErrorAsync("X轴清理报警", "清理失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴清理报警失败");
await ShowErrorAsync("X轴清理报警", ex.Message);
}
}
[RelayCommand]
private async Task XAxisEmergencyStopAsync()
{
try
{
if (!XAxisIsConnected) return;
var result = await _xAxisService.EmergencyStopAsync();
if (!result)
{
await ShowErrorAsync("X轴急停", "急停失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴急停失败");
await ShowErrorAsync("X轴急停", ex.Message);
}
}
[RelayCommand]
private async Task XAxisSetSpeedAsync()
{
try
{
if (!XAxisIsConnected) return;
var result = await _xAxisService.SetSpeedAsync(XAxisTargetSpeed);
if (!result)
{
await ShowErrorAsync("X轴设置速度", "设置失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "X轴设置速度失败");
await ShowErrorAsync("X轴设置速度", ex.Message);
}
}
#endregion
#region Rz轴命令
[RelayCommand]
private async Task RzAxisConnectAsync()
{
try
{
RzAxisStatus = "连接中...";
var result = await _rzAxisService.ConnectAsync(ComPort, BaudRate);
RzAxisIsConnected = result;
RzAxisStatus = result ? "已连接" : "连接失败";
if (result)
{
_rzAxisService.StartMonitoring();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴连接失败");
RzAxisStatus = "连接异常";
}
}
[RelayCommand]
private async Task RzAxisDisconnectAsync()
{
try
{
await _rzAxisService.DisconnectAsync();
RzAxisIsConnected = false;
RzAxisStatus = "未连接";
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴断开失败");
}
}
[RelayCommand]
private async Task RzAxisSetOriginAsync()
{
try
{
if (!RzAxisIsConnected) return;
var result = await _rzAxisService.SetOriginAsync();
if (!result)
{
await ShowErrorAsync("Rz轴原点设置", "设置失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴原点设置失败");
await ShowErrorAsync("Rz轴原点设置", ex.Message);
}
}
[RelayCommand]
private async Task RzAxisGoHomeAsync()
{
try
{
if (!RzAxisIsConnected) return;
var result = await _rzAxisService.GoHomeAsync();
if (!result)
{
await ShowErrorAsync("Rz轴回原点", "执行失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴回原点失败");
await ShowErrorAsync("Rz轴回原点", ex.Message);
}
}
[RelayCommand]
private async Task RzAxisStartAsync()
{
try
{
if (!RzAxisIsConnected) return;
await _rzAxisService.SetSpeedAsync(RzAxisTargetSpeed);
_rzAxisService.TargetPosition = RzAxisTargetPosition;
var result = await _rzAxisService.MoveAbsoluteAsync();
if (!result)
{
await ShowErrorAsync("Rz轴启动", "启动失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴启动失败");
await ShowErrorAsync("Rz轴启动", ex.Message);
}
}
[RelayCommand]
private async Task RzAxisStopAsync()
{
try
{
if (!RzAxisIsConnected) return;
var result = await _rzAxisService.StopAsync();
if (!result)
{
await ShowErrorAsync("Rz轴停止", "停止失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴停止失败");
await ShowErrorAsync("Rz轴停止", ex.Message);
}
}
[RelayCommand]
private async Task RzAxisResetAlarmAsync()
{
try
{
if (!RzAxisIsConnected) return;
var result = await _rzAxisService.ResetAlarmAsync();
if (!result)
{
await ShowErrorAsync("Rz轴清理报警", "清理失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴清理报警失败");
await ShowErrorAsync("Rz轴清理报警", ex.Message);
}
}
[RelayCommand]
private async Task RzAxisEmergencyStopAsync()
{
try
{
if (!RzAxisIsConnected) return;
var result = await _rzAxisService.EmergencyStopAsync();
if (!result)
{
await ShowErrorAsync("Rz轴急停", "急停失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴急停失败");
await ShowErrorAsync("Rz轴急停", ex.Message);
}
}
[RelayCommand]
private async Task RzAxisSetSpeedAsync()
{
try
{
if (!RzAxisIsConnected) return;
var result = await _rzAxisService.SetSpeedAsync(RzAxisTargetSpeed);
if (!result)
{
await ShowErrorAsync("Rz轴设置速度", "设置失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rz轴设置速度失败");
await ShowErrorAsync("Rz轴设置速度", ex.Message);
}
}
#endregion
#region 双轴控制
[RelayCommand]
private async Task ConnectAllAxesCommandAsync()
{
await ConnectAllAxesAsync();
}
[RelayCommand]
private async Task DisconnectAllAxesAsync()
{
try
{
await _xAxisService.DisconnectAsync();
await _rzAxisService.DisconnectAsync();
XAxisIsConnected = false;
RzAxisIsConnected = false;
XAxisStatus = "未连接";
RzAxisStatus = "未连接";
StopStatusMonitoring();
}
catch (Exception ex)
{
_logger.LogError(ex, "断开所有轴失败");
}
}
[RelayCommand]
private async Task EmergencyStopAllAsync()
{
try
{
if (XAxisIsConnected)
await _xAxisService.EmergencyStopAsync();
await Task.Delay(50);
if (RzAxisIsConnected)
await _rzAxisService.EmergencyStopAsync();
_logger.LogInformation("所有轴急停完成");
}
catch (Exception ex)
{
_logger.LogError(ex, "急停所有轴失败");
}
}
#endregion
#region 诊断命令
[RelayCommand]
private async Task DiagnoseCommunicationAsync()
{
try
{
_logger.LogInformation("=== 开始通信诊断 ===");
// 1. 检查串口
var ports = System.IO.Ports.SerialPort.GetPortNames();
_logger.LogInformation("可用串口: {Ports}", string.Join(", ", ports));
// 2. 测试 X 轴通信
if (XAxisIsConnected)
{
_logger.LogInformation("测试 X 轴通信...");
// 诊断位置数据
await Task.Run(() =>
_xAxisService.GetDriver().Diagnose32BitData(1, AzDriverAddresses.ActualPosition, 20000));
await Task.Delay(200);
}
// 3. 测试 Rz 轴通信
if (RzAxisIsConnected)
{
await Task.Delay(100);
_logger.LogInformation("测试 Rz 轴通信...");
await Task.Run(() =>
_rzAxisService.GetDriver().Diagnose32BitData(2, AzDriverAddresses.ActualPosition, 20000));
}
_logger.LogInformation("=== 通信诊断结束 ===");
}
catch (Exception ex)
{
_logger.LogError(ex, "通信诊断失败");
}
}
[RelayCommand]
private async Task Test32BitDataConversionAsync()
{
try
{
_logger.LogInformation("=== 测试 32 位数据转换 ===");
// 测试值
int[] testValues = { 1000, 20000, -1000, -16000, 32767, -32768, 100000, -100000 };
foreach (var value in testValues)
{
_logger.LogInformation("测试值: {Value}", value);
// 测试 X 轴
if (XAxisIsConnected)
{
// 写入目标位置
bool writeResult = await Task.Run(() =>
_xAxisService.GetDriver().Write32BitData(1, AzDriverAddresses.TargetPosition, value));
if (writeResult)
{
await Task.Delay(100);
// 读取回来验证
int readValue = await Task.Run(() =>
_xAxisService.GetDriver().Read32BitData(1, AzDriverAddresses.TargetPosition));
bool match = value == readValue;
_logger.LogInformation("X轴写入/读取: {Match}, 写入={Write}, 读取={Read}",
match ? "✅" : "❌", value, readValue);
}
}
await Task.Delay(200);
}
_logger.LogInformation("=== 数据转换测试结束 ===");
}
catch (Exception ex)
{
_logger.LogError(ex, "数据转换测试失败");
}
}
#endregion
#region 辅助方法
private async Task ShowErrorAsync(string operation, string message)
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
MessageBox.Show(
$"{operation}: {message}",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error
);
});
}
#endregion
#region 清理资源
public void Dispose()
{
if (_isDisposed) return;
_logger.LogInformation("开始释放 TestViewModel 资源");
StopStatusMonitoring();
_monitoringCts?.Dispose();
_xAxisService?.StopMonitoring();
_rzAxisService?.StopMonitoring();
_isDisposed = true;
GC.SuppressFinalize(this);
_logger.LogInformation("TestViewModel 资源已释放");
}
~TestViewModel()
{
Dispose();
}
#endregion
}
}
四、应用入口和依赖注入配置
App.xaml.cs
csharp
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModbusMultiSlave.Controllers;
using ModbusMultiSlave.Services;
using ModbusMultiSlave.ViewModels;
namespace ModbusMultiSlave
{
public partial class App : Application
{
[DllImport("kernel32.dll")]
private static extern bool AllocConsole();
public static IServiceProvider ServiceProvider { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
AllocConsole(); // 显示控制台窗口,方便调试
var services = new ServiceCollection();
// 配置日志
services.AddLogging(loggingBuilder =>
{
loggingBuilder.SetMinimumLevel(LogLevel.Debug);
loggingBuilder.AddConsole();
loggingBuilder.AddProvider(new CustomLoggerProvider(
Path.Combine(Environment.CurrentDirectory, "modbus.log")));
});
// 注册核心服务(单例)
services.AddSingleton<AzDriverController>();
// 注册轴服务(X轴 - 从站1,Rz轴 - 从站2)
services.AddSingleton<IAxisService>(provider =>
{
var driver = provider.GetRequiredService<AzDriverController>();
var logger = provider.GetRequiredService<ILogger<AxisService>>();
return new AxisService(1, "X轴", logger, driver);
});
services.AddSingleton<IAxisService>(provider =>
{
var driver = provider.GetRequiredService<AzDriverController>();
var logger = provider.GetRequiredService<ILogger<AxisService>>();
return new AxisService(2, "Rz轴", logger, driver);
});
// 注册 ViewModel
services.AddSingleton<TestViewModel>();
// 如果需要主窗口,可以这样注册
// services.AddSingleton<MainWindow>();
ServiceProvider = services.BuildServiceProvider();
// 创建并显示主窗口
var mainWindow = new MainWindow
{
DataContext = ServiceProvider.GetRequiredService<TestViewModel>()
};
mainWindow.Show();
base.OnStartup(e);
}
}
}
五、关键问题和解决方案
5.1 问题1:一个COM口多个连接
问题描述 :尝试为每个轴创建独立的 SerialPort 实例连接同一个 COM 口。
解决方案 :使用单例模式的 AzDriverController 共享连接。
5.2 问题2:通信时序冲突
问题描述:两个轴同时通信导致 Modbus 数据帧冲突。
解决方案:
- 使用
lock语句确保同一时间只有一个通信操作 - 不同从站之间添加最小通信间隔(50ms)
- ViewModel 中使用信号量串行化状态更新
5.3 问题3:32位寄存器转换错误
问题描述:寄存器值 65535 和 49536 被错误解析。
解决方案:
- 正确处理有符号32位整数(二进制补码)
- 添加详细的诊断日志
- 支持不同数据格式解析
5.4 问题4:控制命令需要点动操作
问题描述:AZ 驱动器控制字需要写命令后清零。
解决方案 :在 SendControlCommand 方法中实现写命令 → 延迟 → 清零的序列。
六、使用说明
6.1 安装依赖
xml
<!-- 在 .csproj 文件中添加 -->
<PackageReference Include="NModbus" Version="3.0.90" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
6.2 配置硬件
- 将两个 AZ 驱动器连接到同一个 RS485 总线
- 设置不同的从站地址(X轴=1,Rz轴=2)
- 确保所有设备使用相同的波特率、数据位、停止位、校验位
6.3 运行应用
- 编译并运行应用程序
- 观察控制台输出,查看通信日志
- 使用界面按钮测试各个功能
七、性能优化建议
7.1 批量读取优化
csharp
/// <summary>
/// 批量读取多个寄存器
/// </summary>
public ushort[] ReadMultipleRegisters(byte slaveId, ushort startAddress, ushort count)
{
lock (_lock)
{
try
{
return _master.ReadHoldingRegisters(slaveId, (ushort)(startAddress - 1), count);
}
catch (Exception ex)
{
_logger.LogError(ex, "批量读取寄存器失败");
return Array.Empty<ushort>();
}
}
}
7.2 异步优化
csharp
/// <summary>
/// 异步读取状态(避免阻塞 UI)
/// </summary>
public async Task<AzDriverStatus> ReadStatusAsync(byte slaveId)
{
return await Task.Run(() => ReadStatus(slaveId));
}
八、常见问题排查
8.1 通信超时
- 检查串口参数是否与硬件匹配
- 增加
ReadTimeout和WriteTimeout - 检查物理连接是否正常
8.2 从站无响应
- 确认从站地址设置正确
- 检查 RS485 终端电阻是否正确连接
- 使用 Modbus 调试工具验证通信
8.3 寄存器值错误
- 使用诊断工具查看原始寄存器值
- 检查数据格式(有符号/无符号,高位/低位)
- 验证驱动器手册中的寄存器定义
九、总结
本文详细介绍了如何使用 C# 和 NModbus 库实现一个 COM 口控制多个 Modbus RTU 从站。关键点包括:
- 共享连接设计:单例模式管理串口连接
- 通信时序控制:锁和间隔确保通信不冲突
- 数据格式处理:正确解析32位有符号整数
- 详细日志:便于调试和问题排查
- 依赖注入:清晰的服务管理和生命周期控制
这个解决方案已经过实际项目验证,可以直接用于生产环境。希望对您的项目有所帮助!