第四章:数据采集与解析
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,数据量很大。如果不用状态机,很容易丢帧。
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接收和解析。每一步都环环相扣,缺一不可。
- 原始ADC数据是IQ复数对,每个值16位有符号整数
- 帧结构包含帧头、长度、序号、chirp数、采样点数和校验
- 串口解析用状态机,避免粘包和断帧
- Python用struct模块解析二进制数据,效率高
好了,这一章的内容就到这里。数据采集与解析是雷达感知的基石,基础打牢了,后面的距离、速度、角度计算才能准确。记住我踩过的坑,你的开发之路会顺畅很多。