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
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}')
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 本章知识结构图
下面这张图总结了串口数据收发的核心流程和关键方法:
3.6 避坑指南
- 不要用 str 直接传: write() 只接受 bytes,字符串必须先 encode()。
- read() 可能读不够: 永远检查返回值的长度,不要假设一次读全。
- 编码不一致 = 乱码: 确认设备的编码方式,必要时用 hex() 打印原始数据辅助判断。
- readline() 的换行符: 有些设备用 \r\n,readline() 只认 \n,记得清洗数据。
- 超时设置: 不设 timeout 的话,read() 可能永远阻塞。我一般设 1-3 秒。