使用后台线程不断从串口读取数据

3

由于串口通信是异步的,早在我开始涉及与RS 232设备通信的项目时,我就意识到必须有一个后台线程不断读取接收到的数据。现在,我正在使用IronPython(.NET 4.0),因此我可以访问.NET内置的SerialPort类。这使得我可以编写如下代码:

self.port = System.IO.Ports.SerialPort('COM1', 9600, System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One)
self.port.Open()
reading = self.port.ReadExisting() #grabs all bytes currently sitting in the input buffer

很简单。但是正如我所说,我希望能够不断检查此端口是否有新数据到达。理想情况下,我希望操作系统随时告诉我是否有等待的数据。真是好消息,我的祷告得到了回答,这里提供了一个DataReceived事件!

self.port.DataReceived += self.OnDataReceived

def OnDataReceived(self, sender, event):
    reading = self.port.ReadExisting()
    ...

很遗憾,不保证触发 DataReceived 事件,所以它没什么用处。
数据接收事件 DataReceived 并不保证每个字节都能触发该事件。
因此,需要编写监听线程。我很快就使用了 BackgroundWorker 实现了这一点,它只是反复调用 port.ReadExisting()。它会读取传入的字节,并在看到行尾符(\r\n)时将读取的内容放入 linebuffer 中。然后程序中的其他部分查看 linebuffer 是否有完整的行等待使用。
现在,这显然是一个经典的生产者-消费者问题。生产者是 BackgroundWorker,将完整的行放入 linebuffer 中。消费者是一些代码,尽可能地快速从 linebuffer 中消耗这些行。
但是,消费者效率有些低下。目前,它一直在检查 linebuffer,每次都会失望地发现它为空;虽然偶尔也会发现里面有一行在等待。最好的方法是如何优化消费者,使其只在有可用行时才唤醒?这样消费者就不会不断地访问 linebuffer,从而可能引入一些并发问题。
另外,如果有更简单/更好的方法来从串行端口读取数据,我会很乐意听取建议!
3个回答

4
我不明白为什么您不能使用“DataReceived”事件。正如文档所述,“DataReceived”事件无法保证对于每个接收的字节都会引发该事件。请使用“BytesToRead”属性确定缓冲区中尚未读取的数据量。这只是在说明您不能保证为每个单独的数据字节得到一个分离的事件。您可能需要使用“BytesToRead”属性检查并查看端口上是否有多个可用字节,并读取相应数量的字节。

我明白了 - ReadExisting会在那个时间点给我所有可用的字节吗?如果是的话,DataReceived实际上会很有用。 - Aphex
是的,ReadExisting 应该可以满足你的需求。但请确保你仔细阅读文档,ReadExisting 并不能保证获取到所有字节,它明确指出“根据编码读取所有立即可用的字节”。 - heavyd
1
那么,一个读取缓冲区中此时所有可用字节的万无一失的方法是 port.Read(buffer, 0, port.BytesToRead),我猜测是这样吗? - Aphex

3

不需要使用旋转线程轮询串口。

我建议使用SerialPort.BaseStream.BeginRead(...)方法。它比尝试使用SerialPort类中的事件要好得多。调用BeginRead会立即返回并注册一个异步回调,一旦读取完成就会调用该回调方法。在回调方法中,您调用EndRead,它返回读入提供的缓冲区的字节数。BaseStream(SerialStream)继承自Stream,并遵循.Net Streams的一般模式,非常有用。

但是需要牢记的是,是.Net线程调用回调函数,所以您需要快速处理数据,或将任何繁重的工作传递给您自己的线程之一。我强烈建议阅读以下链接,特别是备注部分。

http://msdn.microsoft.com/en-us/library/system.io.stream.beginread.aspx


2
这里是一些VB代码,表达了我对如何完成这个任务的看法:
Dim rcvQ As New Queue(Of Byte()) 'a queue of buffers
Dim rcvQLock As New Object

Private Sub SerialPort1_DataReceived(ByVal sender As System.Object, _
                                     ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) _
                                 Handles SerialPort1.DataReceived

    'an approach
    Do While SerialPort1.IsOpen AndAlso SerialPort1.BytesToRead <> 0
        'what if the number of bytes available changes at ANY point?
        Dim bytsToRead As Integer = SerialPort1.BytesToRead
        Dim buf(bytsToRead - 1) As Byte

        bytsToRead = SerialPort1.Read(buf, 0, bytsToRead)

        'place the buffer in a queue that can be processed somewhere else
        Threading.Monitor.Enter(rcvQLock)
        rcvQ.Enqueue(buf)
        Threading.Monitor.Exit(rcvQLock)

    Loop

End Sub

就其价值而言,与此非常相似的代码已经可以在接近1Mbps的速度下处理串口数据了。


这是一个很好的例子,几乎完美地实现了我的生产者正在做的事情。你如何处理并发,即当一个或多个消费者需要访问 rcvQ 以读取一行时怎么办? - Aphex

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