拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

毫米波雷达感知链路:从ADC采样到目标列表的完整实现与避坑指南

毫米波雷达感知链路:从ADC采样到目标列表的完整实现与避坑指南

1. 毫米波雷达感知链路到底在做什么

毫米波雷达这东西,刚入行的时候觉得它神秘,做久了发现它其实就是一个从模拟电压到数字目标列表的翻译机器。你给它供电、给它时钟,它往外吐点云或者目标列表,中间这条链路就是感知链路。标题里说的“从 ADC 到目标列表”,恰好是这条链路里最核心、也最容易踩坑的一段——ADC 采样之后的每一步,都决定了你最终拿到的目标是真目标还是鬼影。

先把这条链路的大致轮廓画出来,不画图,用文字说清楚。射频前端把 77GHz 附近的回波信号混频到基带,得到 I/Q 两路模拟信号,然后 ADC 对这两路信号采样,变成数字域的一串复数样本。这些样本先做距离维 FFT,再做多普勒维 FFT,接着做恒虚警检测(CFAR),最后做角度估计和聚类,输出目标列表。听起来就四步,但每一步都有大量细节能把人折磨到怀疑人生。

我见过太多项目,射频前端调得挺好,天线阵列也设计得不错,结果目标列表里全是杂波或者距离模糊,最后查来查去发现是 ADC 采样时序和后续 FFT 处理没对齐。所以这篇文章不打算只讲概念,而是把这条链路拆开,讲清楚每个环节在干什么、为什么这么干、以及实际做的时候哪些地方容易翻车。

适合谁看?如果你刚接触毫米波雷达,想搞明白从原始 ADC 数据到目标列表之间到底发生了什么,这篇能给你一个完整的框架。如果你已经在做雷达信号处理,但总觉得某些环节“知其然不知其所以然”,比如为什么距离 FFT 之前要加窗、为什么 CFAR 的参考窗长度要那么选,那这篇也能帮你把逻辑理顺。我不打算堆公式,但关键参数的计算过程会写清楚,让你能直接抄作业。

2. 感知链路的整体设计与模块拆解

2.1 为什么是“ADC 到目标列表”而不是“天线到目标列表”

很多人一上来就讲天线、讲波形,但真正做工程的人知道,ADC 是模拟世界和数字世界的分界线。天线和射频前端决定了你能看到什么,但 ADC 之后的处理决定了你能把看到的东西还原成什么。把起点定在 ADC,是因为从这一刻开始,数据完全在数字域里流动,你可以用代码去调试、去复现、去优化。而 ADC 之前的环节,更多是硬件设计和射频调试的范畴,改一版硬件的成本太高了。

另一个原因是,ADC 采样率、位宽、通道数这些参数,直接决定了后续处理的复杂度和上限。比如你用 12 位 ADC 和用 8 位 ADC,动态范围差了一大截,CFAR 的门限策略就得跟着变。再比如采样率决定了最大不模糊距离和多普勒范围,这些参数在系统设计阶段就要算清楚,不然后面怎么调都调不出来。

2.2 链路的核心模块与数据流

整条链路可以分成五个大模块:ADC 采样与数据对齐、距离维处理、多普勒维处理、CFAR 检测、角度估计与聚类。每个模块的输出都是下一个模块的输入,数据格式从最初的实数或复数样本,逐步变成距离-多普勒矩阵、检测点云,最后变成带距离、速度、角度的目标列表。

数据流的关键在于维度变换。ADC 采样得到的是按时间排列的样本,经过距离 FFT 后变成按距离排列的频谱,再经过多普勒 FFT 后变成距离-多普勒二维矩阵。这个矩阵里的每个点代表一个距离和速度的组合,值的大小代表回波强度。CFAR 在这个矩阵上滑动,找出超过自适应门限的点,这些点就是潜在目标。最后角度估计利用多个接收通道之间的相位差,算出目标的方位角,再通过聚类把同一个目标的多个检测点合并成一个。

2.3 方案选型背后的考量

在实际项目中,这条链路有两种常见的实现方式:FPGA 实时处理和DSP/CPU 后处理。FPGA 的优势是并行度高、延迟低,适合做 FFT 和 CFAR 这种计算密集但结构规整的操作。DSP 或 CPU 的优势是灵活,适合做聚类、跟踪这些逻辑复杂的环节。我见过不少项目用 FPGA 做前端 FFT 和 CFAR,然后把检测点传给 DSP 做角度和聚类,这样分工比较合理。

还有一种选择是全 FPGA 流水线,从 ADC 采样到目标列表全部在 FPGA 里完成。这种方案延迟极低,但开发难度大,调试周期长。如果你的项目对实时性要求极高,比如车载雷达需要毫秒级响应,那全 FPGA 是值得考虑的。但如果只是做原型验证或者对延迟不那么敏感,用 FPGA 加 DSP 的混合方案会更稳妥。

注意:不管选哪种方案,ADC 采样数据和后续处理之间的时序对齐都是必须解决的问题。我见过一个项目,FPGA 里 FFT 的帧起始信号和 ADC 的采样起始信号差了半个时钟周期,导致距离谱上出现固定的杂散,查了一周才发现是时序问题。

3. 核心细节解析与实操要点

3.1 ADC 采样:不只是“采个样”那么简单

ADC 采样看起来简单,实际上有几个参数直接决定了整条链路的性能上限。第一个是采样率,它必须满足奈奎斯特准则,但毫米波雷达里通常用的是带通采样,采样率只要大于信号带宽的两倍就行。比如你的基带信号带宽是 10MHz,那采样率至少 20MSPS。但实际选的时候会留一些余量,比如用 25MSPS 或 30MSPS,方便后续做数字滤波。

第二个是位宽。12 位 ADC 的动态范围大约是 72dB,8 位只有 48dB。毫米波雷达的回波信号动态范围很大,近处强目标反射和远处弱目标反射可能差 60dB 以上,所以 12 位是起步,16 位更好。但位宽越高,数据量越大,后续处理的压力也越大。我一般建议在原型阶段用 14 位或 16 位,量产阶段再根据实际需求权衡。

第三个是通道一致性。毫米波雷达通常有多个接收通道,每个通道的 ADC 增益、偏置、相位响应必须一致,否则角度估计会出问题。实际调试的时候,我会先给所有通道输入同一个测试信号,然后比较采样后的幅度和相位,把不一致的通道校准掉。这个校准系数要存在非易失存储器里,每次上电后加载。

// 一个简单的 ADC 采样值滤波函数,用于去除突发噪声 #define FILTER_LEN 8 int16_t adc_filter(int16_t new_sample) { static int16_t buffer[FILTER_LEN] = {0}; static uint8_t index = 0; static int32_t sum = 0; sum -= buffer[index]; buffer[index] = new_sample; sum += new_sample; index = (index + 1) % FILTER_LEN; return (int16_t)(sum / FILTER_LEN); }

这个滑动平均滤波在 ADC 采样后立刻做,能有效抑制高频噪声。但要注意,滤波会引入群延迟,如果后续做 FFT,这个延迟会影响相位,需要在角度估计时补偿掉。

3.2 距离维 FFT:加窗与补零的取舍

距离维 FFT 的作用是把时域采样变成距离谱。假设一个 chirp 的采样点数是 N,采样率是 fs,调频斜率是 S,那么距离分辨率是 c/(2B),其中 B 是调频带宽。距离谱的第 k 个点对应的距离是 kcfs/(2S*N)。这些公式看起来枯燥,但实际调参的时候必须算清楚,不然你连目标在哪个距离档位都找不到。

加窗是距离维 FFT 之前必须做的。不加窗的话,强目标的旁瓣会淹没弱目标。常用的窗有汉宁窗、汉明窗、布莱克曼窗。汉宁窗的主瓣宽度是矩形窗的两倍,但旁瓣衰减快很多。我一般先用汉宁窗,如果发现弱目标还是被淹没,再换布莱克曼窗。但窗越复杂,主瓣越宽,距离分辨率会下降,所以要在旁瓣抑制和分辨率之间权衡。

补零是另一个常见操作。比如你只有 256 个采样点,但想得到 1024 点的距离谱,就可以在后面补 768 个零再做 FFT。补零不会提高真实分辨率,但会让谱线更密,方便后续 CFAR 滑动。我通常会把 FFT 点数补到采样点数的 4 倍,这样 CFAR 的参考窗有足够的滑动空间。

3.3 多普勒维 FFT:速度模糊与解模糊

多普勒维 FFT 是在多个 chirp 之间做的。假设一帧有 M 个 chirp,每个 chirp 在同一个距离单元上的采样值组成一个长度为 M 的序列,对这个序列做 FFT 就得到多普勒谱。多普勒分辨率是 λ/(2MTc),其中 Tc 是 chirp 周期。最大不模糊速度是 λ/(4*Tc)。

这里有个经典问题:速度模糊。如果目标速度超过最大不模糊速度,多普勒谱上会出现折叠,你算出来的速度是错的。解决方法是发两组不同 Tc 的 chirp,或者用中国剩余定理解模糊。实际项目中,我一般会先用一组 chirp 做粗测,如果发现速度接近边界,再发第二组确认。

多普勒维 FFT 之前也要加窗,但窗的类型可以和距离维不同。距离维更关注旁瓣抑制,多普勒维更关注速度分辨率,所以多普勒维可以用主瓣更窄的窗,比如汉明窗。

3.4 CFAR 检测:门限怎么定才不虚不漏

CFAR 是整条链路里最需要经验的一步。它的核心思想是:用待检测单元周围的参考单元估计噪声功率,然后根据虚警概率设定门限。参考单元不能包含待检测单元本身,也不能包含可能的目标,所以通常会在待检测单元两侧各留几个保护单元。

参考窗的长度和虚警概率有关。虚警概率越低,门限越高,漏检越多;虚警概率越高,门限越低,虚警越多。我一般先用 1e-6 的虚警概率试,如果发现虚警太多,再降到 1e-7 或 1e-8。但虚警概率不能无限降,因为噪声估计本身有方差,参考单元太少的话,门限会抖动。

CFAR 还有几种变体:CA-CFAR(单元平均)、OS-CFAR(有序统计)、GO-CFAR(最大选择)、SO-CFAR(最小选择)。CA-CFAR 在均匀噪声环境下性能最好,但在多目标环境下,强目标的旁瓣会抬高门限,导致弱目标漏检。OS-CFAR 对多目标更鲁棒,但计算量大。我一般会在 FPGA 里实现 CA-CFAR,然后在 DSP 里做二次筛选,把明显不合理的检测点去掉。

CFAR 类型适用场景优点缺点
CA-CFAR均匀噪声虚警控制好多目标环境下性能下降
OS-CFAR多目标鲁棒性强计算量大,需要排序
GO-CFAR杂波边缘抗杂波边缘多目标环境下漏检
SO-CFAR多目标抗遮蔽杂波边缘虚警高

3.5 角度估计与聚类:从点云到目标列表

角度估计利用多个接收通道之间的相位差。假设两个接收天线间距为 d,回波到达角为 θ,那么相位差是 2πdsin(θ)/λ。通过测量相位差,就能反算出 θ。但相位差测量有模糊,当 d 大于半波长时,会出现角度模糊。所以天线间距通常设计为半波长,或者用多组不同间距的天线解模糊。

角度估计的方法有FFT 法、MUSIC、ESPRIT等。FFT 法最简单,适合均匀线阵,但分辨率受限于阵列长度。MUSIC 和 ESPRIT 分辨率高,但计算量大,适合在 DSP 里做。我一般会在 FPGA 里做 FFT 角度估计,得到粗角度,然后在 DSP 里用 MUSIC 做精估计。

聚类是把同一个目标的多个检测点合并成一个。常用的聚类算法有DBSCAN、K-means、层次聚类。DBSCAN 不需要预先指定簇的数量,适合雷达点云这种密度不均的数据。聚类的参数(如邻域半径、最小点数)需要根据实际场景调,我一般会先用一个保守的参数,然后根据目标列表的稳定性慢慢调。

4. 实操过程与核心环节实现

4.1 从 ADC 原始数据到距离-多普勒矩阵的完整流程

假设你手头有一块毫米波雷达板,ADC 采样数据已经通过 FPGA 或采集卡传到了电脑上,数据格式是每个 chirp 的 I/Q 两路,每个采样点 16 位有符号整数。下面是从原始数据到距离-多普勒矩阵的完整步骤。

第一步,数据解析与通道分离。把原始数据按帧、chirp、采样点、通道的维度重新排列。通常一帧有 M 个 chirp,每个 chirp 有 N 个采样点,有 R 个接收通道。数据量是 MNR*2(I 和 Q)。解析的时候要注意字节序,FPGA 传过来的数据可能是大端,也可能是小端,搞错了就全是乱码。

第二步,直流偏置去除。ADC 采样值通常有直流偏置,尤其是零中频架构。去除的方法是每个通道的每个 chirp 减去该 chirp 的均值。这个操作在距离 FFT 之前做,否则直流分量会在距离谱的零距离处形成强峰。

第三步,距离维加窗与 FFT。对每个 chirp 的每个通道,先加汉宁窗,然后做 N 点 FFT(补零到 4N)。得到的结果是复数,取模值得到距离谱。这时候数据维度是 M4NR。

第四步,多普勒维加窗与 FFT。对每个距离单元,取所有 chirp 的对应值,加汉明窗,做 M 点 FFT(补零到 4M)。得到距离-多普勒矩阵,维度是 4N4MR。取模值后,这个矩阵就是 CFAR 的输入。

import numpy as np def range_doppler_processing(adc_data, num_chirps, num_samples, num_rx): # adc_data shape: (num_chirps, num_samples, num_rx, 2) # 分离 I/Q i_data = adc_data[:, :, :, 0].astype(np.float32) q_data = adc_data[:, :, :, 1].astype(np.float32) complex_data = i_data + 1j * q_data # 去除直流偏置 complex_data = complex_data - np.mean(complex_data, axis=1, keepdims=True) # 距离维加窗与 FFT range_window = np.hanning(num_samples) complex_data = complex_data * range_window[np.newaxis, :, np.newaxis] range_fft = np.fft.fft(complex_data, n=4*num_samples, axis=1) # 多普勒维加窗与 FFT doppler_window = np.hamming(num_chirps) range_fft = range_fft * doppler_window[:, np.newaxis, np.newaxis] doppler_fft = np.fft.fft(range_fft, n=4*num_chirps, axis=0) # 取模值 rd_matrix = np.abs(doppler_fft) return rd_matrix

这段代码可以直接跑,但要注意几个细节。np.fft.fft默认是对最后一个轴做 FFT,所以用axis参数指定维度。加窗的时候要广播到正确的维度,不然会出错。取模值之后,通常还会做对数变换,把动态范围压缩到 60dB 左右,方便可视化。

4.2 CFAR 的 FPGA 实现要点

如果你要在 FPGA 里实现 CFAR,有几个坑必须提前避开。第一个是参考窗的滑动方式。CFAR 是在距离-多普勒矩阵上二维滑动的,但 FPGA 的资源有限,通常先做距离维 CFAR,再做多普勒维 CFAR,或者只做距离维 CFAR。我一般建议先做距离维,因为距离维的参考单元更容易对齐。

第二个是噪声估计的数值精度。FPGA 里做除法很耗资源,所以通常用乘法代替除法。比如门限因子 α 可以预先算好,然后乘以噪声估计值。α 的计算公式是 α = N*(Pfa^(-1/N) - 1),其中 N 是参考单元数,Pfa 是虚警概率。这个公式在 MATLAB 里算好,直接存成定点数。

第三个是流水线设计。CFAR 需要同时访问多个参考单元,所以要用移位寄存器或 BRAM 做缓存。我一般会用两个 FIFO,一个存距离维的参考单元,一个存多普勒维的参考单元。流水线的深度要匹配 FFT 的输出速率,不然会丢数据。

提示:FPGA 里做 CFAR 的时候,建议先用 MATLAB 或 Python 生成测试向量,然后在仿真里对比 FPGA 输出和软件输出。我见过一个项目,FPGA 的 CFAR 门限因子定点化之后精度不够,导致虚警率比预期高了一个数量级,后来把定点位宽从 16 位加到 24 位才解决。

4.3 角度估计的校准与实现

角度估计的精度很大程度上取决于通道校准。校准分两步:幅度校准和相位校准。幅度校准是给每个通道乘一个系数,让所有通道对同一个信号的响应幅度一致。相位校准是给每个通道加一个相位偏移,让所有通道的相位对齐。

校准的方法很简单:在雷达前方放置一个强反射目标(比如角反射器),采集数据,然后计算每个通道相对于参考通道的幅度比和相位差。这些比值和差值就是校准系数。实际使用的时候,把校准系数应用到每个通道的采样数据上。

def calibrate_channels(adc_data, calib_amp, calib_phase): # adc_data shape: (num_chirps, num_samples, num_rx, 2) # calib_amp shape: (num_rx,) # calib_phase shape: (num_rx,) i_data = adc_data[:, :, :, 0].astype(np.float32) q_data = adc_data[:, :, :, 1].astype(np.float32) complex_data = i_data + 1j * q_data # 应用幅度和相位校准 calib_complex = calib_amp * np.exp(1j * calib_phase) complex_data = complex_data * calib_complex[np.newaxis, np.newaxis, :] return complex_data

校准之后,角度估计的流程是:对每个检测点,取所有接收通道在对应距离-多普勒单元的值,组成一个向量,然后做角度 FFT 或 MUSIC。角度 FFT 的点数通常补到 64 或 128,角度分辨率大约是 λ/(d*N_fft),其中 d 是天线间距,N_fft 是 FFT 点数。

4.4 聚类与目标列表生成

聚类是在检测点云上做的。每个检测点有距离、速度、角度、幅度四个属性。DBSCAN 的邻域半径通常根据距离分辨率、速度分辨率和角度分辨率来定。比如距离分辨率是 0.1m,速度分辨率是 0.2m/s,角度分辨率是 5 度,那邻域半径可以设为 (0.2m, 0.4m/s, 10度)。最小点数一般设为 3 到 5,太少容易把噪声聚成目标,太多会漏掉弱目标。

聚类之后,每个簇的中心就是目标的位置,簇内点的幅度加权平均就是目标的幅度。如果簇内点的速度差异很大,说明这个簇可能包含多个目标,需要进一步拆分。我一般会用速度的方差来判断,如果方差超过阈值,就用 K-means 再分一次。

from sklearn.cluster import DBSCAN def cluster_detections(detections, eps, min_samples): # detections shape: (num_points, 3) -> (range, velocity, angle) clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(detections) labels = clustering.labels_ targets = [] for label in set(labels): if label == -1: continue # 噪声点 cluster_points = detections[labels == label] target_range = np.mean(cluster_points[:, 0]) target_velocity = np.mean(cluster_points[:, 1]) target_angle = np.mean(cluster_points[:, 2]) targets.append((target_range, target_velocity, target_angle)) return targets

这段代码用了 sklearn 的 DBSCAN,实际在 DSP 里实现的时候需要自己写,但逻辑是一样的。注意 eps 参数是距离、速度、角度的联合距离,所以需要先归一化,不然量纲不一样会导致聚类偏向某个维度。

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

5.1 距离谱上出现固定杂散

这是最常见的问题之一。距离谱上出现不随目标移动的固定峰,通常有几个原因:ADC 采样时钟和 chirp 起始信号不同步、直流偏置去除不干净、电源纹波耦合到模拟前端。排查的时候,先把 ADC 输入短接到地,看距离谱上还有没有峰。如果有,说明是数字域的问题,检查时钟和时序。如果没有,说明是模拟域的问题,检查电源和射频前端。

我遇到过一次,距离谱上每隔 16 个点就有一个杂散,后来发现是 FPGA 里 FFT 的旋转因子表地址位宽不够,导致高位溢出。这种问题很隐蔽,因为杂散的位置和 FFT 点数有关,换一个点数杂散位置就变了。

5.2 速度模糊导致目标速度跳变

速度模糊的表现是目标速度在最大不模糊速度附近跳变。比如最大不模糊速度是 10m/s,目标实际速度是 12m/s,你算出来可能是 -8m/s。解决方法是发两组不同 Tc 的 chirp,然后用中国剩余定理解模糊。但中国剩余定理对噪声敏感,Tc 的比值最好选互质的数,比如 3:4 或 4:5。

还有一种方法是多帧跟踪。如果目标在连续几帧里的速度跳变很大,但距离变化平滑,那很可能是速度模糊。可以用卡尔曼滤波预测速度,然后和解模糊的结果对比,选最接近预测值的那个。

5.3 CFAR 虚警太多或漏检太多

虚警太多通常是门限太低,或者噪声估计不准。先检查参考单元里有没有目标,如果有,换 OS-CFAR 或者增大保护单元。漏检太多通常是门限太高,或者目标太弱。先检查目标的信噪比,如果信噪比低于 10dB,可能需要降低虚警概率或者增加积累时间。

我一般会用一个自适应门限:在 CFAR 之后,统计检测点的数量,如果太多,就提高门限;如果太少,就降低门限。这个自适应过程可以在 DSP 里做,每帧调整一次,调整步长不要太大,不然会震荡。

问题现象可能原因排查方法解决方案
距离谱固定杂散时钟不同步短接 ADC 输入检查时序,重新同步
速度跳变速度模糊对比多帧速度发两组 chirp 解模糊
虚警太多门限太低统计检测点数量提高门限或换 OS-CFAR
漏检太多门限太高检查信噪比降低门限或增加积累
角度估计偏差通道不一致对比通道响应重新校准通道

5.4 角度估计出现镜像目标

角度估计出现镜像目标,通常是通道间相位不一致或者天线间距误差导致的。排查的时候,先看校准系数是否过期,然后检查天线间距是否和设计值一致。如果天线是 PCB 天线,加工误差可能导致间距偏差,需要用实测值修正。

还有一种可能是角度模糊。如果天线间距大于半波长,就会出现角度模糊,一个目标会在两个角度上同时出现。解决方法是减小天线间距,或者用多组不同间距的天线解模糊。

5.5 聚类把多个目标合并成一个

聚类把多个目标合并成一个,通常是邻域半径太大或者最小点数太小。先减小邻域半径,看看能不能分开。如果还是分不开,可能是两个目标在距离-速度-角度上太接近,这时候需要提高分辨率,比如增加 chirp 数或者增加天线数。

我遇到过一次,两个目标在距离上差 0.5m,但聚类半径设了 1m,结果合并了。后来把半径降到 0.3m,就分开了。但半径太小又会导致同一个目标被拆成多个簇,所以需要根据实际场景调。

注意:聚类参数没有万能值,必须根据你的雷达参数和场景来调。我一般会先用一个保守的参数,然后根据目标列表的稳定性慢慢调。如果目标列表在连续几帧里跳变很大,说明聚类参数不合适。

6. 一些实操心得与后续扩展

做毫米波雷达感知链路,最深的体会是:ADC 之后的每一步都是环环相扣的,任何一个环节的参数没调好,都会在最终的目标列表上体现出来。我见过太多项目,射频前端做得很好,但目标列表就是不稳定,最后查来查去发现是 CFAR 的门限因子定点化精度不够,或者聚类半径设得太大。

另一个体会是,仿真和实测的差距往往在细节里。仿真的时候,噪声是高斯白噪声,目标是理想点目标,但实测的时候,噪声有色,目标有扩展,还有多径和杂波。所以仿真通过的参数,实测不一定通过。我一般会在仿真里把参数调到一个比较宽松的范围,然后在实测里慢慢收紧。

后续如果想进一步扩展,可以考虑几个方向。一是多帧跟踪,把连续几帧的目标列表关联起来,用卡尔曼滤波做平滑和预测,这样目标列表会更稳定。二是自适应波形,根据场景动态调整 chirp 参数,比如在远处用长 chirp 提高信噪比,在近处用短 chirp 提高刷新率。三是机器学习,用神经网络做 CFAR 或者聚类,替代传统的阈值方法,但需要大量标注数据。

最后分享一个小技巧:在 ADC 采样之后立刻做一次简单的滑动平均滤波,能有效抑制高频噪声,而且计算量很小,适合在 FPGA 里做。但滤波的窗口长度不要太大,不然会引入太大的群延迟,影响后续的相位测量。我一般用 4 到 8 个点的滑动平均,效果就很好。

返回列表