使用Python实时读取串行数据

44

我正在使用Python脚本通过2Mbps的串口从PIC微控制器收集数据。

PIC在2Mbps下有完美的定时,FTDI USB-串口在2Mbps下也非常出色(经过示波器验证)。

我发送大约15个字符大小的消息100-150次/秒,并且那里的数字会递增(以检查是否有丢失消息等)。

我的笔记本电脑上运行Xubuntu虚拟机,我可以通过Putty和我的脚本(Python 2.7和pySerial)读取串口。

问题:

  • 通过Putty打开串口时,我可以看到所有消息(消息中的计数器逐一递增)。完美!
  • 通过pySerial打开串口时,我可以看到所有消息,但是每秒只接收到大约5条消息(仍然递增1条消息),但它们可能存储在某个缓冲区中,因为当我关闭PIC的电源时,我可以去厨房回来仍然在接收消息。

以下是代码(我省略了大部分代码,但循环是相同的):

ser = serial.Serial('/dev/ttyUSB0', 2000000, timeout=2, xonxoff=False, rtscts=False, dsrdtr=False) #Tried with and without the last 3 parameters, and also at 1Mbps, same happens.
ser.flushInput()
ser.flushOutput()
While True:
  data_raw = ser.readline()
  print(data_raw)

有人知道为什么pySerial从串口读取到行末需要这么长时间吗? 需要帮助。

我希望能够实时获取数据。

谢谢。


1
你可以尝试使用ser.read()代替ser.readline()吗? - Tim
我使用了ser.read()来读取数据,当没有收到换行符时,逐个字符地将它们添加到字符串中,速度仍然保持不变。 - Vasco Baptista
不要等到收到\n再打印输出,每当一个字符到达时就立即打印出来。你是突然接收到大量字符还是每个字符都单独到达? - Tim
1
由于我之前使用了ser.read()和print(),所以每行只能输出一个字符。 - Vasco Baptista
使用已接受的解决方案(删除超时并使用inWaiting()),仍然可能需要Python在打印整个消息(多字节)之前等待所有字节的接收。这在逻辑上会导致延迟。您的系统仍然是实时的吗?您能确认一下吗? - Pe Dro
4个回答

42

你可以使用inWaiting()函数来获取输入队列中可用的字节数。

然后你可以使用read()函数来读取这些字节,类似于以下内容:

While True:
    bytesToRead = ser.inWaiting()
    ser.read(bytesToRead)

为什么在这种情况下不使用readline()?来自文档:

Read a line which is terminated with end-of-line (eol) character (\n by default) or until timeout.

每次读取时,您都在等待超时,因为它等待 eol。串行输入 Q 保持不变,只是需要很长时间才能到达缓冲区的“末尾”。更好地理解:您像赛车一样向输入 Q 写入,像老爷车一样进行读取 :)


2
它起作用了,现在我将尝试分离所有字符串并为每个字符串运行我的代码。我会很快给出反馈。 - Vasco Baptista
是的,我明白,但由于我的消息类似于:213531\n 616516\n 516861\n我认为它应该读取得非常快...无论如何,我的超时时间是2秒,而消息在传输到我的脚本后就已经更快了(在readline之后打印到屏幕上)。这意味着它从未达到过期时间。即使添加\r\n也没有解决任何问题。将timeout = 0并没有改变任何东西,速度相同,一切都一样(除了当我关闭PIC时它被锁定)。 - Vasco Baptista
@Vasco Baptista 还有一件事,print() 受你所使用的终端影响... 我会给你一个参考链接 post,尝试阅读它,它可能会有所帮助。 - Kobi K
感谢@Kobi K的提示,我知道这个,但是为了检查足够的次数,因为我不会在我的最终脚本中打印消息。此外,脚本现在正在工作,我添加了一些额外的代码来分离我接收到的消息并将它们放入队列中以便在其他线程中处理。感谢大家的帮助。 - Vasco Baptista
那么,串行数据是否会排队等待直到从缓冲区中读取?例如,如果您执行serial.write(b'100\n') serial.write('b'200\n') serial.write(b'300\n')。然后进行serial.readline(),您会得到>100,如果您继续执行readline,它会获取下一个在队列中的值吗? - bretcj7
@bretcj7 是的,readline() 以 CR 结尾,意味着读取一个以 '\n' 结束的行。 - Kobi K

7
一个非常好的解决方案可以在这里找到:

这是一个作为pyserial对象包装器的类。它允许您在没有100% CPU的情况下读取行。它不包含任何超时逻辑。如果发生超时,self.s.read(i)返回一个空字符串,您可能想要抛出异常以指示超时。

根据作者的说法,它也应该很快:

下面的代码给我790 kB / sec,而用pyserial的readline方法替换代码只给我170kB / sec。

class ReadLine:
    def __init__(self, s):
        self.buf = bytearray()
        self.s = s

    def readline(self):
        i = self.buf.find(b"\n")
        if i >= 0:
            r = self.buf[:i+1]
            self.buf = self.buf[i+1:]
            return r
        while True:
            i = max(1, min(2048, self.s.in_waiting))
            data = self.s.read(i)
            i = data.find(b"\n")
            if i >= 0:
                r = self.buf + data[:i+1]
                self.buf[0:] = data[i+1:]
                return r
            else:
                self.buf.extend(data)

ser = serial.Serial('COM7', 9600)
rl = ReadLine(ser)

while True:

    print(rl.readline())

5

当你打开串口时,需要将超时时间设置为“无”:

ser = serial.Serial(**bco_port**, timeout=None, baudrate=115000, xonxoff=False, rtscts=False, dsrdtr=False) 

这是一个阻塞命令,所以你需要等待直到收到以换行符 (\n 或 \r\n) 结尾的数据:

line = ser.readline()

一旦收到数据,它将尽快返回。


3

来自手册

参数timeout的可能值: … x将超时设置为x

以及

readlines(sizehint=None, eol='\n')读取一系列行,直到超时。sizehint被忽略,只是为了与内置文件对象的API兼容。

请注意,此函数仅在超时时返回。

因此,您的readlines最多每2秒返回一次。像Tim建议的那样使用read()


我在使用readlines时遇到了问题。所以我尝试了Kobi K的解决方案。它有效了,现在我只需要解析接收到的数据并分离字符串即可。 - Vasco Baptista

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