第2章:Python串口库安装与配置

好,咱们正式开始动手了。这一章要解决一个最基础的问题——怎么让Python能和串口设备说上话。说白了,就是装个库、配个参数、扫个端口。别小看这几步,我在现场调试时,十次有八次的问题都出在配置上。

2.1 pySerial库安装

pySerial是Python操作串口的标配,没有之一。我个人习惯用pip安装,简单粗暴:

pip install pyserial

如果你用的是Python 3,可能会遇到pip3的问题:

pip3 install pyserial

嗯,这里要注意。有些工控机是离线环境,没法联网。我曾经在一条汽车产线上遇到过,现场网络被严格管控。那怎么办?提前下载好whl文件,用U盘拷进去:

pip install pyserial-3.5-py2.py3-none-any.whl

验证是否装成功,打开Python交互环境,敲一行:

import serial
print(serial.__version__)

不报错,就说明成了。如果报错说找不到模块,多半是pip装到了别的Python环境里。你想想看,一台工控机里可能装了Python 2.7、Python 3.6、Anaconda好几个版本,很容易搞混。

我的小技巧:python -m pip install pyserial 可以确保装到当前激活的Python环境里。

2.2 串口参数配置

串口通信不是插上线就能聊的。你得告诉双方:波特率多少?数据位几位?停止位几位?有没有校验?这些参数必须完全一致,否则收到的全是乱码。

我见过最典型的坑:一个工程师调了半天,发现波特率设成了9600,但设备实际是19200。嗯,这种低级错误我也犯过。

来看一个标准的配置代码:

import serial

ser = serial.Serial(
    port='COM3',        # Windows下用COM口,Linux下用/dev/ttyUSB0
    baudrate=115200,    # 波特率,常见的有9600、19200、38400、115200
    bytesize=8,         # 数据位,通常是8位
    parity='N',         # 校验位:N无校验、E偶校验、O奇校验
    stopbits=1,         # 停止位:1位或2位
    timeout=1           # 超时时间,单位秒。不设的话会一直等
)

参数说明,我整理成了一张表,方便你对照:

参数名 说明 常用值
port 串口号 COM1~COM256, /dev/ttyUSB0
baudrate 波特率 9600, 19200, 38400, 115200
bytesize 数据位 5, 6, 7, 8
parity 校验位 'N', 'E', 'O', 'M', 'S'
stopbits 停止位 1, 1.5, 2
timeout 超时(秒) None(阻塞), 0(非阻塞), 正数
注意: timeout这个参数特别重要。设成None的话,如果设备没回应,程序会一直卡死在那。我在调试一个扫码枪时就吃过这个亏,程序跑了一夜没出结果。

2.3 端口扫描与枚举

很多时候你根本不知道设备插在了哪个COM口上。尤其是USB转串口的设备,每次插拔端口号都可能变。怎么办?写个扫描程序。

pySerial自带了一个工具函数,可以列出所有可用的串口:

import serial.tools.list_ports

ports = serial.tools.list_ports.comports()
for port in ports:
    print(f"端口: {port.device}")
    print(f"描述: {port.description}")
    print(f"硬件ID: {port.hwid}")
    print("-" * 40)

输出大概长这样:

端口: COM3
描述: USB Serial Port (COM3)
硬件ID: USB VID:PID=1A86:7523 SER=5 LOCATION=1-2
----------------------------------------
端口: COM1
描述: 通信端口 (COM1)
硬件ID: ACPI PNP0501
----------------------------------------

我个人习惯是根据硬件ID来识别设备。比如某个PLC的USB转串口芯片是CH340,VID/PID是固定的,我就可以写个自动匹配的逻辑:

def find_serial_port(vid_pid="1A86:7523"):
    ports = serial.tools.list_ports.comports()
    for port in ports:
        if vid_pid in port.hwid:
            return port.device
    return None

这样不管插在哪个USB口上,程序都能自动找到它。我在做自动化测试台架时就是这么干的,省去了手动配置的麻烦。

2.4 常见异常处理

串口通信最容易出异常。你想想看,硬件连接、驱动、权限、端口占用,任何一个环节出问题都会报错。我总结了几个最常见的:

2.4.1 serial.serialutil.SerialException

这个异常最常见。原因通常是端口不存在、被占用或者权限不够。

try:
    ser = serial.Serial('COM10', 115200)
except serial.SerialException as e:
    print(f"打开串口失败: {e}")
    # 常见错误信息:
    # "could not open port COM10: FileNotFoundError"  -> 端口不存在
    # "could not open port COM3: PermissionError"     -> 权限不足或被占用

2.4.2 serial.serialutil.Timeout

读写超时。设了timeout之后,如果设备在规定时间内没回应,就会抛出这个异常。

try:
    data = ser.read(100)  # 读100个字节
except serial.serialutil.Timeout:
    print("读取超时,设备可能未响应")

2.4.3 乱码问题

这个不算异常,但比异常更让人头疼。乱码的原因99%是波特率或数据格式不匹配。我曾经调试一个称重仪表,波特率设对了,但数据位设成了7位,结果读出来的重量值全是错的。

避坑指南: 遇到乱码,先检查波特率,再检查数据位、停止位、校验位。如果还不行,用示波器看波形——这是最笨但最有效的方法。

2.4.4 完整的异常处理模板

我写串口程序时,习惯用一个统一的异常处理结构:

import serial
import serial.tools.list_ports

def safe_serial_open(port, baudrate=115200, timeout=1):
    try:
        ser = serial.Serial(port, baudrate, timeout=timeout)
        print(f"成功打开 {port},波特率 {baudrate}")
        return ser
    except serial.SerialException as e:
        print(f"打开失败: {e}")
        # 尝试自动扫描可用端口
        ports = serial.tools.list_ports.comports()
        print("当前可用端口:")
        for p in ports:
            print(f"  {p.device} - {p.description}")
        return None

# 使用示例
ser = safe_serial_open('COM3')
if ser:
    try:
        data = ser.read(10)
        print(f"收到数据: {data.hex()}")
    except serial.serialutil.Timeout:
        print("读取超时")
    finally:
        ser.close()

这个模板我用了好几年,基本覆盖了90%的异常场景。你拿去改改就能用。

最后说一句: 串口调试时,记得先确认设备供电正常、线缆没接反。我见过太多人花半天查代码,最后发现是TX和RX接反了。嗯,我也干过这种事。