【发布时间】:2017-01-21 10:00:34
【问题描述】:
编辑:我已经添加了发送代码和我收到的输出示例。
我正在从连接到嵌入式系统的 USB“虚拟”串行端口读取数据。我写了两种接收数据的方法,一种是同步的,一种是异步的。同步的工作,异步的会丢失或打乱一点输入数据。我不知道为什么第二个失败了。
有效的方法调用 SerialPort.Read 并将读取超时设置为零,并请求接收缓冲区中的所有内容。我检查返回值以查看实际读取了多少字节,然后将数据放入循环缓冲区以供其他地方使用。此方法由定时器中断调用,它可以完美地接收串行数据(通常以高于 1.6 Mbps 的速率,不会丢失数据)。但是,轮询计时器对我来说已经成为一个问题,我更愿意在我的其余代码中异步接收数据。
丢失数据的方法在串口 BaseStream 上等待 ReadAsync 并循环直到取消。这种方法有点有效,但它经常无序返回数据包的前导字节,相当频繁地丢失一个字节(大约每几千个数据字节一次),偶尔会丢失数百个连续字节从一个数据包。
这里可能存在两个完全不同的问题,因为丢失更大块数据似乎与更高的数据速率和更重的系统活动相关。问题的特定部分可能是由于缓冲区溢出造成的——可能是由于 USB 调度程序遇到延迟时 USB 握手失败——但我在这里展示的示例仅在 50 处传输了非常少量的数据毫秒间隔,系统空闲,除了这个测试例程。
我观察到 ReadAsync 经常在一次读取时返回数据包的第一个字节,并在下一次读取时返回数据包的其余部分。我相信这是预期的行为,因为 MSDN 说如果在一段时间内没有数据可用,ReadAsync 将返回它收到的第一个字节。但是,我认为这种行为在某种程度上与我的问题有关,因为当单个字节丢失或乱序时,它“始终”是第一个字节,其余的数据包正常到达。
当数据包很小时,数据包前面的“丢失”字节通常(但不总是)似乎在下一次读取在数据包的其余部分之后传递,而这对我来说完全没有意义。对于较大的数据包,这种情况偶尔仍会发生,但更常见的是,当数据包较大时,第一个字节会丢失。
我进行了广泛的搜索,并阅读了我能找到的关于这个主题的每一个 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 串行 BaseStream 上使用 ReadAsync 会丢弃/加扰字节?
问题 2: 如果 ReadAsync 不能可靠地以正确的顺序返回所有接收到的字节,我可以在传统的 SerialPort.Read 周围放置一个异步包装器并等待/循环它吗我不必从计时器轮询?我读到这是一个坏主意,但我也读到 SerialPort 类在内部是异步的,所以也许这样可以吗?还是我唯一的选择是把它放在工作线程上,让它把所有时间都花在等待上?
我的代码如下。我已经设置了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);
}
}
这是第二种方法,Edited 删除@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
=====
我花了几个星期来追踪这个问题,它最初表现为正在开发的产品的奇怪行为。我很确定我一定做错了什么,但我只是看不到它,此时我非常渴望任何想法或建议!
【问题讨论】:
-
什么是serialInBuffer?您的 async 方法不应使用共享状态,尤其是在这不是线程安全的情况下。另外,尾递归是个坏主意……
-
@Lucero 代码块顶部的 cmets 解释说 serialInBuffer 是循环字节缓冲区类的一个实例。我将编辑以说明它是线程安全的,但这不是问题。我通过在接收到的数据进入serialInBuffer 之前引发一个事件来审核它。我将注释代码以表明这一点。
-
@Lucero 为什么在这种情况下尾递归是个坏主意?我最初是在一个 While (true) 循环中使用它,但对其进行了更改,使其看起来更像 Ben Voigt 发布的内容,并使逻辑可见,而无需在代码块之间跳转。
-
因为这会为任务对象积累内存,直到完成。 stackoverflow.com/questions/31966833/tail-recursion-with-tasks
-
@Lucero 好的,感谢您提出这个问题。我已经更改了该代码,但正如预期的那样,数据丢失问题仍然存在。我添加了显示问题的输出的附加信息和示例。
标签: c# serial-port usbserial