4. UART发送数据帧解码
任务定义
本案例使用逻辑分析仪采集MCU的UART TX引脚电平信号,通过Python脚本解析原始电平跳变数据,还原出实际发送的字节内容与帧格式。输入为逻辑分析仪导出的CSV采样数据(时间戳+电平),输出为解码后的十六进制字节序列及可读ASCII字符,并验证帧头、帧尾与校验字段是否符合预期协议。
实现方案
采用Python标准库实现UART异步串行解码器。核心逻辑包括:根据波特率计算位周期,识别起始位下降沿,按位采样数据位,校验停止位,最后按协议帧格式提取有效载荷并计算校验和。解码器不依赖第三方库,可直接处理逻辑分析仪导出的CSV文件。
代码实现
import csv
import sys
# ========== 配置参数 ==========
BAUD_RATE = 115200 # 与MCU UART配置一致
BIT_TIME_US = 1_000_000 / BAUD_RATE # 每位时间(微秒)
SAMPLE_RATE_MHZ = 24 # 逻辑分析仪采样率(MHz)
SAMPLE_INTERVAL_US = 1_000_000 / (SAMPLE_RATE_MHZ * 1_000_000) # 采样间隔(微秒)
# 协议定义:帧格式 = 帧头(0xAA) + 长度(1字节) + 数据 + 校验和(1字节) + 帧尾(0x55)
FRAME_HEADER = 0xAA
FRAME_TAIL = 0x55
def parse_csv(filepath):
"""读取逻辑分析仪导出的CSV,返回[(时间戳_us, 电平), ...]"""
samples = []
with open(filepath, 'r') as f:
reader = csv.reader(f)
header = next(reader) # 跳过表头
for row in reader:
if len(row) < 2:
continue
time_ns = float(row[0]) # 假设第一列为时间(纳秒)
level = int(float(row[1])) # 第二列为电平
samples.append((time_ns / 1000.0, level)) # 转为微秒
return samples
def decode_uart(samples):
"""从采样点中解码UART字节序列"""
bytes_out = []
i = 0
n = len(samples)
while i < n - 1:
# 寻找下降沿(起始位):当前为高,下一时刻为低
if samples[i][1] == 1 and samples[i+1][1] == 0:
start_time = samples[i+1][0]
# 采样起始位中点
mid_start = start_time + BIT_TIME_US / 2
# 找到最接近mid_start的采样点
j = i + 1
while j < n and samples[j][0] < mid_start:
j += 1
if j >= n:
break
# 确认起始位为低
if samples[j][1] != 0:
i = j
continue
# 采样8个数据位(LSB first)
byte_val = 0
valid = True
for bit_idx in range(8):
sample_time = start_time + BIT_TIME_US * (1.5 + bit_idx)
# 找到最接近的采样点
k = j
while k < n and samples[k][0] < sample_time:
k += 1
if k >= n:
valid = False
break
bit = samples[k][1]
byte_val |= (bit << bit_idx)
if not valid:
break
# 采样停止位(应为高)
stop_time = start_time + BIT_TIME_US * 9.5
k = j
while k < n and samples[k][0] < stop_time:
k += 1
if k >= n:
break
stop_bit = samples[k][1]
if stop_bit == 1: # 停止位有效
bytes_out.append(byte_val)
# 移动到停止位之后
i = k + 1
continue
else:
i = k + 1
continue
i += 1
return bytes_out
def parse_frame(data_bytes):
"""按协议解析帧,返回(帧头, 长度, 数据, 校验和, 帧尾)或None"""
if len(data_bytes) < 4:
return None
# 查找帧头
for idx in range(len(data_bytes) - 3):
if data_bytes[idx] == FRAME_HEADER:
length = data_bytes[idx+1]
if idx + 2 + length + 1 < len(data_bytes): # 长度+校验+帧尾
payload = data_bytes[idx+2:idx+2+length]
checksum = data_bytes[idx+2+length]
tail = data_bytes[idx+3+length]
if tail == FRAME_TAIL:
# 校验和验证:所有数据字节累加取低8位
calc_sum = (FRAME_HEADER + length + sum(payload)) & 0xFF
if calc_sum == checksum:
return (FRAME_HEADER, length, payload, checksum, tail)
return None
def main():
if len(sys.argv) < 2:
print("用法: python uart_decoder.py <logic_analyzer.csv>")
return
samples = parse_csv(sys.argv[1])
if not samples:
print("错误: 未读取到有效采样数据")
return
decoded = decode_uart(samples)
print(f"解码得到 {len(decoded)} 个字节: {[hex(b) for b in decoded]}")
frame = parse_frame(decoded)
if frame:
header, length, payload, checksum, tail = frame
print(f"帧解析成功:")
print(f" 帧头: 0x{header:02X}")
print(f" 长度: {length}")
print(f" 数据: {[hex(b) for b in payload]}")
print(f" 校验: 0x{checksum:02X} (计算值匹配)")
print(f" 帧尾: 0x{tail:02X}")
ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in payload)
print(f" ASCII: {ascii_repr}")
else:
print("未找到符合协议的完整帧")
if __name__ == "__main__":
main()
输入数据格式说明
逻辑分析仪导出CSV需包含两列:第一列为时间戳(纳秒),第二列为电平值(0或1)。采样率需≥24MHz以保证每位至少采样200个点。若使用其他采样率,需修改SAMPLE_RATE_MHZ常量。
验证方法
使用MCU开发板(如STM32)以115200-8-N-1配置发送以下测试帧:AA 05 48 65 6C 6C 6F 2A 55(帧头、长度5、数据"Hello"、校验和0x2A、帧尾)。将逻辑分析仪探头连接TX引脚,捕获数据导出为CSV后运行本脚本。
判定标准:脚本输出应包含5个数据字节[0x48, 0x65, 0x6C, 0x6C, 0x6F],帧解析成功且校验和匹配,ASCII显示为"Hello"。若解码字节与发送不一致,检查波特率配置或采样率设置。
关键实现说明
解码器通过检测下降沿定位起始位,在每个位周期的中心点采样以避开信号边沿抖动。停止位校验可过滤毛刺干扰。帧解析采用滑动窗口查找帧头,校验和算法为帧头、长度与所有数据字节累加后取低8位,与发送端约定一致。若实际发送数据不含帧协议,脚本仍可输出原始字节序列,便于直接观察通信内容。