简介:这份源码面向无线通信方向的学生、研究人员与工程师,聚焦MIMO-OFDM系统在不同信噪比下的频谱效率仿真,并覆盖DFT码本设计、beam训练与波束扫描等关键环节,适合具备一定通信原理与MATLAB基础、希望深入理解多天线系统性能评估的读者。压缩包共3个文件,均为m脚本,整体约5KB,体量轻便,便于直接阅读与二次修改。内容围绕MIMO信道模型建立、OFDM信号生成与解调、DFT码本计算、SNR对频谱效率的影响分析,以及波束训练与扫描算法展开,读者可据此复现仿真流程,理解误码率随SNR下降而频谱效率提升的内在逻辑,并掌握码本选择与波束方向优化对系统性能的作用。目前已有157人学习下载,适合作为无线通信系统设计与优化的入门参考。
1. 从一条 SNR 曲线说起:MIMO-OFDM 频谱效率仿真到底在算什么
把信噪比从 0 dB 拉到 30 dB,盯着频谱效率曲线往上爬,这件事听起来像教科书习题,但真动手搭过 MIMO-OFDM 链路级仿真的人都知道,坑几乎全在“曲线为什么没按预期爬”上。这个标题讲的就是这么一套东西:在 MIMO-OFDM 系统里,用 DFT 码本做波束训练和波束扫描,然后观察频谱效率随 SNR 增加的变化趋势,并且带源码。它解决的不是“通信原理是什么”,而是“我想复现一条合理的 SE-SNR 曲线,码本怎么建、波束怎么扫、训练开销怎么扣、仿真怎么不翻车”。适合已经懂 OFDM 基本结构、想往毫米波/大规模 MIMO 波束管理方向落地的工程师和研究生,也适合手里有源码但跑出来曲线不对劲、想找参照系的人。
频谱效率(Spectral Efficiency, SE)本质上是每赫兹每秒能传多少比特,单位 bit/s/Hz。在 MIMO-OFDM 里,它同时受三件事牵制:信道矩阵的秩和条件数、波束成形/预编码带来的阵列增益、以及为了选波束而付出的训练开销。SNR 增加时,如果只算理想容量,曲线会近似对数线性上升;但一旦把 DFT 码本量化误差、波束训练阶段的开销、波束扫描的粒度算进去,曲线会在高 SNR 段出现饱和或斜率变缓。很多人第一次跑仿真,看到高 SNR 处 SE 不涨了,以为是代码 bug,其实是波束量化误差和训练开销在“吃”增益。这篇就按“先立住模型、再搭码本、再跑训练与扫描、最后看曲线和排错”的顺序讲透。
2. 把系统模型立住:MIMO-OFDM 与 DFT 码本怎么对应到代码
2.1 先确定天线形态与信道模型,别一上来就写预编码
动手前必须先把三件事定死:天线阵列是均匀线阵(ULA)还是均匀面阵(UPA)、子载波数量与带宽、信道是窄带还是宽带。MIMO-OFDM 的“OFDM”意味着每个子载波上是一个窄带 MIMO 信道,所以宽带信道要按抽头延迟线建模再转到频域。常见做法是:先给时域多径信道h(tau),做 FFT 得到每个子载波的频域信道矩阵H[k],维度是Nr x Nt。如果这一步偷懒直接用独立同分布复高斯,跑出来的 SE 会偏乐观,因为丢掉了子载波间相关性,波束扫描的物理意义也弱了。
我一般会固定这样一组基线参数,方便横向对比:载频 28 GHz,带宽 100 MHz,子载波间隔 60 kHz,Nt=16(ULA),Nr=4,径数 4,最大时延扩展约 100 ns。这些数值不是唯一解,但能让 DFT 码本的波束宽度和信道角度扩展匹配得上,不至于出现“码本波束比信道还窄”的玄学现象。
2.2 DFT 码本:一行代码生成,但角度栅格要想清楚
DFT 码本的本质是把阵列导向矢量在角度上均匀采样。对 Nt 元 ULA,第 m 个码字写成:
import numpy as np def dft_codebook(Nt, oversampling=1): """生成 ULA 的 DFT 码本。 Nt: 天线数 oversampling: 过采样因子,1 表示码字数等于天线数 返回: (Nt, Nt*oversampling) 的复数矩阵,每列是一个码字 """ N = Nt * oversampling codebook = np.zeros((Nt, N), dtype=complex) for n in range(N): # 第 n 个码字的相位斜坡,对应空间频率 n/N for m in range(Nt): codebook[m, n] = np.exp(1j * 2 * np.pi * m * n / N) / np.sqrt(Nt) return codebook cb = dft_codebook(16, oversampling=4) # 16 天线,4 倍过采样,共 64 个波束 print(cb.shape) # (16, 64)逻辑说明:外层循环遍历码字数 N,内层遍历天线索引 m,指数项2*pi*m*n/N就是导向矢量的相位。除以sqrt(Nt)是做功率归一化,保证每个码字二范数为 1,这样后续算波束增益时不会因为码本功率不一致而误判。参数说明:oversampling是关键。取 1 时码字数等于天线数,波束较宽、扫描快但量化误差大;取 4 或 8 时波束更密,量化损失小,但训练和扫描的候选数成倍增加。高 SNR 下如果 oversampling 太小,SE 曲线会提前饱和,这就是很多人“SNR 加了但 SE 不动”的第一个原因。
2.3 频谱效率的计算口径:容量、可达速率还是带开销的净速率
这一步最容易口径不一致。标题说“频谱效率随 SNR 增加”,但 SE 有三种常见算法:理想香农容量log2(det(I + SNR/Nt * H H^H))、采用某码本后的可达速率、以及扣掉波束训练开销后的净频谱效率。三者数值能差出一大截。做波束训练和扫描的仿真,必须用第三种,否则训练开销白算了。
def se_ideal(H, snr_db): """理想 MIMO 容量,H 为 Nr x Nt 频域信道""" Nr, Nt = H.shape snr = 10 ** (snr_db / 10) return np.real(np.log2(np.linalg.det( np.eye(Nr) + snr / Nt * H @ H.conj().T))) def se_with_beam(H, w, snr_db): """给定发射波束 w 后的可达速率(接收端理想合并)""" snr = 10 ** (snr_db / 10) heff = H @ w # 等效 Nr x 1 信道 return np.real(np.log2(1 + snr * np.linalg.norm(heff) ** 2))逻辑说明:se_ideal用行列式算的是理想容量,作为上界参照;se_with_beam把发射波束 w 作用到信道上,得到等效单流信道再算速率。参数说明:snr_db是发射端每根天线的信噪比口径,如果你按接收总功率定义 SNR,snr/Nt这一项要相应调整,否则曲线整体平移。净频谱效率还要再乘(1 - T_train / T_total),T_train 是波束训练占用的符号数,T_total 是总符号数。这个开销因子在高 SNR 段是曲线饱和的主要推手之一。
3. 波束训练与波束扫描:从候选码本到选出一个波束
3.1 波束扫描的两种范式:穷举与分层
波束扫描最直白的做法是穷举:发射端依次用码本里每个码字发训练符号,接收端测量接收功率,选最大者。候选数 N 时,训练开销正比于 N。分层(hierarchical)扫描先用宽波束粗定位,再用窄波束细化,开销能降到 log 级别,但实现复杂、宽窄波束的码本设计要额外处理。仿真里我建议先用穷举把基线跑通,因为穷举的结果是“最优可达”的参照,分层扫描的增益损失可以拿它做对比。
def beam_sweep_exhaustive(H, codebook, snr_db, noise_var=1.0): """穷举波束扫描:返回最优码字索引与对应接收功率""" best_idx, best_power = -1, -1.0 for idx in range(codebook.shape[1]): w = codebook[:, idx] heff = H @ w # 接收功率 = |Heff|^2 * 发射功率,这里发射功率归一化为 1 power = np.linalg.norm(heff) ** 2 if power > best_power: best_power, best_idx = power, idx return best_idx, best_power逻辑说明:遍历每个码字,算等效信道能量,取最大。参数说明:noise_var在纯波束选择阶段不影响排序,因为噪声对所有候选是加性同分布,但如果你要做“训练阶段的检测概率”,就要把它加进接收信号再比较。注意这里没有加噪声,是理想功率比较,真实训练要加,否则会高估选择正确率。
3.2 训练开销怎么扣进 SE:符号级账要算清
波束训练不是免费的。假设每个候选波束发一个 OFDM 符号做训练,穷举 N 个候选就花 N 个符号。若一个调度周期共 T_total 个符号,净频谱效率就是可达速率乘以(T_total - N) / T_total。这个因子在低 SNR 时影响小,因为速率本来就低;高 SNR 时速率高,被扣掉的绝对量就大,曲线因此变平。
def se_net(H, codebook, snr_db, T_total=140, n_candidates=None): """扣掉穷举训练开销后的净频谱效率""" if n_candidates is None: n_candidates = codebook.shape[1] idx, _ = beam_sweep_exhaustive(H, codebook, snr_db) w = codebook[:, idx] se_gross = se_with_beam(H, w, snr_db) overhead = (T_total - n_candidates) / T_total return se_gross * overhead, idx逻辑说明:先选波束,再算毛速率,最后乘开销因子。参数说明:T_total=140对应一个常见调度周期(比如 1 ms、60 kHz 子载波间隔下约 140 个符号),n_candidates就是码本列数。如果你把 oversampling 从 1 提到 4,候选数从 16 变 64,开销因子从 0.886 掉到 0.543,高 SNR 段 SE 直接砍掉近一半。这就是为什么“码本越密越好”是错的,密到一定程度,训练开销吃掉的比量化误差省下的还多。
3.3 把 SNR 扫描跑起来:一条可复现的主循环
把上面几块拼起来,就是标题要的“随 SNR 增加”的仿真主循环。每个 SNR 点做多次信道实现取平均,才能得到平滑曲线。
def simulate_se_vs_snr(snr_list, n_real=200, Nt=16, Nr=4, oversampling=4): cb = dft_codebook(Nt, oversampling) se_ideal_avg, se_net_avg = [], [] for snr_db in snr_list: se_i, se_n = 0.0, 0.0 for _ in range(n_real): # 简化的频域信道:单子载波、含角度扩展的多径叠加 H = (np.random.randn(Nr, Nt) + 1j * np.random.randn(Nr, Nt)) / np.sqrt(2) se_i += se_ideal(H, snr_db) se_n += se_net(H, cb, snr_db)[0] se_ideal_avg.append(se_i / n_real) se_net_avg.append(se_n / n_real) return se_ideal_avg, se_net_avg snr_list = np.arange(0, 31, 5) ideal, net = simulate_se_vs_snr(snr_list) for s, a, b in zip(snr_list, ideal, net): print(f"SNR={s:2d} dB ideal={a:.2f} net={b:.2f} bit/s/Hz")逻辑说明:外层扫 SNR,内层做n_real次信道实现取平均。参数说明:n_real=200是精度和耗时的折中,少于 50 次曲线会抖得看不出趋势;oversampling=4是波束密度,改它就能观察量化误差与训练开销的权衡。跑完你会看到:低 SNR 段 net 和 ideal 差距主要来自波束量化,高 SNR 段差距主要来自训练开销,两条曲线在高 SNR 处明显分叉。
4. 避坑与排查:SE 曲线不对时先查这五处
4.1 现象:高 SNR 段 SE 完全不涨,曲线压平
原因:最常见是训练开销因子没随 SNR 变化被正确扣除,或者码本 oversampling 太小导致量化误差成为瓶颈。量化误差在 SNR 高时相对更突出,因为信号本身已经很强,波束没对准的损失变成主导。解决:先把 oversampling 提到 8 看曲线是否继续上升,如果上升说明是量化瓶颈;再把训练开销因子单独打印出来,确认高 SNR 点确实乘了(T_total - N)/T_total。
4.2 现象:SE 数值大得离谱,30 dB 时超过 20 bit/s/Hz
原因:多半是 SNR 口径重复计算。se_ideal里已经除了 Nt,如果信道矩阵 H 又做了功率归一化,等于 SNR 被放大。另一个可能是用了理想容量公式却声称是“带波束训练的结果”。解决:固定一个已知点手算校验,比如 Nt=Nr=1、SNR=10 dB 时单天线容量应为log2(1+10)=3.46,对不上就查口径。
4.3 现象:波束扫描选出的索引每次都在跳,没有稳定最优
原因:信道实现之间独立,单次快照的最优波束本来就不同,这不是 bug。但如果你在同一个信道上重复扫描结果还在跳,那是接收功率计算里混入了随机噪声且没做平均。解决:同一信道下训练阶段对多个训练符号的接收功率取平均再比较,或者确认beam_sweep_exhaustive里没有误加随机项。
4.4 现象:改了 oversampling 但曲线几乎不变
原因:候选数变了,但训练开销因子没跟着变,或者你只改了码本生成却没把新码本传进se_net。解决:在se_net里打印codebook.shape[1]和overhead,确认两者同步变化。另一个隐蔽原因是信道角度扩展太宽,宽波束已经够用,密码本带来的量化增益被信道本身的空间散射抹平了。
4.5 现象:低 SNR 段 net 反而高于 ideal
原因:几乎一定是口径错误,比如 ideal 用了snr/Nt而 net 用了snr,或者 ideal 的信道矩阵维度写反。解决:把两个函数在同一个 H、同一个 SNR 下单独调用对比,先让单流单天线场景两者相等,再逐步加天线和波束,逐层定位。
5. 进阶:让 SE-SNR 曲线更接近真实系统的三个技巧
第一个技巧是给波束训练加“训练信噪比”与“数据信噪比”的区分。真实系统里训练符号往往功率更高或占用更多资源,才能保证选对波束。仿真时可以设训练阶段 SNR 比数据阶段高 3 到 6 dB,再看净 SE 曲线,你会发现高 SNR 段的饱和点往后推了,因为选错波束的概率下降了。这个细节很多开源仿真没有,但它是毫米波系统波束管理里的常规操作。
第二个技巧是用分层码本替代单层 DFT 码本做粗扫。做法是先构造一组宽波束(比如把相邻 DFT 码字相干叠加),粗扫选出候选区间,再在区间内用窄 DFT 码字细扫。训练开销从 N 降到N_coarse + N_fine,在 Nt=64 这种规模下收益很明显。代价是宽波束增益低,低 SNR 时粗扫容易选错区间,所以分层扫描的 SE 曲线在低 SNR 段可能反而不如穷举,这个交叉点值得你在仿真里标出来。
第三个技巧是给 SE 曲线加置信区间。只画均值曲线容易掩盖方差,尤其在小n_real时。我习惯在每次 SNR 点记录 SE 的样本标准差,画成阴影带。如果 20 dB 处的阴影带宽超过 1 bit/s/Hz,说明你的信道实现次数不够,或者信道模型里角度扩展参数设得太极端,结论不可信。
| 技巧 | 改动位置 | 预期效果 | 代价 |
|---|---|---|---|
| 训练/数据 SNR 分离 | 训练阶段加功率偏置 | 高 SNR 饱和点后移 | 需重新标定功率口径 |
| 分层码本粗扫+细扫 | 替换 beam_sweep 逻辑 | 大 Nt 下开销大降 | 低 SNR 粗扫易错 |
| SE 置信区间 | 主循环记录方差 | 结论可信度可判断 | 计算量翻倍 |
最后说个我自己的习惯:每次改完码本或开销模型,先只跑 SNR=0、15、30 三个点,和上一版对比,确认变化方向符合物理直觉再跑全曲线。血泪经验是,直接跑 31 个点等半小时,结果发现是索引写错,后悔药没得吃。这套仿真值不值得做,取决于你要不要碰波束管理这个方向——只要碰,DFT 码本加波束扫描就是绕不开的基本功,先把这条 SE-SNR 曲线跑顺,后面加分层、加压缩反馈、加多用户,都是在这条基线上长出来的。希望帮到你。
本文还有配套的精品资源,点击获取