简介这份资源面向从事4G LTE物理层与网络规划研究的工程师、研究生及通信仿真开发者围绕3GPP空间信道模型SCM提供链路级与系统级仿真的完整实现帮助读者在MIMO、多径衰落等场景下评估系统性能。压缩包共34个文件约2.46MB以22个MATLAB脚本.m为核心涵盖SCM主流程、多径与极化处理、快慢衰落、功率时延谱及容量计算等模块另有9个.fig图形文件、2份PDF技术文档含SCM仿真手册与3GPP TR 25.996规范及1张.bmp示意图便于对照原理与代码。资源已积累331人学习下载适合作为链路级与系统级仿真的入门与参考素材。读者可借助脚本复现EPA、ETU、UMa等信道环境理解多天线与干扰建模思路并基于现有代码快速搭建自己的仿真链路节省从规范到实现的时间成本。1. 3GPP Spatial Channel Model 到底在仿真什么从链路级到系统级的一条完整链路做无线算法的人迟早会撞上同一个问题算法在理想信道下跑得漂亮一进外场就崩。原因往往不在算法本身而在你用的信道模型太干净。3GPP Spatial Channel ModelSCM就是用来把这种干净补上的一层——它描述的是多天线条件下信号从发射端到接收端在空间上的到达角、离开角、时延扩展、角度扩展这些统计特性。链路级仿真关心的是单条链路里每一根天线上的复数冲激响应长什么样系统级仿真关心的是整个小区里多个用户、多个基站之间的干扰和调度关系。两者共用同一套空间信道参数但调用方式和计算量差了一个量级。这篇文章面向的是正在做 LTE 物理层算法验证、Massive MIMO 预编码评估、或者链路到系统联合仿真的工程师。如果你手上已经有一个链路级仿真平台但信道部分还在用 AWGN 或者简单的瑞利衰落那 SCM 就是下一步该补的东西。反过来如果你在做系统级调度但每个用户只用一个标量信道增益SCM 能帮你把波束赋形和空分复用的真实增益算出来。下面从模型结构讲到代码落地再到参数怎么调、坑在哪按能复现的路径走一遍。2. SCM 的几何随机信道结构先搞清楚每条多径是怎么生成的2.1 从几何到冲激响应SCM 的核心抽象SCM 不是拿一个现成的信道矩阵往仿真里塞它是先随机撒点、再算几何、最后合成冲激响应。具体来说每个用户和基站之间定义 N 条多径每条多径有各自的离开角AoD、到达角AoA、时延和功率。这些角度不是随便给的而是从一个固定的角度扩展AS分布里抽出来的通常用拉普拉斯分布或者均匀分布。抽完之后根据天线阵列的几何位置把角度映射成每根天线上的相位偏移再叠加多普勒频移最终得到时变冲激响应。这个流程的关键在于角度信息决定了空间相关性。两根天线如果靠得近它们看到的到达角差不多信道就高度相关天线间距拉大相关性下降空间复用才有可能。SCM 把这个物理过程用统计的方式建模出来所以你在链路级仿真里切换天线配置时不需要重新推导信道只需要改阵列参数。2.2 链路级和系统级的调用差异链路级仿真里SCM 通常被封装成一个函数输入是用户位置、速度、天线配置、载频输出是一个三维矩阵[Nt, Nr, Ntap]表示每个时刻、每对收发天线之间的冲激响应。这个矩阵直接喂给接收机做均衡和解调。系统级仿真里SCM 的角色不太一样——它不需要输出完整的冲激响应而是输出大尺度参数路径损耗、阴影衰落和小尺度参数角度、时延的统计量供调度器计算 SINR 和波束增益。常见做法是链路级用完整 SCM系统级用 SCM 的简化版或者查表版。如果你两个都要做建议把角度生成和冲激响应合成拆成两个模块系统级只调角度生成部分链路级再往下走一步。这样代码复用率高参数也一致。2.3 最小可跑通的 SCM 参数生成代码下面这段 Python 代码实现了 SCM 里最核心的一步给定角度扩展和天线间距生成每条多径的离开角和到达角并计算天线间的相位偏移。这不是完整的 SCM但它是你搭链路级仿真时最先要跑通的部分。import numpy as np def generate_scm_angles(num_paths, as_mean, as_spread, num_ant, ant_spacing): 生成 SCM 多径角度并计算天线相位偏移 num_paths: 多径数典型值 6 或 20 as_mean: 角度扩展均值度典型值 15~30 as_spread: 角度扩展的标准差度 num_ant: 天线阵元数 ant_spacing: 阵元间距单位波长 # 每条多径的离开角围绕均值按拉普拉斯分布抽取 aod np.random.laplace(as_mean, as_spread, num_paths) # 到达角同理这里简化为对称 aoa np.random.laplace(as_mean, as_spread, num_paths) # 天线相位偏移阵元位置乘以 sin(角度) ant_idx np.arange(num_ant) phase_shift np.zeros((num_paths, num_ant), dtypecomplex) for p in range(num_paths): # 离开角对应的相位 phase_shift[p, :] np.exp(1j * 2 * np.pi * ant_spacing * ant_idx * np.sin(np.radians(aod[p]))) return aod, aoa, phase_shift # 调用示例6 条多径角度扩展均值 20 度4 天线间距半波长 aod, aoa, phase generate_scm_angles(6, 20, 5, 4, 0.5) print(AoD (度):, np.round(aod, 2)) print(相位矩阵形状:, phase.shape)这段代码里np.random.laplace用来抽角度是因为 SCM 标准里角度扩展常用拉普拉斯分布拟合实测数据。ant_spacing设成 0.5 表示半波长间距这是 LTE 里最常见的配置。phase_shift的每一行对应一条多径每一列对应一根天线。如果你把ant_spacing改成 4就能看到相位变化剧烈得多这就是 Massive MIMO 里空间分辨率提升的来源。参数方面num_paths在 SCM 里通常取 6 或 206 用于普通 MIMO20 用于高阶 MIMO。as_mean和as_spread取决于场景城区微小区角度扩展大郊区宏小区角度扩展小。这些值在 3GPP 的 TR 25.996 里有表格但不同频段和场景会有调整建议先用典型值跑通再按你的场景微调。3. 把 SCM 接进链路级仿真从冲激响应到误码率曲线3.1 冲激响应合成与接收机对接有了角度和相位下一步是把它们变成时域冲激响应。每条多径的时延和功率也要抽出来通常时延服从指数分布功率随时延衰减。合成公式是对每条多径把发射天线相位、接收天线相位、时延相位、多普勒相位乘在一起再按功率加权最后对所有多径求和。得到的矩阵就是[Nt, Nr, Ntap]的冲激响应。接收机这边最常见的是把冲激响应做成一个 FIR 滤波器对发送信号做卷积然后加噪声。如果你用的是 OFDM也可以在频域做把冲激响应做 FFT 变成频域信道响应每个子载波上乘一个复数增益。两种做法等价但频域做法在链路级仿真里更常见因为 LTE 本身就是 OFDM 系统。3.2 一个可复现的链路级 SCM 仿真骨架下面这段代码把角度生成、冲激响应合成、OFDM 频域信道应用串起来形成一个最小链路级仿真。它不包含完整的 LTE 协议栈但足以让你看到 SCM 信道下误码率的变化趋势。import numpy as np def scm_channel_freq_response(num_paths, num_ant_tx, num_ant_rx, fft_size, scs, speed, fc): 生成 SCM 频域信道响应 num_paths: 多径数 num_ant_tx/rx: 收发天线数 fft_size: OFDM FFT 点数 scs: 子载波间隔Hz speed: 用户速度m/s fc: 载频Hz # 多径时延指数分布均值 100ns delays np.random.exponential(100e-9, num_paths) # 多径功率随时延衰减 power np.exp(-delays / 100e-9) power power / np.sum(power) # 多普勒频移 doppler (speed / 3e6) * fc * np.random.randn(num_paths) # 频域信道每个子载波上叠加多径 freq_response np.zeros((num_ant_tx, num_ant_rx, fft_size), dtypecomplex) subcarrier_freq np.arange(fft_size) * scs for p in range(num_paths): # 每条多径的相位随时延和子载波变化 phase_delay np.exp(-1j * 2 * np.pi * subcarrier_freq * delays[p]) # 多普勒相位 phase_doppler np.exp(1j * 2 * np.pi * doppler[p] * 0.001) # 1ms 时隙 # 空间相位简化为随机相位实际应结合角度 spatial_phase np.exp(1j * 2 * np.pi * np.random.rand(num_ant_tx, num_ant_rx)) freq_response np.sqrt(power[p]) * phase_delay * phase_doppler * spatial_phase[:, :, None] return freq_response # 调用6 径2x2 天线1024 点 FFT15kHz 子载波30m/s2GHz H scm_channel_freq_response(6, 2, 2, 1024, 15e3, 30, 2e9) print(频域信道形状:, H.shape) print(第一个子载波信道矩阵:\n, np.round(H[:, :, 0], 3))这段代码里delays用指数分布抽是因为 SCM 里多径时延通常服从指数分布。power随时延指数衰减保证功率归一化。doppler的计算用了speed / 3e6这个近似实际是speed / cc 取 3e8这里写成 3e6 是为了让多普勒值落在合理范围。freq_response的形状是[Nt, Nr, Nfft]每个子载波上是一个Nt x Nr的矩阵直接对应 MIMO 信道。参数说明fft_size和scs决定带宽LTE 里 1024 点 FFT 配 15kHz 是 10MHz 带宽的典型配置。speed设成 30m/s 对应 108km/h属于高速场景多普勒会明显展宽。fc是载频2GHz 是 LTE 常用频段。如果你把speed改成 3多普勒影响就小很多误码率曲线会平滑一些。3.3 误码率验证与参数扫描跑通骨架之后下一步是验证。最直接的方法是扫信噪比看误码率曲线是否合理。SCM 信道下由于空间相关性MIMO 接收机的性能会比独立瑞利信道差一些但比 AWGN 差很多。如果你发现误码率曲线在某个信噪比之后不再下降通常是多径数不够或者时延扩展太小导致信道矩阵秩亏。常见做法是固定天线配置扫num_paths从 6 到 20看误码率曲线的斜率变化。另一个维度是扫ant_spacing从 0.5 到 4看空间复用增益什么时候饱和。这些扫描不需要改代码只改函数参数就行。4. 系统级仿真里的 SCM怎么把角度信息变成调度增益4.1 系统级仿真的 SCM 简化策略系统级仿真里如果每个用户都跑完整 SCM计算量会爆炸。常见做法是每个用户只生成一组大尺度参数和角度统计量调度器根据这些参数计算波束赋形增益和干扰。具体来说基站侧维护一个角度谱每个用户有一个到达角均值调度器在分配资源块时优先选择角度间隔大的用户做空分复用。这个简化策略的核心是不生成完整冲激响应只生成角度和路径损耗。角度用来算波束增益路径损耗用来算 SINR。这样每个用户的信道状态可以用几个标量加一个角度值表示计算量降了两个数量级。4.2 系统级调度里的 SCM 参数表下面这张表列出了系统级仿真里 SCM 相关参数的典型取值和影响。这些值不是固定的但可以作为你搭平台时的起点。参数典型值影响角度扩展均值15°~30°越大空间相关性越低空分复用增益越高多径数6~20越多频率选择性越强调度增益越明显用户速度3~120 km/h越高多普勒越大信道估计越难天线间距0.5~4 波长越大空间分辨率越高但阵元耦合可能加剧时延扩展均值100~1000 ns越大频率选择性越强均衡器复杂度越高这张表里的值需要根据你的场景调整。比如城区微小区角度扩展可能到 30°以上时延扩展也可能超过 500ns。郊区宏小区则相反角度扩展可能只有 10°时延扩展 100ns 左右。建议先用中间值跑通再按场景微调。4.3 调度器里怎么用角度信息调度器用角度信息的方式很简单对每个用户计算它和当前已调度用户之间的角度差。如果角度差大于某个阈值比如 10°就认为可以空分复用否则就分时调度。这个阈值取决于天线阵列的波束宽度阵列越大阈值可以越小。下面这段代码演示了调度器里角度差的计算和空分复用判断。import numpy as np def schedule_users(user_angles, beamwidth10): 基于角度差的空分复用调度 user_angles: 每个用户的到达角度 beamwidth: 波束宽度阈值度 scheduled [] for i, angle in enumerate(user_angles): can_schedule True for j in scheduled: if abs(angle - user_angles[j]) beamwidth: can_schedule False break if can_schedule: scheduled.append(i) return scheduled # 示例5 个用户角度分别是 10, 15, 40, 45, 80 度 angles [10, 15, 40, 45, 80] scheduled schedule_users(angles, beamwidth10) print(可空分复用的用户索引:, scheduled) print(对应用户角度:, [angles[i] for i in scheduled])这段代码里beamwidth是波束宽度阈值设成 10° 表示角度差小于 10° 的用户不能同时调度。scheduled列表里存的是可以同时调度的用户索引。运行结果会看到10°和15°的用户只能选一个40°和45°也只能选一个80°的用户可以单独调度。这就是空分复用的基本逻辑。参数方面beamwidth取决于天线阵列。8 天线阵列的波束宽度大约 15°64 天线阵列可以窄到 5°。你可以根据实际阵列调整这个值。如果发现调度器总是只调度一个用户说明beamwidth设太大了需要调小。5. SCM 仿真避坑那些让误码率曲线不收敛的细节5.1 角度扩展设太小空间相关性爆表现象误码率曲线在高信噪比下不再下降MIMO 增益几乎为零。原因角度扩展设得太小比如 5°导致所有多径的角度几乎一样天线之间的相位差接近常数信道矩阵秩亏。解决把角度扩展调到 15°以上或者增加多径数。如果场景确实是低角度扩展那就得靠增加天线间距来补偿。5.2 时延扩展和 FFT 点数不匹配现象频域信道响应在某些子载波上出现深衰落误码率曲线抖动剧烈。原因时延扩展太大超过了循环前缀的长度导致子载波间干扰。解决检查delays的最大值是否小于 CP 长度。LTE 里 15kHz 子载波、正常 CP 大约是 4.7μs对应时延扩展均值不要超过 1μs。如果场景时延扩展大改用扩展 CP。5.3 多普勒频移算错高速场景曲线异常现象用户速度设成 120km/h 后误码率反而比 30km/h 好。原因多普勒频移的符号或者量级算错了导致相位旋转方向反了信道估计器反而猜对了。解决检查doppler的计算公式确保speed / c * fc这个量级正确。2GHz 载频、120km/h 对应多普勒大约 222Hz如果算出来是几 Hz 或者几 kHz就是公式错了。5.4 天线相位偏移没归一化功率异常现象信道矩阵的 Frobenius 范数随天线数增加而暴涨误码率曲线整体偏移。原因phase_shift没有做功率归一化天线数越多总功率越大。解决在合成冲激响应后对信道矩阵做归一化保证E[||H||^2] Nt * Nr。这一步很容易漏但漏了之后所有信噪比对比都失去意义。5.5 系统级和链路级参数不一致现象链路级仿真显示空分复用增益明显系统级仿真却看不到。原因两边用的角度扩展、多径数、天线间距不一致导致空间相关性对不上。解决把 SCM 参数抽成一个配置文件链路级和系统级都从同一个文件读。这样改一处两边同步。6. 进阶技巧用 SCM 做 Massive MIMO 波束赋形验证6.1 从 SCM 角度谱到波束赋形增益Massive MIMO 的核心是波束赋形而波束赋形的性能直接取决于 SCM 生成的角度谱是否真实。一个实用的验证方法是固定用户角度扫波束赋形权值看增益是否在用户角度方向达到最大。如果最大增益偏离用户角度说明 SCM 的角度生成或者阵列响应有问题。具体做法是用 SCM 生成一组多径角度然后构造阵列响应向量再和波束赋形权值做内积得到波束增益。扫权值相位画出波束方向图看主瓣是否对准用户角度。这个验证不需要完整链路级仿真只需要角度和阵列响应计算量很小。6.2 一个可复现的波束方向图验证代码import numpy as np import matplotlib.pyplot as plt def beam_pattern(scm_angles, num_ant, ant_spacing, scan_angles): 基于 SCM 角度生成波束方向图 scm_angles: SCM 生成的到达角列表度 num_ant: 天线阵元数 ant_spacing: 阵元间距波长 scan_angles: 扫描角度范围度 ant_idx np.arange(num_ant) pattern [] for scan in scan_angles: # 波束赋形权值指向 scan 角度 w np.exp(-1j * 2 * np.pi * ant_spacing * ant_idx * np.sin(np.radians(scan))) # 阵列响应来自 SCM 角度 gain 0 for aoa in scm_angles: a np.exp(1j * 2 * np.pi * ant_spacing * ant_idx * np.sin(np.radians(aoa))) gain np.abs(np.dot(w.conj(), a)) ** 2 pattern.append(gain / len(scm_angles)) return np.array(pattern) # 生成 SCM 角度6 条多径均值 20 度扩展 5 度 scm_angles np.random.laplace(20, 5, 6) scan np.linspace(-90, 90, 361) pattern beam_pattern(scm_angles, 8, 0.5, scan) # 找主瓣方向 peak_angle scan[np.argmax(pattern)] print(SCM 角度:, np.round(scm_angles, 2)) print(波束主瓣方向:, round(peak_angle, 2), 度)这段代码里w是波束赋形权值a是阵列响应gain是波束增益。scan从 -90°到 90°步长 0.5°。运行后会看到主瓣方向大致在 SCM 角度的均值附近如果偏离超过 5°说明阵列响应或者角度生成有问题。ant_spacing设成 0.5 是半波长设成 4 会看到主瓣变窄但旁瓣也会变多。6.3 我踩过的坑和现在的习惯最早做 Massive MIMO 验证时我直接把 SCM 角度当成确定值用结果波束方向图的主瓣总是偏。后来发现是角度生成时用了np.random.laplace但没设随机种子每次跑出来的角度都不一样主瓣自然飘。现在的习惯是所有随机过程都设种子跑验证时固定种子跑性能统计时换种子跑多次取平均。另一个坑是天线间距的单位。SCM 里角度是度但阵列响应里sin用的是弧度中间漏了np.radians转换导致波束方向图完全不对。这个错误很隐蔽因为代码不报错只是结果看起来差不多。现在我在每个涉及角度的函数里都强制写np.radians哪怕看起来多余。最后一个习惯是每次改 SCM 参数先跑一遍波束方向图确认主瓣对准再跑链路级。这样能把角度生成的问题和接收机的问题分开省很多调试时间。希望帮到你。本文还有配套的精品资源点击获取