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

3 投票
3 回答
11983 浏览
提问于 2025-04-16 07:00

因为串口通信是异步的,所以在我进行与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事件并不能保证每接收到一个字节就会被触发。

所以我又回到了写监听线程的路上。我很快就用一个BackgroundWorker实现了这个,它不断调用port.ReadExisting()。这个方法会读取到来的字节,当它看到行结束符(\r\n)时,就把读取到的内容放入一个linebuffer中。然后我程序的其他部分会查看linebuffer,看看有没有完整的行可以使用。

这明显是一个经典的生产者-消费者问题。生产者是BackgroundWorker,它把完整的行放入linebuffer中。而消费者是一些代码,它尽可能快地从linebuffer中取出这些行。

不过,消费者的效率有点低。现在它不断检查linebuffer,每次都失望地发现里面是空的;不过偶尔也能找到一行等待处理。有什么好的办法可以优化这个,让消费者只在有行可用时才被唤醒吗?这样消费者就不会一直在访问linebuffer,这可能会引发一些并发问题。

另外,如果有更简单或更好的方法来不断读取串口数据,我也很乐意听听建议!

3 个回答

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的速度处理串口数据。

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

4

我不明白为什么你不能使用 DataReceived 这个事件。正如文档所说的:

DataReceived 事件并不能保证每收到一个字节就会触发一次。你可以用 BytesToRead 属性来判断缓冲区里还有多少数据可以读取。

这就是说,你不能指望每收到一个字节数据就会有一个单独的事件被触发。你可能需要先检查一下端口上是否有多个字节可以读取,这可以通过 BytesToRead 属性来实现,然后再读取相应数量的字节。

撰写回答