4、UART串口通信:串口参数配置、发送与接收数据、数据帧解析、奇偶校验实战
UART串口,说白了就是嵌入式设备之间最常用的“对话”方式。我做了这么多年硬件,几乎每个项目都离不开它。从传感器数据采集,到与上位机通信,再到固件升级,串口始终是那个最可靠的老伙计。
这一章,咱们就把它彻底搞明白。不只是调个库发个数据那么简单,我会把参数配置的坑、数据帧解析的套路、还有奇偶校验那些容易被忽略的细节,都掰开揉碎了讲给你听。
4.1 串口参数配置:别让“波特率”坑了你
配置串口,第一件事就是对齐参数。两边对不上,神仙也救不了。我见过太多新手,代码写得飞起,结果就是收不到数据,最后发现是波特率设错了。
| 参数 | 常见值 | 说明 |
|---|---|---|
| 波特率 | 9600, 115200, 460800 | 每秒传输的符号数,越高越快,但受线长和干扰影响 |
| 数据位 | 7, 8 | 一帧中有效数据的位数,通常用8位 |
| 停止位 | 1, 1.5, 2 | 帧结束标志,1位最常用 |
| 校验位 | None, Even, Odd | 奇偶校验,用于简单检错 |
| 流控制 | None, RTS/CTS | 硬件流控,一般不用 |
在Python里,用pyserial库配置串口非常简单。我个人习惯把参数写在一个字典里,方便维护。
import serial
# 我个人习惯这样组织参数,清晰明了
serial_config = {
'port': '/dev/ttyUSB0', # Linux下常见,Windows是COM3
'baudrate': 115200,
'bytesize': serial.EIGHTBITS,
'parity': serial.PARITY_NONE,
'stopbits': serial.STOPBITS_ONE,
'timeout': 1 # 读超时,单位秒
}
ser = serial.Serial(**serial_config)
# 检查是否打开成功
if ser.is_open:
print(f"串口 {ser.port} 已打开,波特率 {ser.baudrate}")
timeout参数很关键。设为None会一直阻塞等待,设为0是非阻塞模式。我一般设1秒,既不会卡死,也不会漏数据。
4.2 发送与接收数据:基础操作与常见陷阱
发送数据很简单,ser.write()就行。但接收就有讲究了。你想想看,数据是源源不断流过来的,你怎么知道什么时候读完?
这里有几种常见策略:
- 固定长度读取: 知道每次会来多少字节,直接用
ser.read(10)。 - 按行读取: 数据以换行符
\n结尾,用ser.readline()。很多AT指令模块就是这么干的。 - 超时读取: 不知道长度,就设个超时,读不到新数据就认为结束了。
- 帧头帧尾检测: 这是最可靠的方式,后面会细讲。
# 发送数据
data_to_send = b"Hello, UART!\n"
ser.write(data_to_send)
# 接收数据 - 按行读取
response = ser.readline()
print(f"收到: {response}")
# 接收数据 - 固定长度
response = ser.read(10)
print(f"收到10字节: {response.hex()}")
# 接收数据 - 超时读取所有可用数据
import time
time.sleep(0.1) # 等一会儿,让数据收完
response = ser.read(ser.in_waiting) # 读取缓冲区所有数据
print(f"收到 {len(response)} 字节: {response}")
ser.in_waiting返回的是接收缓冲区中当前可读的字节数。但要注意,这个值随时在变。如果你读得太快,可能只读到一部分数据。我一般会在发送后加一个time.sleep(0.05),给设备一点响应时间。
4.3 数据帧解析:从原始字节到结构化数据
原始字节流就像一堆散落的珠子,你得用线把它们串起来。这根“线”,就是数据帧协议。
一个典型的数据帧结构长这样:
帧头(2字节) + 数据长度(1字节) + 数据(N字节) + 校验(1字节) + 帧尾(1字节)
举个例子,我做过的一个温湿度传感器项目,它的协议是这样的:
| 字节偏移 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头1 |
| 1 | 0x55 | 帧头2 |
| 2 | 数据长度 | 从本字节之后到校验之前的数据长度 |
| 3 ~ 3+N-1 | 有效数据 | 比如温度、湿度值 |
| 3+N | 校验和 | 前面所有字节的累加和取低8位 |
| 4+N | 0x0D 0x0A | 帧尾 (CR+LF) |
解析这种帧,我的套路是这样的:
- 不断从串口读取字节,存入一个环形缓冲区。
- 在缓冲区里搜索帧头
0xAA 0x55。 - 找到帧头后,读取数据长度字段。
- 检查缓冲区是否已经有足够的数据(帧头 + 长度 + 数据 + 校验 + 帧尾)。
- 提取完整帧,校验校验和和帧尾。
- 如果校验通过,解析数据;否则丢弃这一帧,继续搜索下一帧。
def parse_frame(buffer):
"""从缓冲区中解析一帧数据,成功返回(帧数据, 剩余缓冲区),失败返回(None, 缓冲区)"""
# 搜索帧头 0xAA 0x55
header = bytes([0xAA, 0x55])
idx = buffer.find(header)
if idx == -1:
return None, buffer # 没找到帧头
# 检查是否有足够的数据来读取长度字段
if len(buffer) - idx < 3:
return None, buffer # 数据不够
data_len = buffer[idx + 2] # 数据长度
frame_len = 2 + 1 + data_len + 1 + 2 # 帧头+长度+数据+校验+帧尾
if len(buffer) - idx < frame_len:
return None, buffer # 完整帧还没到
frame = buffer[idx:idx + frame_len]
# 校验帧尾
if frame[-2:] != bytes([0x0D, 0x0A]):
# 帧尾错误,丢弃这一帧,继续搜索
return None, buffer[idx + 1:]
# 校验校验和
checksum = sum(frame[2:-3]) & 0xFF # 从长度字段到校验前一个字节
if checksum != frame[-3]:
print(f"校验和错误: 计算值 {checksum:#x}, 接收值 {frame[-3]:#x}")
return None, buffer[idx + 1:]
# 解析成功
payload = frame[3:-3] # 有效数据
return payload, buffer[idx + frame_len:]
# 使用示例
buffer = b''
while True:
# 从串口读取数据
chunk = ser.read(ser.in_waiting or 1)
if chunk:
buffer += chunk
payload, buffer = parse_frame(buffer)
if payload:
# 解析成功,处理数据
print(f"收到一帧数据: {payload.hex()}")
4.4 奇偶校验实战:简单但有效的检错手段
奇偶校验,说白了就是给每个字节加一个“标签”,告诉接收方这个字节里1的个数是奇数还是偶数。它不能纠错,但能发现单比特错误。
有几种模式:
- 无校验 (None): 不校验,最快。
- 偶校验 (Even): 保证每个字节(包括校验位)中1的个数为偶数。
- 奇校验 (Odd): 保证每个字节(包括校验位)中1的个数为奇数。
- 标记校验 (Mark): 校验位始终为1。
- 空格校验 (Space): 校验位始终为0。
在pyserial里配置奇偶校验很简单:
# 偶校验
ser = serial.Serial(
port='/dev/ttyUSB0',
baudrate=115200,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS
)
# 奇校验
ser = serial.Serial(
port='/dev/ttyUSB0',
baudrate=115200,
parity=serial.PARITY_ODD,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS
)
但要注意,奇偶校验只能检测奇数个比特错误。如果两个比特同时翻转,校验位反而对了,错误就漏过去了。所以,在干扰严重的环境里,我建议用CRC校验,而不是奇偶校验。
如果你想自己实现奇偶校验,也很简单:
def calculate_parity(data, parity_type='even'):
"""计算数据的奇偶校验位"""
ones_count = bin(data).count('1')
if parity_type == 'even':
return 0 if ones_count % 2 == 0 else 1
elif parity_type == 'odd':
return 1 if ones_count % 2 == 0 else 0
else:
return 0
# 示例:计算字节 0x53 的偶校验位
data = 0x53 # 二进制 0101 0011,有4个1
parity_bit = calculate_parity(data, 'even')
print(f"0x{data:02X} 的偶校验位是: {parity_bit}") # 输出 0,因为4是偶数
4.5 本章知识体系
下面这张图,把本章的核心知识点串起来了。你可以把它当成一个思维导图,看看自己是不是都掌握了。
嗯,这一章的内容就到这里。串口通信看似基础,但里面的门道不少。参数配置、数据收发、帧解析、奇偶校验,每一个环节都可能成为你项目中的“绊脚石”。希望我分享的这些经验和代码,能帮你少走一些弯路。