☰
MIMO-OFDM波束训练与频谱效率仿真:DFT码本、波束扫描及SNR影响分析
2026/10/5 7:17:58 网站建设 项目流程

简介:这份源码面向无线通信方向的学生、研究人员与工程师,聚焦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, oversampling=1): """ 生成 DFT 码本 n_tx: 发射天线数 oversampling: 过采样因子,1 表示标准 DFT 码本 返回: (n_tx, n_tx * oversampling) 的复数矩阵 """ n_beams = n_tx * oversampling codebook = np.zeros((n_tx, n_beams), dtype=complex) 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 码本(oversampling=1)的波束间隔等于阵列的瑞利分辨率,过采样到 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_taps=6, snr_db=10): """ 生成频域 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), dtype=complex) 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_subcarriers

gain是等效信道的功率增益,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_repeat=4): """ 带噪声的波束扫描,每个配对重复 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_gain

n_repeat越大,噪声平均效果越好,但训练时间越长。SNR 低的时候,n_repeat要取 8 甚至 16,否则选出的波束可能偏一个方向。这里只在中心子载波上算,是为了让代码跑得快,实际仿真可以对所有子载波求和。

3.3 训练开销对频谱效率的修正

前面算的频谱效率是理想波束赋形下的上界。实际系统里训练符号不传数据,所以有效频谱效率要乘一个开销因子:

def effective_spectral_efficiency(se_ideal, n_tx_beams, n_rx_beams, n_symbols_per_slot=14): """ 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_tx=16, n_rx=4, n_subcarriers=64, n_realizations=50): codebook_tx = dft_codebook(n_tx, oversampling=2) codebook_rx = dft_codebook(n_rx, oversampling=2) 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_db=snr_db) best_tx, best_rx, _ = beam_sweeping_with_noise( h_freq, codebook_tx, codebook_rx, snr_db, n_repeat=4) 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 results

n_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_subcarriers,64 天线过采样 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降到 32,n_tx降到 8,把逻辑跑通再放大。另外固定随机种子np.random.seed(42)可以让曲线可复现,方便对比不同参数。

6. 进阶技巧:用分级码本把训练开销压下来

穷举扫描的复杂度是Nt_beams * Nr_beams,天线数一上来就不可行。实际系统用分级码本,第一级用宽波束粗扫,第二级在候选方向附近用窄波束细扫。仿真里可以这样实现:先生成一个粗码本,波束数少但每个波束宽;再生成一个细码本,只在粗扫选出的方向附近生成候选。

def hierarchical_codebook(n_tx, level=2): """ 分级码本:level=1 粗码本,level=2 细码本 """ if level == 1: return dft_codebook(n_tx, oversampling=1) else: coarse = dft_codebook(n_tx, oversampling=1) 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。不然跑了几十组之后,根本分不清哪条线对应哪个配置。这个血泪经验在写论文或者做方案对比的时候特别有用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询