3. 串口数据收发基础:write()与read()方法、readline()与read_until()、数据编码与解码

串口通信说白了就是两个设备之间通过一根线(或几根线)按位传输数据。你写一个字节,对方读一个字节,就这么简单。但实际项目中,怎么把数据写对、怎么把数据读全、怎么让双方理解同一段二进制数据——这些才是真正考验功底的地方。

我记得刚入行那会儿,调试一个温湿度传感器,串口数据死活读不对。后来发现是编码问题——传感器发的是 ASCII 字符串,我当二进制流处理了。嗯,从那以后我再也不敢小看 bytes 和 str 的转换了。

3.1 write() 与 read():最基础的收发方法

在 pyserial 里,write() 和 read() 是最底层的收发接口。它们直接操作字节流,不关心数据含义。

3.1.1 write() 方法

write() 接收一个 bytes 对象,返回实际写入的字节数。注意,它不接收字符串。

import serial

ser = serial.Serial('COM3', 115200, timeout=1)

# 正确写法:发送字节数据
data = b'\x01\x03\x00\x00\x00\x01'
written = ser.write(data)
print(f'实际写入 {written} 字节')

# 错误写法:直接传字符串会报错
# ser.write('hello')  # TypeError: unicode strings are not supported
我曾经踩过的坑: write() 返回的字节数不一定等于你传入的长度。尤其在 USB 转串口适配器上,驱动层可能拆分数据包。所以 永远不要假设一次 write 就全部发送成功,要检查返回值。

3.1.2 read() 方法

read(size) 从接收缓冲区读取最多 size 个字节。如果缓冲区数据不足,它会等待直到超时或凑够指定字节数。

# 读取 10 个字节
resp = ser.read(10)
print(f'收到 {len(resp)} 字节: {resp}')

# 读取所有可用数据(不指定 size)
all_data = ser.read(ser.in_waiting)

我个人习惯用 ser.in_waiting 先查一下缓冲区有多少数据,再决定读多少。这样可以避免 read() 长时间阻塞。

方法 参数 返回值 说明
write(data) bytes 对象 int(实际写入字节数) 不保证一次写完所有数据
read(size) int(要读取的字节数) bytes 对象 可能返回少于 size 的字节
read() 无参数 bytes 对象 读取所有可用数据,直到超时

3.2 readline() 与 read_until():按行/按分隔符读取

很多工业设备(比如 Modbus RTU、GPS 模块、AT 指令设备)的数据是以换行符或特定分隔符结尾的。这时候用 readline() 或 read_until() 就方便多了。

3.2.1 readline() 方法

readline() 读取直到遇到换行符 \n(默认)。它返回包含换行符在内的完整一行数据。

# 读取一行数据(以 \n 结尾)
line = ser.readline()
print(f'收到一行: {line}')

# 去掉换行符
clean_line = line.rstrip(b'\r\n')
print(f'清洗后: {clean_line}')
小技巧: 很多设备用 \r\n 作为行结束符。readline() 默认只认 \n,所以返回的数据里可能还带着 \r。我一般用 rstrip(b'\r\n') 一次性去掉所有尾部空白字符。

3.2.2 read_until() 方法

read_until(expected) 读取直到遇到指定的字节序列。它比 readline() 更灵活,可以自定义结束符。

# 读取直到遇到 b'\x0a'(换行符)
data = ser.read_until(b'\x0a')

# 读取直到遇到 b'OK'(常用于 AT 指令响应)
response = ser.read_until(b'OK')
print(f'收到完整响应: {response}')

你想想看,如果设备返回的数据不是按行分割的,而是以 0x7E 或 0xFF 结尾,readline() 就没法用了。这时候 read_until() 就是你的救星。

方法 结束条件 典型应用场景
readline() 遇到 \n(可配置) NMEA 0183 协议、AT 指令、日志输出
read_until() 遇到指定的字节序列 Modbus 帧结束符、自定义协议、二进制数据流

3.3 数据编码与解码:bytes 与 str 互转

这是串口编程里最容易出问题的地方。串口传输的是 字节,但我们在 Python 里处理的是 字符串。怎么在两者之间正确转换?

3.3.1 为什么需要编码/解码?

串口硬件只认识 0 和 1。你发送的每个字符(比如 'A')必须被编码成对应的字节值(0x41)。接收端收到字节后,再解码回字符。如果编码方式不一致,就会出现乱码。

我在项目中遇到过一台德国产的温控器,它用 ISO-8859-1 编码发送数据。我用 UTF-8 解码,结果所有特殊字符都变成了问号。折腾了半天才发现是编码不匹配。

3.3.2 字符串 → 字节:encode()

# 字符串转字节(默认 UTF-8)
text = "温度: 25.3°C"
data = text.encode('utf-8')
print(data)  # b'\xe6\xb8\xa9\xe5\xba\xa6: 25.3\xc2\xb0C'

# 指定其他编码
data_ascii = text.encode('ascii', errors='ignore')  # 忽略无法编码的字符
print(data_ascii)  # b'\xe6\xb8\xa9\xe5\xba\xa6: 25.3C'

3.3.3 字节 → 字符串:decode()

# 字节转字符串
raw = b'\xe6\xb8\xa9\xe5\xba\xa6: 25.3\xc2\xb0C'
text = raw.decode('utf-8')
print(text)  # 温度: 25.3°C

# 处理解码错误
try:
    text = raw.decode('ascii')
except UnicodeDecodeError:
    text = raw.decode('ascii', errors='replace')
    print(f'解码失败,替换为: {text}')
核心原则: 发送前用 encode() 把字符串变成字节,接收后用 decode() 把字节变回字符串。编码方式必须双方约定一致。工业设备常用 ASCII、UTF-8、GB2312,老设备可能用 Latin-1。

3.3.4 常见编码方式对比

编码方式 字节数/字符 适用场景 注意事项
ASCII 1 字节 英文字母、数字、控制字符 不支持中文、特殊符号
UTF-8 1-4 字节 通用、跨平台 中文占 3 字节,注意缓冲区大小
GB2312/GBK 1-2 字节 中文设备、国产仪器 与 UTF-8 不兼容,容易乱码
Latin-1 1 字节 欧洲字符、老设备 每个字节直接映射到 0-255 的字符

3.4 综合示例:一个完整的收发流程

下面是一个实际项目中用到的代码片段。它发送一条查询指令,然后读取设备返回的响应,并正确解码。

import serial

ser = serial.Serial(
    port='COM3',
    baudrate=9600,
    bytesize=8,
    parity='N',
    stopbits=1,
    timeout=2
)

# 发送查询指令(十六进制)
command = bytes.fromhex('01 03 00 00 00 01 84 0A')
ser.write(command)

# 读取响应(按行读取)
response = ser.readline()
print(f'原始字节: {response}')

# 尝试解码
try:
    text = response.decode('ascii')
    print(f'解码结果: {text}')
except UnicodeDecodeError:
    # 如果解码失败,可能是二进制数据
    hex_str = response.hex()
    print(f'十六进制: {hex_str}')

ser.close()
我个人习惯: 在调试阶段,先用 hex() 打印原始字节,确认数据格式后再决定解码方式。这样可以避免被乱码误导。

3.5 本章知识结构图

下面这张图总结了串口数据收发的核心流程和关键方法:

串口数据收发知识结构图 串口数据收发核心流程 发送端:字符串 → 字节 encode() 编码 write() 发送字节 接收端:字节 → 字符串 read() / readline() decode() 解码 readline() 按行读取(遇到 \n) 适合文本协议、日志 read_until() 按分隔符读取 适合二进制协议 编码/解码核心 encode() ↔ decode() 关键:发送前 encode(),接收后 decode(),编码方式必须双方约定一致

3.6 避坑指南

  • 不要用 str 直接传: write() 只接受 bytes,字符串必须先 encode()。
  • read() 可能读不够: 永远检查返回值的长度,不要假设一次读全。
  • 编码不一致 = 乱码: 确认设备的编码方式,必要时用 hex() 打印原始数据辅助判断。
  • readline() 的换行符: 有些设备用 \r\n,readline() 只认 \n,记得清洗数据。
  • 超时设置: 不设 timeout 的话,read() 可能永远阻塞。我一般设 1-3 秒。