如何在.NET中正确读取串口数据

3
我很抱歉要问这样的问题,但是我在使用.NET的SerialPort类可靠地读取串口数据方面遇到了困难。
我的第一种方法:
static void Main(string[] args)
{
    _port = new SerialPort
    {
        PortName = portName,
        BaudRate = 57600,
        DataBits = 8,
        Parity = Parity.None,
        StopBits = StopBits.One,
        RtsEnable = true,
        DtrEnable = false,
        WriteBufferSize = 2048,
        ReadBufferSize = 2048,
        ReceivedBytesThreshold = 1,
        ReadTimeout = 5000,
    };    

    _port.DataReceived += _port_DataReceived;
    _port.Open();

    // whatever
}

private void _port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{           
    var buf = new byte[_port.BytesToRead];
    var bytesRead = _port.Read(buf, 0, buf.Length);

    _port.DiscardInBuffer();
    for (int i = 0; i < bytesRead; ++i)
    {
        // read each byte, look for start/end values, 
        // signal complete packet event if/when end is found
    }
}

所以这里有一个显而易见的问题:我正在调用DiscardInBuffer,因此在事件被触发后进入的任何数据都会被丢弃,即我正在丢失数据。
现在,SerialPort.Read()的文档甚至没有说明它是否推进了流的当前位置(真的吗?),但我已经找到其他来源声称它会这样做(这是有道理的)。然而,如果我不调用DiscardInBuffer,我最终会出现RXOver错误,即我处理每条消息的时间太长,缓冲区溢出了。
所以......我真的不喜欢这个接口。如果我必须在单独的线程上处理每个缓冲区,我会这样做,但这会带来自己的一系列问题,我希望我错过了什么,因为我对这个接口没有太多的经验。

1
听起来你需要将读取和处理分离。如果你移除所有的处理,你能得到一个干净的读取吗?TPL DataFlow 是一个不错的选择。 - spender
1
这个问题有非常严重的错误。DicsardInBuffer()实际上丢弃任何内容的可能性非常低。你只是通过Read()调用清空了接收缓冲区。首先打开Handshake,这是防止溢出的主要防御措施。如果这不起作用(DtrEnable=false非常奇怪),那么降低波特率。 - Hans Passant
@HansPassant:嗯,我知道数据正在丢失,但你可能是对的。我正在盲目地遵循设备制造商的方向。我会调查一下。 - Ed S.
文档没有说明Read()是否会推进位置,因为串口是不可寻址的——它甚至没有位置。(缓冲区也是FIFO,没有为随机访问做任何准备) - Ben Voigt
@BenVoigt:当然,但底层流肯定是这样的。当然,它会从流中缓冲,所以并不简单。我只是不明白为什么不指定我是否实际上正在从流中删除字节。你真的需要知道这一点。 - Ed S.
3个回答

9

哈哈,我刚刚自己找到了那篇文章。我会试一下的,谢谢。 - Ed S.
@Ben:你刚刚救了我的命!BaseStream异步读取的效果非常好! - Meister Schnitzel
@MeisterSchnitzel:很高兴能够帮助。 - Ben Voigt

4
为了正确处理串行端口的数据,您需要执行一些操作。
首先,在接收事件中不要处理数据。将数据复制到其他地方并在另一个线程上进行任何处理。(对于大多数事件都是如此-在事件处理程序中进行任何耗时的处理都是不好的,因为它会延迟调用方并可能引入问题。您还需要小心,因为您的事件在与您的主应用程序不同的线程上被引发)
其次,您无法保证当您接收数据时会收到完整的数据包或者只有部分数据包-它可能以小片段的形式到达。
因此,您应该创建自己的缓冲区(足够大,可以容纳多个数据包),并在收到数据时将其附加到您的缓冲区中。然后在另一个线程中,您可以处理缓冲区,查看是否可以从中解码出数据包,然后使用该数据。您可能需要跳过部分数据包的结尾,直到找到有效数据包的开头。如果没有足够的数据构建完整的数据包,则可能需要等待一段时间,直到更多数据到达为止。
您不应该调用端口上的Discard - 只需读取数据并使用它。每次调用您时,将有另一个数据片段需要处理。它不会记住之前调用的数据-每次调用您的事件时,它都会给出一小段自上次调用您以来到达的数据。只需使用您收到的数据并返回即可。
最后一个建议是:除非您确切地需要更改端口的设置使其正常运行,否则不要更改任何端口的设置。因此,您必须设置波特率,数据/停止位和奇偶校验,但请避免尝试更改Rts / Dtr,缓冲区大小和读取阈值等属性,除非您有足够的理由认为您比串行端口的作者更懂硬件。大多数串行设备现在都按照行业标准工作,更改这些低级选项很可能会引起麻烦,除非您正在与某些不寻常的设备交互并且您对硬件非常了解。
特别是将ReceivedBytesThreshold设置为1可能会导致您提到的故障,因为这意味着您要求串行端口每秒57,600次仅使用一个字节调用您的事件处理程序-在您开始获得再入式调用之前每个字节只有0.017毫秒的时间处理。

握手协议应该始终明确配置;不要依赖于先前的应用程序将其留在您需要的状态。 - Ben Voigt
谢谢。昨晚我有机会进行了一些测试,像我想的那样,我正在处理中出现了停顿。我可以在另一个线程上处理数据,没有问题,但是接收到的字节数阈值不是可选的。就像你说的,每次可能无法获得完整的数据包。此外,数据包的长度是可变的。我的代码可以处理这个问题,但这意味着每次传输一个字节时都必须检查数据。此外,阈值为1并不能保证每次调用事件只读取一个字节,它只是表示只要接收到一个字节,就会被调用。 - Ed S.
哦,这些设置确实来自设备制造商。然而,丢弃输入缓冲区的建议也是如此,所以我现在对他们的文档有些怀疑。 - Ed S.
当端口超过其阈值、缓冲区已满或传输中断时,端口将传递数据,因此您可以在不尝试使用阈值“强制”串行端口的情况下正常接收单字节数据包。设备制造商可能会列出甚至建议许多不需要的详细低级设置 - 我的方法是从简单开始,只有在(如果)发现默认选项效果不佳时才添加复杂性。 - Jason Williams
当从SerialPort对象接收到数据时,DataReceived事件会在辅助线程上引发。http://msdn.microsoft.com/en-us/library/system.io.ports.serialport.datareceived%28v=vs.110%29.aspx - skinnedknuckles
当下一批数据到达时会发生什么?它会被阻塞吗,因为其数据传递线程正忙?还是会在另一个线程上重新调用您的事件处理程序?而且,如何处理部分数据包?答案是:如果您缓冲数据并在自己的处理线程上完成所有繁重的工作,并且您的事件处理程序尽可能快地将控制返回给调用者,则所有这些都无关紧要。 - Jason Williams

0

DiscardInBuffer通常只在打开串口后立即使用。对于标准串口通信来说是不必要的,因此你的dataReceived处理程序中不应该包含它。


我理解那部分,但它并没有回答我的问题。我还没有时间去研究@HansPassants的建议,但不管怎样,要么是我正在做一些本质上错误的事情(正如他所建议的),要么就是我处理数据包的时间实在太长了。 - Ed S.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接