自研毫米波雷达感知平台PLFM_RADAR:全链路信号处理与目标跟踪实践
发布时间:2026/10/4 11:06:56 锦皓数字建站

1. 项目整体设计与思路拆解1.1 不绕弯子PLFM_RADAR到底做的是什么PLFM_RADAR是我折腾了几个月的一套雷达感知处理平台。PLFM我按Platform来读RADAR就是雷达本身合在一起就是“平台雷达”的意思。其实这个圈子里还有一层巧合雷达波形里的LFM本来就是线性调频Linear Frequency Modulation的缩写前面加个P既可以理解成Platform也可以理解成Pulse-LFM这类调频脉冲正好和整个项目的调性相符。整套平台做的事情可以这样概括把雷达前端得到的中频采样数据经过距离维FFT、多普勒维FFT、恒虚警检测、测角估计、点云聚类这一条完整链路最终输出稳定的目标轨迹并且提供可视化和数据回放能力。这个项目解决的核心问题很现实毫米波雷达在智能家居、智慧零售、交通感知、机器人导航这些场景里越来越普及但市面上的SDK大多是黑箱你拿到的往往只剩点云结果中间任何一级算法想改都改不了。PLFM_RADAR就是为了把这条链路整个攥在自己手里每一级数据都能单独导出、单独查看、单独调参。适合的读者是对雷达有基本概念、希望深入研究信号处理和目标跟踪、又不想被厂商SDK绑死的那类开发者。1.2 为什么我不沿用厂商SDK而选择自研其实最开始我也不是一上来就要造轮子。TI的毫米波开发板、英飞凌的雷达传感器、还有一些国产模组都带官方SDK和demo。但实际用了几个星期之后我遇到了几个绕不开的问题。第一是中间数据的可观测性太差。SDK给你一串点和目标信息但你要想看某个目标的信号特征是建立在哪些原始chirp上的基本做不到所有过程全被封装在库的黑盒里。第二是定制算法的侵入性太强。比如我想在某个场景里把CFAR的参考窗口改成非对称结构在厂商框架里要改的是驱动底层配置经常牵连到性能优化工具链改动一个参数就要重新烧录重新标定。第三是移植性差。同一套算法逻辑换了另外一个雷达模组代码几乎要重写因为数据格式、帧结构、接口风格完全不一样。所以PLFM_RADAR一开始就定了一个基本原则前端适配层薄薄的中间算法层厚厚的后端输出层标准化的。所谓前端适配薄就是只做数据格式转换和帧结构解析把不同雷达的原始数据统一成内部格式中间算法层是全项目的核心所有信号处理和目标跟踪都放在这里与具体硬件完全无关后端输出层则负责把结果封装成统一的点云、目标列表和原始数据包方便上层应用调用。这个架构到了后期真的省了非常多时间。我先后换过两款雷达模组算法层一行代码都没有动。1.3 三层架构的设计细节与模块边界具体拆开来看平台分成采集适配层、处理核心层、应用输出层三大块。采集适配层里有一个统一的Reader接口每个硬件对应一个实现类输出内部定义好的RadarFrame。处理核心层内部再分两条流水线一条是信号处理子流水线输入RadarFrame输出点云帧另一条是目标跟踪子流水线输入点云帧输出TrackList。应用输出层就简单了把点云和轨迹以JSON、CSV或者实时回调的方式吐出去。模块边界我卡得比较死。每个子模块只依赖前面一个模块的输出不允许跨层调用。比如点云聚类模块绝对不允许反过来读取原始ADC数据跟踪模块也绝对不允许直接碰信号处理参数。这样做的逻辑是一旦发现某个环节有问题可以精确定位问题来源而且每个模块都能单独写单元测试。实测下来信号处理部分的改动频率最高采集和输出层只要写对了基本很少再动边界划分对后期维护极其友好。2. 雷达数据链路与核心处理细节2.1 FMCW雷达的数据流主线毫米波雷达最常见的体制是FMCW也就是调频连续波。它的基本思路是发射频率随时间线性变化的chirp信号遇到目标之后产生回波回波和本振混频得到一个中频信号。这个中频信号的频率和目标距离是线性对应的所以叫FMCW。放到PLFM_RADAR里要处理的数据流就是这一串中频采样值。我内部定义的数据帧结构大概是这样的每帧包含若干个chirp每个chirp又包含若干次ADC采样。用Python表示就是shape为[n_chirps, n_samples_per_chirp]的复数数组实部是I路虚部是Q路。对于MIMO模式每个发射天线对应的数据单独存成这种数组。这个数据结构是整个平台的地基后面所有处理环节都围绕它展开。数据格式统一这件事看起来很简单但真到了对接多家雷达的时候你就会知道它有多重要。如果雷达模组不支持直接吐原始中频数据也没关系。平台内置了一个模拟数据源可以根据你设置的雷达参数生成点目标的中频信号加入指定信噪比的噪声输出格式和真实雷达完全一致。这套模拟源在前期调试帮了大忙可以不依赖硬件随时验证算法等于把硬件依赖和算法开发彻底解耦了。2.2 距离维FFT、多普勒维FFT与CFAR检测拿到一帧原始数据之后第一步做的是距离维FFT。因为中频信号的频率正比于距离做一次FFT就能把不同距离的反射能量区分开。每个chirp做一次FFT得到的是距离维频谱把所有chirp的距离维频谱堆在一起形成一个中间矩阵。接下来对这个矩阵的每个距离单元做第二维FFT也就是多普勒维FFT得到的就是距离-多普勒图业内通常叫RD图。RD图上每个峰值既代表距离又代表速度但并不是每个峰值都是真正目标。旁瓣、地杂波、天线耦合信号都会形成虚假峰这时候就需要恒虚警检测也就是CFAR。我用的是经典的CA-CFAR在距离维和多普勒维都开一个滑窗把窗口内平均能量作为本地噪声估计然后对待检测单元能量比上噪声超过设定倍数就判为目标。参数一般参考单元设12个保护单元4个虚警率定在1e-4量级。CFAR这块最大的坑在于边缘单元的处理。滑窗滑到矩阵边缘时参考单元会越界很多人直接省略结果边缘总是产生大量虚假目标。我最后采用的做法是边缘越界的参考单元不参与平均同时把判决门限稍微调高一点用这种方式补偿统计量减少带来的偏差。这一改边缘虚警率肉眼可见地降了下来整个画面的干净程度明显提升。2.3 测角、点云生成与目标聚类RD图上经过CFAR后留下的每个单元代表一个潜在反射点。接下来要做的是测角。毫米波雷达通常是多天线接收目标到达不同天线会有相位差这个相位差和角度相关。测角最直接的方式是做一次FFT波束成形把所有天线的复值在角度维上做FFT峰值对应位置就是目标角度。有了距离、速度、角度这三个量一个点就出来了。点云数据里包含距离、速度、方位角、信噪比这些属性。到了这一步信号处理流水线基本完工但点云还不能直接当目标使用。一个目标可能反射出好几个点杂波也还有残留。所以我对点云做一次DBSCAN聚类把紧挨着的点合成一个簇簇的中心作为候选目标簇的强度作为目标可信度。DBSCAN的参数要根据雷达分辨率来调。我的经验是聚类半径调到距离分辨率的1.5倍左右比较合适。太小会把一个目标切碎输出就会看到同一个目标被拆成好几段轨迹太大又会把相邻目标粘到一起做到后面你会发现两辆并排的车被当成一个目标。最小点数一般设两个点以上这样单点杂波不容易通过聚类过滤聚出来的候选目标在真实场景里的稳定性明显更高。2.4 参数选型的计算过程不靠拍脑袋很多人问我雷达参数怎么定。其实这个完全可以算出来不用拍脑袋。我用的是一个典型配置载波频率77GHzchirp带宽2GHz单个chirp持续时间100微秒ADC采样率10MHz每帧128个chirp。距离分辨率是c/(2B)也就是3×10^8除以4×10^9得到0.075米意味着能分辨两个目标的最小间距是7.5厘米。最大测距范围由采样率和调制斜率决定算下来约75米对应256个采样点。速度维分辨率取决于帧时长的倒数公式是λ/(2NT)。λ在77GHz下大约是3.9毫米带入N128、T100微秒算出来是0.15米/秒。最大不模糊速度是λ/(4T)算下来大概是9.7米/秒。如果你要在车载场景测高速目标这个配置就不够了需要缩短chirp周期或者降低调制频点。这些参数每一个都直接影响后续处理的动态范围和计算量所以平台里所有FFT尺寸都根据参数配置动态计算而不是在代码里写死。这个习惯在换配置时能省下巨量的修改时间。3. 实操过程PLFM_RADAR模块化实现全记录3.1 开发环境与工程目录规划这个项目我用了Python加NumPy做算法原型C实现两个关键计算模块的加速。Python的好处是改算法、看中间结果都非常快NumPy做FFT和多维切片基本不需要复杂循环。可视化部分用了PyQtGraph它比Matplotlib更适合实时数据刷新一帧几百个点的点云显示完全无压力。整个工程目录是这样划分的。plfm_radar/下面分drivers/、dsp/、tracking/、visualization/、configs/、utils/几个目录。drivers/放雷达模组适配代码和模拟数据源dsp/放信号处理链路每个算法模块一个文件tracking/放数据关联和卡尔曼滤波相关代码visualization/放实时2D点云和轨迹绘制configs/放所有雷达参数和算法参数统一采用YAML格式。这个安排一开始看起来有点过度设计但等到算法换了一版又一版之后我才意识到这种按职责拆目录的做法有多值。3.2 数据采集模块从硬件私有协议到统一结构数据采集层的接口我定义为read_frame()返回一个内部统一的RadarFrame对象。对接某个具体雷达模组时适配代码要做的核心工作就是解析它私有的帧协议。比如某款雷达原始帧里前8字节是版本号和帧号后面是每个chirp的IQ数据交错排列帧尾还夹带温度信息。我就在适配代码里先把无用信息剥掉再重排成[n_chirps, n_samples]复数数组填到标准的RadarFrame结构里面去。这里我犯过一个低级但非常典型的错误帧号字节序解析错了。雷达数据包头用的是小端序我按大端序读出来帧号全部变成很大且不连续的数字起初以为是雷达掉帧排查了半天才发现是字节序问题。所以后来我在适配层加了一个自检函数每接收100帧就检查一次帧号连续性一旦出现乱序就报出来。这个自检在切换硬件时能快速暴露协议解析问题强烈建议你也加上。模拟数据源的实现反而很有意思。它并不是真的在时域上生成完整的FMCW波形而是直接在频域设置目标响应再反变换回时域。设定一个目标在某个距离和速度上就把这个目标对应的中频信号叠加进chirp数据里再加上高斯白噪声。这种方法比直接生成时域波形要快一个数量级用在算法调试阶段非常节省时间而且可以精确控制目标数量和信噪比方便做不同场景的回归测试。3.3 信号处理模块从原始帧到点云的流水线实现信号处理模块是整个平台的重头戏。我把它组织成一个可配置流水线每个环节都是独立函数参数从配置统一读取。流水线第一步是距离维FFT输入是RadarFrame的IQ数组对每行chirp做快速傅里叶变换输出尺寸不变的距离维频谱。第二步是加窗这一步很多人会忽略但窗函数直接决定旁瓣电平。我用的是汉明窗能让距离维主峰更干净代价是主瓣稍微宽一点点。在相同检测门限下主瓣变宽并不致命但旁瓣压低以后虚假检测会少很多。第三步多普勒维FFT是所有处理中最费时间的环节。正常做法是对每个距离单元上的所有chirp做FFT如果写个循环128个距离单元要循环128次。我的做法是把距离维FFT结果转置成[n_samples, n_chirps]然后切片索引完一次性对整个矩阵做FFT这样底层BLAS可以并行处理整块数据。实测下来这一步速度比循环实现快了接近十倍优化效果立竿见影。检测环节是前面提过的CFAR。实现时我先求噪声底数用卷积的方式做滑窗平均然后根据噪声底数和门限系数生成检测掩码再把掩码中的连续区域合并。连续区域合并用的是八邻域连通域标记算法这样能避免同一个目标在RD图上产生多个重叠峰值。处理完这一步之后每个检测单元对应了距离、多普勒偏移量以及所在天线通道的幅值相位测角只差最后一步。3.4 轨迹跟踪模块关联、滤波与生命周期管理点云聚合成候选目标之后还不能直接当结果用。实际场景里目标可能暂时被遮挡点云也可能间歇性丢失如果每一帧都只输出当前检测而不过跨帧关联那输出数据就会出现闪烁和跳变。所以我写了一个轻量化的跟踪模块对每个候选目标做最近邻帧间关联给每条轨迹配一个卡尔曼滤波器对目标的位置和速度做平滑预测。跟踪生命周期这件事做过的人都知道有多繁琐。我维护一个轨迹列表每条轨迹自带状态未确认、已确认、已消亡。第一次出现的候选目标先放入未确认池连续两帧都能关联上才升级为已确认轨迹已确认轨迹如果连续丢失超过五帧就标记为消亡并从列表里移除。这个机制看似简单却能过滤掉大量临时杂波让整个界面上输出的目标数量稳定下来不会出现一帧冒出几十个目标、下一帧又全部消失的闹心画面。卡尔曼滤波我选用了最基础的常速度模型状态量是位置和速度观测量是位置。这里最关键的参数是过程噪声协方差矩阵它决定滤波器对目标机动的敏感程度。我一开始把过程噪声调得很小结果在室外测试时遇到人突然转弯轨迹拖尾非常明显要将近一秒才跟上去。后来把过程噪声系数提高了几个数量级跟踪响应才变得跟手。所以调这组参数时别怕数值大机动场景下的舒适区间往往超预期。3.5 可视化回放与数据落盘可视化部分我用PyQtGraph搭建了二维俯视图界面。左边显示当前帧点云每个点按照信噪比着色右边显示目标轨迹轨迹历史位置连成线当前速度用箭头表示。界面下方实时显示帧率、检测点数、目标数。这套界面虽然简单但调试算法时真的管用。某个角度方向突然出现批量假目标在画面上看非常显眼马上就能定位到大概率是CFAR门限或者DBSCAN参数出了问题。数据落盘我用了两份文件原始IQ数据的二进制流和检测结果CSV。原始IQ数据量非常大一帧可能几百KB连续录半小时会占用很多硬盘空间所以我加了zstd压缩。回放时逐帧解压按照原始帧结构恢复基本上能做到逐帧不丢。这个能力在后期离线复现bug时价值极高。很多现场问题在界面上只有几秒钟表现靠录制好的原始数据回去慢慢折腾比在现场反复试错效率高太多了。4. 常见问题、性能优化与排查实录4.1 零距离副峰问题DC偏置带来的假目标最开始跑真实雷达时我发现RD图的零距离位置始终存在一个很亮的峰而且在零速度附近也出现一片高能量区域。这是典型的DC偏置问题。雷达前端模拟电路在混频后会引入直流偏置如果不处理它在距离维FFT里表现为零频峰再经过多普勒FFT就变成零距离零速度副峰还会因为频谱泄漏污染邻近的距离单元。解决方法是每个chirp先减去均值也就是DC平衡而且必须放在距离维FFT之前顺序不能反。做完均值相减之后零距离副峰下降了将近20dB整个RD图背景干净了很多。如果做完之后还有残留可以在距离维FFT之后避开靠近零频的几个距离单元但工程上还是优先做时域减均值从源头把问题解决掉。4.2 速度模糊目标超出最大不模糊速度怎么办有一阵子我在测行人快速跑过雷达的场景发现目标速度显示为负值方向直接反了。这就是速度模糊。当目标速度超过λ/(4T)时多普勒频率发生混叠速度估计会被折叠到负方向。要解决它可以从两个方向入手。最直接的是缩短chirp周期T增大最大不模糊速度但这样做的代价是同样帧长下的最大测距距离也会受影响。另一个方向是发射不同调制斜率的chirp组成两套配置利用两组测量值在模糊速度数列上的交点来判断真实速度这就是多普勒解模糊。我在平台里把第二种方案做成了可选模块。发射两种不同调制斜率的chirp组对每个候选检测点分别计算模糊速度再搜索一致的速度组合来做解模糊。效果基本可靠但计算量增加不少而且对信噪比要求更高。目前它只是默认关闭的实验性功能实际应用主要还是通过调整chirp周期来控制速度范围这也是雷达工程里最稳妥的做法。4.3 天线相位不一致测角偏差的隐患与校准测角原理上靠天线阵元间的相位差但硬件上天线之间的馈线长度、封装差异会造成固定相位偏差。如果不校准测角结果会出现系统性偏移正前方的目标测出来可能偏了好几度。近距离下这只是几厘米的误差到了远距离场景就变成半米甚至一米的位置偏差这对目标跟踪来说是致命伤。我在平台里做了一套简易校准流程放一个已知位置的角反射器在雷达正前方和正侧方各采集一段数据计算各天线相对于参考天线的相位差保存到校准文件。运行时把校准相位补偿进每个天线的复数据中。做完这套校准之后测角均方根误差从几度降到了1度以内。整个校准过程不需要暗室一个开阔场地和一个反射器就能完成效果已经很够用。4.4 性能优化让单帧处理从40毫秒降到8毫秒实时处理最怕卡顿我一开始也卡。用profiler把整套DSP链路跑了一遍发现时间主要消耗在两个方面多普勒维FFT的循环调用以及CFAR的逐单元计算。多普勒维FFT用矩阵化解决把数组从[n_chirps, n_samples]转成[n_samples, n_chirps]之后整体做FFT数据量不变但底层库能把计算并行起来这一步立竿见影。CFAR优化用到了图像处理里的卷积思路。先把RD矩阵的模方做一次滑窗平均等价于用均匀核做卷积然后在这个平滑后的模方图上做除法和阈值比较。CFAR的时间复杂度从逐单元串行计算变成矩阵运算又白赚一波向量化加速。实测下来一帧128×256的RD矩阵从约40毫秒压缩到8毫秒左右基本满足35帧/秒以上的处理速度。如果你还要接着压可以考虑用等尺寸的实FFT代替复数FFT以及在保证性能的前提下减小CFAR参考窗尺寸。4.5 常见问题速查表现象可能原因排查方向零距离始终有亮峰DC偏置未消除检查是否做了时域减均值确认顺序是否在FFT前目标速度方向反了速度模糊增大不模糊速度或开启多普勒解模糊目标角度系统性偏移天线相位不一致做一次角反射器校准并保存校准文件边缘距离大量假点CFAR边缘单元处理不当检查边缘参考单元处理方式和门限补偿一帧处理耗时过高FFT或CFAR循环未向量化改成矩阵运算用卷积法替代逐单元滑窗轨迹闪烁频繁关联门限过紧或帧率不足扩大关联窗口、增大过程噪声系数内存持续增长可视化或历史数据未释放限制缓存帧数历史数据交落盘模块管理5. 实测验证与经验心得5.1 室内长廊实测场景搭建与数据采集平台功能稳定之后我在一条6米长的室内长廊做了实测。场景很简单一端放毫米波雷达目标是一辆手推车和一个人测试点主要有两个目标被遮挡时轨迹能否继续保持多个目标同时在视野中时点云能否稳定区分。雷达参数就用了前面提到的FMCW配置帧率设为25帧/秒。实测数据回放了几个小时整体表现符合预期。单人走动的全程轨迹连贯没有出现明显跳变手推车静止时点云集中在一个小区域里坐标抖动在厘米级。比较意外的是金属推车表面存在镜面反射偶尔会让目标点跳到侧面另一个反射点这个现象靠DBSCAN聚类压制了一部分但没有彻底消除属于雷达物理特性的客观限制。5.2 长时间稳定性与资源占用长时间跑下来最需要注意的其实是内存管理。Python侧因为多帧数据没有及时释放曾经出现过内存缓慢上涨的情况。排查后定位到是可视化模块保留了全部历史点云无限制增长。修正方案是只保留最近200帧点云用于绘制历史数据全部交给落盘模块输出不驻留内存。改完以后连续跑了12小时内存占用稳定在1.2GB以内没有再出现漂移。处理时延方面整条链路单帧平均耗时约20毫秒包含采集、DSP、跟踪、可视化四个环节。折算下来帧率可以跑到接近50帧/秒留出了不少余量。这个时延水平对机器人导航、安防监控这类低延迟感知场景是完全够用的。如果后面还想继续压时延可以考虑把信号处理核心挪到C侧并预先分配好所有中间数组来避免重复申请内存。5.3 踩坑之后的独家心得回头看看最值得写下来的经验有三条。第一条是处理流程一定要做成可配置流水线不要把所有算法堆在一个大文件里。刚开始我也把所有代码写在一起参数一多改起来就是灾难改一个门限要在五六个函数里同步修改漏改一处就是难以发现的隐性bug。后来拆成独立模块、用YAML统一驱动之后调参效率至少翻了三倍。第二条是原始数据落盘能力要早期就做。很多问题不是当场能发现的保存下原始IQ数据回去慢慢复盘远比在调试界面上反复试要高效。我后来好几个疑难杂症都是通过回放录制的数据定位出来的包括天线相位漂移和DC偏置的问题。如果没有回放能力这些问题在现场可能要把你折磨一整天。第三条是模拟数据源一定要尽早接入。我没有硬件时就用模拟源把整条链路跑通后面接入真实雷达时只需要换一个Reader实现类。这种做法把硬件依赖和算法开发彻底分开等于把整个项目的调试周期缩短了一大截。如果你也在做类似的雷达感知平台建议按这个节奏走不要抱着真实雷达从零开始调。5.4 后续还能怎么扩展最后分享一下后续的扩展方向。当前版本的角度维处理还是直接的FFT波束成形下一步可以加入MIMO虚拟孔径和超分辨测角算法把横向分辨能力提上去。跟踪这块也可以从最近邻关联换成多目标联合关联减少密集场景下的关联冲突。平台对外接口目前是JSON和CSV我计划再出一版基于共享内存的零拷贝接口给上层C应用用进一步压缩端到端时延。这些方向我都已经在逐个验证了等有了阶段性的结果和数据我还会把新的踩坑过程和优化心得整理出来继续聊更细的实现细节。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。