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?除非你设备手册明确写了
避坑指南: 我曾经在调试一个ELM327模拟器时,波特率设成了9600,结果数据全是乱码。折腾了半小时才发现设备默认是38400。所以拿到设备第一步,先看手册确认默认参数!

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设备对话了。