首页 > 解决方案 > C# SerialPort 多线程发送/接收冲突

问题描述

我正在尝试在 C# 命令行应用程序(状态机)和通过 SerialPort 连接的外部设备之间进行通信。我有一个 .Net Core 3.1.0 应用程序并且正在使用 Sytem.IO.Ports (4.7.0)。我正在使用 FTDI USB 串行适配器。

我必须监视发送到我的计算机的数据指示的设备状态,并根据设备状态回复命令以进入下一个状态。

使用 Putty 发送和接收工作时没有问题

当使用命令行作为键盘输入和输出时,它可以正常工作

不幸的是,当从另一个线程向设备发送数据时,两个站点似乎同时发送,并且发生了使我的目标崩溃的冲突。

    Initialization:
    _port = new SerialPort(
                    Parameters.TelnetCOMport,
                    Parameters.TelnetBaudrate,
                    Parameters.TelnetParity,
                    Parameters.TelnetDataBits,
                    Parameters.TelnetStopBits);
                _port.DataReceived += Port_DataReceived;
                _port.ErrorReceived += Port_ErrorReceived;
                _port.Handshake = Handshake.RequestToSend;
                _port.RtsEnable = true;
                _port.DtrEnable = true;
                _port.Encoding = Encoding.ASCII;
                _port.Open();

   private void Send_Command(string command)
    {
        lock (_portLock)
        {
            _port.Write(command + "\n");
        }
    }

    private string _dataReceived;

    private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        lock (_portLock)
        {
            byte[] data = new byte[_port.BytesToRead];
            _port.Read(data, 0, data.Length);
            _dataReceived += Encoding.UTF8.GetString(data);
            ProcessDataReceived();
        }
    }

我试图用锁来避免这种情况,但它没有帮助。唯一有帮助的是在收到一个状态指示器后添加一个很长的延迟,以确保在发送新命令之前设备绝对不会发送任何内容。

所以我的问题是: 如何在从多线程应用程序通过 SerialPort 发送数据时避免冲突?

标签: c#multithreading.net-coreserial-port

解决方案


感谢您评论我的问题!

事实证明,发生错误是因为 .NET Framework 的 System.IO.Ports 类的质量非常差。

由于 System.IO.Ports 类不能正确处理 Clear-To-Send (CTS),因此可能会发生冲突的发送/接收。

我将引用 Ben Voigts 非常好的文章

.NET 附带的 System.IO.Ports.SerialPort 类是一个明显的例外。说得委婉些,它是由计算机科学家设计的,他们的工作范围远远超出了他们的核心竞争力领域。他们既不了解串行通信的特性,也不了解常见的用例,它显示了。在发货之前,它也无法在任何真实世界场景中进行测试,没有发现缺陷会导致记录的接口和未记录的行为都乱扔垃圾,并使使用 System.IO.Ports.SerialPort(以下简称 IOPSP)的可靠通信成为真正的噩梦。(StackOverflow 上的大量证据证明了这一点,来自在 Hyperterminal 而非 .NET 中工作的设备......

最糟糕的 System.IO.Ports.SerialPort 成员,它们不仅不应该被使用,而且是代码味道很重的迹象,并且需要重新架构所有 IOPSP 使用:

  • DataReceived 事件(100% 冗余,也完全不可靠)
  • BytesToRead 属性(完全不可靠) Read、ReadExisting、ReadLine 方法(处理错误完全错误,并且是同步的)
  • PinChanged 事件(关于您可能想知道的每件有趣的事情都乱序传递)

为了解决这个问题,我通过执行以下操作编写了自己的SerialPorts类:

  1. 我从 MSDN 阅读了这篇关于串行通信的完美文章
  2. 我从github下载了示例 MTTTY 代码
  3. 我将 MTTTY 示例重新设计为 .dll,以便可以从 C# 调用它
  4. 我学会了如何从 C调用C# 中的回调
  5. 我重新设计了 MTTTY 示例以运行连续侦听的 ReaderThread 和可以通过UDP 数据报调用的WriterThread

C# 代码如下所示:

    #region Load C DLL
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate void ProgressCallback(IntPtr buffer, int length);

    [DllImport("mttty.dll")]
    static extern void ConnectToSerialPort([MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback telnetCallbackPointer,
        [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback statusCallbackPointer,
        [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback errorCallbackPointer);
    #endregion

    #region Callbacks 
    private void TelnetCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = new byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);
        Console.WriteLine(Encoding.UTF8.GetString(safeBuffer));
    }

    private void StatusCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = new byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);            
        Console.WriteLine("Status: "+Encoding.UTF8.GetString(safeBuffer));
    }

    private void ErrorCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = new byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);
        Console.WriteLine("Error: "+Encoding.UTF8.GetString(safeBuffer));
    }
    #endregion

然后调用监听器线程,如下所示

    private static Socket _sock;
    private static IPEndPoint _endPoint;

    public StartSerialPortListenerThread()
    {
        // This socket is used to send Write request to thread in C dll
        _sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        IPAddress serverAddr = IPAddress.Parse("127.0.0.1");
        _endPoint = new IPEndPoint(serverAddr, 5555);
        
        // This starts the listener thread in the C dll
        Task.Factory.StartNew(() =>
        {
            ConnectToSerialPort(TelnetCallback, StatusCallback, ErrorCallback);
        });
    }

通过发送 UDP 数据报很容易调用 send 命令

 private void SendCommand(string command)
    {
        byte[] sendBuffer = Encoding.ASCII.GetBytes(command + '\n');
        _sock.SendTo(sendBuffer, _endPoint);
    }

我只是非常仔细地接触了 MTTTY 示例,因此我的 DLL 的工作方式与示例非常相似(非常可靠!)。

到目前为止,我还没有提供此代码,因为我必须进行一些清理。如果您想尽快获得现状,我可以按要求将其发送给任何人。


推荐阅读