
简介这份源码面向无线通信方向的学生、研究人员与工程师聚焦MIMO-OFDM系统在不同信噪比下的频谱效率仿真并覆盖DFT码本设计、beam训练与波束扫描等关键环节适合具备一定通信原理与MATLAB基础、希望深入理解多天线系统波束管理的读者。压缩包共3个文件均为m脚本整体约5KB体量轻便便于快速阅读与二次修改。内容围绕MIMO信道模型建立、OFDM信号生成与解调、DFT码本计算、SNR对频谱效率的影响分析以及beam训练和波束扫描算法展开读者可据此复现仿真流程、观察频谱效率随SNR变化的趋势并掌握码本选择与波束方向优化的实现思路。目前已有157人学习适合作为无线通信系统设计与优化的实践参考。1. 从一条 SNR 曲线说起MIMO-OFDM 的频谱效率到底卡在哪把 SNR 从 0 dB 拉到 30 dB盯着频谱效率曲线看很多人第一次跑 MIMO-OFDM 仿真都会愣一下低信噪比区间曲线爬得挺快到了中高 SNR 却明显变缓甚至出现平台。这不是代码写错了而是波束训练和波束扫描的开销在吃增益。MIMO-OFDM 把空间维度和频域维度叠在一起频谱效率的上限由信道秩、码本量化精度、训练开销三者共同决定。DFT 码本负责把连续波束方向量化成有限个候选beam 训练负责在收发之间选出最优配对波束扫描负责在训练阶段遍历这些候选。SNR 增加时如果码本粒度不够细或者训练开销随天线数线性增长频谱效率就会被量化误差和导频开销拖住。这套仿真适合做链路级性能评估的工程师、准备做波束管理方案选型的同学以及想搞清楚“为什么 SNR 涨了频谱效率不涨”的从业者。下面按可复现的路径把码本生成、训练流程、扫描策略和 SNR 扫描脚本一层层拆开。2. DFT 码本与系统模型先把仿真骨架搭对2.1 为什么选 DFT 码本而不是随机码本在 MIMO-OFDM 波束赋形里码本决定了波束方向的候选集合。常见做法是用 DFT 码本因为它的列向量是正交的波束方向在空间上均匀分布且生成方式只依赖天线数不需要信道先验。随机码本虽然在某些场景下能降低量化误差但训练阶段无法保证覆盖均匀容易出现波束空洞。DFT 码本的另一个好处是可以用 FFT 快速生成仿真里不用存大矩阵。对于均匀线性阵列天线数 Nt 的 DFT 码本第 k 个码字可以写成import numpy as np def dft_codebook(n_tx, oversampling1): 生成 DFT 码本 n_tx: 发射天线数 oversampling: 过采样因子1 表示标准 DFT 码本 返回: (n_tx, n_tx * oversampling) 的复数矩阵 n_beams n_tx * oversampling codebook np.zeros((n_tx, n_beams), dtypecomplex) for k in range(n_beams): for n in range(n_tx): codebook[n, k] np.exp(-1j * 2 * np.pi * k * n / n_beams) / np.sqrt(n_tx) return codebook这段代码里oversampling控制波束的密集程度。标准 DFT 码本oversampling1的波束间隔等于阵列的瑞利分辨率过采样到 2 或 4 可以减小量化误差但训练候选数也成倍增加。/ np.sqrt(n_tx)是归一化保证每个码字的发射功率为 1。实际仿真里如果天线数超过 32用循环矩阵生成会比双重循环快很多但逻辑是一样的。2.2 OFDM 资源网格与信道生成MIMO-OFDM 的频谱效率定义为单位带宽上的可达速率单位是 bit/s/Hz。仿真里需要先搭 OFDM 资源网格子载波数、循环前缀长度、符号数。常见配置是 1024 个子载波、72 个 CP、14 个符号对应一个时隙。信道用 3GPP 的抽头延迟线模型每个抽头加一个发射角这样波束方向才有意义。def generate_channel(n_tx, n_rx, n_subcarriers, n_taps6, snr_db10): 生成频域 MIMO 信道 返回: (n_subcarriers, n_rx, n_tx) 的频域信道 # 时域抽头功率按指数衰减 tap_power np.exp(-np.arange(n_taps) / 2) tap_power tap_power / np.sum(tap_power) # 每个抽头的发射角均匀分布 aoa np.random.uniform(-np.pi/2, np.pi/2, n_taps) h_freq np.zeros((n_subcarriers, n_rx, n_tx), dtypecomplex) for tap in range(n_taps): # 发射阵列响应 at np.exp(1j * np.pi * np.arange(n_tx) * np.sin(aoa[tap])) / np.sqrt(n_tx) # 接收端假设全向简化为随机相位 ar np.exp(1j * 2 * np.pi * np.random.rand(n_rx)) / np.sqrt(n_rx) h_tap np.sqrt(tap_power[tap]) * np.outer(ar, at.conj()) # 每个抽头在频域有相位旋转 for sc in range(n_subcarriers): h_freq[sc] h_tap * np.exp(-1j * 2 * np.pi * tap * sc / n_subcarriers) return h_freq这里aoa是每个抽头的发射角at是发射阵列响应ar是接收端响应。频域信道通过时域抽头的 DFT 得到。注意h_freq的维度是子载波、接收天线、发射天线后面算频谱效率时要在每个子载波上做 SVD 或者波束赋形。SNR 在加噪声时体现这里先只生成信道。2.3 频谱效率的计算口径频谱效率不是把 SNR 代进香农公式就完事。MIMO-OFDM 里每个子载波的信道不同总频谱效率是各子载波容量的平均。如果用了波束赋形等效信道是h_eff h * w其中w是选出的码字。对于单流波束赋形频谱效率为def compute_spectral_efficiency(h_freq, w_tx, snr_db): h_freq: (n_subcarriers, n_rx, n_tx) w_tx: (n_tx,) 选出的发射码字 snr_db: 标量信噪比 返回: 平均频谱效率 bit/s/Hz n_subcarriers h_freq.shape[0] snr_linear 10 ** (snr_db / 10) se_sum 0.0 for sc in range(n_subcarriers): h_eff h_freq[sc] w_tx # (n_rx,) gain np.linalg.norm(h_eff) ** 2 se_sum np.log2(1 snr_linear * gain) return se_sum / n_subcarriersgain是等效信道的功率增益snr_linear * gain是后处理信噪比。这里假设接收端做最大比合并所以取范数平方。如果做多流需要把h_eff写成矩阵然后算log2(det(I ...))。仿真里先跑单流把波束训练的逻辑跑通再扩到多流。3. Beam 训练与波束扫描把候选波束选出来3.1 训练帧结构与导频开销Beam 训练的核心是在收发之间找到一组码字使等效信道增益最大。常见做法是发射端遍历码本接收端遍历码本每个配对发一个训练符号接收端测量参考信号接收功率。训练开销等于Nt_beams * Nr_beams个符号。如果天线数 64过采样 2发射 128 个波束接收 8 个波束就是 1024 个训练符号。在 14 个符号的时隙里根本放不下所以实际系统会用分级扫描或者压缩感知。仿真里为了看清 SNR 的影响先做穷举扫描把上界跑出来。def beam_training(h_freq, codebook_tx, codebook_rx, snr_db): 穷举波束训练返回最优发射和接收码字索引 n_tx_beams codebook_tx.shape[1] n_rx_beams codebook_rx.shape[1] best_gain -np.inf best_tx 0 best_rx 0 for i in range(n_tx_beams): for j in range(n_rx_beams): gain_sum 0.0 for sc in range(h_freq.shape[0]): h_eff codebook_rx[:, j].conj() h_freq[sc] codebook_tx[:, i] gain_sum np.abs(h_eff) ** 2 if gain_sum best_gain: best_gain gain_sum best_tx i best_rx j return best_tx, best_rx, best_gain这段代码里codebook_rx[:, j].conj() h_freq[sc] codebook_tx[:, i]是接收合并后的等效信道。gain_sum对所有子载波求和选最大的配对。穷举的复杂度是Nt_beams * Nr_beams * N_subcarriers子载波多的时候很慢仿真里可以只在中心子载波上选或者用子载波分组。3.2 波束扫描的顺序与 SNR 的关系波束扫描有两种顺序先发射后接收或者联合扫描。先发射后接收的做法是发射端固定一个波束接收端遍历所有波束记录最佳接收波束然后换下一个发射波束。这样训练开销是Nt_beams * Nr_beams但接收端只需要存一个索引。联合扫描是每个配对都测开销一样但实现简单。SNR 低的时候测量噪声大容易选错波束所以实际系统会在训练符号上做多次重复然后平均。仿真里可以加一个n_repeat参数模拟重复测量。def beam_sweeping_with_noise(h_freq, codebook_tx, codebook_rx, snr_db, n_repeat4): 带噪声的波束扫描每个配对重复 n_repeat 次 n_tx_beams codebook_tx.shape[1] n_rx_beams codebook_rx.shape[1] snr_linear 10 ** (snr_db / 10) best_gain -np.inf best_tx 0 best_rx 0 for i in range(n_tx_beams): for j in range(n_rx_beams): gain_avg 0.0 for _ in range(n_repeat): noise (np.random.randn() 1j * np.random.randn()) / np.sqrt(2 * snr_linear) h_eff codebook_rx[:, j].conj() h_freq[0] codebook_tx[:, i] gain_avg np.abs(h_eff noise) ** 2 gain_avg / n_repeat if gain_avg best_gain: best_gain gain_avg best_tx i best_rx j return best_tx, best_rx, best_gainn_repeat越大噪声平均效果越好但训练时间越长。SNR 低的时候n_repeat要取 8 甚至 16否则选出的波束可能偏一个方向。这里只在中心子载波上算是为了让代码跑得快实际仿真可以对所有子载波求和。3.3 训练开销对频谱效率的修正前面算的频谱效率是理想波束赋形下的上界。实际系统里训练符号不传数据所以有效频谱效率要乘一个开销因子def effective_spectral_efficiency(se_ideal, n_tx_beams, n_rx_beams, n_symbols_per_slot14): se_ideal: 理想频谱效率 n_tx_beams, n_rx_beams: 训练波束数 n_symbols_per_slot: 每个时隙的符号数 n_train n_tx_beams * n_rx_beams overhead n_train / (n_train n_symbols_per_slot) return se_ideal * (1 - overhead)这个修正很关键。当天线数增加n_tx_beams线性增长overhead趋近于 1有效频谱效率反而下降。这就是为什么高 SNR 下曲线会出现平台甚至回落。仿真里要把这个因子加进去否则看到的曲线会过于乐观。4. SNR 扫描与结果分析把曲线跑出来4.1 主仿真循环的写法把前面的模块串起来主循环对每个 SNR 点生成信道、做波束训练、算理想频谱效率和有效频谱效率。为了减少随机性每个 SNR 点跑多次信道实现然后平均。def run_simulation(snr_db_list, n_tx16, n_rx4, n_subcarriers64, n_realizations50): codebook_tx dft_codebook(n_tx, oversampling2) codebook_rx dft_codebook(n_rx, oversampling2) results [] for snr_db in snr_db_list: se_ideal_avg 0.0 se_eff_avg 0.0 for _ in range(n_realizations): h_freq generate_channel(n_tx, n_rx, n_subcarriers, snr_dbsnr_db) best_tx, best_rx, _ beam_sweeping_with_noise( h_freq, codebook_tx, codebook_rx, snr_db, n_repeat4) w_tx codebook_tx[:, best_tx] se_ideal compute_spectral_efficiency(h_freq, w_tx, snr_db) se_eff effective_spectral_efficiency( se_ideal, codebook_tx.shape[1], codebook_rx.shape[1]) se_ideal_avg se_ideal se_eff_avg se_eff results.append((snr_db, se_ideal_avg / n_realizations, se_eff_avg / n_realizations)) return resultsn_realizations取 50 到 100曲线才平滑。snr_db_list从 0 到 30步长 5。跑完之后用 matplotlib 画出来横轴 SNR纵轴频谱效率两条线分别对应理想和有效。4.2 参数怎么调天线数、过采样、重复次数天线数是最敏感的。n_tx从 16 加到 64理想频谱效率会涨但训练开销也涨。过采样因子oversampling从 1 加到 4量化误差减小但候选波束数翻倍。重复次数n_repeat在低 SNR 要加大高 SNR 可以降到 1。下面这张表是几个典型配置的对比跑的时候可以照着改参数低 SNR 推荐高 SNR 推荐影响n_tx1632 或 64理想 SE 涨开销涨oversampling12 或 4量化误差降候选数涨n_repeat8 或 161 或 2抗噪好训练时间长n_subcarriers64256频率选择性更明显调参的时候先固定其他只动一个看曲线怎么变。比如把oversampling从 1 改到 2理想 SE 会涨一点但有效 SE 可能因为开销翻倍而下降。这个权衡就是仿真要回答的问题。4.3 结果解读为什么高 SNR 下有效频谱效率会回落跑出来的曲线通常是这样理想 SE 随 SNR 单调上升逐渐饱和有效 SE 在低 SNR 跟理想接近到中高 SNR 开始分叉甚至下降。分叉的原因是训练开销固定而理想 SE 的增长变缓开销占比相对变大。如果天线数很大训练符号数超过数据符号数有效 SE 会直接掉到零。这不是仿真 bug而是波束管理在毫米波系统的真实瓶颈。解决方向是分级码本、压缩感知或者利用信道稀疏性减少训练波束数这些可以在仿真里继续加模块验证。5. 避坑与排查跑这个仿真最容易翻车的 5 个地方5.1 码本没有归一化导致 SNR 定义错位现象频谱效率曲线整体偏高30 dB 时超过理论上限。原因dft_codebook里忘了除以np.sqrt(n_tx)发射功率不为 1等效 SNR 被放大。解决检查每个码字的二范数是否为 1用np.linalg.norm(codebook[:, k])验证。归一化之后compute_spectral_efficiency里的snr_linear才是真正的发射 SNR。5.2 波束训练时用了全子载波求和导致跑不动现象beam_training跑一次要几分钟n_realizations根本跑不完。原因三重循环Nt_beams * Nr_beams * N_subcarriers64 天线过采样 2 就是 128 个发射波束8 个接收波束64 个子载波一次训练 65536 次内积。解决只在中心子载波或者每 8 个子载波取一个上做训练选出的波束对所有子载波通用。或者用矩阵化写法把h_freq重塑成矩阵一次算完。5.3 噪声加在了错误的位置现象低 SNR 下选出的波束和最优波束完全无关有效 SE 接近零。原因beam_sweeping_with_noise里噪声加在了h_eff上但h_eff是复数标量噪声功率应该按接收符号的 SNR 来定。如果snr_linear是发射 SNR经过信道增益后接收 SNR 会变。解决先算接收符号功率|h_eff|^2再按snr_linear * |h_eff|^2生成噪声或者直接把噪声方差设为1 / snr_linear并确保信号功率归一化。5.4 训练开销因子算反了现象有效 SE 比理想 SE 还高。原因effective_spectral_efficiency里写成了se_ideal * (1 overhead)或者overhead算成了n_symbols_per_slot / n_train。解决训练符号不传数据有效 SE 一定小于理想 SE。overhead n_train / (n_train n_symbols_per_slot)乘(1 - overhead)之后一定变小。跑完先检查这一条如果有效大于理想就是公式错了。5.5 信道实现次数太少导致曲线抖动现象SNR 扫描曲线锯齿状相邻点忽高忽低。原因n_realizations只有 5 或 10随机信道没有平均掉。解决每个 SNR 点至少 50 次实现低 SNR 点可以加到 100。如果跑得慢先把n_subcarriers降到 32n_tx降到 8把逻辑跑通再放大。另外固定随机种子np.random.seed(42)可以让曲线可复现方便对比不同参数。6. 进阶技巧用分级码本把训练开销压下来穷举扫描的复杂度是Nt_beams * Nr_beams天线数一上来就不可行。实际系统用分级码本第一级用宽波束粗扫第二级在候选方向附近用窄波束细扫。仿真里可以这样实现先生成一个粗码本波束数少但每个波束宽再生成一个细码本只在粗扫选出的方向附近生成候选。def hierarchical_codebook(n_tx, level2): 分级码本level1 粗码本level2 细码本 if level 1: return dft_codebook(n_tx, oversampling1) else: coarse dft_codebook(n_tx, oversampling1) fine [] for k in range(coarse.shape[1]): # 在每个粗波束方向附近生成 4 个细波束 for offset in [-0.5, -0.25, 0.25, 0.5]: beam np.exp(1j * 2 * np.pi * (k offset) * np.arange(n_tx) / (n_tx * 4)) fine.append(beam / np.sqrt(n_tx)) return np.array(fine).T第一级用coarse扫 16 个波束选出最好的一个第二级只在那个波束附近用 4 个细波束。总训练符号从 168 降到 16 48开销降了接近一半。分级码本的代价是第二级可能选错因为第一级粗扫在低 SNR 下容易受噪声影响。仿真里可以对比穷举和分级的有效 SE看 SNR 多低的时候分级开始吃亏。验证方法很简单固定信道实现分别用穷举和分级跑波束训练比较选出的波束索引和最终有效 SE。如果分级选出的波束在穷举最优波束的相邻方向且有效 SE 差距小于 5%就说明分级可用。我一般会在 SNR 0 到 10 dB 之间多跑几组因为这个区间噪声对粗扫的影响最大。另一个技巧是把波束扫描和信道估计合在一起。训练符号既用来选波束也用来估等效信道这样数据符号的导频开销可以省掉。仿真里可以在beam_sweeping_with_noise之后用选出的波束重新生成等效信道然后算 MMSE 估计的 MSE看 SNR 和 MSE 的关系。这个扩展能让仿真更接近真实链路。最后说个习惯每次改参数之前先把当前配置和曲线存下来文件名带上n_tx、oversampling、n_repeat。不然跑了几十组之后根本分不清哪条线对应哪个配置。这个血泪经验在写论文或者做方案对比的时候特别有用。希望帮到你。本文还有配套的精品资源点击获取