ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F4实现Livox雷达PPS硬件时间同步方案详解

STM32F4实现Livox雷达PPS硬件时间同步方案详解 做室外激光雷达建图最烦的不是标定而是时间戳。我最初用Livox Avia跑Fast-LIVO车一动起来点云就有拖尾转弯稍微急一点地图边缘直接“糊”掉。查了一圈问题出在雷达点云时间戳用的是内部上电计时和IMU、相机根本不在一个时间轴上。后来用了一块STM32F4把GPS的PPS秒脉冲转成Livox能识别的硬件同步信号同时把整秒UTC时间写进雷达拖尾和重影基本消失里程计也稳下来了。这篇文章把整套方案从原理到代码再到踩坑记录完整写出来给正在做雷达同步、跑多传感器融合的同学一个可以直接上手的参考。这套方案的核心思路是用STM32F4的通用定时器TIM2/TIM3做PPS信号的捕获和转发GPS接收机提供PPS秒脉冲和NMEA时间消息Livox雷达接收PPS后把内部时钟对齐到UTC整秒从而让每个激光点的出厂时间戳带上全局时间基准。整个过程不依赖高级定时器也不需要额外采购专用的时间同步板卡硬件成本可以压得很低。1. 为什么Livox要做PPS硬件同步一场由时间戳引发的“血案”1.1 Livox非重复扫描机制下时间戳误差为什么这么致命Livox系列雷达用的是非重复扫描方式光路径是类似玫瑰线的图案每一帧扫描路径都不完全一样。这种方式的好处是时间越长点云覆盖越密但代价是雷达必须给每个点都打上精确的采集时间运动补偿和去畸变都要靠这个时间戳。问题就出在这里如果雷达时间是上电后的内部累计时间没有和外部时钟对齐那么雷达时间轴和IMU时间轴之间的偏差就是随机的。Fast-LIVO这类紧耦合系统里一个点云点的时间错了哪怕几十毫秒投影到IMU坐标系里的位姿就差了一大截。我实测在车速30km/h时50ms的时间误差对应约0.4m的位置误差地图直接废掉。1.2 PPS秒脉冲和UTC时间是怎么配合工作的PPS全称Pulse Per Second每秒输出一个脉冲上升沿严格对齐UTC整秒时刻。GPS接收机通过串口输出NMEA语句比如$GPRMC里面包含当前日期时间和定位状态PPS脉冲则负责“精对秒”。打个比方GPS消息告诉你“现在是12点00分00秒”PPS就是那个“滴”的整点报时。消息负责校准几点几分脉冲负责校准那个精确到微秒的跳变沿。两者配合才能让雷达知道“此刻是哪个整秒”否则只有PPS没有UTC消息雷达只知道秒边界不知道几点几分。Livox的PPS同步模式正是这套逻辑雷达接收外部PPS秒脉冲同时通过SDK或串口拿到UTC时间在PPS上升沿把内部时钟对齐到整秒之后每个点云点就可以打上基于UTC的高精度时间戳。1.3 不做硬件同步靠软件补时间戳行不行Livox SDK也提供了软件方式获取雷达时间甚至可以把雷达时间戳转换成主机时间。但软件方案受限于网络/串口传输延时、调度抖动误差通常在几十毫秒到几百毫秒之间。对于静止场景或者低速移动影响不大但一旦车辆快速运动、平台旋转运动畸变会非常明显。硬件同步的优势在于时间基准在物理层面直接对齐。雷达在PPS上升沿把内部时钟拉回整秒之后依靠本地高稳晶振维持误差只取决于晶振精度和温漂。实测下来同步完成后点云时间戳相对于GPS时基的误差能控制在微秒级别和IMU时间戳对齐后整体延迟抖动大幅下降。2. STM32F4定时器选型与时钟树分析方案设计的关键一步2.1 为什么选通用定时器而不是高级定时器或SysTickSTM32F4的定时器资源看着很多但真正适合做PPS捕获和转发的其实就那几类。TIM1和TIM8是高级定时器带刹车输入、互补输出、重复计数等一堆复杂功能对PPS同步来说纯属冗余SysTick虽然用起来简单但容易被RTOS和中断抢占时间精度不稳定。通用定时器TIM2-TIM5是最合适的选择。TIM2和TIM5是32位计数器TIM3和TIM4是16位计数器。32位定时器在低预分频下不容易溢出做长周期时间累计非常关键。我在这个项目里用TIM2做PPS输入捕获用TIM3输出PPS转发信号两个定时器配合一个负责“收”PPS一个负责“发”PPS。这里有个常见误区很多人以为通用定时器只能做简单的定时中断实际上通用定时器在输入捕获、PWM输出、编码器接口方面都非常灵活完全够做高精度时间同步。Livox雷达的PPS输入信号本质上就是一个1Hz的方波边沿用定时器输入捕获是最直接的方案。2.2 PPS捕获与转发的整体方案架构整体方案是这样的GPS接收机输出PPS秒脉冲和NMEA串口消息。PPS接到STM32F4的PA0引脚PA0复用为TIM2_CH1输入捕获通道GPS串口输出接到USART1解析出UTC时间。STM32F4的TIM3输出PPS信号给Livox雷达。为什么不在雷达端直接接GPS的PPS两个原因一是有些GPS模块的PPS电平是5V或者开漏输出和Livox雷达PPS输入电平不匹配中间需要转换二是STM32F4在中间可以做“信号整形缓冲”顺便把时间信息通过串口或SDK同步给雷达。用TIM3输出PPS其实就是用PWM模式产生一个1Hz、占空比约10%的脉冲上升沿和TIM2捕获到的PPS上升沿对齐。这种“捕获-转发”架构的好处是STM32F4既是PPS的中继器同时也是整个系统的时间基准维护者。后续如果要多雷达同步、或者给IMU提供硬触发信号都可以在STM32F4上扩展。2.3 定时器时钟树的坑APB1分频和2倍频陷阱STM32F4的定时器时钟源有个特别容易踩的坑当APB1预分频系数大于1时挂载在APB1总线上的定时器时钟是APB1时钟的2倍而不是直接等于APB1时钟。具体到STM32F407这样的芯片系统主频168MHz时默认APB1分频系数为4所以APB1外设时钟为42MHz但挂载在APB1上的TIM2-TIM7、TIM12-TIM14的时钟是42MHz×284MHz。很多人配定时器的时候直接把预分频设为42-1以为得到了1MHz计数频率实际算出来是2MHz时间全乱套。在配置PPS捕获之前务必先用RCC_GetClocksFreq()把当前定时器时钟打印出来确认一下。代码里预设PSC83就是为了在84MHz时钟下得到1MHz计数频率这样每个计数周期正好是1微秒后面做时间戳换算就非常方便。3. 核心实现PPS捕获、时间基准计算与Livox SDK对接3.1 硬件连接与引脚分配我用的硬件是常见的STM32F407最小系统板加一个NEO-M8N GPS模块。接线表如下信号GPS模块STM32F4备注PPS输出PPS引脚PA0TIM2_CH13.3V电平如果GPS输出5V需分压TXDTXPA9USART1_RXNMEA串口数据RXDRXPA10USART1_TX可选用于配置GPSGNDGNDGND必须共地PPS转发TIM3_CH1PA6Livox雷达PPS_IN电平需确认雷达兼容性NEO-M8N的PPS引脚默认是3.3V CMOS输出直接接STM32F4没问题。但有些GPS模块比如某些工业级板卡的PPS是5V或RS232电平一定要先看手册确认否则容易烧引脚。Livox雷达的PPS输入引脚不同型号定义不同。Avia的GPS接口在连接器里需要查对应型号的用户手册确认引脚定义。我见过有同学把雷达的PPS和GND接反直接把同步接口烧了所以接线前务必核对资料。电平方面多数Livox雷达PPS输入兼容3.3V但保险起见最好用示波器先量一下STM32F4输出的波形。3.2 定时器输入捕获配置与代码分析TIM2配置为输入捕获模式捕获PPS上升沿。代码如下void TIM2_PPS_Capture_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; TIM_ICInitTypeDef TIM_ICInitStruct {0}; __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_PULLDOWN; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF1_TIM2; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); htim2.Instance TIM2; htim2.Init.Prescaler 84 - 1; // 84MHz / 84 1MHz计数周期1us htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFFFFFF; // 32位最大计数 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(htim2); TIM_ICInitStruct.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; TIM_ICInitStruct.ICSelection TIM_ICSELECTION_DIRECTTI; TIM_ICInitStruct.ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStruct.ICFilter 0x0F; HAL_TIM_IC_ConfigChannel(htim2, TIM_ICInitStruct, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); }输入滤波设为0x0F是为了滤掉PPS信号上的毛刺。PPS边沿抖动一般在纳秒级但长距离走线可能引入干扰加上滤波更稳。注意输入捕获的触发边沿一定要和GPS模块输出的极性匹配大多数GPS模块PPS输出是高电平脉冲上升沿对应整秒时刻但也有少数模块默认输出低电平脉冲那就得改成下降沿触发。捕获回调函数里除了记录计数器的值还要检查PPS周期是否在正常范围。PSC83时计数频率1MHz相邻两个PPS上升沿之间的计数值应该在100万个左右。如果偏差太大说明信号可能丢失或者混入了噪声这种情况下不能更新基准时间。void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { uint32_t capture HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_1); static uint32_t last_capture 0; uint32_t delta capture - last_capture; last_capture capture; if (delta 999000 delta 1001000) { pps_anchor.timer_ticks capture; pps_anchor.valid 1; } else { pps_delta_invalid_cnt; } } }为什么要把PPS周期检查做这么严因为GPS模块在冷启动、弱信号环境下可能输出不稳定的PPS甚至短暂丢失。如果这个时刻更新了时间基准雷达时间戳就会跳变建图效果比不同步还差。3.3 时间基准的计算与溢出处理有了PPS锚点之后任意时刻的全局时间戳可以这样计算uint64_t GetGlobalTimeUs(void) { uint32_t now __HAL_TIM_GET_COUNTER(htim2); uint32_t delta now - pps_anchor.timer_ticks; return pps_anchor.gps_utc_us delta; }这里pps_anchor.gps_utc_us是上一个PPS整秒对应的UTC微秒时间从GPRMC语句解析出来后更新。delta是当前计数器值相对于PPS锚点的偏移单位是微秒因为计数频率1MHz。这个公式的关键在于TIM2是32位计数器溢出周期约71.5分钟但在增量计算中无符号整数减法会自动处理溢出回绕所以只要保证两次读取间隔小于2^32微秒计算就是正确的。真正需要处理的是gps_utc_us的维护。这个值在GPRMC有效定位后在下一个PPS上升沿被写入。伪代码如下// 在解析到有效的$GPRMC后 if (gprmc_valid pps_anchor.valid) { // 当前PPS上升沿对应下一整秒所以gps_utc_us 解析时间 1秒 pps_anchor.gps_utc_us GprmcToUtcUs(gprmc) 1000000ULL; }这里有个细节容易搞反GPRMC语句里的时间是这条语句发出时刻所在的整秒而串口解析完成可能已经过了几十毫秒对应的PPS上升沿是下一个整秒所以要加1秒。实际项目中我是在捕获中断里设置一个标志位在主循环里判断标志位后再用最新的GPRMC时间去更新gps_utc_us这样不会在中断里做耗时的解析操作。3.4 用TIM3输出PPS信号给Livox雷达PPS转发用TIM3的PWM输出模式产生1Hz脉冲。TIM3是16位计数器直接输出1Hz的PWM需要很大的预分频不过PWM输出本来就不需要太高的计数精度关键是对齐上升沿。我的做法是TIM2捕获到PPS上升沿后在中断里直接修改TIM3的ARR和CCR让TIM3的PWM输出上升沿和TIM2捕获上升沿保持一致。代码核心部分void TIM3_PPS_Output_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; TIM_OC_InitTypeDef sConfigOC {0}; __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF2_TIM3; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); htim3.Instance TIM3; htim3.Init.Prescaler 8400 - 1; // 84MHz/8400 10kHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 9999; // 10kHz/10000 1Hz htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1000; // 占空比10% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); }在TIM2捕获中断中同步TIM3计数器和输出让上升沿对齐。3.5 GPRMC消息解析要点NMEA消息解析是老生常谈但有几个坑值得单列出来。第一是校验和。$GPRMC语句末尾有*XX形式的异或校验和不校验就解析的话串口偶发误码会导致时间跳变。第二是定位状态位。$GPRMC的消息格式里有个字段A代表有效定位V代表无效。GPS没有锁星时PPS信号可能不稳定GPRMC里的时间也不可信必须把这个字段作为更新UTC时间的前置条件。第三是UTC时间转Unix时间戳。GPRMC里的日期是ddmmyy格式时间为hhmmss.sss。需要转换成UTC秒数然后换算成微秒。注意这里一定要用UTC时间不要加时区偏移否则雷达内部UTC时钟会差8小时虽然PPS同步不受影响但在分析点云和GPS时间戳时会出现整小时级别的偏移。3.6 与Livox SDK的对接设置PPS同步模式硬件信号接好之后还需要通过Livox SDK告诉雷达进入PPS同步模式。在Livox SDK 2.x中调用同步模式设置接口将同步模式设为PPSkPpsSync。livox::LidarSyncMode sync_mode livox::LidarSyncMode::kPpsSync; livox_lidar_set_sync_mode(handle, sync_mode);设置成功后就等雷达上报同步状态。可以通过SDK回调读取雷达状态确认时间同步是否完成。实测中PPS信号接入正常的情况下雷达通常在上电后几秒内完成同步。还有一点雷达获取UTC时间的方式除了SDK下发部分型号也支持通过串口输入GPRMC消息。具体看雷达型号支持哪种方式。我在Avia上是用SDK下发UTC时间因为SDK已经封装好了不用另接线缆。4. 实操踩坑记录同步失效和精度劣化的排查实践4.1 坑一定时器时钟算错时间戳每秒偏快做完第一版程序发现雷达点云时间戳和GPS时间对比每秒快了大概190微秒。用示波器量PPS波形对得上问题就出在定时器计数频率不对。后来查代码发现我在RCC配置里把PCLK1当成了定时器时钟源按42MHz配置PSC42-1实际定时器时钟是84MHz所以计数频率是2MHz。把PSC改成84-1之后偏差消失。这个坑很多人都会踩建议在初始化完成后把定时器时钟频率打印出来核对。4.2 坑二中断响应抖动导致PPS锚点抖动TIM2捕获中断里除了读取CCR我还顺手做了一些任务调度相关的操作结果发现PPS锚点时间戳抖动达到±50微秒。原因是中断里做了太多耗时操作导致读取CNT值的时间波动。解决办法是把中断函数精简到极致只做捕获值读取、PPS周期检查和标志位置位其他全部移到主循环处理。同时把TIM2中断优先级设为2比串口和定时器其他中断都高。优化后抖动降到±1微秒以内。4.3 坑三Livox雷达同步状态一直未锁定PPS信号正常GPRMC解析也正常但SDK上报雷达同步状态一直是未同步。排查思路先检查PPS信号的波形驱动能力。STM32F4的GPIO带载能力强但长线传输容易衰减Livox雷达PPS输入端如果阻抗匹配不好边沿会变缓。我在雷达端并联了一个10k上拉边沿改善不少。再检查UTC时间是否已经下发。只给PPS不给UTC时间雷达虽然能检测到PPS但无法建立绝对时间基准。要确保SDK的UTC时间设置在上电初始化阶段就完成。最后检查PPS信号和UTC时间是否“配对”。如果PPS上升沿对应的整秒时刻和SDK下发的UTC时间不一致雷达会判定时间异常。这里我是用示波器同时量PPS上升沿和串口输出确认时间对齐后重新下发了一次UTC时间才正常。4.4 坑四GPS冷启动时PPS输出不稳定GPS模块在冷启动搜索卫星阶段PPS输出时有时无时间基准完全不可信。如果这时候把PPS锚点更新了后果就是时间戳整体偏移。我的做法是加入“锁定超时”逻辑连续5个PPS周期都在1s±0.1%范围内才认为GPS PPS稳定此时才允许更新锚点。一旦中间出现周期异常计数器清零重来。故障现象排查方向解决方法PPS信号无输出检查GPS模块供电和锁星状态等待GPS锁定检查天线定时器计数频率不对确认APB1分频系数和定时器时钟关系用RCC_GetClocksFreq打印验证雷达同步灯不亮检查PPS电平和雷达接口定义用示波器实测查雷达手册时间戳周期性跳变检查是否偶发无效PPS被纳入锚点增加PPS周期合法性判断UTC时间差8小时检查时区设置统一使用UTC时间4.5 精度验证方法同步做完后要验证精度。我的方法是把STM32F4的某个空闲定时器管脚输出一个固定间隔的方波比如10kHz和PPS做时间戳对比更直接的做法是用示波器的PPS通道做触发观察Livox SDK输出点云时间戳的秒级跳变。实测数据在GPS锁定稳定后点云时间戳的秒级对齐误差在几十微秒以内秒内时间戳线性度良好。对比未做硬件同步时几十甚至几百毫秒的偏差效果差距非常明显。需要说明的是这个精度很大程度取决于雷达本地晶振质量STM32F4只是提供了稳定的秒级校准源秒内漂移仍然由雷达晶振决定。5. 多雷达扩展与后续演进从单一PPS同步到系统级时间基准5.1 多雷达同步的两种实现思路如果一套系统里装了两台Livox雷达每台都独立接一个GPS接收机也可以但成本高且两台雷达的PPS上升沿之间还有微小相位差。更稳妥的做法是用同一个PPS源广播给多台雷达。在STM32F4上实现方案有两种。一是用TIM3输出PPS直接并联给多台雷达的PPS输入管脚前提是STM32F4的GPIO驱动能力足够或者加一个缓冲器二是用另一个通用定时器再输出一路PPS虽然相位可能需要微调但软件上容易独立控制。我在双雷达项目中用了一路PPS并联方案雷达端各加10k上拉实测两台雷达的秒级时间戳只差几十微秒完全满足需求。5.2 与Fast-LIVO等系统的集成硬件同步做完之后还需要在Fast-LIVO里正确配置让算法读取雷达基于UTC的时间戳。在fast-livo的launch文件或配置文件里把雷达时间戳模式设为PPS同步同时确保IMU时间戳和系统时间对齐。我遇到过一个问题雷达时间戳已经用UTC校准了但IMU时间戳还是系统启动后的相对时间两者差了几个小时的启动偏移。后来在配置里把IMU的时间戳也转成系统时间并且用STK/回调函数统一参考时钟问题解决。本质上多传感器融合的核心是让所有传感器的时间戳都落到同一个参考时间轴上PPS同步帮雷达解决了这一半剩下的一半是IMU和相机。5.3 这个方案的扩展空间STM32F4在中间做PPS捕获和转发除了同步雷达还能顺便承担系统时间服务器角色。比如输出PPS给IMU的硬触发同步输入输出同步脉冲给相机曝光触发用同一个时基给多个传感器打时间戳。Fast-LIVO这类系统如果能做到“雷达点云、IMU采样、相机曝光”三路时间全部对齐到GPS时基整个系统的鲁棒性和精度会上一个台阶。另外STM32F4本身的串口资源丰富可以用来接收GPS NMEA消息并转发给其他传感器或主控省掉额外的串口分配。如果项目后续升级到RTK定位也可以把GGA消息里的毫秒级位置信息和PPS同步配合让雷达点云带上更高精度的位置标签。这套方案最大的价值在于它把一个看似复杂的时间同步问题拆解成了定时器捕获、NMEA解析、PPS转发三个清晰模块每块都能单独测试验证。我后来在无人机、无人车项目上复用这套逻辑只需要改引脚和电平匹配整体架构基本不用动。
返回列表