为什么需要桌面调试工具
PipeMonitor 的 STM32 固件开发过程中,经常需要直接连接传感器验证 Modbus 通信——寄存器地址是否正确、数据解析是否准确、轮询是否稳定。用串口助手手动组帧效率太低,所以写了一个 WPF 桌面工具。
这个工具的核心功能很简单:连接 RS485 总线,作为 Modbus RTU 主站轮询压力变送器和超声波流量计,实时显示数据。
COM 口自动识别
插入 USB 转 RS485 模块后,Windows 会分配一个 COM 号(如 COM6)。工具通过 WMI 查询设备描述,帮助用户快速定位正确的端口:
private Dictionary<string, string> GetPortDescriptions()
{
using var searcher = new ManagementObjectSearcher(
"SELECT * FROM Win32_PnPEntity WHERE Caption LIKE '%(COM%'");
foreach (ManagementObject obj in searcher.Get())
{
var caption = obj["Caption"]?.ToString();
var match = Regex.Match(caption, @"\(COM(\d+)\)");
if (match.Success)
{
var portName = $"COM{match.Groups[1].Value}";
var description = caption.Substring(0, match.Index).Trim();
descriptions[portName] = description;
}
}
return descriptions;
}
下拉列表显示 “COM3 (USB-SERIAL CH340)“,选择框只显示 “COM3”,简洁直观。工具还会自动优先选中包含 USB、CH340、CP210 等关键词的端口。
端口列表每秒刷新一次(仅未连接时),通过计算端口列表哈希值避免不必要的 WMI 查询。
NModbus 封装
工具使用 NModbus 库实现 Modbus RTU 通信:
var adapter = new SerialPortAdapter(_serialPort);
_master = _modbusFactory.CreateRtuMaster(adapter);
_master.Transport.Retries = 1; // 允许一次重试
_master.Transport.ReadTimeout = 1000; // Modbus 读超时
封装了一个带重试的读取方法:
private async Task<ushort[]> ReadHoldingRegistersWithRetryAsync(
byte slaveId, ushort startAddress, ushort numberOfPoints)
{
try {
return await _master!.ReadHoldingRegistersAsync(
slaveId, startAddress, numberOfPoints);
}
catch (TimeoutException) {
await Task.Delay(80);
return await _master!.ReadHoldingRegistersAsync(
slaveId, startAddress, numberOfPoints);
}
}
一次超时后等待 80ms 再重试一次,平衡了可靠性和响应速度。
传感器寄存器解析
压力变送器(从站 1)
private const byte PressureSensorSlaveId = 1;
private const ushort REG_PRESSURE_DECIMALS = 25; // 小数位数配置
private const ushort REG_PRESSURE_VALUE = 39; // 压力原始值
private const ushort REG_TEMPERATURE_VALUE = 40; // 温度原始值
// 解析:压力 = 原始值 / 10^小数位数
var pressureScale = Math.Pow(10, decimalPlaces);
Pressure = valueRegs[0] / pressureScale;
Temperature = valueRegs[1] / Math.Pow(10, TemperatureDecimalPlaces);
压力变送器的小数位数是可配置的(1~3 位),工具先读取寄存器 25 获取当前配置,再用正确的缩放因子计算实际值。
超声波流量计(从站 2)
private const byte FlowMeterSlaveId = 2;
private const ushort REG_FLOW_INSTANT = 1; // 瞬时流量(Real4)
private const ushort REG_FLOW_VELOCITY = 5; // 流速(Real4)
private const ushort REG_FLOW_TOTAL_INTEGER = 9; // 累计流量整数部分(U32)
private const ushort REG_FLOW_TOTAL_DECIMAL = 11; // 累计流量小数部分(Real4)
// IEEE 754 单精度浮点解析
private static float ParseReal4(ushort highWord, ushort lowWord)
{
var raw = ((uint)highWord << 16) | lowWord;
return BitConverter.Int32BitsToSingle((int)raw);
}
流量计的瞬时流量和流速使用 IEEE 754 单精度浮点(Real4),两个 16 位寄存器拼成 32 位再转 float。累计流量是整数 + 小数的组合方式。
轮询与 UI 更新
连接成功后自动开始轮询,默认每秒读取一次。每次轮询顺序读取两个从站,单个从站失败不影响另一个:
try { pressureData = await ReadPressureSensorAsync(); }
catch (Exception ex) {
ClearPressureSensorUI();
statusBuilder.Append($"从站1失败: {ex.Message}");
}
try { flowData = await ReadFlowMeterAsync(); }
catch (Exception ex) {
ClearFlowMeterUI();
statusBuilder.Append($"从站2失败: {ex.Message}");
}
使用 _isPolling 标志防止重入,避免上一次轮询未完成时下一次又触发。
界面设计
WPF 窗口采用 Windows 11 沉浸式深色主题,启用了 Mica 云母背景特效:
SourceInitialized += (s, e) =>
{
WindowHelper.EnableDarkMode(this);
WindowHelper.EnableMicaEffect(this);
};
界面布局简洁:顶部是串口选择和波特率设置,中间是连接按钮,下方是压力、温度、流量的实时读数,底部是状态栏。原始寄存器值也显示在界面上,方便调试时核对。
小结
- WMI 查询设备描述,自动识别 USB 转串口模块
- NModbus 封装带超时重试,提高 RS485 总线抖动下的稳定性
- 压力变送器支持动态小数位数,流量计支持 IEEE 754 浮点解析
- 两个从站独立轮询,单个失败不影响另一个的显示
- Windows 11 深色主题 + Mica 背景,视觉效果统一