C# SerialPort 多线程 send/receive 冲突

C# SerialPort multithread send/receive collisions

我正在尝试在 C# 命令行应用程序(状态机)和通过串行端口连接的外部设备之间进行通信。我有一个 .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 发送数据时如何避免冲突?

感谢您评论我的问题!

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

由于 System.IO.Ports class 无法正确处理清除发送 (CTS),因此可能会发生冲突 send/receive。

我会引用 Ben Voigts 非常好article:

The System.IO.Ports.SerialPort class which ships with .NET is a glaring exception. To put it mildly, it was designed by computer scientists operating far outside their area of core competence. They neither understood the characteristics of serial communication, nor common use cases, and it shows. Nor could it have been tested in any real world scenario prior to shipping, without finding flaws that litter both the documented interface and the undocumented behavior and make reliable communication using System.IO.Ports.SerialPort (henceforth IOPSP) a real nightmare. (Plenty of evidence on Whosebug attests to this, from devices that work in Hyperterminal but not .NET...

The worst offending System.IO.Ports.SerialPort members, ones that not only should not be used but are signs of a deep code smell and the need to rearchitect all IOPSP usage:

  • The DataReceived event (100% redundant, also completely unreliable)
  • The BytesToRead property (completely unreliable) The Read, ReadExisting, ReadLine methods (handle errors completely wrong, and are synchronous)
  • The PinChanged event (delivered out of order with respect to every interesting thing you might want to know about it)

为了解决这个问题,我编写了自己的 SerialPorts class,方法如下:

  1. 我从 MSDN
  2. 上读到了关于串行通信的 perfect article
  3. 我从 github
  4. 下载了示例 MTTTY 代码
  5. 我将 MTTTY 示例重新设计为 .dll,以便可以从 C# 调用它
  6. 我从 C
  7. 学会了如何在 C# 中 invoke callbacks
  8. 我已将 MTTTY 示例重新设计为 运行 连续侦听的 ReaderThread 和可通过以下方式调用的 WriterThread UDP 数据报

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 数据报很容易调用发送命令

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

我只是非常仔细地接触了 MTTTY 示例,所以我的 DLL 与示例非常相似(非常可靠!)。

到目前为止我还没有提供这段代码,因为我必须做一些清理工作。如果您想尽快了解现状,我可以按要求将其发送给任何人。