4. 硬件通信基础-串口通信

做血氧仪量产测试,串口通信是绕不开的第一道坎。说实话,我见过不少测试脚本跑不起来,最后发现是串口参数配错了。这章咱们就把串口通信的底裤扒干净。

4.1 RS232/串口协议基础

串口通信,说白了就是一根线发数据,一根线收数据。RS232是早期PC上的标准,现在嵌入式设备里用的多是TTL电平的UART。血氧仪这类设备,内部基本都是TTL串口,通过USB转串口芯片连到电脑上。

串口通信的核心就三个东西:波特率、数据格式、流控。血氧仪量产测试里,流控基本不用,重点在前两个。

我刚开始做血氧仪测试时,遇到一个奇怪问题:读到的数据全是乱码。折腾了半天,发现是波特率设错了。设备用的是115200,我配成了9600。嗯,这种低级错误,犯过一次就记住了。

4.2 pySerial库详解

Python操作串口,pySerial是事实标准。这个库封装了底层系统调用,跨平台用起来很顺手。

安装很简单:

pip install pyserial

核心用法就三步:打开串口、读写数据、关闭串口。我一般这么写:

import serial

ser = serial.Serial(
    port='COM3',      # Windows下用COM口,Linux下用/dev/ttyUSB0
    baudrate=115200,
    bytesize=8,
    parity='N',
    stopbits=1,
    timeout=1         # 这个参数很重要,后面细说
)

# 发送数据
ser.write(b'\xAA\x55\x01\x02\x03')

# 读取数据
data = ser.read(10)   # 读10个字节
line = ser.readline() # 读一行

ser.close()

你想想看,如果timeout不设,read()会一直阻塞在那里。血氧仪测试时,设备可能没响应,脚本就卡死了。所以我建议永远不要省略timeout参数

4.3 串口参数配置

串口参数配置是坑最多的地方。咱们一个一个说。

参数 血氧仪常用值 说明
波特率 115200 / 9600 血氧仪数据量不大,9600也够用
数据位 8 几乎都是8位,别问为什么
停止位 1 1位停止位最常用
校验位 N (无校验) 血氧仪协议里校验位很少用

波特率:血氧仪量产测试,我习惯用115200。速度快,测试效率高。但要注意,有些老款血氧仪只支持9600。我曾经遇到过一批设备,标称115200,实际只能跑9600。排查了一下午,最后用示波器量波形才发现。

数据位:8位数据位是绝对主流。7位数据位?那是老古董了,血氧仪里基本见不到。

停止位:1位停止位就够了。2位停止位用在一些特殊场景,量产测试用不上。

校验位:血氧仪协议里,校验位通常放在数据包内部,而不是串口层。所以串口参数里设'N'就行。

我的习惯:量产测试脚本里,我会把串口参数写成配置文件。换设备型号时,改配置文件就行,不用改代码。

4.4 数据收发与异常处理

数据收发看起来简单,实际坑不少。我总结了几种常见场景。

4.4.1 发送数据

血氧仪通常用固定长度的指令包。比如:

# 血氧仪读取数据指令
cmd = bytes([0xAA, 0x55, 0x01, 0x00, 0x00, 0x00])
ser.write(cmd)

发送后,一般要等设备响应。这里有个细节:发送完不要立即读。设备需要时间处理指令。我一般加个0.1秒的延时:

ser.write(cmd)
time.sleep(0.1)  # 给设备一点处理时间
response = ser.read(20)

4.4.2 接收数据

接收数据时,最怕粘包和断包。血氧仪数据包通常有固定格式,比如:

# 血氧仪数据包格式
# 帧头(2字节) + 长度(1字节) + 数据(n字节) + 校验(1字节)
# 例如: AA 55 05 01 02 03 04 05 0F

我一般这么解析:

def parse_packet(data):
    if len(data) < 4:
        return None
    
    # 检查帧头
    if data[0] != 0xAA or data[1] != 0x55:
        return None
    
    # 获取数据长度
    length = data[2]
    
    # 检查完整包
    if len(data) < 3 + length + 1:
        return None
    
    # 校验
    checksum = sum(data[2:3+length]) & 0xFF
    if checksum != data[3+length]:
        return None
    
    return data[3:3+length]
注意:血氧仪数据包可能有多个帧头。比如数据内容里恰好出现了0xAA 0x55。所以解析时一定要按协议来,不能简单找帧头。

4.4.3 异常处理

量产测试中,串口异常是家常便饭。我遇到过的情况:

  • 设备没上电,串口打不开
  • USB线接触不良,读到一半断连
  • 设备死机,不发数据
  • 波特率配错,全是乱码

我的异常处理模板:

import serial
import serial.tools.list_ports

def open_serial(port, baudrate=115200, timeout=2):
    """打开串口,带重试机制"""
    for retry in range(3):
        try:
            ser = serial.Serial(port, baudrate, timeout=timeout)
            return ser
        except serial.SerialException as e:
            print(f"打开串口失败: {e}")
            time.sleep(1)
    raise Exception(f"无法打开串口 {port}")

def read_with_retry(ser, length, retries=3):
    """带重试的读取"""
    for i in range(retries):
        data = ser.read(length)
        if len(data) == length:
            return data
        print(f"读取数据不完整,重试 {i+1}/{retries}")
    return None

# 使用示例
try:
    ser = open_serial('COM3')
    data = read_with_retry(ser, 10)
    if data:
        print(f"收到数据: {data.hex()}")
    else:
        print("读取超时,请检查设备")
except Exception as e:
    print(f"串口操作失败: {e}")
finally:
    if 'ser' in locals():
        ser.close()
避坑指南:我曾经在量产线上遇到一个问题——测试脚本跑着跑着突然报错,说串口被占用。排查后发现,是上一个测试进程没正常关闭串口。从那以后,我强制要求所有脚本都用with语句管理串口:
with serial.Serial('COM3', 115200, timeout=1) as ser:
    ser.write(cmd)
    data = ser.read(10)
这样即使脚本崩溃,串口也会自动释放。

4.5 实战:血氧仪串口通信模板

最后,给一个完整的血氧仪串口通信模板。这个模板我用了好几年,量产测试中基本没出过问题:

import serial
import time
import logging

class OximeterSerial:
    def __init__(self, port, baudrate=115200, timeout=2):
        self.port = port
        self.baudrate = baudrate
        self.timeout = timeout
        self.ser = None
        self.logger = logging.getLogger(__name__)
    
    def connect(self):
        """连接血氧仪"""
        try:
            self.ser = serial.Serial(
                port=self.port,
                baudrate=self.baudrate,
                bytesize=8,
                parity='N',
                stopbits=1,
                timeout=self.timeout
            )
            self.logger.info(f"连接成功: {self.port}")
            return True
        except serial.SerialException as e:
            self.logger.error(f"连接失败: {e}")
            return False
    
    def send_command(self, cmd):
        """发送指令"""
        if not self.ser or not self.ser.is_open:
            self.logger.error("串口未打开")
            return False
        
        try:
            self.ser.write(cmd)
            self.logger.debug(f"发送: {cmd.hex()}")
            return True
        except serial.SerialException as e:
            self.logger.error(f"发送失败: {e}")
            return False
    
    def read_response(self, length=20):
        """读取响应"""
        if not self.ser or not self.ser.is_open:
            return None
        
        try:
            data = self.ser.read(length)
            if data:
                self.logger.debug(f"收到: {data.hex()}")
            return data
        except serial.SerialException as e:
            self.logger.error(f"读取失败: {e}")
            return None
    
    def close(self):
        """关闭串口"""
        if self.ser and self.ser.is_open:
            self.ser.close()
            self.logger.info("串口已关闭")

# 使用示例
oxi = OximeterSerial('COM3')
if oxi.connect():
    # 发送读取血氧数据指令
    cmd = bytes([0xAA, 0x55, 0x01, 0x00, 0x00, 0x00])
    oxi.send_command(cmd)
    time.sleep(0.2)
    data = oxi.read_response(20)
    if data:
        print(f"血氧数据: {data.hex()}")
    oxi.close()

这个模板把串口操作封装成了类,异常处理、日志记录都包含在内。量产测试时,直接调用就行,省心省力。

好了,串口通信这块就讲这么多。下一章咱们聊聊血氧仪的具体通信协议,到时候会用到今天讲的内容。