从USB串行端口

时间:2017-01-21 10:00:34

标签: c# serial-port usbserial

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

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

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

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

这里可能存在两个完全不同的问题,因为较大的数据丢失块的丢失似乎与较高的数据速率和较重的系统活动相关。问题的这个特定部分可能是由于缓冲区溢出 - 可能是因为USB调度程序遇到延迟时USB握手失败 - 但我在这里展示的示例只有极少量的数据在50处传输msec间隔,除了此测试例程外,系统处于空闲状态。

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

当数据包很小时,"丢失"经常(但不总是)数据包前面的字节似乎是在数据包的剩余部分之后的下一次读取中传递的,这对我来说完全没有意义。对于较大的数据包,这种情况偶尔会发生,但更常见的是,当数据包很大时,第一个字节就会丢失。

我已经进行了广泛的搜索,并且已经阅读了我在这个主题上可以找到的所有问题。我发现其他人似乎有类似的问题(例如:SerialPort.BaseStream.ReadAsync missing the first byte),但没有人接受任何可接受的甚至是合理的解决方案。

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

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

问题2:如果无法使ReadAsync以正确的顺序可靠地返回接收到的所有字节数,那么我可以在传统的SerialPort.Read周围放置一个异步包装器并等待/循环它我不必从计时器投票?我已经读到这是一个坏主意,但我也读过SerialPort类是内部异步的,所以也许这样做可以吗?或者我是唯一的替代方案,将它放在工作线程上,让它花费所有时间等待?

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

这是第一种方法:

// 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
=====

我花了几周时间来追查这个问题,这个问题最初表现在来自正在开发的产品的奇怪行为中。我很确定我一定做错了什么,但我无法看到它,此时我非常渴望任何想法或建议!

2 个答案:

答案 0 :(得分:3)

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

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

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

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

底线:当我按照我使用它的方式使用时,同步SerialPort.Read方法的行为与SerialPort.ReadAsync完全相同。我得出结论,在同步方法周围放置一个异步包装器可能没问题,只需等待它。但是,由于我可以可靠地从基线读取,所以我不需要这样做。

更新:我现在可以使用包含一个连续等待SerialPort.Basestream.ReadAsync的循环的Task从我的串口接收超过3Mbps的速度,并将结果添加到循环缓冲区。

答案 1 :(得分:3)

我知道问题被提出/解决已经有一段时间了,但是在搜索时注意到了。我之前有过类似的“问题”。如今,我在串行端口的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