☰
嵌入式毫米波雷达信号处理实现指南:距离FFT、CFAR与目标跟踪
2026/11/2 14:53:10 网站建设 项目流程

前几天调试到深夜,串口助手上稳稳打出一行数据:目标ID=3,距离3.52米,径向速度0.12米/秒。那一刻我反而比第一次跑通算法时平静,因为从Matlab仿真到嵌入式实机,这条路该踩的坑基本都踩过了。

PLFM_RADAR就是那个项目代号。PLFM是Platform的缩写,RADAR就是雷达,说白了,这套工程是一个完整跑在嵌入式目标平台上的雷达信号处理链路。它的输入是毫米波雷达的原始中频ADC数据,输出是稳定跟踪的目标列表(距离、速度、ID),中间所有环节——加窗、距离FFT、多普勒FFT、CFAR检测、点云聚类、目标跟踪——全部由自己代码完成,不依赖雷达厂商的黑盒算法库。

这篇文章我把整个项目的来龙去脉、核心算法细节、嵌入式的工程化取舍、以及实际调试中踩过的坑全部梳理一遍。如果你正在做车载雷达、机器人避障、智能家居存在感知,或者只是想把雷达算法从PC端移植到MCU/DSP上,这篇内容应该能帮你少走不少弯路。

1. PLFM_RADAR项目定位与整体设计思路

1.1 这个项目到底解决什么问题

很多毫米波雷达模组自带完整固件,接上串口就能输出点云甚至目标列表。刚接触雷达的工程师很容易觉得“这玩意儿好简单”,但实际做系统集成时,黑盒方案的问题就全冒出来了。

第一个问题是不透明。原厂固件的算法流程看不到,能调的参数就是厂商暴露出来的那几个寄存器,内部怎么做的CFAR、怎么聚类、跟踪器是什么模型,一概不知。第二个问题是不可控。点云输出的延迟、抖动、更新率都由固件决定,系统集成想优化某个环节根本没有入手点。第三个问题是场景适配难。停车场、走廊、无人机避障这些场景对雷达的需求完全不一样,黑盒固件是针对“通用场景”调的,到了你的场景里就是各种误检和漏检。

所以PLFM_RADAR最初的定位就很明确:雷达传感器只负责发射、接收和基本的基带处理,把中频ADC采样数据交出来,之后的算法全部自己实现。这个选择有点像买了毛坯房自己排水电,前期费时费力,但后面想加什么功能都不受制于人。最终这套代码跑在Cortex-M7平台上,一帧处理时间控制在10毫秒以内,30fps输出稳得很。

1.2 平台资源够不够:先算一笔账

嵌入式雷达算法最容易翻车的地方,是拿PC端的性能预期去估算MCU平台。我见过太多人先在x86上把算法调到很漂亮,然后一句“应该能跑吧”就上了板子,最后被实时性吊打。

动手之前要先算清楚数据量和算力。以一个典型的FMCW雷达配置为例:每个chirp采样256个IQ点,一帧64个chirp,那么一帧原始数据就是256乘64共16384个复数采样点。一个复数如果按单精度float存,就是8字节,一帧原始数据约128KB——当然这里如果按int16的IQ打包就是64KB。无论哪种,对SPI、DMA这种外设来说流量都不算大,真正的瓶颈在处理器的FFT算力上。

我当时用的平台是600MHz的Cortex-M7,带FPU。用CMSIS-DSP库做256点复数FFT,单精度浮点大概40到70微秒一次,64个chirp的距离FFT累计约3到4.5毫秒;之后做256次64点FFT(多普勒维),约2毫秒;CFAR、聚类、跟踪加一起再算1到2毫秒。这样一帧总耗时大概6到9毫秒,跑30fps还剩不少余量。把账算清楚之后,你才能确定算法用浮点还是定点、需不需要做分帧并行、哪些模块可以下放到DMA。

1.3 模块划分:每一级都能单独调试

整个工程我按数据流的顺序分成了五层,这是后期调试效率高的关键。

  • 数据采集层:通过SPI/CAN接口读取雷达前端输出的ADC数据,用DMA搬运进内存。
  • 信号处理层:对一帧数据做加窗、距离FFT、多普勒FFT,得到二维距离-多普勒能量图。
  • 检测层:在能量图上做CFAR检测,再对过门限的谱峰做聚合,输出点云(每个点包含距离、速度、幅值)。
  • 跟踪层:点云聚类、帧间关联、卡尔曼滤波预测与更新,维护稳定目标ID。
  • 输出层:把目标列表通过串口或CAN按自定义协议发出。

每一层都有明确的输入输出结构体,这意味着每一层都可以脱离整机单独测试。比如只调CFAR参数时,我可以直接造一张固定能量图喂进去,看门限和输出点云是否合理,不用每次都得等整条链路跑完。这种“可单独验证”的特性,在做嵌入式算法时尤其重要,因为板上调试手段有限,能隔离的问题一定要隔离。

2. 核心算法细节与嵌入式实现要点

2.1 FMCW雷达数据链路:为什么先做距离FFT再做多普勒FFT

FMCW的中频信号频率和目标距离成正比,这是整个链路的基础。雷达发出的chirp是频率线性上升的锯齿波,回波和发射信号混频之后得到一个差频信号,这个差频的频率等于目标距离对应的时间延迟乘以调频斜率。所以在单个chirp内部做FFT,谱峰位置对应的就是目标距离,这也就是距离维FFT。

多普勒维FFT则要利用多个chirp。目标只要有径向速度,回波相位就会在chirp和chirp之间产生累积变化,这种变化反映在同一个距离bin在不同chirp上的相位差上。因此需要对“同一个距离单元、不同chirp索引”的数据再做一次FFT,得到的谱峰位置对应的就是目标的径向速度。

这两个FFT的先后顺序不能乱。多普勒FFT的输入必须是从每个chirp的距离FFT结果中,按距离bin抽取出来的序列。也就是说,你先得把每一行chirp变成距离频谱,再按列去取数据才能做多普勒FFT。这个顺序搞反了,速度维度就全乱了。用通俗点的说法:先把每一行的音符拆成频谱,再把每一列相同频率的音符汇总成另一个维度的频谱,顺序反了数据就是对不齐的。

在代码实现上,最容易出错的是数据排布。ADC读进来的数据是“chirp内采样点连续”的,做完距离FFT之后每个chirp内已经变成了距离维,但做多普勒FFT时软件里要按“跨chirp取同一距离bin”的方式访问数据。我一开始用双重循环的时候,索引计算错了三次,最后用一个小工具把中间数据导出来画图才看出问题。建议实现时把数据排布单独封装成函数,输入输出都用结构体描述,省得后面反复踩坑。

2.2 加窗与FFT实现的平台侧细节

很多从Matlab移植过来的代码,第一个问题就是忘了加窗。MCU上直接对原始数据做FFT,等效于用了矩形窗,旁瓣电平很高,大概在主瓣以下13dB左右。这意味着一个强目标的旁瓣可能盖过一个弱目标的真实谱峰,在雷达里就会出现“大目标旁边的小目标完全看不见”的情况。

加汉明窗之后,旁瓣能压到大约-43dB,代价是主瓣宽度差不多变成原来的1.5倍,也就是距离分辨率略有损失。但对绝大多数目标检测场景来说,这个交换非常划算。目标如果靠得太近,可以通过提高带宽来弥补分辨率损失,而不是靠不加窗来硬撑。窗函数系数在初始化时一次性算好存成静态数组,不要在每帧处理时再调用cosf,否则时序上纯粹是浪费。

另一个平台细节是FFT操作的定点与浮点选择。Cortex-M7带FPU,单精度浮点FFT的直接实现成本并不高,CMSIS-DSP一套库也很成熟,不需要自己手写蝶形运算。但要注意CMSIS-DSP要求输入输出缓冲区按32位对齐,很多莫名其妙的HardFault都来自于对齐问题。如果平台是DSP或者没有FPU的MCU,那就得改用Q15或Q31定点FFT,这时尤其要注意中间结果的缩放移位,否则很容易溢出,表现为频谱图上出现一整排无规律的“盐粒噪声”。

2.3 CFAR检测:单元平均恒虚警的参数怎么定

二维FFT出来的是能量谱,接下来怎么判定“哪些谱峰是目标”就很关键了。有人图省事,设定一个固定阈值,效果通常很差。原因是雷达环境里的噪声底不是平的:远距离的噪声电平高,近距离低;有雨雪或杂波时局部噪声还会抬升。固定阈值只能在特定场景里工作,换个环境就废了。

CFAR的做法是“就地取噪”。在待检测的谱峰周围划出一圈参考单元,计算这些参考单元的均值作为当前局部噪声电平估计,再乘一个系数作为门限。这个门限是自适应的,噪声高的地方门限跟着高,噪声低的地方门限跟着低,从而保持稳定的虚警率。

参数怎么给?我实际调试的结果,参考单元数(REF_R)取8到16,保护单元数(GUARD_R)取2到4,阈值系数alpha取线性域10到20倍(对应10到13dB)。保护单元必须有,因为目标能量在FFT后会有几个单元的泄漏,如果不加保护,这些泄漏样本会被当成噪声均值的一部分,导致门限被抬得很高,严重时反而把目标自己压掉。实现时建议在幅值域直接做CFAR,不要先转成dB再处理,因为线性域的噪声模型更贴合瑞利分布,也更省计算量。

2.4 点云聚类与目标跟踪的轻量化思路

CFAR输出的不是目标,而是许多个过门限的谱峰点。一个行人可能同时产生几个散射点,一辆车的散射点能有几十个。如果直接用这些点去关联跟踪,跟踪器会直接被点云数量打爆,而且一个物理目标会分裂出好几个Track。所以必须先把点云聚成簇,一个簇对应一个目标。

在MCU上跑聚类,我不建议直接搬DBSCAN,因为密度聚类的索引树在单片机上有点重。我的做法是栅格聚类:先把距离-多普勒图按距离和多普勒维下采样成网格,然后对网格做8邻域连通域标记。处理一帧256乘64的图,这种轻量聚类实测只需要几百微秒,效果和DBSCAN在这个场景下差别不大。关键参数是两个维度的邻域半径,实际需要根据距离分辨率和多普勒分辨率来折算成物理范围,比如距离上要允许相邻0.3米内的点属于同一个目标。

跟踪器我用的是恒速模型加线性卡尔曼滤波,帧间关联用最邻近匹配。状态量就是距离和速度,观测是距离,速度通过多普勒维直接测量。卡尔曼的核心意义在于用一个带权重的预测来平滑测量噪声,不至于让目标的距离和速度在帧间乱跳。这里有个经验:系统噪声Q不要调得过大,否则滤波的预测会“太相信模型”,在目标转向时反而跟丢;观测噪声R按实测点云抖动来定,静态目标的点云标准差大概是多少,R就取多少。

3. 实操过程:从拿到板子到稳定跑出目标

3.1 工程搭建顺序:先打通原始数据通路

如果你拿到一块雷达前端和一个MCU平台,我建议的工程推进顺序不是先写FFT,而是先保证原始数据通路百分百正确。

第一步,配置雷达芯片的寄存器,把中频ADC数据以原始采样格式通过SPI或CAN输出。第二步,在MCU上开一个DMA收数据的环形缓冲,按chirp和帧的结构解析出帧边界。第三步,把一帧完整数据原样通过串口导到PC,用Matlab或Python验证波形是否符合预期——比如对着一个静止的金属板,看回波中频信号的频率是否对应理论距离。

这个验证步骤看起来简单,但能省下一整周的排障时间。我遇到过几次“FFT结果全不对”的情况,查了半天代码和算法,最后发现原始数据的字节序就不对。数据通路没验证你就直接写算法,所有问题都会混在一起,定位起来极其痛苦。原始数据对得上,问题范围就缩小到算法侧,排查难度直接降一个量级。

3.2 核心代码骨架:距离FFT、CFAR与聚类实现

下面给一段精简后的示意代码,保留了工程中的核心逻辑。

#define RANGE_BINS 256 #define CHIRPS 64 static float hann_window[RANGE_BINS]; static arm_cfft_instance_f32 fft_inst; void radar_fft_init(void) { // 预计算汉明窗,避免每帧重复调用三角函数 for (int i = 0; i < RANGE_BINS; i++) { hann_window[i] = 0.54f - 0.46f * cosf(2.0f * PI * i / (RANGE_BINS - 1)); } arm_cfft_init_f32(&fft_inst, RANGE_BINS); } void range_doppler_process(float* adc_iq, float* rd_map_out) { float range_buf[RANGE_BINS * 2]; // 1. 距离维FFT:每个chirp做一次256点FFT for (int c = 0; c < CHIRPS; c++) { for (int i = 0; i < RANGE_BINS; i++) { range_buf[2*i] = adc_iq[(c * RANGE_BINS + i) * 2] * hann_window[i]; range_buf[2*i + 1] = adc_iq[(c * RANGE_BINS + i) * 2 + 1] * hann_window[i]; } arm_cfft_f32(&fft_inst, range_buf, 0, 1); // 结果按距离bin存入rd_map,此时内存布局为 [chirp索引][距离bin] for (int r = 0; r < RANGE_BINS; r++) { rd_map_out[c * RANGE_BINS * 2 + 2*r] = range_buf[2*r]; rd_map_out[c * RANGE_BINS * 2 + 2*r + 1] = range_buf[2*r + 1]; } } // 2. 多普勒维FFT:对每个距离bin,抽取CHIRPS个chirp的复数据 for (int r = 0; r < RANGE_BINS; r++) { for (int c = 0; c < CHIRPS; c++) { doppler_buf[2*c] = rd_map_out[c * RANGE_BINS * 2 + 2*r]; doppler_buf[2*c + 1] = rd_map_out[c * RANGE_BINS * 2 + 2*r + 1]; } arm_cfft_f32(&fft_inst, doppler_buf, 0, 1); // 计算幅值谱,存入RANGE_BINS x CHIRPS的能量图 for (int d = 0; d < CHIRPS; d++) { float mag = sqrtf(doppler_buf[2*d] * doppler_buf[2*d] + doppler_buf[2*d+1] * doppler_buf[2*d+1]); rd_power[r * CHIRPS + d] = mag; } } }

CFAR与聚类部分的关键片段:

float ca_cfar_threshold(float* rd_power, int r, int d, float alpha, int ref_r, int guard_r) { float noise_sum = 0.0f; int noise_cnt = 0; for (int rr = r - ref_r - guard_r; rr <= r + ref_r + guard_r; rr++) { if (rr < 0 || rr >= RANGE_BINS) continue; for (int dd = d - ref_r - guard_r; dd <= d + ref_r + guard_r; dd++) { if (dd < 0 || dd >= CHIRPS) continue; int dist_r = abs(rr - r); int dist_d = abs(dd - d); if (dist_r <= guard_r || dist_d <= guard_r) continue; noise_sum += rd_power[rr * CHIRPS + dd]; noise_cnt++; } } return alpha * noise_sum / noise_cnt; }

上面的多普勒FFT循环里,arm_cfft实例是256点的,但64点FFT需要用另一个实例。示意代码里写成同一个调用是为了简洁,实际工程中要按FFT点数分别初始化。另外,求幅值的sqrtf在整帧循环里会占不少CPU时间,实际优化时可以用查表近似,或者只在CFAR候选点附近才计算幅值,这能省下不少周期。

3.3 一次真实的走廊标定过程

调试场景选在一条8米长的走廊。目标一是一个人站在3.5米处静止不动,目标二是一个人从5米外以大约每秒0.8米的速度朝雷达走来。雷达参数如下:带宽1GHz、ADC采样率2Msps、每chirp采样256点、一帧64个chirp、帧率30fps。

先算几个关键参数再动手。距离分辨率是光速除以两倍带宽,等于0.15米,也就是说3.5米处的目标会落在第23个距离bin附近。最大测距距离由ADC采样率和调频斜率决定,算出来是5米左右,刚好覆盖走廊场景。多普勒速度分辨率大约0.13米每秒,所以目标二的移动速度在速度维会有约6个bin的偏移。把这些理论值算好,你才知道FFT谱图上该找哪一格,不然两眼一抹黑。

实测过程和预期对得上:静止目标在零速附近的距离bin上出现一个稳定的亮点,且幅度基本不变;移动目标的亮点随着时间逐帧朝更近的距离bin移动,同时多普勒维上出现一个稳定向负方向的偏移。CFAR检测后,两个目标各产生一片点云,经过栅格聚类后收敛成两个簇,跟踪器在第一帧建了ID1和ID2,后面的帧连续关联上了。这一步能跑通,整个项目最核心的部分就算落地了。

调参过程有一笔记录值得分享:CFAR的alpha一开始取12dB,结果走廊尽头的消防栓在40次扫描里被误检了3次,距离还不稳定;把alpha调到14dB后误检消失,但3.5米处人稍微侧身站立时,回波变弱,开始偶发漏检。最后折中在13dB,误检和漏检勉强都压住了。这说明CFAR参数不是越大越好,是在虚警和漏检之间找一个当前场景的平衡点。

3.4 输出协议与调试技巧

目标列表最终要交给上层使用,所以输出协议也要设计得干净一点。我的做法是定一个帧头加可变长目标数据的结构,每个目标固定输出ID、距离、速度、幅值四个字段,用两字节小端存储。串口打印只用1Hz的慢速输出做人工观察,实时数据走DMA发送,避免打印阻塞主循环导致掉帧。

调试时最有用的一个技巧是:把CFAR之后的中间能量图也通过串口上传到PC,用Python画成热力图。这样你能直接看到“目标在哪、门限在哪、为什么这一帧检测成了这样”,比只盯着最终跟踪结果去猜内部状态高了不止一个效率层次。我后来养成了习惯,凡是算法移植,必须留一个中间结果导出开关,平时关掉,出问题时打开。

4. 常见问题与排查技巧实录

4.1 静态目标被检测成移动目标

这是我在PLFM_RADAR里花时间最长的一个问题。现象是走廊尽头完全静止的金属门,在速度维上出现了一个不该有的速度偏移,导致跟踪器常常给它建一个新的ID。

排查后定位到两个原因。第一个是距离维FFT旁瓣太高,强静态目标的旁瓣泄漏到多普勒维上,形成类似低速运动的虚假峰值。解决办法是加窗,汉明窗加上后旁瓣被压低,这个现象明显减轻。第二个原因是CFAR处理时没有对零速bin附近做专门保护。静止目标在速度维上会有一个零速附近的主峰,附近若干bin都会有泄漏,CFAR的参考窗如果不排除这些泄漏单元,门限会失真。后来我把CFAR的保护窗在速度维方向上单独加宽了一档,误检基本消失。

4.2 同一个目标分裂成多个Track

有一次实测中,一个在3米处缓慢走动的人,输出时变成了ID1和ID2两个Track,而且距离差在0.2米左右跳动。第一反应是聚类半径设小了,把同一目标的散射点拆成了两簇。

排查时我先打开了点云输出开关,看原始点云分布,发现确实距离维上点云分成了两簇,分开距离大约0.2米。于是把栅格聚类的距离邻域从0.15米调大到0.3米,点云重新合并成一簇。但紧接着又发现新问题:合并后一个目标偶尔在中间帧丢两帧,然后重新建ID,这其实是跟踪关联门控太紧导致的。把最近邻关联的最大距离从0.3米放宽到0.5米,ID就稳定了。这类问题的通解是:先看点云,点云如果本来就断裂就要调聚类;点云连续但ID还是切换就调关联门控。

4.3 帧率上不去,CPU占用过高

工程初步跑通时帧率只有12fps,离30fps的目标差太远。用定时器测量各级耗时,发现距离FFT和多普勒FFT占了大头,紧随其后的是幅值计算的sqrtf调用。后来做了三个优化:FFT全部改用CMSIS-DSP的浮点函数并开启编译器最高优化等级;幅值计算改成只在CFAR候选点附近才做;点云输出和雷达数据处理拆成两个任务,输出不再占用处理时间。优化后单帧处理时间从25毫秒降到8毫秒左右,帧率稳定在30fps以上。

如果做完这些还不够,还有更狠一点的方案:把距离FFT的结果分帧并行处理,或者对多普勒维使用64点专用FFT函数而不是通用256点函数。真到了那一步,说明你对平台算力的压榨已经到位了。

4.4 排查速查表

现象可能原因排查方法解决方案
静态目标变成移动目标距离FFT旁瓣泄漏、CFAR保护窗不够检查距离能量图旁瓣加窗、加大速度维保护单元
一个目标多个Track聚类半径太小、关联门控太紧打开点云看分布调大栅格聚类邻域、放宽最近邻门控
近距离目标看不见CFAR门限太高、近距离有盲区看CFAR门限曲线降低alpha、检查接收链路饱和
频谱图有规律噪点定点FFT溢出、缓冲区未对齐单帧数据逐级导出改浮点或加缩放移位、对齐缓冲区
帧率不足FFT耗时高、打印阻塞定时器分段计时用CMSIS-DSP、延迟打印、任务拆分
目标距离忽远忽近观测噪声R值设置太小统计静态目标标准差按实测方差调整卡尔曼R

最后说点我自己的经验。雷达算法落地的过程,很容易陷入“反复调一个参数、看一次结果”的循环,效率极低。我后来规定自己每次只改一个变量,跑一组固定场景,把前后两版结果记录在同一张表里对比。另外一个原则是不要等算法优化到完美再上板,性能只要到了可用水平,就赶紧整机联调,系统层面的问题往往比算法层更早暴露。做嵌入式雷达,最大的资源永远是调试时间和一个干净可控的测试场景。你留一块场景专门做回归测试,比在论坛上翻十个帖子都有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询