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"。若解码字节与发送不一致,检查波特率配置或采样率设置。

UART帧解码验证拓扑与信号时序 MCU UART TX 115200-8-N-1 逻辑分析仪 CH0: TX 24MHz采样 信号线 Python解码器 uart_decoder.py CSV导出 UART信号时序(起始位 + 8数据位 + 停止位) 起始位(0) D0 D1 D2 D3 D4 D5 D6 D7 停止位(1) 8.68μs

关键实现说明

解码器通过检测下降沿定位起始位,在每个位周期的中心点采样以避开信号边沿抖动。停止位校验可过滤毛刺干扰。帧解析采用滑动窗口查找帧头,校验和算法为帧头、长度与所有数据字节累加后取低8位,与发送端约定一致。若实际发送数据不含帧协议,脚本仍可输出原始字节序列,便于直接观察通信内容。