4、Python串口通信基础:pyserial库安装与配置、串口参数设置(波特率/数据位/停止位)、读写超时处理

好,咱们进入实战环节的第一个硬核知识点——串口通信。

说实话,做物联网设备采集,串口是绕不开的坎。不管是RS232还是RS485,底层通信协议基本都靠串口。我当年第一次调试温度巡检仪,就是被串口参数折腾了一整天。波特率不对,数据全是乱码;停止位错了,设备直接不理你。嗯,今天咱们就把这些坑填平。

4.1 pyserial库:你的串口瑞士军刀

Python操作串口,最主流的库就是pyserial。它跨平台,Windows、Linux、macOS都能用。我个人习惯用pip安装,简单粗暴:

pip install pyserial

如果你在公司内网环境,或者像我之前在某工厂调试时没网,可以离线安装。先去官网下载whl包,然后:

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

验证是否装好,打开Python交互环境,输入:

import serial
print(serial.__version__)

不报错,就说明成了。我建议你养成这个习惯——装完库先验证,别等到写代码才发现没装上。

小提示: pyserial 3.5是目前最稳定的版本。如果你用Python 3.8以上,直接装最新版就行。我踩过坑,早期版本在某些Linux内核上有bug,读写会卡死。

4.2 串口参数设置:四个关键参数

串口通信就像两个人打电话。你得先约定好:语速多快(波特率)、一句话几个字(数据位)、怎么表示结束(停止位)、要不要校验(校验位)。

咱们直接看代码,这是我最常用的初始化模板:

import serial

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

这里我重点说几个容易翻车的地方:

4.3 波特率:通信的“语速”

波特率必须和设备完全一致。差一点都不行。我曾经调试一台老款巡检仪,手册上写9600,结果怎么读都是乱码。折腾两小时才发现,那台设备实际用的是19200。你想想看,手册也会骗人。

常见波特率对照表:

波特率 适用场景 传输距离
9600 工业仪表、老旧设备 较远(RS485可达1200米)
19200 中速采集场景 中等
115200 高速数据、调试口 较短(建议15米内)

我的建议是:能用9600就别用115200。虽然慢,但稳定。工业现场干扰多,高速率反而容易丢包。

4.4 数据位与停止位:别小看这两个参数

数据位通常是8位,因为一个字节就是8位。但有些老设备用7位,比如某些Modbus ASCII模式。停止位一般是1位,偶尔有设备用2位。

怎么判断?看设备手册。如果手册丢了,有个笨办法——挨个试。我当年在项目现场就这么干过,写个脚本循环尝试所有参数组合,哪个能读到正常数据就用哪个。

注意: 数据位和停止位设置错了,设备不会报错,但读回来的数据全是错的。我曾经因为这个,把-25℃读成了127℃,差点以为冷库坏了。

4.5 读写超时处理:别让程序卡死

这是新手最容易忽略的地方。串口通信不像网络请求,没有自动超时机制。如果你不设置timeout,程序可能永远卡在read()那里。

看这个对比:

# 错误示范:没有超时
ser.read(10)  # 如果设备没发数据,程序永远卡在这里

# 正确做法:设置超时
ser.timeout = 2  # 2秒超时
data = ser.read(10)  # 最多等2秒,没数据就返回空

我一般这样处理读写:

import serial
import time

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

def read_with_retry(expected_len=10, max_retries=3):
    """带重试的读取函数"""
    for i in range(max_retries):
        data = ser.read(expected_len)
        if len(data) == expected_len:
            return data
        print(f"第{i+1}次读取失败,重试中...")
        time.sleep(0.5)
    return None  # 全部失败返回None

# 使用示例
result = read_with_retry(10)
if result:
    print(f"收到数据: {result.hex()}")
else:
    print("读取超时,请检查设备连接")

为什么要重试?因为工业现场干扰多,偶尔丢一帧很正常。我遇到过最极端的情况,某台设备每10次通信就有1次超时。不加重试机制,系统根本跑不稳。

4.6 避坑指南:我踩过的三个坑

坑一:串口被占用
我曾经写了个脚本,打开串口后没关闭就异常退出了。下次再运行,提示“端口被占用”。解决方案:用with语句自动管理资源。

with serial.Serial('COM3', 9600, timeout=1) as ser:
    ser.write(b'hello')
    data = ser.read(5)
# 退出with块后自动关闭串口

坑二:写数据后立刻读
有些设备收到指令后需要时间处理。你写完立刻读,可能什么都读不到。加个延时:

ser.write(b'\x01\x03\x00\x00\x00\x01')
time.sleep(0.1)  # 给设备100ms处理时间
data = ser.read(10)

坑三:Windows和Linux端口名不同
我建议写个自动检测函数:

import sys
import serial.tools.list_ports

def find_serial_port():
    ports = serial.tools.list_ports.comports()
    for port in ports:
        if 'USB' in port.description or 'COM' in port.device:
            return port.device
    return None

port = find_serial_port()
if port:
    print(f"找到串口: {port}")
else:
    print("未找到串口设备")

4.7 完整示例:一个能跑的温度巡检仪采集脚本

最后,给你一个可以直接用的模板。这是我做项目时的标准写法:

import serial
import time
import logging

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class TemperatureCollector:
    def __init__(self, port='COM3', baud=9600):
        self.ser = None
        self.port = port
        self.baud = baud
        
    def connect(self):
        try:
            self.ser = serial.Serial(
                port=self.port,
                baudrate=self.baud,
                bytesize=8,
                parity='N',
                stopbits=1,
                timeout=2
            )
            logging.info(f"串口 {self.port} 连接成功")
            return True
        except Exception as e:
            logging.error(f"连接失败: {e}")
            return False
    
    def read_temperature(self):
        """读取温度值,返回摄氏度"""
        if not self.ser or not self.ser.is_open:
            logging.warning("串口未连接")
            return None
        
        # 发送读取指令(具体指令根据设备手册)
        cmd = bytes.fromhex('01 03 00 00 00 01 84 0A')
        self.ser.write(cmd)
        time.sleep(0.2)
        
        # 读取响应
        response = self.ser.read(7)
        if len(response) == 7:
            # 解析温度值(示例:第3-4字节为温度值)
            temp_raw = (response[3] << 8) | response[4]
            temperature = temp_raw / 10.0  # 假设精度为0.1℃
            return temperature
        return None
    
    def close(self):
        if self.ser and self.ser.is_open:
            self.ser.close()
            logging.info("串口已关闭")

# 使用示例
if __name__ == '__main__':
    collector = TemperatureCollector('COM3', 9600)
    if collector.connect():
        for i in range(5):
            temp = collector.read_temperature()
            if temp is not None:
                print(f"温度: {temp:.1f}℃")
            else:
                print("读取失败")
            time.sleep(1)
        collector.close()

这个脚本我用了好几年,换过几十种设备,核心逻辑基本没变。你把它当成骨架,往里面填具体的协议指令就行。

嗯,串口基础就讲到这里。下一节咱们聊怎么用这个基础去对接真实的温度巡检仪协议。到时候你会发现,今天学的这些参数设置和超时处理,全都能用上。