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]
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 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()
这个模板把串口操作封装成了类,异常处理、日志记录都包含在内。量产测试时,直接调用就行,省心省力。
好了,串口通信这块就讲这么多。下一章咱们聊聊血氧仪的具体通信协议,到时候会用到今天讲的内容。