3、串口数据收发实战:write()与read()方法详解、readline()与read_until()的使用、超时处理机制
串口通信的核心,说白了就是两件事:发出去,收回来。但真正做项目时你会发现,收发之间藏着不少坑。我刚开始用Python的serial库时,以为调个write()和read()就完事了,结果数据要么丢包,要么卡死。嗯,今天咱们就把这些方法掰开揉碎讲清楚。
3.1 write():把数据发出去
write()方法负责向串口发送数据。它接受一个字节串(bytes)参数,返回实际发送的字节数。
import serial
ser = serial.Serial('COM3', 115200, timeout=1)
data = b'Hello, UART!\n'
sent = ser.write(data)
print(f'实际发送了 {sent} 个字节')
这里有个细节:write()只接受bytes或bytearray类型。如果你传字符串进去,会报错。我习惯这样转换:
text = "AT+CMD\r\n"
ser.write(text.encode('utf-8'))
bytes.fromhex()很方便。比如发送0x01 0x02 0x03:ser.write(bytes.fromhex('010203'))。
3.2 read():读指定字节数
read(size)从串口读取size个字节。如果数据不够,它会等——等到超时或凑够数量。
response = ser.read(10) # 读10个字节
print(response)
我在项目中遇到过一个问题:设备返回的数据长度不固定。比如发一条查询指令,有时回8字节,有时回12字节。这时候用固定read(10)就不靠谱了。怎么办?用read()配合超时机制。
3.3 readline():按行读取
readline()读到换行符\n就返回,包含换行符本身。这对处理文本协议特别有用,比如AT指令、NMEA GPS数据。
ser.write(b'AT\r\n')
line = ser.readline()
print(line) # 输出 b'OK\r\n'
注意:readline()依赖超时。如果一直收不到换行符,它会等到超时返回已收到的数据。我曾经调试一个GPS模块,发现readline()偶尔返回空串,排查半天——原来是模块输出\r而不是\n。嗯,协议不标准真坑人。
3.4 read_until():读到指定终止符
read_until(expected)比readline()更灵活。你可以指定任意字节串作为终止条件。
# 读到 'END' 为止
data = ser.read_until(b'END')
print(data)
这个函数在解析自定义协议时很实用。比如设备返回的数据以0xFF 0xFE结尾,你可以这样:
frame = ser.read_until(b'\xFF\xFE')
readline():终止符固定为\n,适合文本协议read_until():终止符自定义,适合二进制协议
3.5 超时处理机制
超时是串口编程的命门。没有超时,程序可能永远卡在read()上。Python的serial库提供两种超时设置:
| 参数 | 类型 | 说明 |
|---|---|---|
timeout |
float/None | 读操作最大等待秒数。None表示无限等待,0表示非阻塞 |
write_timeout |
float/None | 写操作最大等待秒数。默认None |
我常用的几种超时策略:
- 阻塞模式:
timeout=None,一直等到数据来。适合数据稳定、不会丢失的场景。 - 非阻塞模式:
timeout=0,立即返回,没数据就返回空。适合轮询场景。 - 超时模式:
timeout=1,等1秒,没数据就返回。最常用,兼顾响应和可靠性。
# 非阻塞轮询示例
ser.timeout = 0
while True:
data = ser.read(100)
if data:
process(data)
# 做其他事情
time.sleep(0.01)
timeout设成0.001秒,结果在Windows上频繁超时。原因是系统调度精度不够。建议timeout至少设0.01秒以上。
3.6 实战:完整收发流程
下面是一个完整的查询-响应示例,把上面所有知识点串起来:
import serial
def query_device(ser, cmd, expected_len=None, terminator=None):
"""发送命令并读取响应"""
# 清空缓冲区
ser.reset_input_buffer()
# 发送命令
ser.write(cmd.encode() + b'\r\n')
# 读取响应
if terminator:
response = ser.read_until(terminator)
elif expected_len:
response = ser.read(expected_len)
else:
response = ser.readline()
return response
# 使用示例
ser = serial.Serial('COM3', 115200, timeout=2)
result = query_device(ser, 'AT+STATUS', terminator=b'OK\r\n')
print(f'设备状态: {result.decode().strip()}')
你想想看,这个函数覆盖了三种常见场景:定长响应、终止符响应、行响应。实际项目中,我通常先试read_until(),不行再换readline()。
3.7 知识结构图
下面这张图总结了串口收发的方法选择逻辑:
3.8 常见问题与避坑
- 数据粘包:连续发送时,接收方可能一次读到多条数据。我习惯在每条数据后加唯一终止符,或者用固定长度帧。
- 缓冲区残留:每次发送前调用
reset_input_buffer()清空旧数据,避免读到上次的残留。 - 编码问题:中文或特殊字符用
encode('utf-8'),接收后用decode('utf-8')。如果设备用GB2312,记得指定对应编码。 - 硬件流控:如果设备支持RTS/CTS,打开
rtscts=True可以避免数据溢出。但很多廉价模块不支持,别乱开。
- 文本协议优先用
readline() - 二进制协议优先用
read_until() - 不确定时用
read(1)逐字节拼装,配合超时判断结束 - 永远设
timeout,别用None
说白了,串口收发没有银弹。不同设备、不同协议,选对方法才能事半功倍。我刚开始做嵌入式时,一个简单的温湿度传感器折腾了两天,就是因为没搞懂readline()和read_until()的区别。嗯,现在你知道了,应该能少走不少弯路。