为什么需要桌面调试工具

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 背景,视觉效果统一

后续阅读