Python串口读取速度非常慢,如何加快速度?

6
我正在使用以下代码与数字万用表(DMM)通信。
它工作正常。我可以发送命令并读取结果。 我不使用readline,因为我读取的是二进制数据。 问题: 问题在于它非常非常慢。 用Ruby编写的相同代码要快得多。 当Python需要30秒时,Ruby只需要2或3秒(使用相同的速度)。 所以这不是硬件问题。
Ruby代码和Python之间唯一的区别是,在Python中,我使用inWaiting()来读取所有可用字符。在Ruby中,read()函数读取所有字符而不仅仅是一个。 代码: 以下是代码: 在read_retry函数中,我检查需要读取多少个字符。 然后我读取它们,并调用data_is_ok函数检查是否已完成。
正如你所看到的,返回的数据中嵌入了“\r”。 当最后一个字符是“\r”时(没有更多数据可用),读取就完成了。
因此,有一个循环来读取许多块。
import serial
[...]

def data_is_ok(data):
  # No status code yet
  if len(data) < 2: return False

  # Non-OK status
  if len(data) == 2 and data[0] != "0" and data[1] == "\r": return True

  # Non-OK status with extra data on end
  if len(data) > 2 and data[0] != "0":
    raise ValueError('Error parsing status from meter (Non-OK status with extra data on end)')

  # We should now be in OK state
  if data[0] != "0" or data[1] != "\r":
    raise ValueError('Error parsing status from meter (status:%c size:%d)' % (data[0], len(data)))

  return len(data) >= 4 and data[-1] == "\r"

def read_retry():
  retry_count = 0
  data = ""

  while retry_count < 500 and not data_is_ok(data):
    bytesToRead = ser.inWaiting()
    data += ser.read(bytesToRead)
    if data_is_ok(data): return data
    time.sleep (0.001)
    retry_count += 1
  raise ValueError('Error parsing status from meter:  %c %d %r %r' % (data[0],len(data),data[1] == '\r', data[-1] == '\r'))

[...]

# Serial port settings
try:
  ser = serial.Serial(port='/dev/cu.usbserial-AK05FTGH', baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=5, rtscts=False, dsrdtr=False)
except serial.serialutil.SerialException, err:
  print "Serial Port /dev/cu.usbserial-AK05FTGH doesn't respond"
  print err
  sys.exit()

[...]

ser.write(cmd+'\r')
data = read_retry()

我使用了cProfile分析器。大部分时间都花在了time.sleep上。

以下是摘录:

         363096 function calls (363085 primitive calls) in 28.821 seconds

   Ordered by: internal time
   List reduced from 127 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    19050   25.245    0.001   25.245    0.001 {time.sleep}
        1    1.502    1.502    1.502    1.502 {posix.open}

问题:
能否使代码更快?

也许去掉sleep? - John Zwinck
我尝试过了。在这种情况下,我需要去掉500的限制,否则它会停止。但持续时间是相同的,CPU占用率增长到100%。 - user7148991
停止使用inWaiting。尝试使用read(100000)。 - John Zwinck
我尝试了一下。它卡住了,可能是在等待所有字符。所以我将超时时间降低到“0”。然后它就可以工作了,但持续时间仍然是相同的约28秒。 - user7148991
是的,这段代码需要改进,但作为第一步,我会在“读取”后添加一个调试打印语句(例如使用hexdump),以便查看数据到达的时间和方式。 - VPfB
3个回答

6

有几件事情你可以做。

你不需要使用time.sleep()。如果你知道你只想要两个字节,那么就用ser.read(2);如果你想限制等待时间,就用ser.timeout = 0.01 编辑,除非你在另一个线程中。Python线程是贪婪的。I/O操作会释放线程,使主线程运行。但是,在你的情况下,你总是读取缓冲区中的数据 ser.read(ser.inWaiting())。我发现你需要强制串行端口等待 I/O:ser.read(ser.inWaiting() + 32)。只要确保你也有一个超时,这样你就不会永远等待 I/O。此外,你当前的超时时间是5秒,等待时间太长了。结束编辑

你可以尝试使用readlineser.readline(),它可能只能读到'\r\n'。我不确定。

如果你使用 Python3,ser.read()返回的是字节,所以任何比较都将是false。data[0] == '0'需要改为data[0:1] == b'0'

另一件要做的事情是通过调试器运行代码,以确保你得到了预期的数据。有可能是字符串比较或其他问题导致你不必要地循环多次。


我不知道将返回多少字节,这是不可预测的。我已经尝试过使用ser.read(ser.inWaiting() + 32)和timeout = 0.01,但它需要更长时间约39秒。readline读取到'\r',而我读取二进制数据,因此CR可能会被嵌入但意义不同。我使用Python 2.7。但还是感谢这些建议。 - user7148991
我建议使用调试器逐步查看数据。我会将超时时间改为非常短的超时时间,并使用 data += ser.read(2) 循环,因为长度为 2 是要求之一。我还会使用 repr(data) 打印串口读取的所有数据。此外,while 循环不需要将 data_is_ok 作为条件,因为您已经在 if 语句中检查了数据,所以您做了重复的工作。 - justengel
没错。添加调试后发现 Ruby 脚本和 Python 脚本没有经过相同的循环。Python 端完成了更多的工作。读取和休眠函数根本没有参与其中。现在我必须看看我的脚本出了什么问题。谢谢。 - user7148991
ser.readline()在这里非常慢。我计时了2个字节信息的“发送-接收”往返时间,每个周期大约需要1秒钟在pySerial中...看起来是一个pySerial的问题? - X.Arthur

4
我遇到了与pySerial库使用serial.readline方法读取数据极慢的相同问题。我正在运行Windows 7,并尝试了从2.7到3.4不同版本的pySerial。
让我来演示一下我的问题。发射器每0.02秒传输一组新数据,波特率为115200。
如果我执行以下操作:
import serial

if __name__ == '__main__':

    serial_port = serial.Serial('COM26', 115200)   

    while(True):            
        print(serial_port.is_open)
        serial_port.readline()        
        print(serial_port.in_waiting)    

我看到缓冲区中的字节数正在增加。将内容写入缓冲区比读取缓冲区更快。

解决方案是使用以下代码片段中的serial.read方法并一次性读取所有缓冲字节。如果我执行此操作,问题将得到解决。

import serial

if __name__ == '__main__':

    serial_port = serial.Serial('COM26', 115200)

    while(True):        
        print(serial_port.is_open)
        serial_port.read(serial_port.in_waiting)                   
        print(serial_port.in_waiting)

我发现serial_port.read(serial_port.in_waiting)读取速度很快,而且避免了缓冲区中字节积累的问题。

目前我满意这个解决方案,但也许有人能解释一下它为什么会这样行。

谢谢。


1
在`read_retry()`中,尝试使用超时而不是重试计数。
import time

def read_retry():
  timeout = 500 * 0.001
  data = ""
  tic = time.time()
  while toc - tic < timeout and not data_is_ok(data):
    bytesToRead = ser.inWaiting()
    data += ser.read(bytesToRead)
    if data_is_ok(data): return data
    toc = time.time()
  raise ValueError('Error parsing status from meter:  %c %d %r %r' % (data[0],len(data),data[1] == '\r', data[-1] == '\r'))

我试过了。在while循环之前,我需要添加“toc = time.time()”。它可以工作,但是需要大约29秒的时间。我不明白为什么在同一台机器上,Ruby代码要快10倍。 - user7148991

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