拿到一块毫米波雷达,打开SDK里的大段代码,很多人第一反应是懵的:明明只看到“ADC原始数据”几个字,怎么最终产品里就冒出来一堆带距离、速度、角度的目标列表?我当初从通信转过来啃雷达感知链路时,最大的障碍就是没人把整条流水线从头到尾串一遍。这篇东西就是干这个的:从毫米波雷达的ADC采样点开始,一路经过距离维FFT、多普勒维FFT、CFAR检测、角度估计、点云聚类和目标跟踪,把“原始IQ复数流”变成“一串带坐标和速度的目标列表”。内容不挑具体芯片方案,TI的IWR、NXP的S32R、国产的加特兰、岸达等,核心骨架都一样,理解清楚后换个平台只是改寄存器配置的事,真正的工作量全在每一级的数据组织和参数权衡上。
1. 链路源头:先把ADC数据长什么样彻底搞清楚
1.1 中频信号到ADC码值,距离信息藏在频率里
毫米波雷达前端发射的是线性调频连续波(FMCW),一个chirp的频率随时间线性上升。回波和本地发射信号混频后,得到的中频信号频率正比于目标时延,而时延正比于距离。这是整个雷达感知的物理基础,也是最容易被忽略的一步——如果你不理解中频频率,后面所有FFT结果都只是“看着像那么回事的谱线”。
假设调频斜率是S(单位Hz/s),目标距离是R,那么中频频率:
f_IF = S * 2R / c
这里2R是电磁波往返总路程,c是光速。S通常可以自己算:例如chirp带宽B=4GHz,chirp时长T_c=80us,那么S=B/T_c=4GHz/80us=50MHz/us。如果目标在60米处,f_IF = 50MHz/us * 2*60m / 3e8m/s,换算一下约20MHz。这个频率直接落在ADC的采样带宽内,ADC按奈奎斯特准则采样,就把它变成了数字中频信号。
所以ADC采样率的下限由你要探测的最大距离决定。假设最大距离R_max=200m,那么中频频率上限是 f_IF_max = S*2R_max/c。按照刚才的S代入,约66.7MHz。也就是说ADC采样率至少要到133MHz才够覆盖200米全量程。但工程上没人会用这么高的采样率,因为数据量爆炸、功耗也压不住。实际雷达通常会缩短chirp时长、降低斜率,或者说用中频滤波器先把远处噪声滤掉,再用20MSPS、25MSPS这种采样率覆盖40到50米的近中距场景。这里的选择永远是“最大距离”和“ADC带宽/数据量”之间做交易。
ADC输出的不是普通实数序列,而是I/Q两路复数。两路并行采样,一路余弦一路正弦,合成后得到一个复指数信号。后续FFT之所以能区分正负频率、能检测出目标的方向速度,全靠这一组复数。你拿到的数据手册里的“Complex 1x/2x”说的就是这件事,ADC原始数据一定是两路的,千万别当成单通道实数流读。
1.2 一帧数据有多少,排列方式和搬运是第一个坎
这是很多人第一次跑通采集程序之后最崩溃的地方。你以为拿到的是“一整块雷达数据”,其实它是按chirp和frame组织的多维立方体。一个典型的帧数据排列大致是:
[num_tx_receivers, num_chirps_per_frame, num_samples_per_chirp]
比如一个1T4R的雷达,每帧128个chirp,每个chirp采样256个点,那么单帧读取的复数样本数是:4 * 128 * 256 = 131072个复数点。如果I/Q各16bit,单帧就是 131072 * 2 * 2B = 512KB。一帧时间如果10ms,那么持续数据率约50MB/s。
这个量级对现代DSP/FPGA来说不算高,但对于早期用串口抓数据的调试方式就非常难受。我见过不少同学把毫米波雷达的ADC原始数据通过UART往PC传,一帧传完下一帧早丢了。正确做法是让ADC数据走DMA直接灌进DDR里,或者用FPGA高速采集后通过USB3.0/PCIe搬运。热词里经常出现的“FPGA高速ADC采样”,本质就是在采样率上万兆和搬运带宽之间做平衡,往往还得在外围加FIFO缓冲。
数据组织还有个容易出错的点:ADC流到底是“采样点维最快(sample-major)”还是“chirp维最快”。不同厂商的采集库排列方向不一样,TI的DCA1000输出和某些国产雷达的采集板可能正好相反。拿到数据后别急着FFT,先打印一下数据shape,甚至先检查有没有明显的相位连续性。否则后面所有轴全反,结果就是一个歪的图谱。
2. 距离维FFT:把频率谱峰翻译成距离峰
2.1 为什么不是直接对ADC波形找峰
你可能会想:中频频率正比于距离,那直接对ADC序列做频谱分析不就行了吗?确实就是这么做,但为什么非得“一个chirp做一个FFT”,而不是对整段数据做一个超大FFT?这里的关键是chirp之间的时间间隔中,目标位置和速度导致的相位变化是我们后面测速的依据。如果把所有chirp混在一起做FFT,距离和速度的信息就纠缠在一起了,解不回来。
所以标准流程是:对每个chirp的Ns个采样点独立做一次Ns点FFT,得到的就是距离维频谱,每个峰对应一个目标的距离。这个过程在雷达SDK里通常叫Range FFT。做完之后,数据形状从[num_rx, num_chirps, num_samples]会变成[num_rx, num_chirps, num_range_bins]。
距离分辨力由chirp带宽决定:ΔR = c/(2B)。如果B=4GHz,那理论分辨力就是3.75厘米;如果B=250MHz,就是0.6米,基本只能把人识别成一个热源点。要注意区分“分辨力”和“bin宽度”。bin宽度是频谱上每根谱线对应的距离间隔,固定等于 c/(2ST_s*num_samples),也可以写成 c/(2B_total_per_chirp)。分辨力是“两个等幅目标能区分开的最近距离”,加窗之后主瓣展宽,实际分辨力通常是bin宽度的1.5到2倍。
实操时我习惯先把Range FFT之后的幅度谱画成“距离-时间”热力图,第一眼就能发现静态强目标在哪。楼梯、墙角、金属栏杆产生的强反射通常出现在固定距离线上,这是后面做多普勒维处理要抑制的杂波。这个可视化步骤非常值得做,调试进度至少快一倍。
2.2 窗函数、去直流和零填充的真实经验
Range FFT前要不要加窗?这是个老问题。FFT默认矩形窗,主瓣窄但旁瓣高,第一个旁瓣约-13dB。调频雷达里同一个场景往往有强目标(例如金属杆)和弱目标(例如行人)同时存在,强目标的旁瓣会直接淹没弱目标的信号。所以工程上几乎都会加窗,Hann、Hamming、Blackman-Harris都是常见选择。Hamming窗旁瓣约-43dB,主瓣宽度是矩形窗的两倍;Blackman-Harris旁瓣更低,但主瓣更宽。雷达测距里一般推荐Hamming起手,如果发现远距弱目标检测不住,再往Blackman方向试。
除了加窗,还有一个必须处理的是直流偏置。ADC本身会有直流失调,收发链路也会有些耦合泄漏,在零频附近形成一个巨大的峰值。这个峰值会污染前几个距离bin,而且旁瓣扫描整个距离谱。最简单的办法是在做FFT前,把同一通道所有距离采样点的平均值减掉,或者在距离谱里把0号和低几个距离bin直接置零。减平均的做法的代价是会把近距离慢速目标的信号也稍微动一动,但通常能接受。
零填充(zero-padding)是在做FFT前把Ns点数据补零到更大的点数,比如从256点补到1024点,再进行FFT。它并不会提高真实分辨力,但会把谱线密度变密,让峰位置估算更平滑,对后续CFAR取峰值和参数提取有好处。我一般会在后续需要精确峰值提取时用,调试阶段反而尽量少用,因为补零会让数据量翻几倍,实时链路里代价不小。
3. 多普勒维FFT:把静止目标和动目标分开
3.1 慢时间FFT是怎么回事
做完距离维FFT,数据里每个range bin的复数幅值会随着chirp的推进而变化。如果该range bin里有一个运动目标,那么它的相位会逐chirp线性变化,变化速率正比于目标的径向速度。对同一range bin、连续Ns个chirp上的复数序列再做一次FFT,峰值的位置就对应目标的多普勒频率。这一步因为是在chirp与chirp之间(慢时间)做的,所以叫慢时间FFT,也叫Doppler FFT。
经过这步后,数据从 [num_rx, num_chirps, range_bins] 变成 [num_rx, doppler_bins, range_bins]。这就是经典的二维距离-多普勒图(RD图)。RD图上会出现一个个亮点,每个亮点在距离轴上对应目标的距离,在多普勒轴上对应目标的径向速度。
速度换算公式也不复杂,多普勒频率f_D = 2v/λ,慢时间FFT的频率范围和chirp重复频率有关。最大不模糊速度是 v_max = λ/(4T_c),其中T_c是chirp周期。比如60GHz雷达波长约5mm,如果chirp周期T_c=50us,则v_max=25m/s。注意这里用的是chirp周期,不是chirp时长;chirp之间如果还留了很长的空闲时间,最大速度会下降。
速度分辨率则取决于一帧里chirp的个数N_chirp:Δv = λ/(2N_chirpT_c)。如果N_chirp=64、T_c=50us,Δv大约0.78m/s;如果N_chirp=128,分辨率能到0.39m/s。这直接影响你对慢速行走人的判断能力。人的步行速度1到2m/s,如果速度分辨率太粗,人可能就在零速度旁边两个bin来回漂,容易被当成静态物体滤掉。这是很多“雷达明明对着人,目标却时有时无”的根源之一。
3.2 零速杂波抑制、速度模糊与参数联动
RD图上最强的东西通常不是目标,而是静止杂波。墙面、围栏、地面反射,它们都在零速度附近。所以第二步多普勒处理后,第一件事是去掉零速度附近的能量。可以在慢时间FFT之前先做一个高通滤波器,差分解就是最简单的——用相邻两个chirp的复数相减,静态分量抵消,只剩运动目标。这个方法会把近距离慢速目标一起削弱,代价还是在的。
真正的速度模糊问题更麻烦。如果目标径向速度超过v_max,它的多普勒峰会被折叠到- v_max 到 v_max 区间内,显示成一个错误的速度和可能错误的方向。典型例子是汽车以50m/s朝雷达正面冲过来,而v_max只有25m/s,你会解出一个看起来像“远离”的目标。解决思路有三类:用双快chirp(每个距离单元采样两次再结合),在帧内使用非均匀chirp间隔,或者用射频前端设计上的“速度扩展”模式。对多数入门项目,最好的策略是把chirp周期压下来让v_max足够覆盖你的场景。代价是同样的chirp数,一帧时间缩短,速度分辨率变差;或者要增加chirp数,数据量和处理时间又上去了。这就是雷达系统设计的核心权衡:距离、速度、角度、数据量,永远是互相挤压的。
4. 检测与测角:CFAR门限和MIMO角度谱
4.1 CFAR门限:不是“定个高阈值”那么简单
RD图里数据点成千上万,不能把所有超过固定阈值的点当目标。原因很简单,噪声底不是平的,远距离衰减、地形杂波、天线方向图不平坦都会让底噪变化。固定门限虽然好写,但要么近处全是假点,要么远处目标全丢。自适应门限才是工程正解,用的就是CFAR(恒虚警率检测)。
CFAR的思路是“用被测单元周围的平均噪声水平来确定当前门限”。以最常见的单元平均CFAR(CA-CFAR)为例,假设你想检测距离维某根谱线(CUT),左右预留几个保护单元防止目标能量泄漏到参考窗,然后取参考窗里的幅值平方平均作为噪声功率估计P_n,接着乘以门限因子α得到判决门限。只要CUT的功率超过α*P_n,就判定为有效目标。
α不是拍脑袋设的,它由参考单元数N和期望虚警概率P_fa决定:
α = N * (P_fa^(-1/N) - 1)
假设参考单元数N=16,期望虚警概率P_fa=1e-4,那么P_fa^(-1/16)约等于1.78,α约等于12.4。如果噪声功率估计是100,那门限就是1240。这个公式看起来很唬人,其实逻辑很直白:参考单元越多、虚警期望越严格,门限因子就越大。
实操时CFAR一般是在RD图上做二维的,距离维和速度维各取一组参考窗。目标多、杂波强的场景,还会用有序统计CFAR(OS-CFAR)来防止多目标互相抬高噪声估值。但OS-CFAR要把参考窗排序,运算量大不少。我见过很多调参翻车的现场,基本都是忘了留保护单元,导致强目标自身的旁瓣被当成噪声抬高了门限,反而检测不出真正的目标。保护单元一般设成目标主瓣宽度的1到2倍,加了窗之后主瓣有3个bin左右,保护单元至少左右各3个。
4.2 DOA角度估计:虚拟天线阵列怎么出来的
距离和速度都解出来了,但还不够,你只知道“某个距离上有个以某个速度移动的东西”,不知道它在哪个角度。角度估计靠的是多根接收天线之间的相位差。同一目标回波到达不同天线的波程差不同,体现在复数数据上就是相位差。给每个检测点取各天线在相应(range, doppler)bin上的复数形成一个“快拍向量”,再做一次FFT,峰值对应的频率就是到达角(DOA)。
这里有个重要细节:测角用的是发射天线和接收天线联合形成的虚拟阵列。例如2发4收,如果发射波形是时分复用MIMO,那么等效虚拟接收天线数可以达到2*4=8根。每根虚拟天线的相位中心不同,等效成一把8元天线阵列,角度分辨率约 2/N 弧度(阵元间距半波长时),大概14度左右。很多产品广告里的“角分辨率”就是从这来的。想要更高的角度分辨率,要么增加虚拟阵列规模,要么用MUSIC等高分辨率算法,但后者对信噪比和阵元校准极其敏感,工程上通常还是先FFT把主方向求出来,再用超分辨算法做细化。
实际布板时,阵元间距如果大于半波长,会出现栅瓣(角度模糊)。这时候你会看到一个目标在-60度和+60度同时出现两个峰。排查这类问题,优先检查天线间距和虚拟阵元相位中心的校准文件。幅度和相位校准如果没做好,测角能力会严重下降——典型表现是不同位置的静止目标角度出现系统性偏移,而且不同目标性能不一致。
5. 点云到目标列表:聚类、跟踪与最终输出
5.1 一个检测点还不够,先用DBSCAN把点云收拢
过了CFAR和测角,你会拿到一堆检测点,每个点携带距离、多普勒速度、角度、信噪比这些属性。这个阶段在供应商SDK里叫“点云输出”。但点云还不是目标列表——一个行人身上可能有七八个强散射点,一辆车可能有十几个;反过来,一个散射特性弱的行人可能只有一两个点。如果直接把这些点都上报给上层,下游会疯掉。所以目标列表生成前通常要做点云聚类。
工程中用得最多的聚类算法是DBSCAN。它不需要预先指定聚成几类,能把任意形状的点云簇找出来,并且能自动把孤立噪声点标记为离群点。DBSCAN有两个参数:扫描半径eps和最小邻域点数minPts。eps值我会按照雷达在某个距离上的点云密度来标定,比如在10m处一个行人大概能产生4到6个点,同一目标点之间的间距不超过半个距离分辨力或一个角度单元,那就以这个尺度为参考去设。minPts工程上常取3到5,低于3的簇容易是单点噪声。
聚类后每个簇会输出一个合并后的目标点,通常会取离雷达最近的点、信噪比最高的点,或者做加权质心。这里一定要保留簇内点数作为“置信度”参考。我在测试中发现,把“簇内点数”这个特征挂上之后,过滤虚警的难度瞬间降低一半。单点簇大概率是随机噪声或闪烁的旁瓣,直接丢弃会省掉下游很多麻烦。
5.2 跟踪滤波器与航迹管理:把“点”变成稳定的“目标”
聚类后你已经有了一系列每帧都出现的点,但直接输出这些点,会出现两个问题:一是单帧检测会存在漏检,二是目标的瞬时位置抖动很大。解决方式是加一层跟踪滤波。毫米波雷达里最常用的是卡尔曼滤波,状态向量一般取[距离, 径向速度, 角度]及其导数,例如恒定速度(CV)模型或者恒定加速度(CA)模型。
卡尔曼滤波的核心不是把点变平,而是把“预测值”和“观测值”按各自的不确定性加权。雷达的测距误差模型通常是高斯分布,天线测角误差是信噪比的函数,信噪比越低角误差越大。把这些不确定性填进协方差矩阵里,滤波器就能自动决定更相信预测还是更相信观测。
跟踪管理还有个L型流程:新的检测点需要“航迹起始”,连续若干帧关联上才确认;已有航迹会预测下一个时刻的位置,再通过最近邻关联或概率数据关联把新点配到正确航迹上;若连续多帧没有关联到新点,航迹就“终止”。这个过程在产业界叫航迹管理。很多人以为卡尔曼滤波就是目标列表的全部,其实航迹生命周期管理才是真正的工程工作量。我看到过很多半路出家的雷达工程师,算法模型全会,但航迹逻辑写得稀烂,一会目标ID乱跳,一会一截断根重新起头。目标ID的稳定性和航迹置信度,直接决定下游业务层能否接受你的结果。
5.3 输出目标列表的格式和常见扩展信息
最终目标列表本质上是一个结构体数组,每个目标通常包括:目标ID、距离、径向速度、水平角、俯仰角(如果有)、信噪比、RCS估计值、簇点数、航迹置信度、生命周期。很多上游系统还需要目标的多普勒速度是“接近/远离”的符号指示,这取决于速度解算时是否去除了模糊。输出频率一般和帧率一致,常见是10Hz到30Hz。
在实际部署时,我强烈建议保留一个“原始点云”的输出口,不要把所有中间产物都丢弃。很多时候GPS/IMU做融合需要用原始点的SNR做权重,或者做局部场景重放调试,没有原始点云会非常痛苦。这个看似不起眼的扩展字段,能让你在集成和售后阶段省下大量时间。
6. 踩坑记录:从ADC到目标列表的常见问题速查
这章把我在调试和交付里遇到最多的问题整理成一个速查表,每一行都是真实项目里踩过的坑。对照着排查,能省下不少从零观察数据的时间。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 距离谱上0距离附近出现巨大尖峰 | ADC直流偏置、收发泄漏 | 去掉直流分量再做FFT;检查I/Q是否平衡 |
| 目标距离测出来偏大或偏小 | ADC采样率配置和实际不匹配 | 核对chirp配置文件里的采样率、带宽、斜率 |
| 静止目标在速度轴上乱出峰 | 慢时间FFT前未抑制零速杂波;雷达挂在难以固定的结构上 | 加高速滤波,检查机械固定 |
| 近距离目标检测正常,远距离全丢 | 门限因子过高或CFAR参考窗太大 | 降低α,缩小参考窗,检查中频增益 |
| 同一目标同时出现两个角度峰 | 阵元间距大于半波长出现栅瓣;角度校准偏差 | 检查天线尺寸,重新校准相位误差 |
| 行人目标ID在几个轨迹间跳动 | 关联门限设置太紧,目标分裂 | 增大关联门,在点云聚类参数上做调整 |
| 目标速度偶发出现正负跳变 | 速度模糊;chirp周期过长 | 压缩chirp周期,检查V_max边界 |
| 单帧点数爆炸,下游处理变慢 | CFAR门限太松 | 提高α或改用OS-CFAR,提高点云SNR门限 |
除了表格里的现象,我还想特别提一个看似无关紧要的问题——相位噪声。毫米波雷达的ADC数据质量不仅仅看信噪比,还要看相位噪声。热词里频繁出现的“ADC信噪比”,很多人只盯幅度噪声,忽略了相位噪声对测速和测角的影响。相位噪声会直接加宽多普勒谱峰,让速度分辨率变差;在角度维上则体现为谱峰展宽,降低测角精度。如果发现RD图上的亮点边界模糊、速度谱展宽的厉害,不要只怀疑算法,先回头查一下锁相环的参考时钟和电源纹波。
还有一个经验是:不要一上来就追求“多目标完美跟踪”。先把单目标在空旷场景里的链路闭跑通,确认ADC数据方向、Range FFT坐标、Doppler方向、角度坐标系都一致,再叠加多目标和复杂杂波。坐标系一致是雷达调试里最费时间的隐形工作。RD图上的距离轴从零开始,但速度轴有正负,角度谱又有方位角和俯仰角之分,每个SDK里坐标系定义可能都不一样。我吃过这个亏,在TI和国产某平台之间移植时,角度坐标一个按弧度一个按度,输出目标方位直接偏了90度,花了整整两天才定位到是单位换算问题。
最后分享一个我在实际调试时一直会用的小技巧:在ADC原始数据阶段,把I/Q两路分别打出来,看通道幅值是否相等、相位是否正交。IQ不平衡的典型表现是频谱上产生镜像峰,明明只有一个目标,距离谱上却出现两个对称的峰。很多“莫名其妙的目标”都是这个原因。先把ADC链路洗干净,再谈算法调优,顺序不能反。整个感知链路从ADC到目标列表看起来很漫长,但拆开看,每一级都是“做一次变换、做一次筛选、再换个坐标系输出”。只要你能随时画出中间某一级的图,并解释每个峰对应什么物理量,这条链路就已经在你脑子里闭合了。