第四章:数据采集与解析

4.1 原始ADC数据长什么样?

说实话,我第一次拿到毫米波雷达的原始ADC数据时,整个人是懵的。满屏的十六进制数,像天书一样。后来我才明白,这些数据背后藏着目标的距离、速度和角度信息。

原始ADC数据,说白了就是雷达接收到的模拟信号经过模数转换后的数字结果。每个采样点代表一个复数,包含实部(I)和虚部(Q)。我习惯把这对数据叫做“IQ数据对”。

举个例子,一个典型的ADC采样点可能是这样的:

0x0A3F 0xF1C2  // 实部0x0A3F,虚部0xF1C2

每个值通常是16位有符号整数,范围从-32768到32767。嗯,这里要注意:不同厂商的雷达可能用12位或14位,但存储时通常会补齐到16位。

4.2 帧结构:数据是怎么组织的?

原始数据不是乱糟糟堆在一起的。它有自己的“骨架”,我们叫它帧结构。我在项目中遇到过最典型的帧结构是这样的:

字段 大小(字节) 说明
帧头 4 固定魔数,如0xAABBCCDD
帧长度 2 整个帧的字节数
帧序号 2 从0开始递增
chirp数量 1 当前帧包含的chirp数
采样点数 2 每个chirp的采样点数
ADC数据 可变 IQ数据交替存储
校验和 2 CRC16校验

你想想看,如果没有这个结构,解析程序根本不知道哪里是数据开始,哪里是结束。我曾经调试过一个项目,就是因为帧头魔数写错了,导致整个数据流全部错位,花了整整两天才找到问题。

4.3 串口数据流解析

雷达通过串口把数据发出来。串口嘛,就是一字节一字节地往外吐。我们需要在接收端把这些字节重新组装成完整的帧。

我个人习惯用状态机来做这件事。为什么?因为串口数据可能断断续续,也可能粘包。状态机能优雅地处理这些情况。

一个典型的状态机长这样:

状态0:等待帧头第一个字节0xAA
状态1:等待帧头第二个字节0xBB
状态2:等待帧头第三个字节0xCC
状态3:等待帧头第四个字节0xDD
状态4:接收帧长度字段
状态5:接收剩余数据
状态6:校验数据

说白了,就是让程序像流水线一样,一步一步地确认每个字节的位置。我在做车载雷达项目时,串口波特率用的是921600,数据量很大。如果不用状态机,很容易丢帧。

避坑指南: 我曾经因为没处理好串口缓冲区溢出,导致连续丢了几百帧数据。建议接收缓冲区至少设置成最大帧长度的3倍。

4.4 Python读取与解析实战

好了,理论说完了,咱们直接上代码。我习惯用Python的struct模块来解析二进制数据,它能把字节流转换成Python的整数、浮点数等类型。

下面是一个完整的解析示例:

import struct
import serial

def parse_radar_frame(data):
    """
    解析一帧雷达数据
    data: 字节串,包含完整的一帧
    """
    # 检查帧头
    if data[0:4] != b'\xAA\xBB\xCC\xDD':
        print("帧头错误")
        return None
    
    # 解析帧长度
    frame_len = struct.unpack('<H', data[4:6])[0]
    
    # 解析帧序号
    frame_seq = struct.unpack('<H', data[6:8])[0]
    
    # 解析chirp数量和采样点数
    num_chirps = data[8]
    num_samples = struct.unpack('<H', data[9:11])[0]
    
    # 解析ADC数据
    adc_start = 11
    adc_data = []
    for i in range(num_chirps * num_samples):
        # 每个采样点4字节:实部2字节,虚部2字节
        offset = adc_start + i * 4
        i_val = struct.unpack('<h', data[offset:offset+2])[0]
        q_val = struct.unpack('<h', data[offset+2:offset+4])[0]
        adc_data.append((i_val, q_val))
    
    return {
        'frame_seq': frame_seq,
        'num_chirps': num_chirps,
        'num_samples': num_samples,
        'adc_data': adc_data
    }

# 串口读取示例
ser = serial.Serial('COM3', 921600, timeout=1)
buffer = b''

while True:
    # 读取原始字节
    raw = ser.read(1024)
    if not raw:
        continue
    
    buffer += raw
    
    # 查找帧头
    while len(buffer) >= 4:
        if buffer[0:4] == b'\xAA\xBB\xCC\xDD':
            # 尝试解析
            if len(buffer) >= 11:
                frame_len = struct.unpack('<H', buffer[4:6])[0]
                if len(buffer) >= frame_len:
                    frame = parse_radar_frame(buffer[:frame_len])
                    if frame:
                        print(f"收到帧 {frame['frame_seq']}, "
                              f"chirp数 {frame['num_chirps']}, "
                              f"采样点数 {frame['num_samples']}")
                    buffer = buffer[frame_len:]
                    continue
        # 没找到帧头,跳过
        buffer = buffer[1:]

这段代码我用了很多项目,基本没出过问题。你想想看,核心逻辑其实就三步:找帧头、读长度、解析数据。但实际调试时,串口数据可能会夹杂一些乱码,所以一定要做好异常处理。

小技巧: 调试时可以先打印原始字节的十六进制,确认帧结构对不对。我习惯用print(data.hex())来快速查看。

4.5 知识体系总览

为了让你更直观地理解整个数据采集与解析的流程,我画了一张图:

毫米波雷达数据采集与解析流程 雷达硬件 发射/接收信号 ADC采样 IQ数据对生成 帧封装 添加帧头/校验 串口发送 字节流输出 Python接收 串口读取/缓冲 状态机解析 帧同步/校验 数据输出 IQ数据/帧信息 后续处理 距离/速度/角度计算 图:数据从雷达硬件到Python解析的完整链路

这张图把整个流程串起来了。从雷达硬件发射信号,到ADC采样生成IQ数据,再到帧封装、串口发送,最后用Python接收和解析。每一步都环环相扣,缺一不可。

核心要点:
  • 原始ADC数据是IQ复数对,每个值16位有符号整数
  • 帧结构包含帧头、长度、序号、chirp数、采样点数和校验
  • 串口解析用状态机,避免粘包和断帧
  • Python用struct模块解析二进制数据,效率高

好了,这一章的内容就到这里。数据采集与解析是雷达感知的基石,基础打牢了,后面的距离、速度、角度计算才能准确。记住我踩过的坑,你的开发之路会顺畅很多。

可可精品资料库