ARTICLE DETAIL

资讯详情

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

毫米波雷达速度模糊与Doppler相位补偿:TDM-MIMO实测与解模糊方案

毫米波雷达速度模糊与Doppler相位补偿:TDM-MIMO实测与解模糊方案 去年调试一套基于AWR2243的交通雷达时我遇到过一个很折磨人的现场问题道路上一辆匀速行驶的轿车目测速度在15m/s左右但雷达输出的速度却是负的而且速度谱上同时在正方向出现一个强度差不多的假目标。第一反应是速度模糊——拿最大不模糊速度公式一算果然chirp周期设得太长v_max只有10m/s出头15m/s的目标必然折叠。可当我把chirp周期改小、重新采集数据之后假目标还在而且正负对称怎么都消不掉。后来把原始ADC数据一帧一帧dump出来逐个chirp查Range FFT之后的相位序列才发现问题根本不只出在“速度超过v_max”这一层。真正隐蔽的是Doppler维的相位偏置TDM-MIMO下不同发射天线切换带来的固定相位跳变直接在Doppler谱里制造了一个和真实目标对称的镜像峰。这个镜像峰被CFAR检测成了第二个目标解模糊算法自然也被带偏了。这篇文章就把这两个坑放在一起讲先讲清楚速度模糊和Doppler相偏各自的数学本质再给出完整的相位补偿代码和双chirp周期解模糊方案最后分享一些实测踩坑经验。内容适合正在用TI毫米波雷达做脱机数据处理的工程师也适合刚接触DCA1000采集数据、准备自己写RadarCube处理流程的同学。1. 速度模糊与Doppler相位偏置一次测量里的两个坑1.1 速度模糊的本质慢时间采样与NyquistFMCW雷达测速的基本思路是把同一距离门内多个chirp的相位变化提取出来。发射chirp序列时相邻chirp的间隔为T_c这个T_c就是慢时间维度的采样周期。目标运动使得每个chirp之间的回波相位不断累积形成慢时间信号其频率就是Doppler频率f_D 2v / λ其中v是径向速度λ是载波波长。对慢时间信号做FFT就是标准的频谱分析。既然是数字化采样自然受Nyquist定理约束慢时间采样率是1/T_c能无模糊测量的Doppler频率范围只有[-1/(2T_c), 1/(2T_c)]。换算成速度v_max λ / (4T_c)这就是最大不模糊速度的来历。注意这个式子里的4不是2。很多人容易记错我当年也记错过——推导时Doppler频率本身有个2v/λNyquist又要求频率绝对值小于1/(2T_c)两个2叠在一起就是4所以v_max才这么容易随着chirp周期增大而迅速变小。以77GHz雷达λ≈3.9mm为例不同chirp周期对应的v_max大致如下chirp周期Tcv_max77GHz100μs9.74 m/s80μs12.18 m/s60μs16.23 m/s40μs24.35 m/s一旦真实速度v超过v_maxDoppler FFT的峰值就会越过频率边界“折”回来落在错误的位置上。测量到的速度v_det和真实速度v的关系是v_det v - 2m·v_max这里的m是整数取值使得v_det恰好落在[-v_max, v_max]区间。比如v_max12.18m/s、真实速度15m/s时m1v_det15-24.36-9.36m/s。目标明明向前开测出来却是负数这就是车规雷达里常说的“速度折叠”。这个m就是速度模糊问题里的关键未知量。整个解模糊过程本质上就是找这个整数m。但问题来了如果Doppler谱里本身就有很多虚假峰哪怕m的搜索算法写得再好也会匹配到一堆伪解。这就引出了第二个坑。1.2 chirp相位偏置为什么会让Doppler谱“多出一个人”在我那次现场调试里真正让我意外的不是速度折叠而是折叠目标旁边对称出现的那个假峰。检查了天线耦合、反射面、遮挡这些因素之后最后定位到了TDM-MIMO的发射天线切换时序上。TI的毫米波雷达做MIMO时通常采用TDM方式多个发射天线分时发射chirp接收天线同时接收。比如两发四收慢时间维的chirp序列就是TX1、TX2交替出现的。问题在于TX2的chirp相比TX1的chirp在发射时间上会有一个固定的时间偏移——这个偏移在硬件上由chirp配置和PLL锁定时间决定通常就是半个chirp周期。这个偏移本身不是问题问题是ADC采样和混频之后这个时间偏移会转化为每个chirp上的一次固定相位偏移而且这个偏移量还和目标的距离Range bin频率有关。让我用直观的方式解释假设一个匀速目标在慢时间维本来应该是一个理想正弦信号测Doppler就是一条干净的谱线。现在每隔一个chirp信号相位被强制跳变一次相当于在这个正弦信号上叠加了一个周期性方波调相。方波调相在频域会产生什么会产生以目标真实谱线为中心的一系列镜像分量。其中最强的两个分量正好出现在目标速度关于零点的对称位置。如果目标本身就在运动这个镜像峰会让你以为同时存在一个反方向同速的目标。更麻烦的是当目标真实速度接近v_max时真实目标折叠后的谱线和镜像峰的谱线会靠得很近。CFAR检测时两个峰都过了门限后端的关联逻辑就会完全混乱。1.3 为什么解模糊之前必须先补偿相位很多做速度解模糊的方案比如双chirp周期法、多帧速度匹配法前提都是Doppler谱里每个峰对应一个真实目标。一旦谱线里混入了TX切换带来的镜像峰这些方法的搜索空间里就会出现大量伪解。我实际调试时的体会是速度解模糊不是先解决“整数m”而是先解决“谱线干净度”。正确顺序应该是先做Doppler相偏补偿把每个chirp上的固定相位跳变消除得到一个干净的Doppler谱然后再根据速度折叠模型去搜索整数m。跳过第一步直接做解模糊等于让一个视力有问题的人去数远处的鸽子——数错才是正常的。2. TI雷达原始ADC数据里的相位信息解析与预处理2.1 从DCA1000到RadarCube数据格式与重排要补这个相位第一步是把DCA1000采集的原始ADC数据正确解析出来。DCA1000输出的二进制文件本质上是LVDS数据线上比特流的十六位对齐版本。每个复数采样点由两个16bit整数组成实部和虚部连续存储。如果板卡配置为4条lane数据按照lane交织顺序排列即第一组数据属于lane0第二组属于lane1以此类推。下面这个MATLAB读取函数是工程中最常见的形式不依赖TI SDK纯手写文件解析function adcData readDCA1000(filename, numAdcSamples, numChirps, numLanes) fid fopen(filename, rb); rawData fread(fid, uint16); fclose(fid); % 去掉可能存在的文件头部分版本DCA1000支持metadata % rawData rawData(headerLen1:end); % 偶数为I路奇数为Q路 numSamples floor(length(rawData)/2); dataI rawData(1:2:end); dataQ rawData(2:2:end); dataCplx dataI 1j*dataQ; dataCplx dataCplx(1:numSamples); % 四lane交织拆解 % 每个lane在每个采样周期输出一个复数样本 dataMat reshape(dataCplx, numLanes, []); % 重排为主流的RadarCube维度: [numAdcSamples, numChirps, numLanes] adcData zeros(numAdcSamples, numChirps, numLanes); for lane 1:numLanes laneData dataMat(lane, :); adcData(:, :, lane) reshape(laneData, numAdcSamples, numChirps); end end读取之后要注意lane到虚拟天线的映射。不同板卡的LVDS lane分配不同有的lane0对应RX1有的对应RX2这个必须查板卡原理图不能用别人的处理脚本直接套。映射搞错的话角度维完全不可用但距离和Doppler维看起来还正常特别坑。2.2 Range FFT之后的相位模型用上面的函数拿到ADC原始数据后首先做距离维FFTRange FFT。对每个chirp、每个接收天线分别做Nfft点FFT得到的数据矩阵维度是[NfftRange, numChirps, numRxAntennas]。在理想情况下某个距离门k处、第m个chirp的复数值可以写成S[m,k] A·exp(-j·4π·f_c·R/c) · exp(j·2π·f_D·m·T_c)其中第一项是由目标初始距离决定的常数相位第二项是目标运动导致的多普勒相位积累A是目标回波幅度。这个模型是理想化的也是我们期望看到的慢时间维的相位只随速度线性变化做Doppler FFT就能干净地提取速度。但TI这种多天线TDM雷达在实际采集时上式需要修正。每个chirp的发射时刻相对“理想等间隔发射时刻”可能存在一个偏移量Δt_m。这个偏移的来源有几个TDM-MIMO中不同TX天线的切换延迟、PLL每次重触发后的settling时间、ADC采样触发与chirp启动沿之间的相对延时。引入这个偏移后Range FFT的输出变成S[m,k] ≈ A·exp(-j·4π·f_c·R/c) · exp(j·2π·f_D·m·T_c) · exp(-j·2π·f_k·Δt_m)这里f_k是第k个Range bin对应的中频频率f_k k·Fs/NfftRange。新多出来的第三项就是过会儿要重点补偿的相位偏置。2.3 补偿因子的工程推导从上面的相位模型看相位偏置项是exp(-j·2π·f_k·Δt_m)。要消除它只需在Range FFT之后、Doppler FFT之前对每个chirp、每个Range bin乘一个共轭因子C[m,k] exp(j·2π·f_k·Δt_m)这就是整个Doppler相偏补偿的核心公式。看起来简单但工程实现时有几个点容易出错。第一f_k要算准。如果Range FFT做了补零或加窗bin对应的频率分辨率变了f_k的公式也要相应调整。更稳妥的办法是先确认Range FFT的频率轴再逐bin生成补偿矩阵。第二Δt_m的获取。对于TDM-MIMOΔt_m通常是周期性的。以两发为例TX1的chirp取Δt0TX2的chirp取ΔtT_c/2取决于配置里发射天线之间的时间偏移。但实际硬件中的settling时间会带来额外常数项不能完全依赖配置值。工程上最可靠的方法是用一个固定静止目标做标定先不补偿直接看Doppler谱里镜像峰的位置反推出实际的Δt_m再固化到代码里。第三也是最容易碰到的坑补偿因子的符号。不同的推导方式会把相位偏置项定义成正号还是负号这取决于芯片内部混频和下变频的结构。我在项目里吃过一次亏代码里符号写反补偿做完目标速度全部反向还多了一个不该有的谱峰。后来学乖了用静止目标标定静止目标补偿后Doppler频率必须为零如果不是零把补偿因子取共轭再试。3. 完整代码示例Doppler相偏补偿与速度估计3.1 MATLAB处理流水线把上面的推导落地成完整可运行的MATLAB流水线。这个脚本接收DCA1000读取好的adcData输出补偿前后的Range-Doppler谱和速度估计结果%% 处理参数 fc 77e9; % 载频 77GHz c 3e8; % 光速 lambda c/fc; Fs 10e6; % ADC采样率根据实际配置修改 NfftRange 256; % 距离FFT点数 NfftDoppler 128; % Doppler FFT点数 Tc 80e-6; % chirp周期 numChirps 128; % 一个frame内的chirp数 numRx 4; numTx 2; %% 假设adcData已经通过readDCA1000读入并重排 % adcData维度: [numAdcSamples, numChirps, numLanes] % 这里假设lane已经映射成虚拟天线 % 1. 距离维FFT rangeFFT fft(adcData, NfftRange, 1); % 2. 构造慢时间维相位补偿矩阵 % 频率轴对应的Range bin频率 freqAxis (0:NfftRange-1) * Fs / NfftRange; % TDM-MIMO两发: 偶数为TX1, 奇数为TX2 % TX2相对TX1存在Tc/2的时间偏移实际情况根据配置修正 chirpIdx 0:numChirps-1; txIdx mod(chirpIdx, numTx); deltaT zeros(1, numChirps); deltaT(txIdx 1) Tc/2; % TX2的chirp时间偏移 % 补偿因子矩阵: exp(j*2*pi*f_k*deltaT_m) phaseComp exp(1j * 2 * pi * freqAxis. * deltaT); % phaseComp维度: [NfftRange, numChirps] % 3. 对所有接收天线施加Doppler相偏补偿 for rx 1:numRx rangeFFT(:, :, rx) rangeFFT(:, :, rx) .* phaseComp; end % 4. Doppler维FFT dopplerFFT fftshift(fft(rangeFFT, NfftDoppler, 2), 2); % 5. 提取速度轴 fdAxis (-NfftDoppler/2:NfftDoppler/2-1) / NfftDoppler / Tc; velAxis fdAxis * lambda / 2; % 6. 对某个Range bin做峰值检测示例 % 这里取最大能量所在range bin做演示 rdMap squeeze(mean(abs(dopplerFFT), 3)); [~, kPeak] max(max(rdMap, [], 2)); [~, mPeak] max(rdMap(kPeak, :)); detectedVelocity velAxis(mPeak); fprintf(检测速度: %.2f m/s\n, detectedVelocity);这段代码的核心就是第三步。很多人拿到TI原始数据后直接Range FFT再Doppler FFT省略了phaseComp这一步出来的Doppler谱在TDM-MIMO场景下一定会有镜像峰。尤其当目标所在Range bin距离较远时f_k比较大这个相位偏置的影响会成倍放大。3.2 模拟实验补偿前与补偿后的Doppler谱对比如果你手头暂时没有DCA1000采集的数据可以用下面这段模拟脚本看清楚相位跳变对Doppler谱的影响。它不依赖真实硬件只展示了慢时间维信号被方波调相后的频谱变化%% 演示TDM-MIMO相位跳变对Doppler谱的影响 N 256; Tc 80e-6; fD 2000; % 真实Doppler频率 2000Hz对应约3.89m/s rangeFreq 5e6; % 当前Range bin的中频频率5MHz用于构造相位跳变 deltaT Tc/2; % TX2相对TX1的时间偏移 chirpIdx 0:N-1; txIdx mod(chirpIdx, 2); % 理想慢时间信号 sClean exp(1j * 2 * pi * fD * chirpIdx * Tc); % 受TX切换相位跳变污染的信号 % 相位偏置项: exp(j*2*pi*rangeFreq*txIdx*deltaT) phaseJump exp(1j * 2 * pi * rangeFreq * txIdx * deltaT); sPolluted sClean .* phaseJump; % 补偿后的信号: 乘以共轭因子 sCompensated sPolluted .* conj(phaseJump); win hann(N).; fdAxis (-N/2:N/2-1) / N / Tc; velAxis fdAxis * (c/fc) / 2; specClean abs(fftshift(fft(sClean .* win, N))); specPolluted abs(fftshift(fft(sPolluted .* win, N))); specCompensated abs(fftshift(fft(sCompensated .* win, N))); figure; subplot(3,1,1); plot(velAxis, specClean); title(理想Doppler谱); subplot(3,1,2); plot(velAxis, specPolluted); title(存在TX相位跳变); subplot(3,1,3); plot(velAxis, specCompensated); title(相位补偿后);跑完这段脚本你会看到中间那幅图里目标附近出现一个对称镜像峰幅度可能只比真实峰低几个dB。如果目标真实速度本身接近v_max这个镜像峰会直接和折叠后的目标峰混在一起肉眼几乎无法区分。补偿之后镜像峰消失恢复成一条干净谱线。这个模拟足以说明为什么“先补偿、再解模糊”是必须的流程。3.3 Python/numpy版本的补偿实现团队里也有人用Python做算法验证我把补偿逻辑整理成numpy实现逻辑和MATLAB版本完全一致import numpy as np def doppler_phase_compensation(range_fft, range_bin_freq, delta_t): 对Range FFT后的数据做Doppler相偏补偿 参数 ---- range_fft : np.ndarray 维度 [num_range_bins, num_chirps, num_rx] 的复数矩阵 range_bin_freq : np.ndarray 每个Range bin对应的中频频率长度 num_range_bins delta_t : np.ndarray 每个chirp相对理想发射时刻的时间偏移长度 num_chirps 返回 ---- range_fft_comp : np.ndarray 补偿后的Range FFT数据 # 补偿因子: exp(j * 2π * f_k * Δt_m) # 如果实测发现符号反了把这里改成负号 comp np.exp(2j * np.pi * np.outer(range_bin_freq, delta_t)) # comp 维度 [num_range_bins, num_chirps] return range_fft * comp[:, :, np.newaxis]这个函数可以单独复用。实际工程中我建议把读取、重排、Range FFT、相位补偿、Doppler FFT拆成五个独立函数方便单独调试。相位补偿这一个函数单独验证是最重要的因为它的错误最隐蔽。4. 速度解模糊实战双chirp周期方案4.1 双chirp周期的解模糊原理Doppler相偏补偿解决了谱线干净度的问题但速度折叠本身还活着。想扩大无模糊速度范围工程上常用的一种做法是双chirp周期法在同一个frame内交替发射两组不同重复周期的chirp一组chirp周期为T1另一组为T2。两组chirp分别做Doppler处理各自得到折叠速度v_det1和v_det2。因为它们对应的v_max不同对同一个真实速度v两组数据折叠的整数m和n也不同。真实速度满足两个等式v v_det1 2m·v_max1 v v_det2 2n·v_max2解模糊变成寻找整数对(m,n)使得两个候选速度在给定容差内一致。只要T1和T2取得合适组合后的最大不模糊速度会远大于单组的v_max。理论上当v_max1和v_max2成整数比且互质时组合后的不模糊范围等于两者的最小公倍数。实际选择T1、T2时要注意两个限制。一是T_c必须满足最大探测距离约束T_c 2R_max/c否则距离维的采样会混叠到下一个chirp导致目标距离解错。二是两组chirp的Doppler分辨率最好相近否则解模糊搜索时误差不一致容易匹配错。4.2 MATLAB解模糊演示代码下面是一个完整的双chirp周期解模糊脚本用模拟数据验证整个处理链路%% 双chirp周期速度解模糊演示 fc 77e9; c 3e8; lambda c / fc; T1 80e-6; T2 60e-6; vMax1 lambda / (4*T1); % 约12.18 m/s vMax2 lambda / (4*T2); % 约16.23 m/s vTrue 20; % 真实速度 20 m/s超过两个v_max %% 模拟两组chirp的Doppler测量 N 128; fdTrue 2 * vTrue / lambda; % T1组Doppler采样率1/T1折叠 s1 exp(1j * 2 * pi * fdTrue * (0:N-1) * T1); fdDet1 mean(diff(unwrap(angle(s1)))) / (2*pi) / T1; vDet1 fdDet1 * lambda / 2; % T2组 s2 exp(1j * 2 * pi * fdTrue * (0:N-1) * T2); fdDet2 mean(diff(unwrap(angle(s2)))) / (2*pi) / T2; vDet2 fdDet2 * lambda / 2; fprintf(真实速度: %.2f m/s\n, vTrue); fprintf(T180us 测得折叠速度: %.2f m/s (vmax%.2f)\n, vDet1, vMax1); fprintf(T260us 测得折叠速度: %.2f m/s (vmax%.2f)\n, vDet2, vMax2); %% 解模糊遍历整数对(m,n) bestErr inf; vEst 0; for m -5:5 vCand1 vDet1 2*m*vMax1; for n -5:5 vCand2 vDet2 2*n*vMax2; err abs(vCand1 - vCand2); if err bestErr bestErr err; vEst (vCand1 vCand2) / 2; end end end fprintf(解模糊估计速度: %.2f m/s\n, vEst);这段代码运行后会输出类似真实速度: 20.00 m/s T180us 测得折叠速度: -4.35 m/s (vmax12.18) T260us 测得折叠速度: -12.47 m/s (vmax16.23) 解模糊估计速度: 20.00 m/s单看任何一组测量结果都离真实速度差得很远而且方向都反了。但两组折叠速度放在一起通过遍历整数对就能锁定真实速度。这个演示说明了速度解模糊的数学本质不是“猜测”而是利用两组不同模糊区间的观测值互相约束唯一确定m和n。4.3 在TI传感器上配置双chirp组要在TI的mmWave传感器上实际生成两组不同周期的chirp需要在配置文件中定义两个chirp segment group。以AWR2243开发板为例可以在同一个frame内交替发射两组profile不同的chirp一组使用80μs周期、另一组使用60μs周期两个group之间的发射间隔要保证系统能在快速重配置PLL后稳定。配置时最关键的一点是确保DCA1000采集的数据能区分两组chirp。如果两组chirp在同一个frame内连续发射数据流里只有chirp索引后端处理时要用chirp的索引奇偶性或固定的group起始索引把它们拆开。不少人在这一步出错把两组chirp混在一起做Doppler FFT结果速度谱一片混乱。另一个容易被忽略的点是相位补偿的deltaT数组也要跟着chirp分组变化。如果T1和T2两组chirp的发射时序不同TX切换的时间偏移也可能不同不能复用同一份补偿因子。我在代码里建议将deltaT与chirp索引一一对应而不是写成固定值。5. 实测中我踩过的坑和现在的处理习惯5.1 数据位宽、字节序和文件头DCA1000导出文件的格式在不同固件版本下略有差异。早期版本的bin文件没有文件头直接从采样数据开始后续版本如果开启了metadata选项文件头部会多出几百字节的RTL数据不跳过的话整个数据流都会错位。我的习惯是先用十六进制编辑器打开文件看一眼前几行确认数据的起始位置再写解析代码避免用一套脚本吃遍所有版本。字节序问题也常被忽略。DCA1000输出的是小端序16bit整数但有些抓包工具或数据转换脚本会默认按大端处理。判断方法很简单如果I路数据在0附近呈密集小幅度抖动但Q路全是乱数基本就是字节序反了把实部和虚部的高低位互换一次即可。5.2 补偿“部分chirp”比不补偿更糟有人为了提高处理速度只对奇数或偶数的chirp做相位补偿或者隔几个chirp补偿一次认为“反正大部分相位是对的”。我的实测结论是不完整的补偿引入的噪声比不补偿还大。原因在于Doppler FFT是对整个慢时间序列做相干积累任何非均匀的相位跳变都会在频谱上形成随机旁瓣压制信号。补偿必须是一整个frame内所有chirp都做一个都不能漏。这里的实现细节是构造phaseComp矩阵时必然保证和rangeFFT矩阵的维度对齐。如果frame中存在无效chirp或者补零位构造deltaT数组时要显式处理不能靠默认值。出问题时先打印phaseComp在几个关键chirp上的值确认是否按预期交替。5.3 补偿因子的符号务必用静止目标标定正文里我推导的补偿因子是exp(j·2π·f_k·Δt_m)对应相位偏置项为负号的情况。但TI不同芯片、不同固件版本甚至不同板卡的天线布局都可能让相位偏置项的实际符号发生变化。依赖推导值直接上板风险很高。我现在的处理习惯是每次拿到一批新数据先找一个静止强反射目标墙角、金属护栏都行对它的Range bin做Doppler谱分析。如果做完全部标准处理后这个静止目标的速度依然落在非零的Doppler bin上说明补偿符号或者deltaT数值有问题。把补偿因子取共轭再跑一遍静止目标应当归零。这个步骤只需要几分钟但能省下后面几天的排查时间。5.4 与现有CFAR/测速流程衔接相位补偿必须放在Doppler FFT之前、Range FFT之后这一点在代码里体现得很清晰。但如果你用的是TI SDK自带的处理链路比如mmwave demo里的数据处理模块就要特别注意SDK内部某些版本已经做了类似的相位校正再叠加一层自己的补偿就相当于二次补偿会把正确信号也扭曲掉。脱机处理时我建议先用TI官方的mmWaveStudio导出一小段原始数据分别用“官方链路”和“自己的链路”处理对比结果一致后再全面替换。CFAR检测放在相位补偿之后做顺序不能颠倒。因为补偿前的镜像峰会以相干积累的方式消耗掉CFAR的检测裕度导致补偿后真实目标的SNR反而变低。如果处理流程里还有静态杂波抑制比如平均对消建议在相位补偿之后再做避免平均操作把相位校正信息抹掉。还有一个实际建议把每次采集的板卡配置、DCA1000参数和相位补偿系数一起存档。TI雷达的相位特性不会完全一致换一块板卡后直接用旧系数未必能复现正确结果。我吃过一次亏差点以为新板卡硬件有问题后来才发现是补偿系数没跟着更新。做脱机算法开发版本管理不只是管理代码参数文件和标定结果一样重要。
返回列表