C#串口单例 + 端口复用

一台电脑只有1个物理 COM 口,如果在两个地方(主程序 + 子页面)都写了 new SerialPort("COM1")

  • 结果:

    • 谁先 Open(),谁就占坑成功;

    • 后一个再 Open() 会报 "端口已被占用"。

  • 于是出现"程序 A 连上了,程序 B 就连不上"的尴尬。

单例 + 端口复用

1️⃣ 单例(Singleton)

  • 整个进程里只 new 一次 SerialPort。

  • 任何地方都用 同一个对象 → 不会重复占用端口。

  • 代码里用 Lazy<T>static 变量即可:

  • csharp

    复制

    cs 复制代码
    private static readonly Lazy<Powersupple_AE0> _lazy =
        new(() => new Powersupple_AE0());
    public static Powersupple_AE0 Instance => _lazy.Value;

    2️⃣ 端口复用(Port Reuse)

  • 虽然只有一个串口对象,但 端口号可能想换(COM1→ COM2)。

  • 复用逻辑:

    • 端口相同 → 直接返回已打开的串口;

    • 端口不同 → 先关闭旧串口,再 new 一个指向新端口。

  • csharp

    复制

    cs 复制代码
    public void Open(string portName = "COM1", byte address = 1)
    {
        lock (this)
        {
            if (_serial?.IsOpen == true && _serial.PortName == portName)
                return;                // ✅ 端口没变,直接复用
    
            _serial?.Close();          // ❌ 端口变了或没开,先关
            _serial?.Dispose();
    
            _serial = new SerialPort(portName, 4800, ...);
            _serial.Open();
            _serial.WriteLine("...");
        }
    }

"单例"保证整个进程只 new 一次串口,"端口复用"保证即使换端口,也只保留一个活的连接。

cs 复制代码
// 第一次:真正打开
Powersupple_AE800.Instance.Open("COM25", 1);

// 第二次:直接复用
Powersupple_AE800.Instance.SetVoltage(12);

// 换端口:先关旧再开新
Powersupple_AE800.Instance.Open("COM26", 1);

在多个线程里共用同一个串口单例

核心只有一句话:"串口不是线程安全的,必须串行化访问。"

1. 所有读写必须加锁(最简单也最安全)

所有 SendCommand / Open / Close 都用它包裹即可

cs 复制代码
private readonly object _lock = new object();

public void SetVoltage(double v)
{
    lock (_lock)
    {
        SendCommand($"命令");
    }
}

UI 线程、后台线程、定时器线程......无论谁调 ,都走同一把锁,绝不会交叉写入

2. 不要跨线程直接访问 SerialPort 成员

错误示例

cs 复制代码
Task.Run(() => ae800.serialPort.WriteLine("xxx"));   // 可能和 UI 线程冲突

正确做法

cs 复制代码
Task.Run(() => Powersupple_AE800.Instance.SetVoltage(12));
3. 读写超时设置要合理
cs 复制代码
serialPort.ReadTimeout  = 2000;   // 至少 1~2 秒,避免线程饿死
serialPort.WriteTimeout = 1000;
4. 避免"长事务"阻塞其他线程
  • 如果一次发送/接收需要几百毫秒,

    Task.Run(() => ae0.ReadVoltage()) 包一层,
    但锁仍留在单例内部,不会破坏线程安全。

简单模板

cs 复制代码
public void SafeRead()
{
    double v = 0;
    ThreadPool.QueueUserWorkItem(_ =>
    {
        lock (Powersupple_AE0.Instance)
        {
            v = Powersupple_AE0.Instance.ReadVoltage();
        }
        BeginInvoke(() => lblVoltage.Text = $"{命令} V");
    });
}

总结:加同一把锁 + 不要直接碰 SerialPort + 合理超时