使用后台线程不断从串口读取数据
因为串口通信是异步的,所以在我进行与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 个回答
这里有一些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的速度处理串口数据。
其实你不需要一直有个线程在那转圈圈去检查串口。
我建议你使用 SerialPort.BaseStream.BeginRead(...) 这个方法。这个方法比在 SerialPort 类里用事件要好得多。调用 BeginRead 后,它会立刻返回,并注册一个异步回调,这个回调会在读取完成后被调用。在这个回调方法里,你再调用 EndRead,这样就能得到读取到的字节数,并存放在你提供的缓冲区里。BaseStream(也就是 SerialStream)是从 Stream 继承来的,遵循 .Net Streams 的一般模式,这样用起来非常方便。
不过要记住,是 .Net 的线程来调用这个回调,所以你需要快速处理数据,或者把比较复杂的工作交给你自己创建的线程去做。我强烈建议你看看下面的链接,特别是备注部分。
http://msdn.microsoft.com/en-us/library/system.io.stream.beginread.aspx
我不明白为什么你不能使用 DataReceived
这个事件。正如文档所说的:
DataReceived 事件并不能保证每收到一个字节就会触发一次。你可以用 BytesToRead 属性来判断缓冲区里还有多少数据可以读取。
这就是说,你不能指望每收到一个字节数据就会有一个单独的事件被触发。你可能需要先检查一下端口上是否有多个字节可以读取,这可以通过 BytesToRead
属性来实现,然后再读取相应数量的字节。