3、串口基础通信:打开/关闭串口、发送数据、接收数据、超时设置、缓冲区管理

各位同学,咱们今天聊点实在的。串口通信,说白了就是让电脑跟外面的设备「说上话」。你想想看,一个PLC、一个扫码枪、一个称重仪表,它们怎么跟你的上位机程序交流?靠的就是串口。

我刚开始做工业自动化那会儿,第一次调串口,以为就是发个数据、收个数据那么简单。结果呢?设备死活没反应。后来才发现,是串口根本没打开成功。嗯,今天咱们就把这五个基本功彻底讲透。

3.1 打开与关闭串口——这是你的「握手礼」

打开串口,就像你敲门进房间。门没开,你喊破嗓子也没用。在Python里,我们用pyserial这个库来操作。

我个人习惯,打开串口前先检查一下端口是否存在。别问我为什么,有一次我在现场调试,笔记本插了USB转串口线,结果驱动没装好,端口号根本找不到。程序直接崩了,客户在旁边看着,那叫一个尴尬。

import serial

# 打开串口
ser = serial.Serial(
    port='COM3',        # 端口号,Windows用COMx,Linux用/dev/ttyUSB0
    baudrate=9600,      # 波特率,常见的有9600、115200
    bytesize=8,         # 数据位,通常是8
    parity='N',         # 校验位,N无校验,E偶校验,O奇校验
    stopbits=1,         # 停止位,通常是1
    timeout=1           # 超时时间,单位秒
)

# 检查是否打开成功
if ser.is_open:
    print(f"串口 {ser.port} 已成功打开")
else:
    print("串口打开失败")

# 关闭串口
ser.close()
print("串口已关闭")

核心要点:

  • 打开串口时,参数必须跟外设匹配。波特率不对,收到的就是乱码。
  • 用完一定要关闭串口。不关的话,下次再打开就会报「端口被占用」。
  • 我建议用with语句来自动管理,省心。
# 推荐写法:自动关闭
with serial.Serial('COM3', 9600, timeout=1) as ser:
    # 在这里做收发操作
    pass  # 退出with块后自动关闭

3.2 发送数据——把你的指令传出去

发送数据,就是告诉外设「你该干活了」。外设通常只认字节,不认字符串。所以你得把数据转成字节格式。

我记得有一次,我要给一个温控器发送读取温度的指令。手册上写的是十六进制01 03 00 00 00 01 84 0A。我直接写了个字符串发过去,结果温控器理都不理我。后来才意识到,得用bytes类型。

# 发送字符串(需要编码)
ser.write("AT\r\n".encode('utf-8'))

# 发送十六进制字节
cmd = bytes.fromhex('01 03 00 00 00 01 84 0A')
ser.write(cmd)

# 发送整数列表
data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A]
ser.write(bytes(data))

避坑指南:

我曾经犯过一个低级错误——发送完数据后没加换行符。很多外设(比如串口打印机)需要收到\r\n才认为指令结束。不加的话,设备就一直等着。所以,发完数据记得加个换行,除非你的协议明确不需要。

3.3 接收数据——听外设怎么回话

接收数据,就是听外设的回应。这里有个关键点:你不知道外设什么时候回数据,也不知道回多少。所以,读取方式很重要。

常用的读取方法有几种:

  • read(n):读取指定字节数。适合固定长度的回应。
  • readline():读取直到遇到换行符。适合文本协议。
  • read_until(expected):读取直到遇到指定字节。适合自定义协议。
  • in_waiting:先看看缓冲区有多少字节,再决定读多少。
# 方法1:读取固定长度
response = ser.read(8)  # 读取8个字节

# 方法2:读取一行
line = ser.readline()   # 读取直到换行符

# 方法3:读取所有可用数据
import time
time.sleep(0.1)  # 稍微等一等,让数据收完
available = ser.in_waiting
if available > 0:
    data = ser.read(available)
    print(f"收到 {len(data)} 字节: {data.hex()}")

注意:

千万不要用死循环去读串口,比如while True: data = ser.read()。这样会把CPU占满,程序卡死。我见过有人这么写,结果上位机界面完全无响应,只能强制结束进程。

3.4 超时设置——别让程序死等

超时,就是给串口操作设一个「等待上限」。没有超时的话,如果外设没回应,你的程序就会一直卡在那里,像死机了一样。

超时参数在打开串口时设置,也可以在运行中修改:

# 打开时设置超时
ser = serial.Serial('COM3', 9600, timeout=2)  # 2秒超时

# 运行时修改超时
ser.timeout = 3  # 改成3秒

# 读取时使用超时
data = ser.read(10)  # 如果2秒内没收到10个字节,就返回已收到的部分

超时值怎么设?我个人经验:

场景 推荐超时 说明
查询类指令(如读温度) 0.5 ~ 1秒 外设响应通常很快
控制类指令(如启动电机) 2 ~ 5秒 外设可能需要时间执行
批量数据上传 10 ~ 30秒 数据量大,传输时间长
持续监控(如扫码枪) None 或很大 等待数据随时到来

我的经验:

超时设得太短,容易误判为「设备无响应」;设得太长,程序响应慢。我一般先设1秒,如果发现经常超时,再逐步加大。另外,超时返回的数据可能是空的,记得判断一下if data:再处理。

3.5 缓冲区管理——别让数据「堵车」

缓冲区,就是串口驱动里的一块临时存储区。外设发来的数据先存在这里,你的程序再从里面读。如果读得慢,数据就会堆积,新的数据可能覆盖旧的,或者直接丢掉。

我遇到过最头疼的问题:一个扫码枪每秒能扫30个条码,我的程序处理一个条码要0.5秒。结果缓冲区越积越多,最后读到的是乱序的数据。后来怎么解决的?两个办法:

  1. 及时清空缓冲区:每次读取前,先清空一下。
  2. 使用线程读取:开一个独立线程专门读数据,主线程负责处理。
# 清空输入缓冲区
ser.reset_input_buffer()

# 清空输出缓冲区
ser.reset_output_buffer()

# 查看缓冲区数据量
bytes_in = ser.in_waiting   # 输入缓冲区有多少字节
bytes_out = ser.out_waiting # 输出缓冲区有多少字节(通常为0)

实用技巧:

我习惯在每次发送指令前,先清空输入缓冲区。这样能确保收到的回应是当前指令的,而不是上次残留的数据。代码很简单:

ser.reset_input_buffer()  # 清空旧数据
ser.write(command)        # 发送新指令
time.sleep(0.1)           # 等一会儿
response = ser.read(ser.in_waiting)  # 读取回应

3.6 综合示例——一个完整的通信流程

好了,咱们把上面这些串起来,写一个完整的例子。假设我们要跟一个Modbus温控器通信,读取当前温度。

import serial
import time

def read_temperature(port='COM3'):
    """读取温控器温度"""
    try:
        # 1. 打开串口
        with serial.Serial(port, 9600, timeout=1) as ser:
            # 2. 清空缓冲区
            ser.reset_input_buffer()
            
            # 3. 发送读取指令(Modbus协议)
            # 读取保持寄存器,地址0,长度1
            cmd = bytes.fromhex('01 03 00 00 00 01 84 0A')
            ser.write(cmd)
            
            # 4. 等待回应
            time.sleep(0.1)
            
            # 5. 读取回应
            response = ser.read(7)  # Modbus回应固定7字节
            
            # 6. 解析数据
            if len(response) == 7:
                # 第3、4字节是温度值(放大10倍)
                temp_raw = (response[3] << 8) | response[4]
                temperature = temp_raw / 10.0
                return temperature
            else:
                print("回应长度不对,可能通信异常")
                return None
                
    except serial.SerialException as e:
        print(f"串口错误: {e}")
        return None

# 调用函数
temp = read_temperature('COM3')
if temp is not None:
    print(f"当前温度: {temp}°C")

最后提醒一句:

串口通信没有「绝对可靠」这回事。线缆松动、干扰、设备死机,什么情况都可能发生。所以,你的代码一定要有异常处理,要有重试机制。我曾经在一个项目里,就因为没加异常处理,设备掉线后程序直接崩溃,产线停了半小时。从那以后,我每个串口操作都包在try-except里。

好了,这一章的内容就到这儿。打开、关闭、发送、接收、超时、缓冲区——这五个基本功你练熟了,串口通信就算入门了。下一章咱们聊聊更高级的话题:如何用事件驱动的方式处理串口数据,让你的程序更高效。