
本教程旨在解决使用pyserial进行串口通信时,发送命令后无法接收到设备返回数据的问题。核心在于理解设备默认不进行回显,需发送触发响应的命令,并采用如`readline()`等恰当的数据读取方法。同时,文章将探讨其他串口工具“看似”正常工作的原理,并提供优化后的代码示例和注意事项,确保可靠的串口数据交互。
当使用python的pySerial库与串口设备(如热电偶温度计)进行通信时,开发者常遇到一个困惑:代码似乎成功打开了串口并发送了命令,但尝试读取数据时却发现缓冲区为空,没有任何响应。这与在其他串口终端工具(如Termite)中能够正常获取响应的情况形成鲜明对比。本文将深入探讨这一现象背后的原因,并提供一套行之有效的解决方案。
理解串口设备的通信行为:回显与响应
许多串口设备在接收到命令后,并不会默认将其“回显”给发送方。这意味着,即使你成功发送了字符,设备也只是默默地接收并处理,而不会将收到的内容原封不动地发回。
-
为什么其他工具“看似”正常? 在Termite或Minicom这类串口终端软件中,用户可能会看到自己发送的命令立即出现在终端上,随后才是设备的实际响应。这通常是因为这些终端软件内置了“本地回显(Local echo)”功能。当本地回显启用时,终端会将你输入并发送的字符立即显示在屏幕上,让你误以为是设备回传了数据。实际上,这只是软件为了方便用户查看发送内容而进行的一种模拟。当本地回显关闭时,你将只看到设备实际发回的数据。
-
pySerial的默认行为 pySerial库本身不会提供本地回显功能。因此,当你通过ser.write(b’K’)发送命令后,如果设备没有被编程为回传接收到的字符,那么ser.in_waiting自然会返回0,因为设备端没有向串口发送任何数据。
解决策略:触发设备响应与有效数据读取
为了成功从串口设备获取数据,我们需要采取两个关键策略:
-
发送触发响应的命令: 确保你发送的命令是设备协议中明确规定会产生某种响应的“查询”或“操作”命令。例如,发送一个请求设备型号或当前读数的命令,而不是仅仅发送一个控制字符。在示例中,如果字符’K’被预期会返回型号”0309″,那么这个命令就是有效的。
-
采用合适的读取方法:ser.in_waiting仅指示输入缓冲区中等待读取的字节数。它并不能保证在ser.write()之后立即有数据。对于大多数基于行的串口通信,ser.readline()是一个更可靠的选择,它会阻塞直到读取到换行符或达到超时。
立即学习“Python免费学习笔记(深入)”;
以下是一个优化后的pySerial通信示例,展示了如何正确地发送命令并读取设备响应:
import serial import time def establish_serial_connection(port='COM4', baudrate=9600): """ 建立并配置串口连接。 :param port: 串口名称,例如'COM4' (windows) 或 '/dev/ttyusb0' (linux) :param baudrate: 波特率 :return: 串口对象或None(如果连接失败) """ ser = serial.Serial() ser.port = port ser.baudrate = baudrate ser.bytesize = serial.EIGHTBITS ser.stopbits = serial.STOPBITS_ONE ser.parity = serial.PARITY_NONE ser.xonxoff = False ser.rtscts = True # 根据设备要求设置流控制,示例中为True ser.dsrdtr = False ser.timeout = 1 # 设置读取超时时间(秒),非常重要 try: ser.open() print(f"成功打开串口 {port}") return ser except serial.SerialException as e: print(f"打开串口 {port} 失败: {e}") return None def send_and_receive(ser_connection, command_bytes, delay=0.1): """ 向串口发送命令并尝试读取响应。 :param ser_connection: 已建立的串口对象 :param command_bytes: 要发送的命令(字节串) :param delay: 发送命令后等待设备响应的短暂延时(秒) :return: 包含所有解码后响应行的列表,或None(如果通信失败) """ if not ser_connection or not ser_connection.is_open: print("串口未打开或连接无效。") return None try: # 清空输入缓冲区,确保读取的是最新数据 ser_connection.flushInput() print(f"发送命令: {command_bytes.decode('utf-8', errors='ignore').strip()}") ser_connection.write(command_bytes) time.sleep(delay) # 短暂延时,等待设备处理并发送响应 response_lines = [] # 循环读取所有可用的行,直到超时或无数据 while True: line = ser_connection.readline() # 读取一行数据,直到换行符或超时 if not line: break # 读取到空行或超时,表示没有更多数据 try: decoded_line = line.decode('utf-8').strip() response_lines.append(decoded_line) print(f"收到响应: {decoded_line}") except UnicodeDecodeError: # 如果解码失败,打印十六进制表示,方便调试 print(f"收到原始响应(Hex): {line.hex()}") response_lines.append(line.hex()) return response_lines except Exception as e: print(f"通信过程中发生错误: {e}") return None if __name__ == "__main__": serial_port = 'COM4' # 请根据实际连接的串口修改 baud_rate = 9600 # 1. 建立串口连接 ser = establish_serial_connection(serial_port, baud_rate) if ser: try: # 2. 发送查询命令并接收响应 # 假设 'K' 命令用于查询设备型号,并期望返回 "0309" # 注意:某些设备可能需要回车换行符作为命令终止符 command_to_send = b'Krn' responses = send_and_receive(ser, command_to_send) if responses: print("n所有收到的响应:") for res in responses: print(res) else: print("未收到任何响应。") finally: # 3. 关闭串口连接 ser.close() print(f"串口 {serial_port} 已关闭。") else: print("无法建立串口连接,请检查端口和配置。")
注意事项与最佳实践
- ser.timeout 的重要性: 在ser.open()之前设置ser.timeout参数至关重要。它定义了ser.read()或ser.readline()操作的最大等待时间。如果设置为0,读取操作将是非阻塞的,可能在数据未到达时立即返回空。建议设置一个合理的正值(例如1秒),以允许设备有时间响应。
- 流控制(Flow Control): rtscts、xonxoff、dsrdtr等流控制参数应严格按照设备制造商的文档进行配置。不正确的流控制设置可能导致数据丢失或通信中断。
- 命令格式: 某些设备可能要求命令以特定的终止符(如回车符r、换行符n或两者rn)结束。务必查阅设备通信协议文档,确保发送的命令格式完全符合设备要求。
- 编码与解码: 串口通信通常涉及字节数据。发送时需将字符串编码为字节(’K’.encode(‘utf-8’)),接收时则需将字节解码为字符串(line.decode(‘utf-8’))。如果遇到UnicodeDecodeError,说明设备发送的数据可能不是UTF-8编码,或者包含非文本数据。此时,可以尝试其他编码(如’latin-1’)或直接处理原始字节(如打印十六进制)。
- 延时(time.sleep()): 在发送命令后添加一个短暂的延时(如time.sleep(0.05))可以给设备留出处理命令并准备响应的时间,尤其是在慢速设备上。
- 替代方案: 对于主要处理ASCII消息的场景,可以考虑使用如pyAutoPort这类专门设计的库,它们可能提供了更高级的抽象和便利功能,简化通信过程。
总结
通过理解串口设备的通信特性(特别是回显行为),并结合pySerial提供的强大功能,我们可以构建稳定可靠的串口通信程序。关键在于发送能够触发设备响应的命令,并使用readline()等阻塞式、带超时机制的读取方法来有效捕获设备回传的数据。遵循这些原则,将大大提高pySerial串口通信的成功率和健壮性。


