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 知识结构图

下面这张图总结了串口收发的方法选择逻辑:

串口数据收发方法选择逻辑 开始收发 发送还是接收? 发送 write(bytes) 接收 选择读取方法 知道长度?→ read(size) 有换行符?→ readline() 自定义终止符?→ read_until() 所有读取方法都受 timeout 控制,设好超时防止卡死

3.8 常见问题与避坑

  • 数据粘包:连续发送时,接收方可能一次读到多条数据。我习惯在每条数据后加唯一终止符,或者用固定长度帧。
  • 缓冲区残留:每次发送前调用reset_input_buffer()清空旧数据,避免读到上次的残留。
  • 编码问题:中文或特殊字符用encode('utf-8'),接收后用decode('utf-8')。如果设备用GB2312,记得指定对应编码。
  • 硬件流控:如果设备支持RTS/CTS,打开rtscts=True可以避免数据溢出。但很多廉价模块不支持,别乱开。
我的经验法则:
  1. 文本协议优先用readline()
  2. 二进制协议优先用read_until()
  3. 不确定时用read(1)逐字节拼装,配合超时判断结束
  4. 永远设timeout,别用None

说白了,串口收发没有银弹。不同设备、不同协议,选对方法才能事半功倍。我刚开始做嵌入式时,一个简单的温湿度传感器折腾了两天,就是因为没搞懂readline()和read_until()的区别。嗯,现在你知道了,应该能少走不少弯路。