繁体   English   中英

C# SerialPort 多线程发送/接收冲突

[英]C# SerialPort multithread send/receive collisions

我正在尝试在 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 发送数据时避免冲突?

感谢您评论我的问题!

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

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

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

.NET 附带的 System.IO.Ports.SerialPort 类是一个明显的例外。 委婉地说,它是由计算机科学家设计的,这些科学家远远超出了他们的核心竞争力领域。 他们既不了解串行通信的特征,也不了解常见的用例,这说明了这一点。 它也无法在发货之前在任何真实世界场景中进行测试,而不会发现存在文档接口和未文档行为的缺陷,并使使用 System.IO.Ports.SerialPort(以下称为 IOPSP)的可靠通信成为真正的噩梦。 (StackOverflow 上的大量证据证明了这一点,来自在超级终端中工作但不在 .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 数据报可以很容易地调用发送命令

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM