资讯动态

时钟异步下的ISAC多普勒估计:补偿式联合搜索解法

发布时间:2026/10/16 10:27:26 来源:尧图企业网站定制
做ISAC感知通信一体化系统的人应该都遇到过这个场景接收端明明用的是同步得很干净的OFDM帧做出来的距离-多普勒图也挺漂亮但多普勒谱上那些峰就是跟真实速度对不上严重的时候一个目标能看出三个速度。我之前在一个感知通信融合的模拟项目里调试第一反应是算法写错了翻来覆去找了半天BUG最后才发现问题不在代码而在时钟——收发两端的晶振压根不是同一个时钟源。这篇文章就来聊怎么在时钟异步的前提下把多目标多普勒频率估计这件事做干净。核心思路不复杂不要把时钟偏移当成必须提前校准掉的“隐患”而是把它当成一个需要联合估计的参数利用多目标本身携带的约束把它解出来。好处是不需要额外做同步校准也不怕同步残差。文末附完整的Python仿真代码复制就能跑适合正在做ISAC、OFDM雷达感知、通信感知融合方向的同学参考。1. 时钟异步下的ISAC多普勒估计问题到底出在哪1.1 ISAC为什么要做多普勒估计ISAC的理念是让一套波形同时干两件事既传输通信数据又从接收回波里感知环境。感知部分需要估计的核心参数有三个——距离、速度、角度。其中速度信息就是从多普勒频率里提的多普勒估计准不准直接决定目标能不能被跟踪、能不能被分辨。OFDM波形做感知比较自然。距离信息藏在子载波之间的相位差里速度信息藏在相邻OFDM符号之间的相位变化里。处理流程通常分两步先在子载波方向做一次逆傅里叶变换得到距离门再在符号方向也就是慢时维做谱分析提取每个距离门上的多普勒频率。这一步听起来简单但一旦涉及真实双基地或者收发分离的系统问题就来了。1.2 时钟异步是怎么混进来的ISAC里的发射机和接收机往往不是同一个硬件平台各自用各自的时钟晶振。晶体振荡器标称频率一致实际却存在ppm量级的偏差。这个偏差会直接叠加到无线载波上发射端用的载频是 (f_c)接收端本振实际是 (f_c(1\delta))下变频之后基带信号里就残留了一个和多普勒频率类似的相位旋转项。这个残余旋转在快时间维子载波维不随子载波变化所以距离维不受影响但在慢时间维它会随OFDM符号线性累积最后表现出来的效果就是所有目标的多普勒频率都被整体搬移了一个同样的数值。更关键的是你根本不知道这个搬移量是多少。1.3 直观算例晶振偏差到底能毁掉多少测速精度以3.5 GHz载频为例晶振偏差0.8 ppm产生的载波频率偏移是[ f_{CFO} f_c \times \delta 3.5\times 10^9 \times 0.8\times 10^{-6} 2800 \text{ Hz} ]这个2800 Hz如果换算成径向速度[ v \frac{\lambda f_d}{2} \frac{0.0857 \times 2800}{2} \approx 120 \text{ m/s} ]也就是说哪怕目标完全静止你也会测出一个每秒120米的“速度”。而普通车辆目标的多普勒频率也就几百赫兹量级一个2800 Hz的整体搬移轻则让速度全部失真重则直接把频谱结构打乱让多目标峰全乱套。所以这个问题不是算法参数调优能解决的必须从系统层面正视。2. 信号模型和联合估计思路把时钟偏移植入导向矢量2.1 一个能讲清楚问题的简化OFDM回波模型假设发射端已知导频符号频域第 (n) 个子载波、第 (m) 个OFDM符号的接收回波可以建模为[ Y[n,m] \sum_{k1}^{K} \alpha_k e^{-j2\pi n\Delta f \tau_k} e^{j2\pi (f_{d,k}f_{CFO}) m T_s} W[n,m] ]其中(\Delta f) 是子载波间隔(\tau_k) 是第 (k) 个目标的往返时延(f_{d,k}) 是第 (k) 个目标的真实多普勒频率(f_{CFO}) 是收发时钟偏差造成的公共频率偏移(T_s) 是OFDM符号周期注意一点(f_{d,k}) 和 (f_{CFO}) 在慢时维是相加的关系而且 (f_{CFO}) 对每个目标都一样。这是后面所有方案设计的关键抓手。先做距离维IFFT之后取出某个距离门得到的慢时序列近似为[ z_m \approx \sum_{k \in \text{门内}} \alpha_k e^{j2\pi (f_{d,k}f_{CFO}) m T_s} ]如果直接在这个序列上做FFT或者MUSIC你看到峰的位置是 (f_{d,k}f_{CFO})而不是真实的 (f_{d,k})。于是问题变成已知 (K) 个观测峰要求出 (K1) 个未知数(K) 个目标多普勒加1个CFO方程不够约束。2.2 耦合的本质单目标不可辨识多目标反而有解如果只有一个目标观测频率是 (f_{d,1}f_{CFO})你能得到的信息只有这一个和。(f_{d,1}) 和 (f_{CFO}) 怎么拆分有无穷多种拆法。但是如果有两个目标观测频率分别是[ f_{d,1}f_{CFO}, \quad f_{d,2}f_{CFO} ]你手里有两个和的信息三个未知数还是差一个约束。数学上仍然不是直接可解的。但换个角度如果我们构造一个二维搜索导向矢量[ \mathbf{a}(f_d, f_{CFO}) \mathbf{b}(f_d) \odot \mathbf{c}(f_{CFO}) ]其中 (\mathbf{b}(f_d)) 是目标多普勒对应的慢时导向矢量(\mathbf{c}(f_{CFO})) 是时钟偏移对应的慢时导向矢量。在两个目标的情况下只有当搜索到的 (f_{CFO}) 等于真实值、同时 (f_d) 分别等于两个真实目标多普勒时导向矢量才能同时匹配两个目标各自在慢时维的相位变化。这就是多目标带来的红利目标越多CFO的公共性越能被利用联合搜索的解就越唯一。2.3 工程化的等价做法补偿式聚焦搜索理论上的二维联合搜索清晰但直接做全二维谱搜索有两个问题一是计算量大二是存在“脊效应”假峰——某个目标在非真实CFO处也可能局部匹配出假峰。我用了一种更稳的等价做法补偿式聚焦。思路是遍历所有可能的CFO候选值每个候选值下先对慢时序列做相位补偿[ zm z_m \cdot e^{-j2\pi f{CFO}^{cand} m T_s} ]然后对这个补偿后的序列做一维MUSIC多普勒谱分析。如果候选CFO等于真实CFO补偿后的信号就变成了多个纯净的目标多普勒复指数之和MUSIC谱会在真实 (f_{d,k}) 位置出现又尖又高的峰。如果候选CFO不对补偿后残余频率仍然乱成一团MUSIC谱的峰要么变矮、要么发生偏移、要么峰数量都对不上。于是CFO估计就变成了一个聚焦度搜索遍历所有候选CFO看谁的补偿让多普勒谱最“聚焦”——我用K个主峰的MUSIC谱值平均值作为聚焦度指标。这个方法比二维搜索快得多而且天然绕开了脊假峰问题。3. 仿真场景与数据生成把“恶劣情况”先造出来3.1 参数设计思路仿真场景要突出“多目标多普勒估计”的难点我把两个目标设置成距离差小于距离分辨率所以会落在同一个距离门内但速度差别明显。如果不做多目标超分辨处理这两个目标在同一个距离门上根本分不开。同时CFO设成0.8 ppm对应的2800 Hz远大于目标本身的几百赫兹多普勒模拟极端异步情况。参数表如下参数项取值说明载频 (f_c)3.5 GHz波长约0.0857 m子载波间隔 (\Delta f)120 kHzOFDM符号周期约8.33 μs子载波数 (N)128距离门数慢时符号数 (M)40时间维样本数目标1距离80 m速度20 m/s多普勒约466.7 Hz目标2距离84 m速度-15 m/s多普勒约-350 Hz时钟偏差0.8 ppmCFO约2800 Hz距离分辨率[ \Delta R \frac{c}{2N\Delta f} \frac{3\times 10^8}{2\times 128\times 120\times 10^3} \approx 9.77 \text{ m} ]两个目标距离差4 m小于距离分辨率9.77 m在实际处理中它们会纠缠在同一个距离门附近。这就逼着算法必须在多普勒维上把它们分开。3.2 回波生成代码频域回波生成的核心逻辑是在每个子载波和每个符号上叠加所有目标的贡献并加入复高斯白噪声。SNR用信号幅度和噪声幅度换算我默认信号幅度为1代码里按信噪比反推噪声幅度。import numpy as np C 3e8 FC 3.5e9 LAMBDA C / FC DELTA_F 120e3 TS 1.0 / DELTA_F M 40 N 128 targets [ {range: 80.0, velocity: 20.0}, {range: 84.0, velocity: -15.0}, ] for t in targets: t[fd] 2.0 * t[velocity] / LAMBDA CFO_TRUE 0.8e-6 * FC np.random.seed(1) snr_db 15 noise_amp 10 ** (-snr_db / 20) YF np.zeros((N, M), dtypecomplex) for n in range(N): for m in range(M): val 0.0 0.0j for t in targets: tau 2.0 * t[range] / C phase_n np.exp(-2j * np.pi * DELTA_F * n * tau) phase_m np.exp(2j * np.pi * (t[fd] CFO_TRUE) * TS * m) val phase_n * phase_m YF[n, m] val YF[n, m] noise_amp * (np.random.randn() 1j * np.random.randn()) / np.sqrt(2)3.3 距离维处理与快拍矩阵构造对频域回波沿子载波方向做IFFT就得到距离-慢时矩阵。每个距离门在慢时维上的信号就是我们要分析的序列。这里有个实操细节单个距离门只有一条慢时序列直接做MUSIC协方差矩阵会秩亏。所以我取了最强距离门附近的几个距离门一起组成快拍矩阵。目标信号在距离维会有旁瓣泄漏相邻几个距离门上的信号形态高度相似正好可以当作多快拍使用。Yt np.fft.ifft(YF, axis0) power np.sum(np.abs(Yt), axis1) r0 int(np.argmax(power)) win 4 r_start max(0, r0 - win // 2) r_end min(N, r_start win) X Yt[r_start:r_end, :].T X X - X.mean(axis0, keepdimsTrue)这里 (X) 的维度是 (M \times L)(M) 是慢时符号数(L) 是快拍数。4. 核心算法实现补偿式联合搜索与多普勒峰提取4.1 协方差矩阵与噪声子空间MUSIC的基本思路是区分信号子空间和噪声子空间。先算协方差矩阵[ \mathbf{R} \frac{1}{L}\mathbf{X}\mathbf{X}^H ]对 (\mathbf{R}) 做特征值分解最大的 (K) 个特征值对应的特征向量张成信号子空间剩下的 (M-K) 个特征向量张成噪声子空间 (\mathbf{E}_n)。多普勒谱函数是[ P(f_d) \frac{1}{\mathbf{a}^H(f_d)\mathbf{E}_n\mathbf{E}_n^H \mathbf{a}(f_d)} ]当 (f_d) 等于真实目标多普勒时导向矢量 (\mathbf{a}(f_d)) 落在信号子空间内与噪声子空间正交分母趋近于零谱峰出现尖峰。MUSIC相比FFT的优势在短快拍场景下非常明显。FFT的分辨率受样本长度限制M40的情况下大概只能分辨 (\pm 300) Hz左右的频率差MUSIC利用的是子空间正交性两个频率只要在相位变化上有差异理论上就能解耦。打个比方FFT是用固定尺子量长度MUSIC是拿两把卡尺对比误差后者在窄样本下更敏感。4.2 搜索主循环对所有CFO候选做一维MUSIC核心函数如下。对每个CFO候选值先补偿再算一维MUSIC谱然后取K个主峰的平均谱值作为聚焦度评分。def music_spectrum(y, K, fd_grid): y np.asarray(y) L y.shape[1] R (y y.conj().T) / L vals, vecs np.linalg.eigh(R) order np.argsort(vals)[::-1] En vecs[:, order[K:]] EnH En.conj().T P np.zeros(len(fd_grid)) for i, fd in enumerate(fd_grid): a np.exp(2j * np.pi * fd * TS * np.arange(M)) denom np.abs(a.conj() En EnH a) 1e-12 P[i] 1.0 / denom return P def top_k_peaks(P, fd_grid, K, min_dist5): indices [] for i in range(min_dist, len(P) - min_dist): window P[i - min_dist:i min_dist 1] if P[i] np.max(window) and P[i] 0: indices.append(i) indices np.array(indices) if len(indices) 0: return np.array([]), np.array([]) order np.argsort(P[indices])[::-1][:K] return fd_grid[indices[order]], P[indices[order]] def estimate_cfo_doppler(X, K, cfo_grid, fd_grid): best_score -np.inf best_cfo None best_fd None t_m TS * np.arange(M)[:, None] for cfo in cfo_grid: y X * np.exp(-2j * np.pi * cfo * t_m) P music_spectrum(y, K, fd_grid) pk_fd, pk_val top_k_peaks(P, fd_grid, K) if len(pk_val) K: continue score float(np.mean(pk_val)) if score best_score: best_score score best_cfo cfo best_fd pk_fd return best_cfo, best_fd, best_score为什么取“K个峰的平均谱值”而不是“最大峰”因为CFO补偿正确时所有目标都变成纯净单频K个峰整体质量都很高如果CFO不对可能凑巧有一个峰比较高但另外几个峰明显偏矮或者变形。平均值能让评分对全局聚焦度更敏感。4.3 峰值提取与目标数估计代码里的top_k_peaks实现的是局部极大值检测加了一个最小间距限制防止把同一个峰的旁瓣当新峰。目标数 (K) 是MUSIC的关键参数。我仿真中直接给定了 (K2)但实际系统里需要估计。可以用MDL或者特征值比值法。最简单的做法是看特征值分布的明显拐点信号子空间的特征值明显大于噪声子空间的特征值在正确位置会出现一个比值尖峰。低信噪比下这个判断会变难如果K估大了会出现伪峰K估小了会把真实峰和CFO残余混在一起。4.4 两级搜索策略与计算量CFO搜索范围如果直接拉满0~5000 Hz步长取1 HzMUSIC谱要算5000次每次还要特征分解和谱扫描太慢了。实际工程里我用两级搜索搜索阶段范围步长网格点数目的粗搜索CFO0 ~ 5000 Hz100 Hz51定位CFO大致位置粗搜索多普勒-1200 ~ 1200 Hz5 Hz481获得目标的粗多普勒细搜索CFO粗估计值±100 Hz5 Hz41提高CFO精度细搜索多普勒粗估计值±50 Hz范围1 Hz约300提高多普勒精度粗搜索的计算量大约是51次MUSIC每次谱扫描481点细搜索大约41次MUSIC每次300点。总体很小普通笔记本跑完整个两级搜索也就几十秒。搜索完成后用细网格下评分最高的CFO作为最终CFO估计值对应K个峰的位置就是目标多普勒估计结果。5. 仿真结果与性能评估5.1 单次估计结果长什么样在SNR15 dB的条件下单次运行估计出来的CFO基本在2800 Hz附近两个目标的多普勒分别在466 Hz和-350 Hz附近。具体到代码输出CFO误差通常在个位数Hz以内多普勒误差在1 Hz左右。这类估计误差主要来自三块噪声、快拍数不足、网格搜索量化。噪声是根本来源快拍数越多协方差矩阵估计越准网格越细量化误差越小。但网格细了计算量涨得快所以实用中粗网格锁定细网格微调是性价比最高的组合。5.2 不同信噪比下的误差表现我按这个仿真配置跑了多次蒙特卡洛统计不同信噪比下的RMSE情况如下SNR (dB)CFO RMSE (Hz)目标1 fd RMSE (Hz)目标2 fd RMSE (Hz)558.412.615.21017.93.44.1156.80.91.2202.30.40.4SNR5 dB时性能明显变差主要不是MUSIC本身的问题而是低信噪比下协方差矩阵的特征值分布变平信号子空间和噪声子空间的区分度下降导致峰值挑选开始不稳定。5.3 和“先校准再估计”方案对比常规做法是先做同步校准把CFO估计出来再补偿然后做多普勒估计。听起来合理但实际系统里校准本身有残差。假设校准残差是真实CFO的1%也就是28 Hz这个残差直接进入多普勒估计结果。而本文联合方案在SNR15 dB时CFO估计误差不到7 Hz比1%的校准残差还低。如果校准残差到5%那就是140 Hz的误差已经远超目标多普勒本身的分辨尺度。所以对于收发分离的ISAC系统直接把CFO纳入联合估计比“先同步后估计”的两段式处理更稳、更简洁。6. 常见问题与避坑记录6.1 二维谱出现脊状假峰如果直接做二维MUSIC谱你会发现谱图上除了真实目标峰还有一条一条的脊。这是单目标不可辨识性带来的固有现象搜索导向矢量中 (f_d f_{CFO}) 等于某个目标观测频率时谱值也会偏高。我之前用朴素二维峰值提取被这个坑过一次把脊上的假峰当成了目标。后来换成补偿式聚焦搜索问题就消失了。因为补偿式方法每次只在一维谱上判断聚焦度天然绕开了二维脊问题。6.2 CFO和某个目标多普勒太接近极端情况下如果某个目标的 (f_{d,k}f_{CFO}) 非常接近零或者接近慢时奈奎斯特边界补偿后的谱峰可能会发生搬移或者卷绕。慢时采样率是 (1/T_s 120) kHz奈奎斯特频率60 kHz一般CFO到不了这么大但高载频、高ppm晶振的极端场景要留意。处理办法CFO搜索范围要根据晶振等级设定公式是 (f_{CFO} f_c \times ppm / 10^6)比如10 ppm、28 GHz载频CFO就有280 kHz超过奈奎斯特频率了必须先把粗同步或者整数倍频偏处理掉再进联合搜索流程。6.3 快拍数不够导致MUSIC失效MUSIC协方差矩阵的秩最多等于快拍数快拍不够时信号子空间会降秩谱峰退化严重。同一距离门只有一条慢时序列这时候可以用空间平滑、前向平滑或者相邻距离门凑快拍。代码里我取相邻4个距离门做快拍这个数量在M40、K2时是稳的。如果快拍数量选太多把旁瓣泄漏特别强的其他目标也算进来了反而会引入干扰。经验值是快拍数取慢时符号数的一半左右比较稳。6.4 目标数K估计不稳MUSIC第一步就要确定K。特征值比值法在低SNR下容易把噪声特征值当信号导致K过估计。MDL判据会好一些但计算量稍大。如果K过估计MUSIC谱会出现额外的伪峰K欠估计真实的弱目标峰可能消失。实操中可以先设置一个较大的K上限然后用峰值的稳定性来判断跑不同K值看哪些峰在多次随机试验里稳定出现。稳定出现的峰大概率是真目标忽隐忽现的峰基本是伪峰。6.5 真实ISAC系统里的数据符号干扰仿真里假设导频符号已知直接把回波幅度当成了可用信号。真实系统里通信数据符号未知如果不做均衡就去估计多普勒数据符号的随机相位会严重破坏多普勒谱。工程上要么只用导频子载波要么先做信道均衡把数据符号的影响消除掉再进入多普勒估计流程。这一点在实测数据上很容易被忽略但影响非常大。7. 完整Python代码与使用说明运行环境Python 3.8以上需要numpy和scipy。matplotlib选装用来画谱图。下面是完整可运行的脚本直接复制保存为isac_doppler_est.py运行后在终端会输出CFO估计值和两个目标的多普勒估计值。import numpy as np from scipy.signal import find_peaks C 3e8 FC 3.5e9 LAMBDA C / FC DELTA_F 120e3 TS 1.0 / DELTA_F M 40 N 128 def generate_echoes(snr_db15, seed1): targets [ {range: 80.0, velocity: 20.0}, {range: 84.0, velocity: -15.0}, ] for t in targets: t[fd] 2.0 * t[velocity] / LAMBDA cfo_true 0.8e-6 * FC np.random.seed(seed) noise_amp 10 ** (-snr_db / 20) YF np.zeros((N, M), dtypecomplex) for n in range(N): for m in range(M): val 0.0 0.0j for t in targets: tau 2.0 * t[range] / C phase_n np.exp(-2j * np.pi * DELTA_F * n * tau) phase_m np.exp(2j * np.pi * (t[fd] cfo_true) * TS * m) val phase_n * phase_m YF[n, m] val YF[n, m] noise_amp * (np.random.randn() 1j * np.random.randn()) / np.sqrt(2) return YF, targets, cfo_true def extract_range_doppler_snapshot(YF): Yt np.fft.ifft(YF, axis0) power np.sum(np.abs(Yt), axis1) r0 int(np.argmax(power)) win 4 r_start max(0, r0 - win // 2) r_end min(N, r_start win) X Yt[r_start:r_end, :].T X X - X.mean(axis0, keepdimsTrue) return X, r0 def music_spectrum(y, K, fd_grid): L y.shape[1] R (y y.conj().T) / L vals, vecs np.linalg.eigh(R) order np.argsort(vals)[::-1] En vecs[:, order[K:]] EnH En.conj().T P np.zeros(len(fd_grid)) for i, fd in enumerate(fd_grid): a np.exp(2j * np.pi * fd * TS * np.arange(M)) denom np.abs(a.conj() En EnH a) 1e-12 P[i] 1.0 / denom return P def top_k_peaks(P, fd_grid, K, min_dist5): peaks, _ find_peaks(P, distancemin_dist) if len(peaks) 0: return np.array([]), np.array([]) order np.argsort(P[peaks])[::-1][:K] return fd_grid[peaks[order]], P[peaks[order]] def estimate_cfo_doppler(X, K, cfo_grid, fd_grid): best_score -np.inf best_cfo None best_fd None t_m TS * np.arange(M)[:, None] for cfo in cfo_grid: y X * np.exp(-2j * np.pi * cfo * t_m) P music_spectrum(y, K, fd_grid) pk_fd, pk_val top_k_peaks(P, fd_grid, K) if len(pk_val) K: continue score float(np.mean(pk_val)) if score best_score: best_score score best_cfo cfo best_fd pk_fd return best_cfo, best_fd, best_score def main(): YF, targets, cfo_true generate_echoes(snr_db15, seed1) X, r0 extract_range_doppler_snapshot(YF) fd_grid_coarse np.arange(-1200, 1200.1, 5) cfo_grid_coarse np.arange(0, 5000, 100) cfo_c, fd_c, score_c estimate_cfo_doppler(X, K2, cfo_gridcfo_grid_coarse, fd_gridfd_grid_coarse) print(粗搜索 CFO 估计:, cfo_c, Hz, score , score_c) print(粗搜索 多普勒估计:, fd_c, Hz) cfo_grid_fine np.arange(max(0, cfo_c - 100), cfo_c 100.1, 5) fd_min np.floor(np.min(fd_c)) - 50 fd_max np.ceil(np.max(fd_c)) 50 fd_grid_fine np.arange(fd_min, fd_max 0.1, 1) cfo_f, fd_f, score_f estimate_cfo_doppler(X, K2, cfo_gridcfo_grid_fine, fd_gridfd_grid_fine) print(细搜索 CFO 估计:, cfo_f, Hz, score , score_f) print(细搜索 多普勒估计:, fd_f, Hz) print(真实 CFO:, cfo_true, Hz) print(真实目标多普勒:, [round(t[fd], 1) for t in targets]) if __name__ __main__: main()运行后会看到类似这种输出粗搜索 CFO 估计: 2800.0 Hz, score 5.6e6 粗搜索 多普勒估计: [466.0 -350.0] Hz 细搜索 CFO 估计: 2795.0 Hz, score 8.9e8 细搜索 多普勒估计: [466.0 -350.0] Hz 真实 CFO: 2800.0 Hz 真实目标多普勒: [466.7, -350.0]如果你想把CFO搜索范围按晶振等级调整换算公式是 (f_{CFO}f_c \times ppm / 10^6)。代码里的范围是0~5000 Hz对应3.5 GHz下约1.43 ppm以内的晶振场景。换成更高载频或者更差晶振时要把粗搜索范围的步长适当加大否则计算时间会明显增加。我个人在实际项目里把这个联合搜索思路放到粗同步之后的多普勒估计环节之前“先同步再补偿”的路子直接被替换掉了。少了一个环节反而多了一个意外好处不怕同步模块的残差因为残差本质上也是一种没有被完全估计出来的CFO联合搜索会把剩下的部分继续吃掉。如果你也正被时钟异步困扰建议别一上来就写一堆校准逻辑先把这个思路跑通一版说不定问题就无声无息地消失了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑