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秒。结果缓冲区越积越多,最后读到的是乱序的数据。后来怎么解决的?两个办法:
- 及时清空缓冲区:每次读取前,先清空一下。
- 使用线程读取:开一个独立线程专门读数据,主线程负责处理。
# 清空输入缓冲区
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里。
好了,这一章的内容就到这儿。打开、关闭、发送、接收、超时、缓冲区——这五个基本功你练熟了,串口通信就算入门了。下一章咱们聊聊更高级的话题:如何用事件驱动的方式处理串口数据,让你的程序更高效。