SerialPort.BaseStream.ReadAsync 从 USB 串行端口读取时丢弃或扰乱字节

SerialPort.BaseStream.ReadAsync drops or scrambles bytes when reading from a USB Serial Port

编辑: 我已经添加了发送代码和我正在接收的输出示例。


我正在从连接到嵌入式系统的 USB "virtual" 串口读取数据。我写了两种接收数据的方法,一种是同步的,一种是异步的。同步的工作,异步的丢失或扰乱一点点传入的数据。我不知道为什么第二个失败了。

有效的方法 调用 SerialPort.Read 并将读取超时设置为零,它请求接收缓冲区中的所有内容。我检查 return 值以查看实际读取了多少字节,然后将数据放入循环缓冲区以供其他地方使用。此方法由定时器中断调用,它非常适合接收串行数据(通常以高于 1.6 Mbps 的速率且无数据丢失)。但是,轮询计时器对我来说已经成为一个问题,我更愿意通过我的其余代码异步接收数据。

丢失数据的方法等待串口BaseStream上的ReadAsync,循环直到取消。这种方法 有点 有效,但它经常 return 数据包的前导字节乱序,相当频繁地丢失一个字节(大约每几千个数据字节一次) ,并且偶尔会从数据包中丢失数百个连续字节。

这里可能有两个完全不同的问题,因为丢失更大块数据丢失似乎与更高的数据速率和更重的系统相关activity.问题的那个特定部分可能是由于缓冲区超过 运行s——可能是由于 USB 调度程序遇到延迟时 USB 握手失败——但我在这里展示的例子只有非常小的数量数据以 50 毫秒的间隔传输,除此测试例程外,系统处于空闲状态。

我观察到 ReadAsync 经常 return 一次读取数据包的第一个字节,而下一次读取数据包的其余部分。我相信这是预期的行为,因为 MSDN 表示如果一段时间内没有数据可用,ReadAsync 将 return 接收到的第一个字节。但是,我认为这种行为在某种程度上与我的问题有关,因为当单个字节丢失或乱序时,第一个字节是 "always",其余数据包正常到达。

当数据包很小时,数据包前面的 "missing" 字节经常(但不总是)出现在下一次读取 之后 数据包的其余部分,这对我来说完全没有意义。对于较大的数据包,这仍然偶尔会发生,但是当数据包很大时,第一个字节更常见。

我进行了广泛的搜索,并阅读了我能找到的关于该主题的所有 SO 问题。我发现其他人似乎也有类似的问题(例如:SerialPort.BaseStream.ReadAsync missing the first byte),但没有人有任何可接受的甚至似是而非的解决方案。

Ben Voigt (http://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport) 和其他似乎真正了解串行通信的人推荐在 basestream 上使用 ReadAsync,微软的 IOT 团队也推荐了这种方法,所以我不得不相信这种方法应该 工作。

问题 1: 为什么我的代码在 USB Serial BaseStream 上使用 ReadAsync 丢弃/加扰字节?

问题 2: 如果 ReadAsync 无法可靠地 return 所有字节以正确的顺序接收字节,我可以只在传统的周围放置一个异步包装器吗SerialPort.Read 并等待/循环它,这样我就不必从计时器进行轮询了?我读到这是个坏主意,但我也读到 SerialPort class 是内部异步的,所以也许这样可以吗?或者我唯一的选择是把它放在工作线程上,让它把所有的时间都花在等待上吗?

我的代码如下。我已经设置了 serialPort1.ReadTimeout = 0;serialPort1.BaseStream.ReadTimeout = 0;(并且我尝试了其他持续时间)。 我已经启用了 RTS 和 DTR,因为这是一个 USB_serial 端口,它应该在内部处理握手,当我同步读取时它肯定会这样做——但当我从 BaseStream 读取时可能不是这样?

这是第一种方法:

// this method works perfectly when called from a timer.
// SerialPort.ReadTimeout must be set to zero for this to work.
// It handles incoming bytes reliably at rates above 1.6 Mbps.

private void ReadSerialBytes()
{
    if (!serialPort1.IsOpen)
        return;

    if (serialPort1.BytesToRead > 0)
    {
        var receiveBuffer = new byte[serialPort1.ReadBufferSize];

        var numBytesRead = serialPort1.Read(receiveBuffer, 0, serialPort1.ReadBufferSize);
        var bytesReceived = new byte[numBytesRead];
        Array.Copy(receiveBuffer, bytesReceived, numBytesRead);

        // Here is where I audit the received data.
        // the NewSerialData event handler displays the 
        // data received (as hex bytes) and writes it to disk.
        RaiseEventNewSerialData(bytesReceived);

        // serialInBuffer is a "thread-safe" global circular byte buffer 
        // The data in serialInBuffer matches the data audited above.
        serialInBuffer.Enqueue(bytesReceived, 0, numBytesRead);
    }
}

这是第二种方法,已编辑 以删除@Lucero 指出的尾递归。现在我不会 运行 内存不足 :) 但原来的数据丢失问题当然仍然存在。

// This method is called once after the serial port is opened,
// and it repeats until cancelled. 
// 
// This code "works" but periodically drops the first byte of a packet, 
// or returns that byte in the wrong order.
// It occasionally drops several hundred bytes in a row.
private async Task ReadSerialBytesAsync(CancellationToken ct)
{
    while((!ct.IsCancellationRequested) && (serialPort1.IsOpen))
    {
        try
        {
            serialPort1.BaseStream.ReadTimeout = 0;
            var bytesToRead = 1024;
            var receiveBuffer = new byte[bytesToRead];
            var numBytesRead = await serialPort1.BaseStream.ReadAsync(receiveBuffer, 0, bytesToRead, ct);

            var bytesReceived = new byte[numBytesRead];
            Array.Copy(receiveBuffer, bytesReceived, numBytesRead);

             // Here is where I audit the received data.
             // the NewSerialData event handler displays the 
             // data received (as hex bytes) and writes it to disk.
             RaiseEventNewSerialData(bytesReceived);

            // serialInBuffer is a "thread-safe" global circular byte buffer 
            // The data in serialInBuffer matches the data audited above.
            serialInBuffer.Enqueue(receiveBuffer, 0, numBytesRead);
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error in ReadSerialBytesAsync: " + ex.ToString());
            throw;
        }
    }
}

这是来自发送系统的 C++ 代码(带有 ARM 芯片的 teensy 3.2)。 它发送从 00 到 FF 的字节序列,每 50 毫秒重复一次。

 void SendTestData()
 {
    byte asyncTestBuffer[256] = { 0 };
    for (int i = 0; i < 256; i++)
        asyncTestBuffer[i] = i;

    while(true)
    {
    Serial.write(asyncTestBuffer, sizeof(asyncTestBuffer));
    delay(50);
    }
}

传统的同步SerialPort.Read(从定时器调用)完全按照预期接收每个块,没有数据丢失。它看起来像这样,一遍又一遍:

=====
32 msec => Received 256 bytes 
000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====

现在这是 SerialPort.BaseStream.ReadAsync 收到的。在另一个版本中,我附加了一个终端数据包序列号,以证明当我看到一个零后面跟着另一个零时,它们之间并没有真正丢失一个完整的数据包。数据包序列号都存在,所以前导字节确实似乎丢失或乱序传送。

7 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
5 msec => Received 1 bytes 
00
=====
55 msec => Received 1 bytes 
00
=====
4 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
42 msec => Received 1 bytes 
00
=====
5 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
68 msec => Received 1 bytes 
00
=====
7 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
31 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
9 msec => Received 1 bytes 
00
=====
33 msec => Received 1 bytes 
00
=====
10 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
55 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
12 msec => Received 1 bytes 
00
=====
12 msec => Received 1 bytes 
00
=====
15 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
68 msec => Received 255 bytes 
0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====
16 msec => Received 1 bytes 
00
=====
14 msec => Received 256 bytes 
000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF
=====

我花了几个星期的时间来追踪这个问题,它最初表现为正在开发的产品的奇怪行为。我很确定我一定做错了什么,但我就是看不到它,在这一点上我非常渴望得到任何想法或建议!

在逐步完成 .Net SerialPort class(仅安装了 resharper Rclick on SerialPort->Navigate->Decompiled Sources)的反编译源代码后,我终于找到了答案。

答案 #1: 字节乱序问题是由于我的程序之前的错误造成的。我取消并重新启动了 readAsync 循环,但我使用了错误的取消标记,因此有两个循环副本都在等待来自串行端口的 readAsync。两者都对 return 接收到的数据发出中断,但当然这是一个竞争条件,即谁先到达那里。

答案#2:注意我使用同步读取方法的方式:我不使用 Received 事件(不能正常工作)或检查数字要读取的字节数(这是不可靠的)或类似的东西。我只是将超时设置为零,尝试使用大缓冲区读取,并检查我返回了多少字节。

当以这种方式调用时,同步 SerialPort.Read 首先尝试从接收到的数据字节的内部缓存 [1024] 中完成读取请求。如果它仍然没有足够的数据来满足请求,它就会使用完全相同的缓冲区、(调整后的)偏移量和(调整后的)计数对底层 BaseStream 发出 ReadAsync 请求。

底线:按照我的方式使用时,同步 SerialPort.Read 方法的行为与 SerialPort.ReadAsync 完全相同。我的结论是,在同步方法周围放置一个异步包装器并等待它可能会很好。但是,现在我不需要这样做,因为我可以可靠地从 basestream 中读取。

更新: 我现在使用包含持续等待 SerialPort.Basestream.ReadAsync 并将结果添加到循环缓冲区的循环的任务从我的串行端口可靠地接收超过 3Mbps .

更新 2: 我继续收到对 post 我的代码的请求,该代码可以高速接收数据而不会扰乱数据。我能说的主要是:

请勿尝试使用 DataReceived 事件

由于它的编写方式,该事件不可能正常运行。

还有其他方法可以实现此目的,但这里摘录的代码适用于我在 multi-threaded 环境中的生产环境中。此代码在 .net 的“许多”版本中一直稳定。如果不需要,您可以删除所有取消令牌内容。

Task ReadSerialTask;
// this is started when the session starts
protected void BeginLoop_ReadSerialPort()
{
    // New token required for each connection
    // because EndLoop() cancels and disposes it each time.
    CTS_ReadSerial?.Dispose();  // should already be disposed
    CTS_ReadSerial = new CancellationTokenSource();
    ct_ReadSerial = CTS_ReadSerial.Token;

    ReadSerialTask = Task.Run(() => { ReadSerialBytesAsyncLoop(ct_ReadSerial); }, ct_ReadSerial);
}

protected void EndLoop_ReadSerialPort()
{
    try
    {
        CTS_ReadSerial?.Cancel();
        ReadSerialTask?.Wait();
    }
    catch (Exception e)
    {
        var typ = Global.ProgramSettings.DbgExceptions;
        if (e is TaskCanceledException)
        {
            dbg_EventHandler(typ, $"Task Cancelled: {((TaskCanceledException)e).Task.Id}\n");
        }
        else
        {
            dbg_EventHandler(typ, $"Task Exception: {e.GetType().Name}\n");
        }
    }
    finally
    {
        CTS_ReadSerial?.Dispose();
    }
}


private async void ReadSerialBytes_AsyncLoop(CancellationToken ct)
{
    const int bytesToRead = 1024;
    while ((serialPort1.IsOpen) && (!ct.IsCancellationRequested))
    {
        try
        {
            var receiveBuffer = new byte[bytesToRead];
            var numBytesRead = await serialPort1.BaseStream?.ReadAsync(receiveBuffer, 0, bytesToRead, ct);
            var byteArray = new byte[numBytesRead];
            Array.Copy(receiveBuffer, byteArray, numBytesRead);

            InBuffer.Enqueue(byteArray, 0, numBytesRead); // add the new data to a "thread-safe" buffer
        }
        catch (Exception e)
        {
            // Any exception means the connection is gone or the port is gone, so the session must be stopped.
            // Note that an IOException is always thrown by the serial port basestream when exit is requested.
            // In my context, there is no value in passing these exceptions along.
            if (IsHandleCreated)
                BeginInvoke((MethodInvoker)delegate // needed because the serial port is a control on the ui thread
                {
                    if (ConsoleMode != Mode.Stopped)
                        StopSession();
                });
            else
            {
                if (serialPort1?.BaseStream != null)
                {
                    serialPort1?.Dispose();
                }
            }
        }
    }
}

我知道问题 asked/solved 已经有一段时间了,但在搜索时注意到了它。我之前有过同样的"problems"。现在我在串行端口的 BaseStream 上使用 Pipereader 来处理读取。这允许我仅在收到完整消息(并同时接收多条消息)时才清除传入缓冲区。而且看起来效果很好

代码是这样的:

        var reader = PipeReader.Create(serial.BaseStream);
        while (!token.IsCancellationRequested)
        {
            ReadResult result = await reader.ReadAsync(token);

            // find and handle packets
            // Normally wrapped in a handle-method and a while to allow processing of several packets at once 
            // while(HandleIncoming(result))
            // {
                    result.Buffer.Slice(10); // Moves Buffer.Start to position 10, which we use later to advance the reader
            // }

            // Tell the PipeReader how much of the buffer we have consumed. This will "free" that part of the buffer
            reader.AdvanceTo(result.Buffer.Start, result.Buffer.End);

            // Stop reading if there's no more data coming
            if (result.IsCompleted)
            {
                break;
            }
        }

在此处查看管道文档:https://docs.microsoft.com/en-us/dotnet/standard/io/pipelines

我可以确认加扰序列在 NET 6 中仍然存在(或返回?)。

我将在 2022 年 1 月编写我的第一个 NET 6 桌面应用程序,运行 第一次遇到这个问题。我已经使用 SerialPort class 至少 4 或 5 年了,从未遇到过这个问题。我几乎在我编写的所有应用程序中都使用它来与各种设备进行通信。

我才知道这个问题已经存在很长时间了!。我看到的最早的报告是 2012 年的,...它还在吗?,真的吗?

到目前为止,我编写的串口应用程序都是基于 NET Framework 4.7.2 及更早版本。在这个框架中,SerialPort 是 System.dll 的一部分。在 NET 6 中,SerialPort 是移动到 System.IO.Ports.dll 的平台扩展,必须作为块包安装。有没有可能他们移植了一个有漏洞的旧版本?

在我的测试中,我让旧的 NET Framework 4.7.2 应用程序通过物理端口 COM3(无 USB 适配器)每 20 毫秒发送一个字符串。读取字符串的 NET 6 应用程序在同一个桌面上侦听第二个物理端口 (COM4)。两个端口都通过短的 NULL 调制解调器电缆连接,仅连接 TX、RX、GND。没有握手。据我所知,这是打乱的结果,本质上是 运行dom:

<-- port open with tranmission running already -->
 fox jumps over the lazy Dog - 123456789]
[The quick brown fox jumps over the lazy Dog - 123456789]
[The quick brown[The quick brown fox jumps over the lazy Dog - 123456789]
[The quick brown fox jumps over the lazy Dog - 123456789]
[The quick brown fox jumps over the lazy Dog - 123456789]
<--- many good lines removed for brevity --->
[The quick brown fox jumps over the lazy Dog - 123456789]
[The quick brown fox jumps over he lazy Dog - 1t23456789]
[The quick brown fox jumps over the lazy Dog - 123456789]
<--- many good lines removed for brevity --->
[The quick brown fox jumps over the lazy Dog - 123456789]
[The quick brownfox jumps over  the lazy Dog - 123456789]
[The quick brown fox jumps over the lazy Dog - 123456789]

注意第三行是第一行的前三个词!。在那之后只有一个字节错位。它看起来像一个草率的双缓冲区实现 ...

糟糕!我忘记了这些字节!给你!

如果我打开第二个好的旧终端 (Net Framework 4.7.2) 并收听相同的流,输出是完美的。

在 NET 6 项目中,我使用完全相同的 class 我很久以前写的封装 SerialPort 功能以便在项目之间使用它。

到目前为止,我订阅了 SerialPort.DataReceived 事件,然后在事件处理程序中,读取在任务中是这样的(事件处理程序已启动但未等待):

var bytesToRead = _serialPort.BytesToRead;
byte[] Data = new Byte[bytesToRead];
int received = _serialPort.Read(Data, 0, bytesToRead);
... notify the class user.

我将测试此处建议的工作环境...