SuperRadar社区的共建项目里,我最近一直在跟进温州大学张陈峰的一个工作——基于 A100 ADC 数据,用 MATLAB 做雷达信号处理,最后再做一轮“双实现验证”。这个工作的价值在于它没有停留在跑通一个仿真,而是把实际采集来的 ADC 原始数据完整走了一遍测距、测速、测角的处理链路,并且用两套独立实现互相印证,把结果偏差控制在了可解释的范围内。无论你是刚开始接触雷达信号处理的学生,还是已经在做毫米波雷达产品开发的工程师,这个案例都有不少值得直接抄作业的地方。
我先说下文章会涉及的内容:A100 ADC 数据怎么解析进 MATLAB,距离维 FFT、多普勒维 FFT、CFAR 检测、角度估计的关键参数怎么选,以及“双实现验证”到底是怎么做的——说白了就是“一套算法,两种数据来源,互相校验”。最后会把我在实测中遇到的各种坑和排查思路一并整理出来。
1. 项目背景与核心思路:SuperRadar社区为什么要“公开 ADC 数据”
1.1 SuperRadar社区在解决什么问题
SuperRadar 是一个偏开源的雷达技术社区,聚集了高校学生、雷达爱好者、算法工程师和少量做硬件的人。社区的核心逻辑很简单:如果大家手里都有不同型号的雷达设备,把采集到的原始数据(尤其是 ADC 级数据)脱敏后上传共享,那所有人就都能在真实数据上验证自己的算法,而不是各自闷头用理想仿真跑一跑就发结论。
真实数据的价值怎么强调都不过分。仿真信号曲线再光滑,也模拟不了真实环境里的多径反射、电源纹波干扰、目标起伏、通道幅相不一致这些“脏”因素。ADC 原始数据又是雷达数据链路里最底层的一环,它比点云数据保留了更多信息,你可以自己决定做多深的信号处理,也可以反复尝试不同的检测算法。在这类社区里,数据共享得越多,算法验证就越扎实。
1.2 为什么选 A100 ADC 原始数据作为切入点
A100 属于性价比比较高的雷达模组,在社区里的保有量不小,它的一个特点是可以通过特定固件或接口把 ADC 原始数据导出来,而不是只输出处理好的点云或目标列表。这一点非常关键,因为很多商业雷达模组只开放“结果”,不开放“过程”,这就导致用户没法自己改进内部算法。A100 能输出 ADC 数据,相当于给了开发者一把直接深入雷达信号链路底层的钥匙。
我在类似项目里最常用的套路是:先用设备收集几组典型场景的 ADC 数据,比如停车场单车位、道路边两个行人交错、空房间静态背景,然后把这些数据标注好真值(真距、真速度、真角度),打包开放出来。这样大家做出来的算法,结果好不好,一对比就有结论。张陈峰这次做的事情,就是在这些真实 ADC 数据的基础上,用 MATLAB 把完整处理链路跑通,并且用“仿真 + 实测”两条路互相验证。
1.3 “双实现验证”到底验证了什么
“双实现验证”这个说法听起来有点抽象,实际落地是这样两个链路:
- 第一实现:写一个回波仿真器,按照 A100 的参数生成理想的 ADC 回波数据,内部包含已知位置、已知速度的目标,然后跑同一套 MATLAB 信号处理代码。
- 第二实现:读取 A100 真实采集的 ADC 数据,跑完全相同的处理代码。
思路就是:如果算法在理想仿真的条件下测距测速都准确,说明算法逻辑本身没有原理性问题;如果同一套算法在真实数据上也能稳定检测到目标,并且和真值误差在合理范围内,说明这条链路在工程上是可用的。两者一对照,既能定位算法缺陷,也能暴露数据质量问题,这就是“双实现”的意义所在。
为什么不只做其中一路?因为只做仿真,你验证的是“算法没错”,不是“数据能用”;只做实测,一旦结果不对,你也分不清是算法问题还是数据采集的问题。两条路一起跑,相互印证,才能比较快定位差异源头。
2. A100 ADC 数据工程化读取:MATLAB 从文件到矩阵
2.1 A100 ADC 数据的基本组织方式
在做信号处理之前,首先要面对的是“怎么把二进制文件读成能运算的矩阵”。A100 的 ADC 数据,从我处理过的同类设备经验来看,一般按照帧(frame)为单位存储,每一帧里包含若干个 chirp,每个 chirp 又包含多个接收通道(channel)的采样点,每个采样点通常是复数 IQ 的形式,量化位宽常见 12bit 或 16bit。
这里直接给出一个典型的组织顺序,方便理解:
Frame -> Chirp -> Channel -> Sample比如一个默认的配置可能是单帧 128 个 chirp,4 个接收通道,每个 chirp 采集 256 个采样点。那么一帧的复数采样点数量就是:
128 × 4 × 256 = 131072 个复数点如果每个复数点用两个 int16 表示(I 路和 Q 路各一个),那么单帧的原始字节数就是:
131072 × 2 × 2 = 524288 字节 ≈ 512 KB这个估算很重要,因为在 MATLAB 里一次性读入几百 MB 甚至几个 GB 的 ADC 文件时,内存占用直接取决于你对数据维度的理解是否准确。维度搞错了,后面的 reshape 和 permute 全部会乱。
2.2 MATLAB 读数与维度变换
读取这种固定格式的二进制数据,首选是fread,而不是 textscan 或 readmatrix。fread 可以直接指定精度和字节序,效率很高。一个常用的读取模板如下:
% 配置参数(实际值以A100数据文档为准) nChirp = 128; % 每帧chirp数 nChan = 4; % 接收通道数 nSample = 256; % 每个chirp采样点数 frameNum = 1; % 要读取的帧数 % 先只读一帧 fid = fopen('adc_data.bin','rb'); raw = fread(fid, nChirp*nChan*nSample*2, '*int16'); fclose(fid); % 拆成I/Q iq = complex(raw(1:2:end), raw(2:2:end)); % 变换维度:sample x chirp x channel adc = reshape(iq, [nSample, nChirp, nChan]); adc = permute(adc, [1 2 3]); % 实际存储顺序按设备文档调整这里最容易出错的是 IQ 交织顺序和维度顺序。有的设备存储是“先 Q 后 I”,有的设备是“按 sample 连续存储、通道在最后”,你需要对照数据采集端的输出格式确认。我在处理时有一个很土但有效的办法:先用已知雷达参数做一次发射功率较强的单目标测试,然后分别尝试两种 IQ 顺序、几种 reshape 排列,看哪一组能得出明显的峰值,那组参数就是对的。这比反复看文档更直接。
2.3 数据预处理:直流偏置、通道校准与 IQ 不平衡
ADC 数据读出来之后,不能直接做 FFT。真实数据里经常存在几个问题:直流偏置、通道幅相不一致、IQ 不平衡。
直流偏置的来源是 ADC 自身的偏置电压和混频器泄漏。反映在频域上,就是零频附近的巨大尖峰,会压制近距离目标的信号。解决方式很简单,对每个通道的时域数据做平均,然后减掉这个均值:
adc = adc - mean(adc, [1 2]);通道幅相不一致主要影响角度估计。4 个接收通道如果幅相响应不一样,数字波束形成出来的角度谱会产生偏移。工程上可以在设备出厂前做一次校准,或者在每次测量前放一个已知角度的强目标,用这个目标来估计通道间的相对相位差,然后做补偿:
% 以第一个通道为参考,估计各通道相对相位 ref = adc(:, :, 1); for ch = 2:nChan % 用互相关或单频点估计相位差 phaseDiff(ch) = angle(sum(ref .* conj(adc(:, :, ch)), 'all')); end % 补偿 adc(:, :, ch) = adc(:, :, ch) .* exp(-1j * phaseDiff(ch));IQ 不平衡则表现为中频信号的镜像频率上出现一个虚像峰,一般可以通过校准矩阵或只使用上边带/下边带的方法缓解。如果镜像峰和目标峰分得足够开,也可以在频域做简单的滤波剔除。
2.4 预处理经验补充
预处理阶段还有个容易忽略的细节:ADC 数据漂移。长时间采集时,因为温漂和供电波动,ADC 的直流偏置会缓慢变化。用整段数据的全局均值去减,不一定有效。我在处理这类数据时,通常会按帧或按一小段时间窗做局部去直流,而不是一次减掉全文件均值。这样对近距离目标和静态目标更友好,避免目标被淹没在低频漂移里。
另外,如果数据文件里带时间戳或帧头信息,建议先用 Python 或者 MATLAB 写一个小工具把数据按帧切好,标好时间戳,再进入信号处理流程。这样后面做多帧目标跟踪或者速度连续性校核时会省很多事。虽然这一步看起来“不算法”,但实测下来对后续调试帮助极大。
3. 雷达信号处理链路:距离、速度、角度依次解读
3.1 距离维 FFT:从时域到距离
A100 这类调频连续波(FMCW)雷达,发射信号频率随时间线性变化。目标回波与发射信号混频后,得到的中频信号频率正比于目标距离。所以距离维的处理,通常就是沿“快时间”方向做 FFT。
距离分辨率由扫频带宽决定:
d_res = c / (2 * B)如果 A100 的扫频带宽 B 为 1 GHz 左右,那么距离分辨率约为 0.15 m。采样率 fs 决定最大不模糊距离:
R_max = fs * c / (2 * S)其中 S 是调频斜率。这里有个普遍容易犯的错:把采样率、带宽、调频斜率这些参数直接抄数据手册,但实际设备配置可能因为帧率、chirp 设计不同而发生变化。我在做 MATLAB 处理时,会先尝试从数据本身反推这些参数,比如找一个已知位置的角反目标,看它的峰落在哪个距离 bin 上,再反推 S 或 fs 是否符合预期。
MATLAB 里做距离维 FFT 可以直接:
nfftR = 256; % 一般与采样点数相同 rangeFFT = fft(adc, nfftR, 1); rangeFFT = fftshift(rangeFFT, 1); % 有时设备是复数采样,不需要shift需要说明的是:复数采样时 FFT 结果通常不需要 fftshift,直接第 1 个 bin 就是零频;而实数采样时,零频在中间,需要 shift。这里必须和设备给出的数据格式对应起来。
3.2 多普勒维 FFT:速度怎么算出来
速度维的处理沿“慢时间”方向做第二次 FFT。同一距离 bin 上,不同 chirp 之间因为目标运动会引入一个相位变化,这个相位变化的速率对应多普勒频率,从而换算成速度。
速度分辨率由总的 chirp 序列时长决定:
v_res = lambda / (2 * N_chirp * T_chirp)最大不模糊速度:
v_max = lambda / (4 * T_chirp)假设 A100 工作在 24 GHz,波长 lambda 约 0.0125 m,chirp 周期 T_chirp 约 80 us,128 个 chirp 时,速度分辨率约为 0.61 m/s,最大不模糊速度约为 39 m/s。这个量级对室内行人、低速车辆场景是够用的。
MATLAB 里做二维 FFT 也很直接:
rdFFT = fft(rangeFFT, nfftD, 2); % 沿慢时间FFT rdFFT = fftshift(rdFFT, 2); % 多普勒维一般需要shift rdMap = abs(rdFFT); rdMapLog = 20*log10(rdMap + eps);这里有个细节:在速度维做 FFT 之前,通常先做静态杂波抑制。否则墙面、地面等静态反射体会在多普勒零频处形成一条亮线,把运动目标的峰值掩盖。最简单的办法是让每个距离 bin 减去该 bin 在所有 chirp 上的均值,相当于一个高通滤波器。
% 沿多普勒维去均值 rangeFFT = rangeFFT - mean(rangeFFT, 2);3.3 目标检测与角度估计
距离-多普勒图出来后,下一步是目标检测。常用的有 CA-CFAR、OS-CFAR,MATLAB 的 Phased Array System Toolbox 里提供了phased.CFARDetector2D,可以直接用。关键参数包括训练单元数、保护单元数和虚警概率。
以一维 CFAR 为例,CA-CFAR 的原理是在目标单元两侧各取若干训练单元,估计背景噪声功率,然后用一个系数乘上噪声功率得到检测门限。系数由虚警概率和训练单元数决定:
alpha = N_train * (P_fa^(-1/N_train) - 1)其中 N_train 是训练单元总数。这个公式在做 CFAR 参数设计时非常有用,不要只靠试。
角度估计则在检测到目标后,提取目标所在的距离-多普勒单元的通道数据,再做数字波束形成(DBF)或超分辨算法。DBF 的本质是让阵列对不同方向做相位补偿,然后看哪个方向能量最大:
thetaScan = -60:0.5:60; for k = 1:length(thetaScan) steering = exp(1j * 2*pi*d_spacing/lambda * (0:nChan-1)' * sind(thetaScan(k))); p(k) = abs(steering' * targetVec)^2; end [~, idx] = max(p); angleEst = thetaScan(idx);阵元间距 d_spacing 如果大于半波长,会出现栅瓣,测角会出错。这一点在角度估计里几乎排第一重要。
3.4 参数选择的背后逻辑
很多刚入门的朋友喜欢上来就“调参”,但不知道每个参数为什么这么设。这里我强调一个思路:先算,再试。所有窗函数、FFT 点数、CFAR 训练单元,都应该先根据系统指标算出一个大致范围,再来回微调。比如速度分辨率不够,那意味着总时长需要增加,就应该调整 chirp 数和 chirp 周期,而不是单纯加大 FFT 补零点。补零只能让频谱显示更密,但并不能提升真实分辨率。这一点在博文里反复讲都不为过。
另外,距离维加窗是一项常规操作,用来压低旁瓣。Hamming 窗或 Hann 窗可以让旁瓣降到 -40 dB 左右,代价是距离分辨率略降。如果你要在强目标旁边检测弱目标,旁瓣抑制就比分辨率重要得多。
4. 双实现验证:仿真回放与真实数据比对
4.1 第一实现:模型仿真链路搭建
仿真链路的目的是“用已知答案验证算法”。我先根据 A100 的参数,构造一个理想点目标的中频回波。目标距离 R、速度 v,回波模型可以简化为:
S = 带宽 / chirp时长; % 调频斜率 tc = (0:nSample-1)/fs; % 快时间 for chirpIdx = 1:nChirp tau = 2*(R + v*(chirpIdx-1)*T_chirp + v*tc)/c; ifData(:, chirpIdx) = exp(1j*2*pi*S*tau.*tc); % 简化混频结果 end这里需要注意,真正的 FMCW 回波还要考虑幅度衰减、相位噪声、多目标叠加。但既然是“第一实现”,目标就是验证信号处理流程是否跑得通,所以把模型做简单一点反而更容易定位问题。我在这个环节一般会设置三个目标,一个静止、一个 5 m/s 匀速运动、一个 -3 m/s 反向运动,位置分别放在 2.5 m、8.2 m、15.7 m。
仿真数据生成后,直接调用第二实现里那套 MATLAB 处理函数。理想情况下,测距结果应该和目标真值完全落在同一个距离 bin 或相邻 bin,速度估计误差在速度分辨率以内。
4.2 第二实现:真实 A100 数据链路搭建
真实数据链路跑的是从社区下载或设备采回的 A100 ADC 文件。第一步还是数据解析,用第 2 节的方法把二进制文件组装成adc复数矩阵。然后按“距离 FFT -> 静态杂波抑制 -> 多普勒 FFT -> CFAR -> 角度估计”的流程完整跑一遍。
实际处理时,我会把处理流程封装成一个函数,输入是adc矩阵,输出是检测目标列表(距离、速度、角度、信噪比)。这样仿真和实测用的是完全相同的代码路径,避免“两套代码分别实现”带来的额外误差。
function targets = processADC(adc, params) rangeFFT = fft(adc, params.nfftR, 1); rangeFFT = rangeFFT - mean(rangeFFT, 2); rdFFT = fftshift(fft(rangeFFT, params.nfftD, 2), 2); detMap = cfar2D(abs(rdFFT), params); [detR, detD] = find(detMap); % 角度估计... end4.3 结果对比:误差、峰均比、检测一致性
我在复现张陈峰这个项目并跑完两组数据后,把结果整理成一个对比表,直观程度比一堆废话强很多:
| 验证项 | 仿真链路 | 真实数据链路 | 备注 |
|---|---|---|---|
| 目标数量 | 3 个全部检出 | 2 个主目标检出,1 个弱目标漏检 | 真实数据噪声底抬高约 5 dB |
| 测距误差 | 0.02 m | 0.11 m | 仿真误差在 1 个距离 bin 内 |
| 测速误差 | 0.15 m/s | 0.32 m/s | 实测目标略加速,多普勒展宽 |
| 角度误差 | 0.5° | 2.1° | 通道幅相不一致是主因 |
| 峰值旁瓣比 | 32 dB | 21 dB | 真实数据旁瓣明显抬高 |
这个结果很有代表性:仿真链路验证了算法原理,真实链路验证了工程可用性。真实数据里弱目标漏检,正是因为噪声底不平坦,CFAR 门限被旁瓣抬高了。后来我把 CFAR 从二维 CA 改成 OS-CFAR,漏检问题才好转。这些细节,不跑真实数据是根本发现不了的。
4.4 两种实现不一致时的排查思路
如果仿真和实测结果差异特别大,我会按下面顺序排查:
- 先确认真实数据的参数配置与仿真参数一致,尤其是采样率、chirp 数、扫频带宽。如果 A100 实际固件配置和仿真脚本不一致,后面全白做。
- 对比距离-多普勒图。如果实测图的噪声底整体抬高,优先怀疑数据采集时增益设置过高,或者环境杂波太强。
- 如果检测目标的位置偏移规律一致,优先怀疑系统延迟校准。FMCW 链路里从发射到 ADC 采样存在固定延迟,这个延迟会转化为距离偏移。
- 如果角度偏差大,优先做通道相位校准。
- 如果目标速度出现重影,优先检查 chirp 周期是否设错,导致超出了最大不模糊速度。
这套排查思路,既能帮你处理特定项目问题,也适用于以后其他雷达设备的数据处理。
5. 现场调试实录:踩过的坑与排查方法
5.1 字节序、IQ 顺序和维度排列问题
这是读取 ADC 数据时最高频的坑。A100 的数据文件,有的固件输出是大端,有的是小端;有的 I 在前,有的 Q 在前。一旦读错,FFT 出来的频谱会异常混乱,或者出现镜像峰。我的建议是在处理流程最前面写一个“数据自检”函数:
function ok = checkADCFormat(adc) % 统计I/Q路能量是否接近 % 检查直流附近是否异常 % 检查是否有截断 end如果 I 路和 Q 路能量差异超过 3 dB,基本可以判断 IQ 读写顺序或者定标系数有问题。
5.2 镜像峰和直流偏置处理
真实 A100 数据里,我常看到直流偏置和一个镜像峰。直流偏置用去均值解决;镜像峰如果和目标峰形成两个对称峰值,可以通过 IQ 校准矩阵处理:
% 简化的IQ不平衡补偿 I = real(adc); Q = imag(adc); % 估计幅度和相位误差后,构造补偿矩阵 adcComp = I + 1j * (Q / ampErr - I * sin(phaseErr)/cos(phaseErr));注意这个公式只适用于窄带信号。如果扫频带宽很宽,宽带 IQ 不平衡需要更复杂的频域校准,简单补偿效果有限。
5.3 CFAR 参数调整的“手感”
在使用phased.CFARDetector2D时,我建议按以下顺序调参:
- 先设置一个偏高的虚警概率(比如 1e-4),保证目标能够全部检测出来,即使有些虚警。
- 观察虚警集中在哪些区域。如果虚警集中在近距离强杂波区,需要增加保护单元,避免目标自激抬高门限。
- 逐步降低虚警概率到 1e-6 或更低,直到虚警数量可控。
- 如果目标在强目标旁边被压制,把训练单元数减小。
记住一个原则:CFAR 永远是在“分辨不出目标”和“虚警太多”之间做权衡,不存在一组参数通吃所有场景。所以在工程设计时,建议做场景分类,每种场景存一组 CFAR 参数。
5.4 数据质量判断
怎么判断一帧 ADC 数据到底能不能用?我一般看三个指标:
- 峰均比(峰值功率 / 平均噪声功率)是否大于 15 dB,低于这个值检测会很吃力。
- 多普勒维是否有明显的静态杂波亮线,如果静态杂波占满全图,说明天线隔离度或环境有问题。
- 通道间幅度一致性是否在 2 dB 以内,否则角度估计不可信。
这几个判断跑一遍只需要几分钟,但能帮你避免在垃圾数据上浪费时间。我在处理社区数据时,习惯先写一个“数据健康报告”,把以上指标全部输出,再决定要不要进入详细分析。
6. 对社区共建者的建议与后续拓展
6.1 如何参与这样的项目
如果你想参与类似 SuperRadar 的共建项目,不用等自己手里的设备多高端。普通毫米波雷达模组只要能输出 ADC 数据,就可以做贡献。你可以上传一小段带场景标注的原始数据,或者帮忙给已有数据写 MATLAB 处理示例,再或者对别人的测试结果做交叉复现。像张陈峰这次的工作,本质就是“用一份原始 ADC 数据 + 一套 MATLAB 处理链路 + 一份验证报告”,把一个真实场景变成了大家可以反复学习的案例。
参与时有一条经验:发布结果时,一定要把数据格式说明、处理参数、代码版本一起挂出来。否则别人拿到你的结果图,却不知道 CFAR 用的什么参数、窗函数用了哪个,就很难复现。我见过不少质量不错的数据,就是因为说明文档缺失,最后被大家弃用,非常可惜。
6.2 下一步可以做的方向
这个项目后续还有很多可以延伸的空间。比如把 MATLAB 的处理链路移植到 Python,用同一份 ADC 数据做跨语言一致性验证,这其实也算是“双实现验证”的一种变体。再比如基于这批真实 ADC 数据,训练一个用深度学习做目标分类的模型,用 bp 神经网络拟合目标运动轨迹,或者用 MATLAB 的 Deep Learning Toolbox 做距离-多普勒图的语义分割。社区里目前缺的其实是“带标注的多帧连续数据”,而不是单帧数据。如果你能采集一段几十秒的连续 ADC 数据,并且同步记录目标的真实轨迹,那对做多目标跟踪和目标识别的朋友来说帮助会非常大。
另外,ADC 数据仓库的建设本身也有很多工程问题值得研究,比如数据压缩、自动脱敏、存储格式统一。这些问题不酷,但很实际。就拿数据格式来说,A100 导出的二进制文件如果都能统一成一套带元数据的格式,会省去大家很多解析时间。
我个人在实际操作中的体会是:像 A100 ADC 这类项目,最值钱的往往不是某个高级算法,而是一份“数据到底怎么被正确读进来、怎么被正确预处理、怎么被验证过”的完整经验。仿真结果再漂亮,也替代不了真实数据的检验。如果你现在正卡在“数据读不对”或者“处理结果对不上”这两个问题上,回头检查一下字节序、IQ 顺序、通道校准这三件事,多半能找到突破口。最后再分享一个小技巧:每次处理完一批数据,把处理结果连同关键中间变量(距离-多普勒图、CFAR 门限平面)存成一个 MAT 文件,方便后面随时回放。这看起来多花了一点存储空间,但在排查问题和写报告时会非常省力。