3、点云数据采集:驱动开发、数据帧解析、时间戳同步
好,咱们进入正题。点云数据采集,说白了就是让飞控把激光雷达的数据“听懂”并“收进来”。这一步要是没做好,后面什么滤波、建图、避障全是空中楼阁。我这些年调过不少雷达,从几万块的工业级到几百块的DIY模块,踩过的坑能写一本小册子。今天就把核心的三个环节——驱动开发、数据帧解析、时间戳同步——掰开揉碎了讲清楚。
3.1 驱动开发:让飞控“认识”雷达
驱动开发,本质上是写一个中间层。飞控通常跑的是RTOS(比如FreeRTOS),而雷达一般通过UART或SPI接口往外吐数据。我习惯把驱动拆成三层:硬件抽象层、协议适配层、数据分发层。
核心原则:驱动层只负责“收数据”,不负责“理解数据”。解析工作交给上层模块。
举个例子,我曾在某款开源飞控上移植过RPLIDAR A1的驱动。硬件抽象层就是初始化UART、配置DMA、注册中断回调。代码大概长这样:
// 硬件抽象层示例:UART初始化
static void lidar_uart_init(uint32_t baudrate)
{
uart_config_t config = {
.baud_rate = baudrate,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(LIDAR_UART_PORT, &config);
uart_set_pin(LIDAR_UART_PORT, LIDAR_TX_PIN, LIDAR_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(LIDAR_UART_PORT, LIDAR_RX_BUF_SIZE, 0, 0, NULL, 0);
}
嗯,这里要注意:波特率一定要和雷达手册严格对应。我遇到过好几次,因为波特率偏差导致数据全是乱码,排查了半天才发现是晶振精度问题。后来我习惯在驱动里加一个自动波特率检测逻辑,虽然麻烦点,但一劳永逸。
3.2 数据帧解析:从字节流到点云
雷达吐出来的是一串连续的字节流,你得知道哪些字节是包头、哪些是数据、哪些是校验。不同厂家的协议千奇百怪,但万变不离其宗——帧结构通常包含:同步头 + 数据长度 + 数据体 + 校验和。
我个人习惯用状态机来解析。为什么?因为字节流是源源不断的,你没法保证一次读完一整个帧。状态机可以逐字节处理,鲁棒性极好。我曾经见过有人用阻塞式读取,结果雷达一丢包,整个系统就卡死了。
下面是一个简化的状态机解析框架:
typedef enum {
FRAME_STATE_SYNC1,
FRAME_STATE_SYNC2,
FRAME_STATE_LENGTH,
FRAME_STATE_DATA,
FRAME_STATE_CHECKSUM
} frame_state_t;
frame_state_t state = FRAME_STATE_SYNC1;
uint8_t frame_buf[256];
uint16_t frame_len = 0;
uint16_t frame_index = 0;
void lidar_byte_parser(uint8_t byte)
{
switch (state) {
case FRAME_STATE_SYNC1:
if (byte == 0xAA) {
state = FRAME_STATE_SYNC2;
frame_buf[frame_index++] = byte;
}
break;
case FRAME_STATE_SYNC2:
if (byte == 0x55) {
state = FRAME_STATE_LENGTH;
frame_buf[frame_index++] = byte;
} else {
state = FRAME_STATE_SYNC1; // 重新同步
frame_index = 0;
}
break;
case FRAME_STATE_LENGTH:
frame_len = byte;
state = FRAME_STATE_DATA;
frame_buf[frame_index++] = byte;
break;
case FRAME_STATE_DATA:
frame_buf[frame_index++] = byte;
if (frame_index >= frame_len + 4) { // 4字节是同步头+长度+校验
state = FRAME_STATE_CHECKSUM;
}
break;
case FRAME_STATE_CHECKSUM:
// 校验逻辑
if (verify_checksum(frame_buf, frame_index)) {
process_lidar_frame(frame_buf, frame_index);
}
state = FRAME_STATE_SYNC1;
frame_index = 0;
break;
}
}
避坑指南:我曾经遇到过一种雷达,它的同步头不是固定的0xAA 0x55,而是根据数据内容动态变化的。那真是折腾了好几天。后来我加了一个滑动窗口校验,才把问题搞定。所以,永远不要假设协议是完美的,一定要做容错处理。
3.3 时间戳同步:让每一帧都有“时间坐标”
点云数据如果没有准确的时间戳,那在飞控上基本就是废的。你想想看,飞控在高速运动,雷达扫到的一帧数据可能跨越了几十毫秒。如果时间戳不准,融合出来的位置信息全是错的。
时间戳同步有两种主流做法:
- 软件时间戳:在驱动层收到帧尾时,直接读取系统滴答计时器(如FreeRTOS的xTaskGetTickCount())。优点是简单,缺点是精度受任务调度影响。
- 硬件时间戳:利用MCU的输入捕获或PPS(秒脉冲)信号,在硬件层面打时间戳。精度高,但需要额外硬件支持。
我个人的建议是:能用硬件时间戳就别用软件。尤其是当飞控上跑着多个任务时,软件时间戳的抖动可能达到几毫秒,这对高速飞行来说是不可接受的。
下面是一个硬件时间戳的配置示例(基于STM32的TIM输入捕获):
// 硬件时间戳初始化
void timestamp_hw_init(void)
{
TIM_IC_InitTypeDef ic_config;
ic_config.TIM_Channel = TIM_CHANNEL_1;
ic_config.TIM_ICPolarity = TIM_ICPOLARITY_RISING;
ic_config.TIM_ICSelection = TIM_ICSELECTION_DIRECTTI;
ic_config.TIM_ICPrescaler = TIM_ICPSC_DIV1;
ic_config.TIM_ICFilter = 0x0F;
TIM_ICInit(TIM2, &ic_config);
TIM_Cmd(TIM2, ENABLE);
}
// 在中断中读取时间戳
uint64_t get_hw_timestamp(void)
{
return TIM_GetCapture1(TIM2);
}
注意:硬件时间戳虽然准,但要注意计数器溢出问题。如果TIM是16位的,在高速时钟下可能几毫秒就溢出了。我一般会用32位定时器,或者在溢出中断里维护一个软件扩展计数器。
3.4 知识体系总览
为了让你更直观地理解这三个环节的关系,我画了一张流程图:
从这张图可以看得很清楚:硬件接口层负责物理连接,驱动层负责数据搬运,解析层负责提取有效信息,而时间戳同步贯穿其中。这四个环节环环相扣,任何一个出问题,点云数据就没法用。
总结一下:驱动开发要分层解耦,数据解析要用状态机,时间戳同步优先选硬件方案。这三板斧砍下去,点云数据采集这块基本就稳了。
好了,这一章就到这里。下一章咱们聊聊点云数据的预处理——说白了就是怎么把原始数据“洗干净”,让飞控用起来更顺手。