简介:资源包围绕5G非正交多址接入(NOMA)的功率分配问题,整理了完整的实现与学习资料,适合通信工程专业学生及5G物理层算法研究人员。包内含MATLAB脚本、Simulink仿真模型、相关论文PDF及图文说明,覆盖从NOMA基本原理到功率分配算法仿真验证的完整链条。全部文件共9个,压缩包体积仅2.08MB,其中两个.m文件提供可直接运行的功率分配与调制代码,.slx模型支持交互式仿真,PDF文档给出理论支撑。资源目前已有436人浏览学习。通过分析包内模型和代码,读者可深入理解基于信噪比差异的功率分配策略,掌握恒定功率与动态功率分配的实现方式,并学习结合优化算法提升系统吞吐量与公平性的方法,为5G网络部署提供参考。
1. 5G NOMA 功率分配:同样是给用户分功率,为什么你的仿真总比别人差一截
做 5G 物理层仿真时,NOMA(非正交多址)是绕不开的候选方案。它的核心收益不靠频域或时域划分,而靠功率域拉开用户差异,再通过 SIC(串行干扰消除)在接收端把混叠信号一层层剥出来。也就是说,功率分配直接决定用户能否解调、系统吞吐量能到多少、弱用户会不会被饿死。很多人在这个环节翻车:要么把总功率均分给用户导致 SIC 完全失效,要么只盯着和速率优化,结果边缘用户速率惨不忍睹。
这篇笔记从系统模型、信道生成讲起,把分数阶功率分配(FTPA)、注水算法(WF)两种主流方案的核心代码和参数边界铺开,再给出一套可复现的蒙特卡洛仿真框架。适合正在做 NOMA 物理层仿真的同学、需要对比功率分配算法的工程师,以及想验证 SIC 解码链路是否正确的入门者。你会看到每一步的代码、参数怎么设、瓶颈在哪,以及我踩过的一些坑。
2. NOMA 功率分配仿真:先搞清楚系统模型再做算法选型
2.1 单小区多用户下行 NOMA 的基本链路关系
NOMA 下行链路里,基站把多个用户的信号在功率域叠加后从同一资源块发出去。接收端各用户拿到的是混合信号,必须依赖信道差异来区分彼此。最常用的场景是单小区单天线、单载波、K 个用户,基站总发射功率为 P_total,用户 k 的分配功率为 P_k,且满足所有用户功率之和不超过 P_total。
信道模型一般写成复数信道系数 h_k,包含大尺度衰落和小尺度衰落。大尺度用路径损耗指数和距离表示,小尺度用瑞利衰落或莱斯衰落生成。排序规则是核心:用户按信道增益 |h_k|^2 降序排列,增益最强的用户排在最后做 SIC,最弱的用户最先解码,同时把强用户的信号当作干扰直接解调自己的信号。SIC 解码顺序和功率分配顺序必须一致,否则从源头就错了。
信道系数生成是整套仿真的地基。常见做法是先给每个用户一个基准距离 d_k,然后叠加路径损耗和瑞利衰落分量。路径损耗公式通常写成 PL(d) = 128.1 + 37.6 * log10(d),这是城市宏站场景下比较通用的参数组合。瑞利衰落部分生成标准复高斯随机变量就行。这样生成的 |h_k|^2 就同时包含距离远近和快衰落的随机性,排序结果每次仿真都不同,更贴近真实调度。
2.2 频谱效率、用户速率与公平性指标怎么算
NOMA 性能评估离不开两组指标。第一组是吞吐量和频谱效率,第二组是公平性,常用 Jain 公平性指数或最弱用户速率来表征。吞吐量计算前要先确定每个用户的 SINR,而 SINR 的计算顺序完全依赖 SIC 的解码次序。
用户 k 在 SIC 解码后,自身信号受到的干扰来自哪些用户,取决于它排在哪个位置。信道增益最差的用户(通常第一个被解码)会把所有其他用户的信号都当作干扰,SINR 表达式的分母里包含所有强用户的功率加噪声。信道增益最强的用户做 SIC 时已经把所有弱用户的信号消掉了,分母只剩噪声和尚未消除的同层残留干扰。
SINR 算出来后,用户速率用香农公式 R_k = B * log2(1 + SINR_k) 计算,系统总速率是所有用户速率之和。公平性指数则用 K * (sum R_k)^2 / sum (R_k^2) 来算,这个值越接近 1 说明用户间速率越均衡。实际仿真中经常出现系统总速率很高但 Jain 指数很低的场景,说明某几个强用户把资源吃光了。
2.3 关键公式的参数含义与量纲核对
功率分配算法里的每个参数都有明确的物理意义,量纲没对齐是仿真对不上论文数值的第一大原因。功率相关变量统一用瓦特(W)或毫瓦(mW)都行,但噪声功率、信道增益、路径损耗这三者的量纲必须配套。信道增益 |h|^2 是无量纲的复数模平方,路径损耗要转成线性值再乘进去。
噪声功率 N_0 等于热噪声功率谱密度乘以带宽,常温下热噪声谱密度约为 -174 dBm/Hz。带宽 B 若用 MHz,那噪声功率就是 -174 + 10*log10(B_Hz) dBm。换算到线性功率后,SINR 的计算公式才能保证量纲正确。很多新手直接把 SNR 配成固定值,这就丢掉了 NOMA 最核心的距离差异和功率竞争关系。
功率分配比例的常用设置也别拍脑袋。分数阶功率分配里衰减因子 α 一般在 0 到 1 之间取值,论文里常见 0.2、0.4、0.6 这几档。注水算法里的注水电平 λ 要满足总功率约束,是通过二分搜索或牛顿迭代解出来的。这两类算法的参数看着简单,实际跑起来对结果的影响非常敏感,下一章展开对比。
3. 分数阶功率分配与注水算法:两个主流方案的代码级对比
3.1 固定功率分配(FTPA)的公式与 MATLAB 最小实现
固定功率分配也叫分数阶功率分配,核心思想是让信道增益差的用户分到更多功率,用衰减因子 α 控制功率分配的倾斜程度。用户 k 的功率按以下公式计算:P_k = P_total * (|h_k|^2)^(-α) / sum((|h_j|^2)^(-α)),j 遍历所有用户。α = 0 时退化为平均功率分配,α 越大,弱用户分到的功率比例越高。
这个公式实现起来非常短,但必须注意排序在前、计算在后的顺序。以下代码是在 MATLAB 里实现 FTPA 的最小片段,输入是信道系数向量 h 和总功率 P_total:
% 输入: h 是 1xK 的复信道系数向量, P_total 是总发射功率 % 输出: P 是每个用户的分配功率, 满足 sum(P) = P_total function P = ftpa_power_allocation(h, P_total, alpha) % 取信道增益并做归一化 h_gain = abs(h).^2; % 按信道增益降序排序, idx(1) 是信道最强用户 [h_sorted, idx] = sort(h_gain, 'descend'); % 计算每个用户的功率权重: |h_k|^(-alpha) % 注意这里用排序后的信道增益, 保证权重和排序一致 weights = h_sorted.^(-alpha); % 归一化权重, 使总功率等于 P_total P_sorted = P_total * weights / sum(weights); % 映射回原始用户顺序 P = zeros(size(h)); P(idx) = P_sorted; end这段代码里有两个关键点。第一个是sort的descend参数,排序后idx(1)是信道最强用户,后续 SIC 解码也从最强用户开始。第二个是所有权重都按排序后的顺序计算后再映射回去,这一步漏掉的话,功率分配和 SIC 顺序会错位。α 的值直接影响分配结果,建议先跑 α = 0.4 的基准,再往两边扫。
3.2 注水算法的迭代求解与功率约束处理
注水算法的目标是在总功率约束下最大化所有用户的可达速率和,解出来的功率分配满足 KKT 条件。最优解是每个用户的功率 P_k = (λ - 1 / |h_k|^2)^+,其中(x)^+表示 max(0, x),λ 是注水电平,需要满足所有用户的功率之和等于 P_total。由于 NOMA 的用户间干扰使问题不完全是经典的注水形式,实际仿真里常用迭代注水或基于 SIC 的逐次注水,把前一个用户的解码顺序当作已知条件。
实现注水算法的核心是二分搜索 λ。先设 λ 的一个上界和下界区间,然后计算 P_k 判断总和与 P_total 的关系,反复缩小区间。下面这段代码是带 SIC 顺序的迭代注水实现:
% 输入: h_gain 是排序后的信道增益(降序), 和 h 同长度 % 输出: P 是注水后的功率分配向量 function P = iterative_waterfilling(h_gain, P_total, noise_power, max_iter) K = length(h_gain); P = zeros(1, K); % 剩余功率分配给尚未满足条件的用户 remaining = P_total; active_users = 1:K; % 初始假设所有用户都参与注水 for iter = 1:max_iter % 二分搜索注水电平 lambda lambda_low = 0; lambda_high = remaining + max(1 ./ h_gain(active_users)); for bisect = 1:50 lambda_mid = (lambda_low + lambda_high) / 2; % 当前候选功率 P_candidate = max(0, lambda_mid - 1 ./ h_gain(active_users)); if sum(P_candidate) > remaining lambda_high = lambda_mid; else lambda_low = lambda_mid; end end P(active_users) = max(0, lambda_mid - 1 ./ h_gain(active_users)); % 若某些用户功率为 0, 则剔除后重新搜索 zero_users = find(P(active_users) <= 0); if isempty(zero_users) break; end active_users(zero_users) = []; end end这段代码的二分次数设了 50 次,基本能收敛到足够精度。信道增益用排序后的h_gain,因此注水自然会把更多功率分给弱用户。注意lambda_high的初始值,我一般用剩余功率加最大1/h_gain来保证上界够大。若某些用户计算出负功率,说明信道太弱不值得分配功率,直接剔除并重新搜索。
3.3 两种算法的边界条件与选择依据
FTPA 和注水算法的适用场景差异很明显。FTPA 的优势是计算量极低,单轮分配的复杂度是 O(K),适合大规模用户快速仿真和调参扫描。它的弱点是只按信道增益的负指数加权,没有显式考虑噪声和用户间干扰,在用户数超过 6 个时性能会明显偏离最优值。
注水的性能更接近最优,但计算复杂度高,每个时隙都要迭代求解 λ,在蒙特卡洛仿真里跑几百次时总耗时显著增加。选择依据主要看仿真目的:如果是为了画吞吐量和公平性的趋势对比图,FTPA 足够;如果是为了验证 SIC 链路解码的边界或逼近容量上界,注水更合适。
还有一种混合做法,我在实际仿真中常用:先用 FTPA 算出一个初始功率分配,然后只对信道增益前十的强用户组做一次迭代注水微调。这样复杂度增加不多,但能让强用户间的干扰抑制更充分,弱用户的速率也不会被压得太低。
4. 把完整仿真跑起来:信道生成、SIC 解码与蒙特卡洛主循环
4.1 用户位置生成与信道系数计算的可复现代码
一套能复现的仿真必须从随机种子管理开始。MATLAB 里用rng(2024)固定随机数生成器,保证每次跑出来的信道序列完全一致。用户位置按照均匀分布在小区半径内取样,距离直接影响路径损耗,因此也影响功率分配的结果。
信道生成代码里,路径损耗用 dB 表示,瑞利衰落用复数高斯随机变量。最终的信道系数要转成线性幅度。以下是一段完整的信道生成代码:
% 参数设置 rng(2024); num_users = 4; % 用户数 cell_radius = 500; % 小区半径, 单位米 path_loss_exp = 37.6; % 路径损耗指数 shadowing_std = 8; % 阴影衰落标准差, dB noise_power_dBm = -104; % 噪声功率, dBm, 对应 10MHz 带宽 P_total_dBm = 43; % 基站总功率, dBm, 约 20W % 生成用户距离 (均匀分布在小区内) distances = cell_radius * sqrt(rand(1, num_users)); % 路径损耗 dB PL_dB = 128.1 + path_loss_exp * log10(distances); % 阴影衰落 shadowing_dB = shadowing_std * randn(1, num_users); % 瑞利衰落信道系数 (复数) rayleigh = (randn(1, num_users) + 1j * randn(1, num_users)) / sqrt(2); % 合成信道系数: 线性幅度 h = rayleigh .* 10.^(-(PL_dB + shadowing_dB) / 20); h_gain = abs(h).^2;这段代码里小区半径设了 500 米,用户距离用sqrt(rand)而不是rand,是因为均匀分布在圆形区域内时,距离的平方是均匀的,用sqrt才能保证空间分布均匀。噪声功率 -104 dBm 对应约 10 MHz 带宽,如果你用 20 MHz,记得改成 -101 dBm。瑞利衰落除以sqrt(2)是为了让实部和虚部的方差各为 0.5,模平方的均值等于 1。
信道系数计算好后,紧接着做信道增益排序。NOMA 的 SIC 解码顺序完全基于这个排序,排序方式决定了后面所有 SINR 的计算顺序。排序结果每轮蒙特卡洛都不一样,这正是需要蒙特卡洛仿真的原因——单次信道实现只能反映一个随机时刻的链路质量。
4.2 SIC 解码顺序与 SINR 计算的完整链路
SIC 解码的过程是逐级消除干扰,必须严格按信道增益降序来。信道最弱的用户直接解自己的信号,其他所有用户都是干扰;信道次弱的用户解码时,强度最弱的那个用户信号已经被消除了,干扰集合少一个;以此类推。
SINR 计算的数学表达要写清楚。设用户索引按信道增益降序为 u_1(最强), u_2, ..., u_K(最弱)。用户 u_m 的 SINR 分母中包含所有 m+1 到 K 用户的功率,加上一个残留干扰项。理想 SIC 假设残留干扰为 0,但实际仿真里残留因子 β 常取 0.01 到 0.1 之间,对应 -20 dB 到 -10 dB 的消除深度。
以下代码计算理想 SIC 下各个用户的 SINR,并输出速率和公平性指标:
% 输入: h_gain 已排序降序, P 是排序后的功率分配 % 计算每个用户的 SINR K = length(h_gain); SINR = zeros(1, K); noise_linear = 10^(noise_power_dBm / 10) / 1000; % dBm 转 W for m = 1:K % 分子: 自己的信号功率乘信道增益 signal_power = P(m) * h_gain(m); % 分母: 更弱用户的功率之和(它们还没被消除) + 噪声 interference = 0; for n = m+1:K interference = interference + P(n) * h_gain(n); end SINR(m) = signal_power / (interference + noise_linear); end % 速率计算 (bps/Hz) rate = log2(1 + SINR); sum_rate = sum(rate); % Jain 公平性指数 jain_index = (sum_rate)^2 / (K * sum(rate.^2));这段代码的循环里有两点容易写错。第一个是干扰项只累加m+1到K的用户,因为 SIC 解码顺序从弱到强,信道强的用户后解码,此时弱用户信号已经被消除,不再构成干扰。第二个是噪声功率必须先从 dBm 转成瓦特再参与线性计算,直接拿 dBm 值带入公式会出数量级错误。
信号功率的物理意义是基站给该用户分配的功率乘以该用户自身的信道增益,干扰功率同理。因为每个用户看到的是一个叠加信号,功率分配结果的差异直接体现在 SINR 上。
4.3 蒙特卡洛主循环:批处理与结果汇总
蒙特卡洛仿真的目的是统计平均性能,消除随机信道带来的波动。主循环里每次迭代生成一组新距离和信道系数,然后按同一种功率分配算法计算吞吐量和公平性,最后对所有迭代结果取平均。循环次数一般 1000 到 5000 次,城市宏站场景下 1000 次就能得到稳定的均值。
我一般会在循环开始前用tic/toc计时,如果单次信道计算加功率分配超过 5 毫秒,就考虑优化代码。注水算法的二分搜索在大规模迭代时是主要的耗时来源,此时可以把二分次数从 50 降到 20,性能损失很小但速度提升近一倍。
循环结束后把每次迭代的sum_rate和jain_index分别存数组,最后取均值。画图时横轴用 SNR 或用户数,纵轴用吞吐量或公平性指数。多方案对比时,把 FTPA 的三种 α 值、注水算法和平均功率分配放在同一张图里,用不同线型区分。
以下是完整的主循环框架代码:
num_iter = 2000; sum_rate_mean = zeros(1, 4); % 四种方案 jain_mean = zeros(1, 4); % 方案列表: 平均分配, FTPA alpha=0.2, FTPA alpha=0.6, 注水 for iter = 1:num_iter % 重新生成用户距离和信道 (见 4.1 节代码) % 距离每次重新生成, 保证统计独立性 % 按信道增益降序排序 [h_gain_sorted, idx] = sort(h_gain, 'descend'); % 方案1: 平均功率分配 P_avg = (P_total_linear / num_users) * ones(1, num_users); % 方案2: FTPA alpha=0.2 P_ftp02 = ftpa_power_allocation(h, P_total_linear, 0.2); % 方案3: FTPA alpha=0.6 P_ftp06 = ftpa_power_allocation(h, P_total_linear, 0.6); % 方案4: 注水 P_wf = iterative_waterfilling(h_gain_sorted, P_total_linear, noise_linear, 30); % 注意: 所有方案都要按排序后的 h_gain_sorted 计算 SINR % 计算四个方案的 sum_rate 和 jain_index, 累加到均值数组 end sum_rate_mean = sum_rate_mean / num_iter; jain_mean = jain_mean / num_iter;方案间比较时必须保证信道路径完全一致,所以每种算法消耗的都是同一个h,只是分配功率不同。排序操作在循环里只做一次,四个方案共用排序结果。P_ftp02返回的功率顺序是原始用户顺序,计算 SINR 前要重排到降序,否则 SIC 顺序会乱掉。
5. NOMA 功率分配仿真避坑:五条真实踩坑记录
5.1 SIC 解码顺序与功率分配顺序不一致导致速率异常低
某次我跑出的系统总速率比理论值低了近一半,单用户速率全部异常。逐个检查发现功率分配函数返回的功率顺序是按照原始用户编号排的,而 SINR 计算用的信道增益是降序排列,两个序列的顺序没有对齐。SIC 顺序跟着信道增益走,功率分配自然也按同一个顺序,二者一错位,所有 SINR 公式里的干扰项就全错了。
解决方法是功率分配完成后,显式做一次重排映射。代码里用P(idx) = P_sorted把功率放回原始位置,或者干脆把排序后的h_gain_sorted和对应的P_sorted传入 SINR 计算函数,保持同一个顺序从头到尾。
5.2 均分功率导致弱用户 SINR 为负,SIC 链路直接崩溃
平均功率分配看似简单,在用户数大于 3 时弱用户的分母里堆积了所有强用户的信号功率,SINR 经常算出来是负数,log2 后速率变成 NaN。这不是代码 bug,而是 NOMA 的天然约束:功率差异不够大时 SIC 解码条件不满足。
解决方法是放弃均分作为对比基线,改用 FTPA 的 α = 0 退化为均分的近似值,或者只把均分放在用户数等于 2 的小场景里做验证。用户数超过 4 时,至少用 FTPA α = 0.4 起步,避免整个仿真链路被 NaN 污染。
5.3 注水算法不收敛:二分区间上界设太小
注水的二分搜索里,我把lambda_high设成了用户中最大的1 / h_gain,结果迭代 50 次后分配的功率之和明显小于总功率。原因是注水电平 λ 可能大于所有1/h_gain,此时所有用户的功率都是正的,λ 的上界不足导致永远搜不到正确值。
正确的上界应该是remaining + max(1 ./ h_gain),把剩余总功率加上最大的倒数值。这条改完二分搜索每次都能收敛,功率总和误差低于 1e-6。如果你用牛顿迭代法求 λ,记得对目标函数逐步检查,迭代超 100 次仍未收敛就重置初值。
5.4 噪声功率量纲换算错误导致 SNR 偏移 30 dB
有一次仿真的 SNR 整体比论文参考值高了约 30 dB,排查半天发现问题出在10^(noise_power_dBm / 10) / 1000这行。原本写的是10^(noise_power_dBm / 10),没有除以 1000,把 dBm 直接当成 dBW 用了。dBm 转瓦特必须除以 1000,因为 0 dBm 对应 1 mW,而 1 W 是 1000 mW。
量纲错误在功率分配类仿真里影响是全局性的,不只是 SNR 偏移,SIC 解码顺序、功率分配权重、公平性指数全都会跟着变。建议在做完信道生成后先跑一个单次仿真脚本,打印出sum(P)、noise_linear和SINR三个变量,确认量纲对了再跑蒙特卡洛。
5.5 功率分配结果没有归一化导致总功率越界
FTPA 实现里,如果sum(weights)忘记归一化,分配出的总功率会超过 P_total 或不足 P_total。表现在结果上是系统总速率忽高忽低,没有稳定的增长趋势。FTPA 的表达式里分母是所有权重之和,必须有这一步,漏掉等于没做功率约束。
加一层保护:功率分配函数里计算完P后,检查abs(sum(P) - P_total) > 1e-10,超了就报错。这个断言在调试阶段能快速暴露问题,跑大规模仿真时再注释掉,避免每次迭代都做浮点比较浪费计算。
6. 验证你的仿真结果:把正确性证明给别人看的三个方法
仿真跑完不等于结果可信,至少要做三类验证。第一类是退化场景验证:把 α 设为 0,FTPA 退化为均分,此时单用户速率应该和 OMA(正交多址)的结果接近。第二类是对比验证:固定信道不重生成,手动口算一组功率分配和 SINR,和代码输出逐位核对。第三类是趋势验证:随着 SNR 增加,系统总速率应该单调递增,公平性指数应该稳定在某个区间。
我习惯在脚本末尾加一段自动检查逻辑:若isnan(sum_rate)或jain_index > 1,直接输出错误信息中止运行。跑完 2000 次迭代后,打印出均值、标准差和 95% 置信区间,这样论文汇报数值时能交代清楚波动范围。若均值附近抖动超过 5%,就把迭代次数翻倍,多数情况下 2000 次足够。
进阶验证是对比已有文献曲线。把信道路径损耗的指数从 37.6 改成 30.1(对应城区室外热点),观察 FTPA 在弱信道衰减下是否比强信道衰减下更接近注水性能。这个趋势符合理论预期:信道衰减变小后,用户的增益差异缩小,FTPA 固定指数权重的优势减弱,注水的动态调节收益更明显。
搭配一个复杂度对比实验更完整:记录每种方案单次蒙特卡洛迭代的运行时间,FTPA 应该在毫秒级,注水通常在 2 到 3 倍耗时。这两条曲线放在一起,读者就能直观判断性能提升值不值得额外计算开销。
最后一条习惯:所有功率分配函数必须显式接收P_total而不是从全局变量读值。我吃过全局变量的亏,改参数跑对比实验时,上一次的赋值残留让某组仿真全程用错了总功率,浪费了整整半天。把参数全通过函数入参传递,每次调用都是独立上下文,排查问题时不需要回忆全局状态。这个小习惯希望帮到你。
本文还有配套的精品资源,点击获取