第三章 点云数据读取与解析
好,咱们进入实战环节了。前面两章讲了理论基础,现在要真刀真枪地处理数据了。这一章我重点讲TI AWR系列的数据解析,以及串口和网络数据包的处理。说实话,这块坑不少,我当年刚入行时也栽过跟头。
3.1 TI AWR系列数据格式概览
TI的毫米波雷达,比如AWR1243、AWR1843、IWR6843这些,输出的数据格式其实大同小异。它们都遵循一个叫"TLV"的结构——Type、Length、Value。说白了就是:告诉你这段数据是什么类型、有多长、内容是什么。
我个人习惯把数据帧分成三层来看:
- 帧头(Header):包含帧号、时间戳、目标数量等元信息
- TLV列表:一个或多个TLV结构,每个TLV代表一种数据
- 原始数据:可能是点云、可能是热图、也可能是CFAR检测结果
嗯,这里要注意:不同型号的雷达,帧头结构略有差异。比如AWR1843的帧头是40字节,而IWR6843是44字节。我刚开始做移植时,就因为没注意这个差异,解析出来的数据全是乱的。
3.2 数据帧结构分析
咱们拿AWR1843举例。它的数据帧结构长这样:
| 偏移量(字节) | 长度(字节) | 字段名 | 说明 |
|---|---|---|---|
| 0 | 4 | magicWord | 固定为0x01020304,用于帧同步 |
| 4 | 4 | version | 协议版本号 |
| 8 | 4 | totalPacketLen | 整个数据包长度 |
| 12 | 4 | platform | 雷达平台类型 |
| 16 | 4 | frameNumber | 帧序号 |
| 20 | 4 | timeCpuCycles | CPU时钟计数 |
| 24 | 4 | numDetectedObj | 检测到的目标数量 |
| 28 | 4 | numTLVs | TLV数量 |
| 32 | 8 | reserved | 保留字段 |
你看,前4个字节是magic word。我当年调试时,经常因为串口丢包导致magic word对不上,程序直接卡死。后来我加了个滑动窗口搜索机制,才解决了这个问题。
3.3 TLV结构详解
帧头后面跟着的就是TLV列表了。每个TLV的结构如下:
| 偏移量(字节) | 长度(字节) | 字段名 | 说明 |
|---|---|---|---|
| 0 | 4 | type | 数据类型(1=点云,2=目标列表,3=热图...) |
| 4 | 4 | length | 数据段长度(不含TLV头) |
| 8 | length | value | 实际数据 |
常见的TLV type值:
- 1:点云数据(每个点包含x, y, z, doppler, snr等)
- 2:目标列表(跟踪后的目标)
- 3:距离-多普勒热图
- 4:CFAR检测结果
- 6:统计信息
为什么会有这么多类型?你想想看,不同应用场景需要的数据不一样。做目标跟踪的,只需要点云;做环境感知的,可能需要热图。TI把这些都塞进TLV里,你用哪个就解析哪个。
3.4 串口数据读取实战
串口是毫米波雷达最常用的通信方式。我建议用115200或921600的波特率。921600更快,但丢包率也高一些。
下面是我常用的串口读取代码(Python版):
import serial
import struct
class TI_Radar_Parser:
def __init__(self, port, baudrate=921600):
self.ser = serial.Serial(port, baudrate, timeout=0.1)
self.buffer = bytearray()
def read_frame(self):
"""读取一帧完整数据"""
while True:
# 搜索magic word
idx = self.buffer.find(b'\x01\x02\x03\x04')
if idx == -1:
# 没找到,继续读
data = self.ser.read(1024)
if not data:
return None
self.buffer.extend(data)
continue
# 找到magic word,检查长度是否足够
if len(self.buffer) - idx < 40:
# 帧头还没收全
data = self.ser.read(1024)
if not data:
return None
self.buffer.extend(data)
continue
# 解析帧头
header = self.buffer[idx:idx+40]
total_len = struct.unpack('<I', header[8:12])[0]
# 检查整帧是否收全
if len(self.buffer) - idx < total_len:
data = self.ser.read(4096)
if not data:
return None
self.buffer.extend(data)
continue
# 提取完整帧
frame = self.buffer[idx:idx+total_len]
self.buffer = self.buffer[idx+total_len:]
return frame
3.5 网络数据包解析
有些场景下,雷达通过以太网传输数据。比如TI的DCA1000采集卡,就是用UDP把原始数据发到PC上。网络传输的好处是速度快,但坏处是——UDP会丢包。
我曾经在一个项目中,雷达通过千兆网传数据,UDP包动不动就丢个千分之一。你想想看,一帧数据分成几十个UDP包,丢一个整帧就废了。后来我加了重传机制和序列号校验,才算稳定下来。
网络数据包的解析流程:
- 绑定UDP端口,接收数据
- 检查UDP包头的序列号,判断是否丢包
- 根据包头的帧号,把同一帧的UDP包拼起来
- 拼完整后,按TLV格式解析
代码示例(C++版):
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
class UDPRadarReceiver {
private:
int sockfd;
struct sockaddr_in server_addr;
std::map<uint32_t, std::vector<uint8_t>> frame_buffer;
public:
UDPRadarReceiver(int port) {
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
}
bool receive_frame(std::vector<uint8_t>& out_frame) {
uint8_t buf[65536];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int n = recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n <= 0) return false;
// 解析UDP包头,获取帧号和包序号
uint32_t frame_num = *(uint32_t*)(buf + 4);
uint32_t pkt_num = *(uint32_t*)(buf + 8);
uint32_t total_pkts = *(uint32_t*)(buf + 12);
// 把数据加入帧缓冲区
auto& buffer = frame_buffer[frame_num];
buffer.insert(buffer.end(), buf + 16, buf + n);
// 检查是否收齐
if (pkt_num == total_pkts - 1) {
out_frame.swap(buffer);
frame_buffer.erase(frame_num);
return true;
}
return false;
}
};
3.6 点云数据解析实战
好了,前面都是准备工作。现在咱们来解析真正的点云数据。当TLV type=1时,value段就是点云数据。每个点的结构如下:
| 偏移量(字节) | 长度(字节) | 字段名 | 说明 |
|---|---|---|---|
| 0 | 4 | x | X坐标(米,浮点数) |
| 4 | 4 | y | Y坐标(米,浮点数) |
| 8 | 4 | z | Z坐标(米,浮点数) |
| 12 | 4 | doppler | 多普勒速度(m/s) |
| 16 | 4 | snr | 信噪比(dB) |
| 20 | 4 | noise | 噪声水平 |
每个点24字节。如果TLV的length是240,那就有10个点。解析代码很简单:
def parse_point_cloud(tlv_value):
"""解析点云TLV数据"""
points = []
point_size = 24 # 每个点24字节
for i in range(0, len(tlv_value), point_size):
point_data = tlv_value[i:i+point_size]
if len(point_data) < point_size:
break
x, y, z, doppler, snr, noise = struct.unpack('<6f', point_data)
points.append({
'x': x,
'y': y,
'z': z,
'doppler': doppler,
'snr': snr,
'noise': noise
})
return points
3.7 数据校验与错误处理
数据解析不是一帆风顺的。我总结了几种常见问题:
- 帧同步丢失:串口或网络丢包导致magic word找不到
- 长度不匹配:TLV声明的长度和实际数据长度对不上
- 类型错误:解析了不支持的TLV type
- 字节序问题:TI数据是小端(little-endian),有些平台默认是大端
我的处理策略是:
- 每帧都做CRC校验(如果雷达支持的话)
- 解析时加try-catch,遇到异常直接跳过当前帧
- 记录错误日志,方便事后排查
- 设置超时机制,超过1秒没收到完整帧就重置缓冲区
嗯,说到日志,我建议把原始数据也存一份。有时候算法效果不好,回头看看原始数据,能发现很多问题。我就曾经因为雷达天线没对准,导致点云全是噪声,查了三天才发现是安装问题。
3.8 小结
这一章的内容就这些。说白了,数据解析就是个"拆包"的过程——把雷达发来的二进制流,按照协议拆成我们能用的点云数据。核心就三点:
- 找到帧头(magic word)
- 按TLV结构解析
- 做好错误处理
下一章咱们讲点云预处理,包括去噪、下采样、坐标变换这些。到时候你会看到,解析出来的原始点云有多"脏",预处理有多重要。
好,今天就到这里。有问题可以随时交流。