
简介面向无线通信与移动通信方向的研究者、工程师及高年级本科生这份资源围绕多径多普勒信道建模提供了一套小巧实用的Matlab仿真脚本集。包内16个文件以.m脚本为主并有3个.asv自动保存版本整体仅9KB涵盖典型多径信道模型two_ray_model、rayleighnew、多普勒频移计算doppler_or_no、Doppler_shifts、快衰落与频率选择性衰落对比four_case_doppler、frequency_dependent_multipath_fading以及时域/频域视图观察脚本便于快速理解信道模型参数与衰落现象。脚本命名规范、结构清晰可配合教材或论文直接运行调试适合入门级到进阶级读者动手验证理论。资源已有1375人学习对于希望掌握多径衰落、多普勒效应仿真实现并优化通信系统设计的学习者来说是一份值得参考的代码素材。1. 从一辆行驶中的车说起为什么固定信道模型不够用在市区以 30 km/h 行驶的汽车里手机每秒钟会经历几十次明显的信号起伏。坐在副驾看频谱仪能看到同一个频点上的能量在几个毫秒内剧烈跳动如果在信道探测仪上观察冲激响应会看到原本清晰的峰在时间轴上不断游动。这不是硬件问题而是多径与多普勒共同作用的结果发射信号经过建筑物、车辆、地面反射形成多条路径每条路径的长度不同时延自然不同而接收端的运动又让每条路径的到达角与载波之间产生不同的频率偏移。多径多普勒信道模型要解决的就是这件事把多条路径不断变化的相位偏移抽象成一组可计算的参数让仿真系统能复现真实环境中的衰落、频率选择性以及时间选择性。它既要回答信号会裂成几份、每份延迟多久也要回答这些延迟在时间内怎么变。这篇文章会从物理概念讲到可运行的建模代码覆盖从 Clarke 模型到抽头延迟线仿真的一整套落地路径适合正在做链路仿真、基带算法验证或通信系统测试的工程师。2. 多径时延与多普勒频移两个物理量如何变成一组模型参数在搭建模型之前需要把多径和多普勒各自的数学描述弄清楚。这两个效应看似独立实际在仿真里常常耦合多径决定冲激响应的时域结构多普勒决定每个抽头在时间轴上的变化速率两者共同构成一个时变线性系统。2.1 多径时延如何把冲激响应拆成抽头序列发射信号经过不同物理路径到达接收端每条路径的传播距离不同到达时刻也不同。若把接收信号表示为发射信号与信道冲激响应的卷积冲激响应写为h(t, τ) Σ aᵢ(t) · δ(τ - τᵢ(t))其中 aᵢ(t) 是第 i 条路径的复增益τᵢ(t) 是第 i 条路径的时延。在数字仿真中时延 τᵢ 通常量化到采样间隔的整数倍或小数倍于是连续的冲激响应变成有限数量的抽头。抽头时延的覆盖范围决定了信道的时延扩展 τ_max——也就是最后一根抽头与第一根抽头的时延差这个值直接决定最大时延扩展进而决定相干带宽 B_c ≈ 1/τ_max。这里要区分物理路径和抽头两个概念。一条物理路径的时延不一定恰好落在采样点上常见的做法是用时延分辨率为采样周期的抽头来表示一个时延区间内的能量聚合。抽头数并不是越多越好抽头过多会显著抬升仿真计算量而抽头过少又无法表达频率选择性衰落。在工程仿真里通常用功率时延谱PDP来描述各抽头的平均功率再用一组独立的随机过程来控制每个抽头的时变增益。2.2 多普勒功率谱密度Clarke 模型与 Jakes 谱接收端运动时每条路径的相对速度不同产生的多普勒频移也不同。若载波频率为 f_c移动速度为 v光速为 c那么到达角为 θ 的路径产生的多普勒频移为 f_d v · cos(θ) / λ这里 λ c / f_c。不同路径的 θ 各不相同于是接收端看到的不再是单一频偏而是一个分布在 [-f_d_max, f_d_max] 范围内的连续频谱。各方位角均匀到达的散射环境下多普勒功率谱密度呈现经典的 Jakes 谱也叫 Clarke 谱S(f) 1 / (π · f_d_max · √(1 - (f / f_d_max)²))当 f 接近 ±f_d_max 时谱密度趋近无穷仿真时要注意频点采样不要正好落在奇点上。Jakes 谱是一个理论分布实际硬件信道模拟器实现时常用正弦波叠加SOSSum of Sinusoids来近似这个连续谱。下面是一段生成多普勒衰落系数的 Python 代码用正弦叠加法生成符合 Jakes 谱的复数时间序列import numpy as np def jakes_sos(num_sinusoids, num_samples, fd_max, fs, seed42): 用正弦叠加法生成 Jakes 谱衰落序列 参数: num_sinusoids: 正弦分量数量常见取值 8~64 num_samples: 输出样本点数 fd_max: 最大多普勒频移 (Hz) fs: 抽样率 (Hz)注意该抽样率指的是衰落序列自身的更新率 seed: 随机数种子方便复现 返回: h_complex: 复数衰落系数序列幅度服从瑞利分布 rng np.random.default_rng(seed) alpha (2 * np.pi * np.arange(num_sinusoids) rng.uniform(0, 2 * np.pi)) / num_sinusoids t np.arange(num_samples) / fs h np.zeros(num_samples, dtypecomplex) for i in range(num_sinusoids): fd_i fd_max * np.cos(alpha[i]) phi_i rng.uniform(0, 2 * np.pi) h 1 / np.sqrt(num_sinusoids) * np.exp(1j * (2 * np.pi * fd_i * t phi_i)) return h这段代码的关键逻辑是每个正弦分量对应一条等效散射路径其多普勒频移由到达角 alpha[i] 决定初始相位随机所有分量叠加并归一化后合成序列的包络在统计上服从瑞利分布。参数上需要注意fd_max通常由 LoRa、WiFi、蜂窝系统的最高移动速度换算得到fs是衰落采样率必须大于两倍最大多普勒频移否则会产生频谱混叠。2.3 瑞利衰落与莱斯衰落从系数包络判断信道类型Jakes 谱描述的是谱形状而包络分布决定信道属于瑞利还是莱斯。若接收信号中没有占主导地位的直射分量而是大量散射路径叠加由中心极限定理可知同相分量 I 和正交分量 Q 是独立的高斯随机过程包络服从瑞利分布。若存在明显的直射分量比如视距链路则包络服从莱斯分布其概率密度函数中会多出一个由 K 因子控制的峰值项。K 因子定义为直射分量功率与散射分量功率之比。K 0 时退化为瑞利衰落K 值越大衰落越浅信道更接近加性高斯白噪声下的静态信道。实际仿真中通常先把复高斯序列做成形滤波得到散射分量再叠加直射分量的常量幅度从而得到莱斯衰落系数。上一节给出的jakes_sos函数生成的是纯散射分量若想生成莱斯衰落可以在输出序列上叠加一个幅度为sqrt(K / (K 1))的常量相位项。2.4 从物理环境到仿真参数一张典型的功率时延谱表做端到端仿真时直接采信道模型的通用参数即可。常见的做法是参考 3GPP TR 38.901 里的 TDLTapped Delay Line模型它把抽头时延归一化后用公式缩放。下表给出一种典型的非视距NLOS室内场景参数适合作为仿真初值抽头编号归一化时延 (ns)平均功率 (dB)多普勒谱类型100.0Jakes230-1.0Jakes370-2.0Jakes4120-4.0Jakes5200-8.0Jakes6320-12.0Jakes实际使用时将归一化时延乘以期望的均方根时延扩展 DS得到绝对时延信号带宽越大能分辨的抽头间隔越小越能还原真实的频选特性。潮流一点说这张表本质上是在描述能量在时间轴上的散布——业界常说的多径时延扩展分析的就是这张表的一阶矩和二阶矩。3. 用抽头延迟线实现端到端信道模型从参数到可仿真波形上一章给出了模型参数表和 Jakes 谱生成方法这一章把它们拼装成完整的时变信道仿真器。抽头延迟线结构TDL是 IEEE 802.11、3GPP、LTE/NR 仿真中最常用的实现形态。3.1 时变冲激响应每个抽头都有自己的衰落过程TDL 模型的核心结构是y(t) Σ hᵢ(t) · x(t - τᵢ)其中 hᵢ(t) 是第 i 个抽头的复增益随时间变化x(t - τᵢ) 是延迟后的发射信号。每个 hᵢ(t) 都由独立的随机过程产生过程的多普勒功率谱决定其快慢。这里最容易出错的是独立两个字在真实场景中若散射环境共享某些抽头之间会存在相关性但大多数链路级仿真为了简化假设抽头间相互独立这个前提在相干带宽远大于子载波间隔的窄带系统中误差很小。由于每个抽头的时延是固定的TDL 模型天然适用于 OFDM 系统仿真因为 OFDM 的循环前缀只需要超过最大时延扩展即可消除符号间干扰。基于此抽头系数的更新率可以设置为符号率或衰落更新率而不必每个采样点都更新——这能大幅降低仿真耗时。3.2 从 PDP 表生成抽头系数完整 Python 样例下面的代码把上一节给出的 PDP 表转换成可用的时变抽头系数矩阵配合scipy.signal完成多普勒成形滤波import numpy as np from scipy.signal import butter, lfilter def generate_tdl_coeffs(tap_delays, tap_powers_db, fd_max, num_symbols, fs_tap, seed0): 根据 PDP 生成 TDL 时变抽头系数 参数: tap_delays: 各抽头时延 (s) tap_powers_db: 各抽头平均功率 (dB) fd_max: 最大多普勒频移 (Hz) num_symbols: 需要生成的符号数 fs_tap: 抽头衰落采样率 (Hz) seed: 随机种子 返回: taps: 形状为 (num_symbols, num_taps) 的复数矩阵 rng np.random.default_rng(seed) num_taps len(tap_delays) taps np.zeros((num_symbols, num_taps), dtypecomplex) nyq fs_tap / 2.0 cutoff fd_max / nyq b, a butter(1, cutoff, btypelow) for i in range(num_taps): # 生成复高斯序列并对实部虚部做低通滤波以近似 Jakes 谱 noise_c (rng.standard_normal(num_symbols) 1j * rng.standard_normal(num_symbols)) / np.sqrt(2) filtered lfilter(b, a, noise_c) lfilter(b, a, noise_c[::-1])[::-1] filtered filtered / np.sqrt(np.mean(np.abs(filtered)**2)) scale 10 ** (tap_powers_db[i] / 20.0) taps[:, i] filtered * scale return taps这段代码的核心是先产生单位功率复高斯白噪声再通过一阶 Butterworth 低通滤波逼近 Jakes 谱最后按 PDP 表中的相对功率缩放。代码里用lfilter做了一次正反双向滤波来补偿相位偏移使序列的统计特性更平稳。实际工程中如果对谱精度要求更高可以改用第二章的 SOS 法直接生成符合 Jakes 谱的序列再按功率缩放两种方法在信噪比和计算量上各有取舍。3.3 三个必调参数相干时间、相干带宽与衰落采样率参数设置直接影响仿真准确度其中三个参数需要反复确认。相干时间 T_c 定义为信道冲激响应维持近似不变的时间长度近似 T_c ≈ 0.423 / f_d_max。如果 OFDM 符号周期远小于 T_c信道可视为准静止此时每个符号内可以用一个固定信道系数如果符号周期接近或超过 T_c就必须在符号内多次更新抽头。另一个角度是衰落采样率 fs_tap它决定了抽头序列能表达的最大多普勒依据奈奎斯特采样定理至少取 2~4 倍 f_d_max。相干带宽 B_c 与最大时延扩展有关B_c ≈ 1 / τ_max。当信号带宽远大于 B_c 时信道的频率选择性明显子载波间增益起伏大当信号带宽小于 B_c信道近似平坦。仿真中判断是否需要多抽头就看信号带宽与 B_c 的比例。如果带宽低单抽头瑞利信道就够了不必强行使用 TDL——用太多抽头不但拖慢速度还会引入无意义的频域波纹。4. 把信道模型接进基带仿真链路端到端实践与常见坑有了抽头系数矩阵下一步是把发射信号真正经过信道并完成多普勒谱分析。这一章的代码可以直接替代仿真链路中的信道模块。4.1 信号经过时变多径信道卷积与抽头叠加的实现发射信号经过 TDL 信道时需要对每个抽头做时延移位再乘以对应的抽头增益并叠加。每个抽头的时延可以是采样间隔的整数倍也可以引入小数时延滤波器来模拟非整数倍延迟。如下代码实现整数倍时延的快速卷积版本def apply_tdl_channel(tx, taps, tap_delays, fs): tx: 发射基带信号形状为 (N,) taps: 抽头系数矩阵形状为 (num_symbols, num_taps) tap_delays: 各抽头时延 (s) fs: 系统采样率 (Hz) 返回: rx: 接收信号 delay_samps [int(round(d * fs)) for d in tap_delays] max_delay max(delay_samps) N len(tx) max_delay rx np.zeros(N, dtypecomplex) # 每个符号间隔内假设抽头系数恒定 block_size len(tx) // taps.shape[0] for i, d in enumerate(delay_samps): coeffs taps[:, i] # 将每段系数扩展成逐采样的系数序列 coeff_upsampled np.repeat(coeffs, block_size) if len(coeff_upsampled) len(tx): coeff_upsampled np.pad(coeff_upsampled, (0, len(tx) - len(coeff_upsampled))) rx[d:dlen(tx)] coeff_upsampled * tx return rx这段代码的要点在于系数与信号的对应关系抽头系数矩阵的行数等于符号数而发射信号长度为符号数与块长的乘积。代码中用np.repeat将每个符号上的系数保持到该符号内的所有采样点模拟符号内信道近似不变的准静态假设。如果仿真场景要求快衰落可以把taps的维度扩展到采样级但那样会显著增加内存占用工程上也可以不必因为实际硬件信道模拟器同样以符号为粒度更新。4.2 验证多普勒频谱对抽头序列做 FFT 分析生成信道后需要检查生成的多普勒谱是否符合理论。方法是对某一抽头的长时间衰落序列做傅里叶变换得到其频谱再与理论 Jakes 谱对比。下面代码计算并归一化多普勒谱def doppler_spectrum(coeffs, fs_tap): spectrum np.fft.fftshift(np.abs(np.fft.fft(coeffs))**2) freqs np.fft.fftshift(np.fft.fftfreq(len(coeffs), 1/fs_tap)) spectrum / spectrum.max() return freqs, spectrum在参数设置正确时频谱形状应呈中间低、两端高的浴盆形状。如果观察到频谱集中在 0 频附近而没有抬高两端很可能是一阶滤波器阶数不够需要用更高阶数的 Butterworth 或改用 Jakes SOS 法。如果频谱出现周期性的尖峰则可能是衰落序列采样率不足导致频谱周期延拓。4.3 工程落地的三个常见坑第一个坑是时延量化误差。当抽头时延不是采样周期的整数倍时直接取整会引入额外的相位旋转窄带系统可能无感但宽带系统里会造成频率响应纹波。解决办法是使用分数延迟滤波器如 sinc 插值或者用时延相位旋转近似但前提是信号带宽有限。第二个坑是抽头功率归一化。多数仿真要求信道平均增益为 1即 Σ 10^(Pᵢ/10) 1。在应用 PDP 表时如果功率直接用 dB 值转线性再累加往往总增益不足或超过应该在生成系数后做一次归一化。第三个坑是块长与系数更新率不匹配。给apply_tdl_channel传入的taps行数与发射信号长度不匹配时代码会静默截断系数导致信道能量突变。建议在程序入口加一个断言判断len(tx) // block_size taps.shape[0]不满足就主动报错。5. 用最小二乘信道估计反推冲激响应一个项目里能直接用的验证技巧仿真做完后最终要回答我生成的信道是否符合预期尤其是多径时延的位置和功率是否准确。这里有一个不依赖复杂探测接收机的做法用已知的发射序列 x在接收端做最小二乘LS信道估计。假设接收信号 y Xh n其中 X 是由发射序列构成的卷积矩阵h 是待估计的冲激响应向量n 是噪声。LS 估计为 h_hat (XᴴX)⁻¹Xᴴy实际计算时用训练序列的自相关矩阵求逆。对于 OFDM 系统更简单的做法是用频域 LS 估计H_hat[k] Y[k] / X[k]再 IDFT 得到时域冲激响应。下面是一段适合仿真后处理的小脚本用 Zadoff-Chu 或随机 QPSK 序列作为探测信号估计冲激响应并找出峰值位置def ls_channel_estimate(rx_seq, tx_seq, min_delay, max_delay): 通过循环相关估计时域冲激响应 rx_seq: 接收序列 tx_seq: 发射探测序列 N len(tx_seq) # 频域 LS 除以发送序列 RX np.fft.fft(rx_seq, N) TX np.fft.fft(tx_seq, N) H_freq RX / TX h_est np.fft.ifft(H_freq, N) # 只保留有效时延窗内的抽头 h_windowed np.zeros_like(h_est) h_windowed[min_delay:max_delay] h_est[min_delay:max_delay] return h_est, h_windowed使用时需要注意发射探测序列的频域幅度不应出现过零值否则除法会放大噪声。Zadoff-Chu 序列具备恒包络特性是这类估计的首选。把 LS 估计出的峰值与理论 PDP 表对比能直观看出每一路多径时延是否吻合、多普勒展宽是否导致谱泄漏。这个技巧还能顺带验证信道归一化估计出的总能量应当接近发射能量偏差超过 1 dB 就要回头查抽头功率和归一化逻辑。本文还有配套的精品资源点击获取