NOMA仿真链路实战:QPSK调制、ZF检测与SIC解码的MATLAB实现
2026/9/23 15:00:59 网站建设 项目流程

简介:这份资源面向无线通信方向的学生与研究人员,聚焦5G及未来网络中的非正交多址接入(NOMA)技术,提供一套可直接运行的MATLAB仿真代码,用于理解功率域多址、QPSK调制与零强迫(ZF)解码的完整链路。压缩包共3个文件,均为.m脚本,体积约2KB,分别承担主程序仿真、QPSK调制与软解调功能,结构紧凑、便于快速上手。已有244人学习下载,说明其在同类教学与科研场景中具备一定参考价值。读者可借助该代码复现NOMA系统在同一时频资源上叠加多用户信号的过程,观察SIC逐级解码与ZF接收矩阵对多用户干扰的抑制效果,并进一步调整功率分配、信噪比与用户数量等参数,评估系统性能变化。对于需要完成课程设计、毕业课题或算法验证的读者,这套脚本可作为理解NOMA与QPSK结合原理、对比线性接收策略的实用起点。

1. NOMA 与 ZF 检测的 MATLAB 仿真:从 QPSK 调制到 SIC 解码的完整链路

如果你正在做 5G 多址接入相关的课程设计或论文复现,大概率绕不开 NOMA 这个关键词。非正交多址接入的核心思路是在同一时频资源块上,用功率域叠加多个用户的信号,接收端再通过串行干扰消除逐级解码。这套源码包围绕 NOMA 技术展开,包含 QPSK 调制、迫零检测和软解调三个核心模块,适合通信工程专业的学生或刚接触 NOMA 的工程师用来搭建可运行的仿真链路。它解决的不是“NOMA 是什么”这种概念问题,而是“怎么在 MATLAB 里把一条完整的 NOMA 链路跑通并看到误码率曲线”这个落地问题。你拿到手就能改参数、换信道、对比不同检测算法,不用从零搭框架。

2. 拆开源码包:三个文件如何串起一条 NOMA 链路

2.1 文件分工与调用关系

拿到一个源码包,我习惯先看文件命名和调用关系,而不是急着跑主程序。这个包的结构很清晰,三个 .m 文件各司其职:

文件名角色核心功能
modulation.m发送端调制比特流到 QPSK 符号的映射
test2zf.m主仿真程序信道生成、功率分配、ZF 检测、SIC 解码、误码率统计
qpsk_demodulation_soft.m接收端软解调从含噪符号计算对数似然比,输出软信息

调用链是test2zf.m作为入口,内部先调modulation.m完成每个用户的 QPSK 调制,经过功率域叠加和信道传输后,再用 ZF 矩阵做线性检测,最后调qpsk_demodulation_soft.m输出软比特,送入 SIC 循环逐用户解码。这个结构的好处是模块解耦——你想换调制方式,只改modulation.m;想换检测算法,只改test2zf.m里的检测矩阵部分。

常见做法是把这三个文件放在同一个工作目录下,MATLAB 的当前文件夹指向该目录,直接运行test2zf.m即可。不需要额外的工具箱依赖,基础 MATLAB 环境就能跑。

2.2 QPSK 调制模块的实现细节

modulation.m做的事情本质上是格雷码映射。QPSK 每个符号携带 2 比特信息,将比特对映射到四个相位点上。来看这个模块的典型实现逻辑:

function symbols = modulation(bits) % modulation.m - QPSK 调制 % 输入: bits - 比特流 (列向量, 长度为偶数) % 输出: symbols - 复数符号向量 bits = bits(:); % 强制列向量 N = length(bits) / 2; % 符号数 b = reshape(bits, 2, N).'; % 每行一个比特对 % 格雷码映射: 00->1+1j, 01->-1+1j, 11->-1-1j, 10->1-1j real_part = 1 - 2*b(:,1); % 第1比特决定实部 imag_part = 1 - 2*b(:,2); % 第2比特决定虚部 symbols = (real_part + 1j*imag_part) / sqrt(2); % 归一化功率 end

这段代码的关键在于归一化那一步。除以sqrt(2)是为了让平均符号功率为 1,这样后面加噪声时信噪比的定义才准确。如果你忘了归一化,SNR 的计算会偏 3dB,误码率曲线整体左移,看起来性能“变好”了,实际上是标尺错了。格雷码映射的选择也不是随便定的——相邻相位点之间只差一个比特,这样在信噪比不高时,一个符号判错大概率只错一个比特,而不是两个比特同时错。

参数方面,输入比特流长度必须是偶数,否则reshape会报错。如果你从更上层传下来的比特流长度不确定,建议在调用前加一句bits = bits(1:2*floor(end/2));做截断保护。

2.3 ZF 检测与 SIC 解码的主循环

test2zf.m是整个仿真的骨架。它要完成的事情包括:生成多用户比特流、QPSK 调制、功率分配、叠加、过信道、ZF 检测、SIC 逐级解码、统计误码率。核心流程如下:

% test2zf.m - NOMA ZF 检测主程序(核心片段) clear; clc; close all; %% 参数设置 N_user = 2; % 用户数 N_bits = 100000; % 每用户比特数 SNR_dB = 0:2:20; % 信噪比扫描范围 N_iter = 100; % 每个 SNR 点的蒙特卡洛次数 %% 功率分配系数 (满足 p1 + p2 = 1, p1 > p2) p = [0.8, 0.2]; % 近端用户功率大,远端用户功率小 ber = zeros(N_user, length(SNR_dB)); for s_idx = 1:length(SNR_dB) snr_lin = 10^(SNR_dB(s_idx)/10); err_count = zeros(1, N_user); for iter = 1:N_iter % 1. 生成比特并调制 bits = cell(1, N_user); syms = cell(1, N_user); for u = 1:N_user bits{u} = randi([0 1], N_bits, 1); syms{u} = modulation(bits{u}); end % 2. 功率域叠加 x = zeros(size(syms{1})); for u = 1:N_user x = x + sqrt(p(u)) * syms{u}; end % 3. 过瑞利衰落信道 + AWGN h = (randn(N_user,1) + 1j*randn(N_user,1)) / sqrt(2); noise = (randn(size(x)) + 1j*randn(size(x))) / sqrt(2*snr_lin); y = zeros(size(x)); for u = 1:N_user y = y + h(u) * sqrt(p(u)) * syms{u}; end y = y + noise; % 4. ZF 检测矩阵 H = h .* sqrt(p).'; % 等效信道矩阵 W = pinv(H); % ZF 均衡矩阵 % 5. SIC 逐级解码 (按功率从大到小) [~, order] = sort(p, 'descend'); y_res = y; for k = 1:N_user u = order(k); y_eq = W(u,:) * y_res; % ZF 均衡 llr = qpsk_demodulation_soft(y_eq, 1); % 软解调 bits_hat = llr < 0; % 硬判决 err_count(u) = err_count(u) + sum(bits_hat ~= bits{u}); % 重构已解码用户的信号并从接收信号中减去 if k < N_user x_hat = modulation(double(bits_hat)); y_res = y_res - h(u) * sqrt(p(u)) * x_hat; end end end ber(:, s_idx) = err_count / (N_bits * N_iter); end %% 绘图 figure; semilogy(SNR_dB, ber(1,:), 'b-o', SNR_dB, ber(2,:), 'r-s'); xlabel('SNR (dB)'); ylabel('BER'); legend('近端用户 (高功率)', '远端用户 (低功率)'); grid on;

这段代码有几个值得注意的设计点。第一,功率分配系数p的选择直接影响两个用户的性能——高功率用户先解码,信噪比条件好,误码率低;低功率用户后解码,要承受残余干扰,误码率相对高。这是 NOMA 的固有特性,不是 bug。第二,ZF 均衡矩阵用pinv而不是直接求逆,因为当用户数接近天线数时信道矩阵可能接近奇异,伪逆更稳健。第三,SIC 循环里每解码一个用户就重构信号并减去,这个“后悔药”机制是 NOMA 区别于正交多址的关键——先解的用户帮后面的用户清路。

参数调整建议:N_bits至少设到 10000 以上,否则高 SNR 点误码率统计不收敛;N_iter在 SNR 较低时可以少一些,SNR 高时要多跑几次才能抓到足够的错误比特。如果你把N_user改成 3 或 4,记得同步调整p的长度和h的维度。

3. 软解调模块:LLR 计算与 SIC 的配合

3.1 软信息为什么比硬判决更值得用

qpsk_demodulation_soft.m输出的是对数似然比,而不是直接的 0/1 判决。这个区别在 NOMA 的 SIC 场景下特别重要。硬判决只告诉你“这个比特是 0 还是 1”,软解调告诉你“这个比特是 0 的概率是 0.9,是 1 的概率是 0.1”。在 SIC 逐级干扰消除时,软信息能让重构信号更准确,减少误差传播。

来看软解调的实现:

function llr = qpsk_demodulation_soft(y, noise_var) % qpsk_demodulation_soft.m - QPSK 软解调 % 输入: y - 接收符号向量 % noise_var - 噪声方差 % 输出: llr - 对数似然比向量 (长度 = 2 * 符号数) y = y(:); N = length(y); llr = zeros(2*N, 1); % QPSK 星座点 (归一化后) % s00 = (1+1j)/sqrt(2), s01 = (-1+1j)/sqrt(2) % s11 = (-1-1j)/sqrt(2), s10 = (1-1j)/sqrt(2) for k = 1:N r = y(k); % 第1比特的 LLR: 比较实部 llr(2*k-1) = 2 * sqrt(2) * real(r) / noise_var; % 第2比特的 LLR: 比较虚部 llr(2*k) = 2 * sqrt(2) * imag(r) / noise_var; end end

这里的 LLR 计算利用了 QPSK 星座的对称性——实部决定第一个比特,虚部决定第二个比特,两者独立。系数2*sqrt(2)/noise_var来自最大似然准则的推导,noise_var越小(信噪比越高),LLR 的绝对值越大,表示判决越“自信”。

3.2 软解调与 SIC 的接口设计

test2zf.m的 SIC 循环里,软解调的输出经过硬判决后用于重构信号。这里有一个容易翻车的地方:重构时用的是硬判决后的比特,而不是软信息本身。如果你直接把 LLR 值当信号幅度去重构,量纲对不上,减法会出错。正确的做法是bits_hat = llr < 0先做硬判决,再用modulation重新映射成符号。

另一个细节是noise_var的传递。在 SIC 的后续阶段,由于已经减去了前面用户的信号,残余干扰的方差不再是原始噪声方差,而是噪声加上未完全消除的干扰。严格来说,每一级 SIC 后noise_var应该更新。但在用户数较少(2-3 个)且功率分配合理的情况下,直接用原始噪声方差也能跑出合理的误码率曲线。如果你发现远端用户的误码率曲线出现“地板效应”(高 SNR 时不再下降),大概率就是残余干扰没建模准确。

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

4.1 误码率曲线不下降或出现平台

现象:SNR 增加到 20dB 以上,远端用户的 BER 仍然在 10^-2 附近徘徊,曲线出现明显的“地板”。

原因:最常见的原因是 SIC 解码顺序搞反了。NOMA 要求先解码高功率用户,再解码低功率用户。如果你按用户编号顺序解码,而用户 1 恰好是低功率用户,那它的信号会被高功率用户淹没,解码错误率极高,后续重构和减法也会连带出错。

解决:在 SIC 循环前加一句[~, order] = sort(p, 'descend');,确保按功率从大到小解码。另外检查p向量是否满足sum(p) == 1,功率不归一化会导致 SNR 定义偏移。

4.2 QPSK 星座图旋转或缩放异常

现象:接收端星座图不是标准的四个点,而是被旋转了一个角度,或者点与点之间的距离不对。

原因:信道相位没有补偿。在test2zf.m里,如果 ZF 均衡矩阵没有正确使用信道估计值h,或者h的维度与用户数不匹配,均衡后的符号会残留相位旋转。

解决:检查H = h .* sqrt(p).'这一行,确保h是列向量、sqrt(p)是行向量,点乘后得到N_user × 1的等效信道。然后用W = pinv(H)得到1 × N_user的均衡向量。如果h的维度搞错,MATLAB 可能不报错但结果完全错误。

4.3 软解调输出全为正或全为负

现象qpsk_demodulation_soft返回的 LLR 向量所有值符号相同,硬判决后全是 0 或全是 1。

原因noise_var传参错误。如果noise_var传了一个负数或者零,LLR 计算会出问题。另一个可能是接收符号y没有经过均衡就直接送进了软解调,导致相位完全随机。

解决:在调用软解调前打印noise_var确认它是正数,且量级与 SNR 匹配。SNR=10dB 时noise_var大约是 0.1,SNR=0dB 时大约是 1。如果数量级差太多,检查 SNR 到噪声方差的转换公式。

4.4 蒙特卡洛仿真跑得太慢

现象N_bits = 100000N_iter = 100时,跑完整个 SNR 扫描需要几十分钟甚至更久。

原因:MATLAB 的 for 循环在大量迭代时效率低,尤其是内层还有逐符号的软解调循环。

解决:把qpsk_demodulation_soft.m里的逐符号循环改成向量化操作:

% 向量化版本 - 替换逐符号循环 llr = zeros(2*N, 1); llr(1:2:end) = 2 * sqrt(2) * real(y) / noise_var; llr(2:2:end) = 2 * sqrt(2) * imag(y) / noise_var;

这一改动能把软解调的速度提升一个数量级。另外,N_iter在 SNR 较高时可以适当减少,因为高 SNR 下误码事件本身就少,不需要那么多次蒙特卡洛。

4.5 用户数增加后 ZF 检测性能急剧恶化

现象:从 2 用户扩展到 4 用户后,即使 SNR 很高,误码率也降不下来。

原因:ZF 检测的核心是矩阵求逆,当用户数增加时,等效信道矩阵H的条件数变差,求逆会放大噪声。这是 ZF 的固有缺陷——它只消除干扰,不考虑噪声增强。

解决:如果必须用线性检测,可以换成 MMSE 准则,在消除干扰和抑制噪声之间做折中。把W = pinv(H)改成W = inv(H'*H + eye(N_user)/snr_lin) * H',这就是 MMSE 均衡矩阵。代价是干扰不能完全消除,但噪声放大问题会缓解很多。另一个方向是增加接收天线数,让H变成矩阵而不是向量,ZF 的性能会明显改善。

5. 从能跑到好用:参数扫描与性能对比的实操技巧

把链路跑通只是第一步,真正有价值的是用这套代码做参数对比。我一般会固定其他变量,只扫一个参数,看误码率曲线的变化趋势。比如功率分配系数p对两个用户性能的影响——当p = [0.9, 0.1]时,近端用户几乎不受干扰,但远端用户的信噪比极低;当p = [0.6, 0.4]时,两个用户的性能更均衡,但近端用户的优势被削弱。这个折中关系是 NOMA 论文里经常讨论的“公平性 vs 总吞吐量”问题,用仿真曲线说话比纯理论推导直观得多。

具体操作上,我会把test2zf.m里的p改成可扫描的向量,外层再加一层循环:

p_list = [0.9 0.1; 0.8 0.2; 0.7 0.3; 0.6 0.4]; snr_fixed = 10; % 固定 SNR 看功率分配的影响 for p_idx = 1:size(p_list, 1) p = p_list(p_idx, :); % ... 运行仿真,记录两个用户的 BER ... fprintf('p = [%.1f, %.1f], BER = [%.4e, %.4e]\n', ... p(1), p(2), ber(1), ber(2)); end

跑完这个扫描,你会得到一张表,清楚看到功率分配怎么影响两个用户的误码率。这张表可以直接放进论文或报告里,比一张模糊的曲线图更有说服力。

另一个值得做的对比是 ZF 和 MMSE 的检测性能。在test2zf.m里加一个开关变量detector_type,分别用pinv(H)和 MMSE 矩阵跑一遍,把两条曲线画在同一张图上。你会发现在低 SNR 区域 MMSE 明显优于 ZF,高 SNR 区域两者趋近——这个结论和教科书一致,但自己跑出来的曲线会让你对“为什么 MMSE 更鲁棒”有肌肉记忆。

还有一个容易被忽略的参数是信道模型。这套代码默认用的是瑞利衰落信道,h = (randn + 1j*randn)/sqrt(2)。如果你想模拟视距场景,可以把h改成固定值加小扰动,比如h = [1; 0.5] + 0.01*(randn(2,1)+1j*randn(2,1))。信道条件一变,ZF 检测的性能排序可能完全不一样——这也是我每次改信道模型后都会重新跑一遍基线的原因。

从那以后我每次拿到新的通信仿真代码,都强制先跑一遍默认参数确认基线,再改一个参数跑一遍对比,确认改动方向符合预期后才开始大规模扫描。这个习惯帮我省了很多“改了三个参数不知道哪个出了问题”的后悔药。希望帮到你。

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

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

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

立即咨询