☰
NOMA-OFDMA仿真:功率分配与SIC接收机实现及BER曲线分析
2026/9/26 7:45:44 网站建设 项目流程

简介:这份资源是面向无线通信学习者与科研人员的NOMA与OFDMA对比仿真工具包,聚焦两种多址接入技术的性能分析与融合设计。内容涵盖OFDMA与NOMA的收发链路、编解码、功率分配及误码率评估等核心模块,适合具备一定通信原理基础、希望借助MATLAB仿真验证NOMA-OFDMA融合方案的中高级读者。压缩包共13个文件,以11个m脚本为主体,辅以1个mat结果数据和1个zip归档,整体约156KB,脚本覆盖数据生成、OFDM收发、NOMA与OFDMA编解码、Turbo编译码及BER曲线绘制等环节,结构紧凑便于按模块调试。目前已有381人学习下载,可作为理解非正交与正交多址差异、复现误码率对比实验的参考素材,帮助读者快速搭建仿真环境并观察不同接入方式在频谱效率与复杂度上的权衡。

1. NOMA-OFDMA 仿真到底在算什么:从一张误码率曲线说起

如果你最近在调 NOMA-OFDMA 的仿真,大概率遇到过这种场景:功率分配因子调了半天,BER 曲线要么在高信噪比段翘起来,要么和理论值差了三四个数量级。这不是玄学,而是 NOMA 和 OFDMA 在资源映射层面耦合之后,很多默认假设不再成立。NOMA-OFDMA 仿真要解决的核心问题,是在 OFDMA 的子载波网格上叠加功率域非正交多址,让同一时频资源承载多个用户,再通过 SIC 逐级剥离。它适合正在做多址接入对比、想验证功率分配与用户配对策略的通信方向从业者。这篇笔记按“先立模型、再跑链路、最后排错”的顺序展开,所有参数都给出可改的入口,你照着搭一遍就能得到自己的曲线。

2. 先把 NOMA-OFDMA 的链路模型立住:子载波、功率域与 SIC 怎么摆

2.1 OFDMA 子载波网格与 NOMA 功率复用的叠加关系

OFDMA 的本质是把带宽切成互不重叠的子载波,每个用户独占若干子载波,接收端靠频率正交性分离。NOMA 反过来,允许两个或多个用户在同一个子载波上同时发送,靠功率差异区分。把两者拼在一起,常见的做法是:先把用户分组,组内用户共享同一组子载波,组间仍按 OFDMA 分配不同子载波。这样既保留了 OFDMA 调度灵活的优点,又在组内引入了 NOMA 的过载能力。

仿真里最容易含糊的是资源网格的维度定义。我一般用三个维度描述:子载波索引、OFDM 符号索引、用户索引。假设子载波总数 N_sc,每组占用 N_sub 个子载波,组内用户数 K。那么第 m 个符号上,第 n 个子载波上的发送信号可以写成组内所有用户信号的叠加:

x[n,m] = Σ_{k=1}^{K} sqrt(p_k) * s_k[n,m]

其中 p_k 是第 k 个用户的功率,s_k 是调制符号。接收端第 k 个用户看到的是自己信号加上其他用户的干扰,以及信道噪声。这个式子是后面所有代码的骨架,功率分配因子怎么设、SIC 顺序怎么定,都围绕它展开。

2.2 功率分配因子与用户配对:两个决定曲线走向的变量

功率分配是 NOMA 里最敏感的参数。组内用户信道增益差异越大,NOMA 相对 OFDMA 的增益越明显。常见做法是给远用户分配更大功率,近用户分配更小功率,这样远用户先被解码,近用户靠 SIC 把远用户信号减掉。固定功率分配简单但不够稳,分数功率分配更常用:

p_far = P_total * α p_near = P_total * (1 - α)

α 一般取 0.6 到 0.8 之间。α 太小,远用户信噪比不够,SIC 第一级就出错;α 太大,近用户被压制,整体吞吐下降。仿真时建议把 α 做成扫描变量,先看 0.5 到 0.9 的 BER 变化,再定工作点。

用户配对策略同样关键。随机配对实现简单,但增益波动大。基于信道增益排序的配对更稳:把用户按增益从大到小排列,最强配最弱,次强配次弱。这样组内增益差最大,NOMA 增益最明显。代码里用一个排序加首尾配对就能实现,后面会给出具体写法。

2.3 SIC 接收机的最小实现步骤

SIC 是 NOMA 接收端的核心。它的逻辑是:先解功率最大的用户,判决后重构该用户信号,从接收信号里减掉,再解下一个用户。仿真里实现 SIC 要注意三点:第一,重构时要用估计到的信道和已知的功率分配因子;第二,判决错误会传播,所以第一级用户的 BER 必须足够低;第三,减掉信号后的残余干扰要计入下一级。

最小实现可以按下面这个流程走:

  1. 接收信号 y = h * x + n,h 是信道,n 是噪声。
  2. 按功率从大到小排序用户,得到解码顺序。
  3. 对第一个用户,直接做均衡和判决,得到符号估计。
  4. 用估计符号、信道和功率重构该用户信号。
  5. 从 y 中减去重构信号,得到剩余信号。
  6. 对下一个用户重复步骤 3 到 5,直到所有用户解完。

这个流程在代码里就是一个循环,循环次数等于组内用户数。注意每次减之前要确认重构信号和接收信号在同一个尺度上,否则减完反而更糟。

3. 用 Python 把 NOMA-OFDMA 链路跑通:从参数表到 BER 曲线

3.1 仿真参数表与初始化代码

先把参数固定下来,后面所有代码都引用这张表。参数不是死的,你可以按自己的场景改,但改完要重新检查量纲和归一化。

参数含义典型值
N_sc子载波总数64
N_sub每组子载波数16
K组内用户数2
N_symOFDM 符号数1000
mod_order调制阶数4(QPSK)
alpha功率分配因子0.7
SNR_dB信噪比扫描范围0 到 20
channel信道类型瑞利衰落

初始化代码负责生成用户位置、信道增益和功率分配。下面这段可以直接抄:

import numpy as np N_sc = 64 N_sub = 16 K = 2 N_sym = 1000 mod_order = 4 alpha = 0.7 SNR_dB = np.arange(0, 21, 2) # 生成组内用户信道增益,瑞利衰落 def gen_channel(K, N_sub, N_sym): h = (np.random.randn(K, N_sub, N_sym) + 1j * np.random.randn(K, N_sub, N_sym)) / np.sqrt(2) return h # 功率分配:远用户 alpha,近用户 1-alpha def power_alloc(alpha, K): p = np.zeros(K) p[0] = alpha p[1] = 1 - alpha return p

逻辑说明:gen_channel 生成复高斯信道,实部虚部各服从 N(0,1/2),保证总功率为 1。power_alloc 按远用户优先分配功率,p[0] 对应远用户,p[1] 对应近用户。参数 alpha 控制分配比例,改它就能扫描功率分配对 BER 的影响。

3.2 发送端:QPSK 调制与子载波映射

发送端要做三件事:生成比特、QPSK 调制、把符号映射到子载波上。NOMA 的特点是同一子载波上叠加多个用户的符号,所以映射时不是一人一份,而是叠加后发送。

def qpsk_mod(bits): # bits: (N_sym * log2(mod_order),) bits = bits.reshape(-1, 2) symbols = (1 - 2 * bits[:, 0]) + 1j * (1 - 2 * bits[:, 1]) return symbols / np.sqrt(2) def tx_signal(K, N_sub, N_sym, p): tx = np.zeros((N_sub, N_sym), dtype=complex) user_bits = [] for k in range(K): bits = np.random.randint(0, 2, N_sub * N_sym * 2) sym = qpsk_mod(bits).reshape(N_sub, N_sym) user_bits.append(bits) tx += np.sqrt(p[k]) * sym return tx, user_bits

逻辑说明:qpsk_mod 把每两个比特映射成一个 QPSK 符号,并做 1/sqrt(2) 归一化,保证符号功率为 1。tx_signal 对每个用户生成独立比特流,调制后乘以对应功率再叠加。返回的 tx 是叠加后的发送信号,user_bits 留给接收端算 BER 用。参数 p 来自 power_alloc,改 alpha 就改了 p。

3.3 接收端:SIC 循环与 BER 统计

接收端是仿真里最容易翻车的地方。SIC 循环写错一步,BER 曲线就会完全不对。下面这个实现按功率从大到小解码,每解一个就减掉一个。

def sic_receiver(rx, h, p, user_bits, N_sub, N_sym): K = h.shape[0] order = np.argsort(p)[::-1] # 功率从大到小 residual = rx.copy() ber = np.zeros(K) for idx in order: # 均衡 eq = residual * np.conj(h[idx]) / (np.abs(h[idx])**2 + 1e-10) # 判决 bits_hat = np.zeros(N_sub * N_sym * 2) bits_hat[0::2] = (eq.real < 0).astype(int).flatten() bits_hat[1::2] = (eq.imag < 0).astype(int).flatten() # 算 BER ber[idx] = np.mean(bits_hat != user_bits[idx]) # 重构并减掉 sym_hat = qpsk_mod(bits_hat).reshape(N_sub, N_sym) residual -= np.sqrt(p[idx]) * h[idx] * sym_hat return ber

逻辑说明:order 按功率降序排列,保证先解远用户。均衡用最大比合并的简化形式,分母加 1e-10 防止除零。判决把实部虚部分别与 0 比较,得到比特估计。算完 BER 后,用估计比特重新调制,乘以功率和信道,从 residual 里减掉。注意 residual 初始是接收信号,每轮减一次,减的顺序必须和解码顺序一致。

参数说明:h 是信道矩阵,p 是功率向量,user_bits 是发送端返回的原始比特。N_sub 和 N_sym 用来 reshape。如果你把 K 改成 3,这个循环会自动多跑一轮,但功率分配和排序逻辑要相应调整。

3.4 主循环:扫描信噪比并画出 BER 曲线

把发送、信道、接收串起来,外层扫 SNR,内层跑蒙特卡洛。下面这段是主循环:

import matplotlib.pyplot as plt ber_avg = np.zeros((len(SNR_dB), K)) for i, snr in enumerate(SNR_dB): noise_var = 10 ** (-snr / 10) for trial in range(50): h = gen_channel(K, N_sub, N_sym) p = power_alloc(alpha, K) tx, user_bits = tx_signal(K, N_sub, N_sym, p) rx = np.zeros_like(tx) for k in range(K): rx += h[k] * tx rx += np.sqrt(noise_var / 2) * (np.random.randn(N_sub, N_sym) + 1j * np.random.randn(N_sub, N_sym)) ber = sic_receiver(rx, h, p, user_bits, N_sub, N_sym) ber_avg[i] += ber ber_avg[i] /= 50 plt.semilogy(SNR_dB, ber_avg[:, 0], 'o-', label='Far user') plt.semilogy(SNR_dB, ber_avg[:, 1], 's-', label='Near user') plt.xlabel('SNR (dB)') plt.ylabel('BER') plt.legend() plt.grid(True) plt.show()

逻辑说明:外层扫 SNR,每个 SNR 跑 50 次蒙特卡洛取平均。噪声方差按 10^(-SNR/10) 算,实部虚部各分一半。接收信号是各用户信号经过信道后的叠加,再加噪声。SIC 接收机返回每个用户的 BER,最后画半对数曲线。远用户曲线通常下降慢,近用户下降快,这是 NOMA 的典型特征。

参数说明:trial 次数影响曲线平滑度,50 次够看趋势,要发论文建议 1000 次以上。noise_var 的计算假设信号功率归一化为 1,如果你改了调制或功率归一化,这里要同步改。

4. 仿真里最容易翻车的五个地方:排查与避坑

4.1 现象:BER 曲线在高 SNR 段不再下降,反而翘起来

原因:SIC 重构时尺度不对,减掉的信号比实际接收信号大或小,残余干扰随 SNR 升高反而更明显。常见的是重构时忘了乘信道 h,或者功率 p 用错。

解决:检查 residual -= sqrt(p[idx]) * h[idx] * sym_hat 这一行,确认 h 和 p 的索引与当前解码用户一致。可以在减之前打印 residual 的功率和重构信号的功率,两者应该接近。

4.2 现象:近用户 BER 比远用户还高

原因:功率分配反了,或者 SIC 解码顺序反了。NOMA 里远用户功率大,先解,近用户功率小,后解。如果 order 按升序排,先解近用户,近用户会被远用户强干扰压死。

解决:确认 order = np.argsort(p)[::-1] 是降序。再检查 power_alloc 里 p[0] 是不是远用户。如果用户编号和信道增益的对应关系乱了,先固定用户顺序再调。

4.3 现象:曲线整体平移,和理论值差固定 dB 数

原因:噪声方差计算和信号功率归一化不匹配。QPSK 归一化用了 1/sqrt(2),但噪声方差没跟着调,导致等效 SNR 偏移。

解决:统一归一化基准。要么信号功率归一化为 1,噪声方差按 10^(-SNR/10);要么都不归一化,按实际功率算。改完在一个 SNR 点上手算理论 BER 对一下。

4.4 现象:蒙特卡洛次数够了但曲线仍然抖动

原因:信道生成每次独立,但用户配对没固定,导致组内增益差随机变化。随机配对会让 NOMA 增益时有时无。

解决:把用户配对改成基于信道增益排序的固定配对。每次生成信道后先排序,再按首尾配对,这样组内增益差稳定,曲线更平滑。

4.5 现象:K 改成 3 之后 SIC 循环出错

原因:功率分配只写了两个用户,第三个用户功率为 0,或者排序后索引越界。

解决:power_alloc 要支持任意 K。常见做法是按信道增益排序后,用分数功率分配公式给每个用户分功率,而不是硬编码两个值。K 变大时,SIC 循环本身不用改,但每级减完后的残余干扰会累积,BER 会变差,这是正常的。

5. 把 NOMA-OFDMA 仿真用起来:从对比 OFDMA 到扫参数找边界

5.1 加一条 OFDMA 基线,看 NOMA 到底赢在哪

光看 NOMA 的 BER 不够,要证明它有价值,得和 OFDMA 对比。OFDMA 里每个用户独占子载波,没有组内干扰,接收端不用 SIC。改法很简单:把 tx_signal 里的叠加去掉,每个用户只在自己的子载波上发。接收端直接均衡判决,不跑 SIC 循环。

对比时看两个指标:一是相同总功率下,NOMA 的和速率是否高于 OFDMA;二是远用户的 BER 是否可接受。NOMA 的增益主要来自远用户,近用户可能比 OFDMA 差,这是功率分配的代价。我一般把 alpha 从 0.5 扫到 0.9,画一条和速率曲线,找峰值点。

5.2 扫功率分配因子和用户数,找工作边界

仿真最大的价值是找边界。固定 SNR=10dB,扫 alpha 从 0.5 到 0.9,步长 0.05,看远用户和近用户的 BER 交叉点。交叉点对应的 alpha 就是公平性最好的点。再固定 alpha,扫 K 从 2 到 4,看和速率什么时候不再增长。K 太大时,SIC 残余干扰累积,和速率反而下降,这个转折点就是组内用户数的上限。

扫参数时记得把蒙特卡洛次数调大,否则曲线毛刺太多,看不出趋势。我一般扫参数用 200 次,画最终曲线用 1000 次。

5.3 一个容易忽略的技巧:信道估计误差的影响

前面所有仿真都假设接收端知道完美信道。实际系统里信道估计有误差,SIC 重构时用的 h 不完美,残余干扰会更大。加误差的方法是在接收端把 h 换成 h_hat = h + e,e 是估计误差,方差设为信道方差的某个比例。跑一遍会发现,近用户对误差更敏感,因为它在 SIC 后面,前面减不干净全落到它头上。

这个技巧能帮你判断方案鲁棒性。如果误差 5% 就崩,说明功率分配太激进,要留余量。如果误差 20% 还能用,说明方案有实用价值。

5.4 我踩过的坑和现在的习惯

最早做 NOMA-OFDMA 仿真时,我直接把 NOMA 的代码和 OFDMA 的代码拼在一起,结果子载波映射对不上,BER 曲线像噪声。后来养成习惯:先单独跑通 OFDMA,确认 BER 和理论 QPSK 一致;再单独跑通 NOMA 单载波,确认 SIC 逻辑对;最后才拼成 NOMA-OFDMA。每次改参数只改一个,改完立刻和上一版对比。这样出问题能快速定位是哪一层引入的。

另外,仿真代码一定要版本管理。功率分配、SIC 顺序、噪声归一化这三处最容易改乱,每次改动记一笔,不然过两天自己都忘了当时为什么那么写。希望帮到你。

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

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

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

立即咨询