3、Python串口通信基础:pySerial库安装、串口参数配置、打开/关闭串口、读写基础数据、异常处理
好,咱们进入实战环节的第一个硬核知识点——串口通信。
做OBD数据采集,说白了就是让电脑跟汽车的ECU(发动机控制单元)对话。而对话的桥梁,就是串口。你想想看,一辆车的OBD-II接口,底层走的其实就是串口协议(虽然物理层是CAN总线,但很多ELM327模拟器转出来就是串口)。所以,不会玩串口,就别谈OBD采集了。
3.1 pySerial库安装
Python操作串口,几乎所有人用的都是pySerial这个库。我个人习惯用pip安装,简单粗暴。
pip install pyserial
嗯,这里要注意:库名是pyserial,但导入的时候是import serial。我第一次用的时候也愣了一下,以为装错了。其实没装错,就是这样的设计。
3.2 串口参数配置
串口通信不是插上线就能聊的。双方得约定好「通话规则」。规则包括四个核心参数:波特率、数据位、停止位、校验位。
咱们直接看代码,边看边解释:
import serial
# 创建串口对象,并配置参数
ser = serial.Serial(
port='COM3', # Windows下是COM口,Linux下是/dev/ttyUSB0之类的
baudrate=38400, # 波特率:每秒传输的符号数
bytesize=8, # 数据位:通常是8位
parity='N', # 校验位:N=无校验,E=偶校验,O=奇校验
stopbits=1, # 停止位:通常是1位
timeout=1 # 超时时间:读操作等待1秒
)
我来逐个说说这些参数在实际项目中的坑:
| 参数 | 常见值 | 我的经验 |
|---|---|---|
| 波特率 | 9600, 38400, 115200 | OBD-II标准常用38400或115200。我遇到过用9600的旧设备,慢得让人抓狂 |
| 数据位 | 7, 8 | 几乎都是8位。7位是古董设备用的,你基本碰不到 |
| 校验位 | N, E, O | OBD通信一般用N(无校验)。用校验反而容易出问题 |
| 停止位 | 1, 1.5, 2 | 99%的情况用1。用2?除非你设备手册明确写了 |
3.3 打开/关闭串口
创建串口对象时,如果指定了port参数,串口会自动打开。但我建议你显式地控制打开和关闭,这样更安全。
import serial
# 方式一:先创建,再手动打开
ser = serial.Serial()
ser.port = 'COM3'
ser.baudrate = 38400
ser.open()
# 干活...
# 用完一定要关
ser.close()
为什么强调要关?因为串口是独占资源。你不关,别的程序就用不了。更严重的是,如果你程序崩溃了没来得及关,串口可能一直处于占用状态,得重启电脑才能释放。
with语句来自动管理串口生命周期,这样即使出异常也能自动关闭。
with serial.Serial('COM3', 38400, timeout=1) as ser:
# 在这个块里干活,退出时自动关闭
pass
3.4 读写基础数据
串口通信的核心就两个操作:写指令和读响应。OBD-II的请求-响应模式就是基于这个。
写数据
# 发送AT指令(ELM327的配置指令)
ser.write(b'ATZ\r\n') # 复位指令,\r\n是回车换行
# 发送OBD请求
ser.write(b'010D\r\n') # 请求车速数据
注意:串口发送的是字节,不是字符串。所以要用b''前缀。我见过新手直接传字符串,结果设备完全不响应。
读数据
# 读取固定字节数
data = ser.read(10) # 读10个字节
# 读取一行(直到遇到换行符)
line = ser.readline() # 常用方式
# 读取所有可用数据
all_data = ser.read(ser.in_waiting) # in_waiting返回缓冲区字节数
实际项目中,我最常用的是readline()。因为ELM327的响应是按行返回的,每行以\r\n结尾。
time.sleep(0.1)再读,否则可能读到空数据。
3.5 异常处理
串口通信是最容易出幺蛾子的地方。线松了、设备没电、端口被占用……各种情况。所以异常处理必须到位。
import serial
import serial.tools.list_ports
try:
ser = serial.Serial('COM3', 38400, timeout=1)
# 检查串口是否真的打开了
if not ser.is_open:
raise Exception('串口打开失败')
ser.write(b'ATZ\r\n')
response = ser.readline()
print(f'响应: {response}')
except serial.SerialException as e:
print(f'串口异常: {e}')
# 常见原因:端口不存在、被占用、权限不足
except Exception as e:
print(f'其他错误: {e}')
finally:
if 'ser' in locals() and ser.is_open:
ser.close()
print('串口已关闭')
这里我分享一个实用技巧:在打开串口前,先枚举一下可用端口:
ports = serial.tools.list_ports.comports()
for port in ports:
print(f'端口: {port.device}, 描述: {port.description}')
我曾经在客户现场调试,设备死活连不上。一查,原来设备插在了COM5,而我代码里写死了COM3。从那以后,我写的工具都会先打印可用端口列表,让用户选择。
- 串口操作一定要放在
try-except里 finally块中一定要关闭串口- 超时时间别设太长,我一般设1秒。设太长程序会卡死
- 如果遇到
PermissionError,八成是串口被别的程序占了(比如串口助手)
好了,串口通信的基础就这些。说白了就是三板斧:配参数、读写数据、处理异常。下一节咱们就用这些知识,真正跟OBD-II设备对话了。