做5G物理层仿真的朋友,一定绕不开3GPP TR 38.901这套信道模型。最近我在复现一个基于大规模MIMO-NOMA的毫米波系统,把混合波束成形和OFDM全部串起来,用Matlab做端到端仿真。这个项目对应的是一个很典型的组合:5G毫米波、大规模天线阵列、非正交多址,以及OFDM调制。整篇博客我会从系统设计出发,拆解关键参数,再放上可落地的代码片段,最后把我在调试中踩过的坑都列出来。无论你是在校研究生,还是刚接触通信物理层仿真的工程师,都可以照着搭一套自己的仿真平台。
1. 先厘清这套系统到底在仿什么
1.1 为什么是3GPP TR 38.901而不是传统信道模型
很多朋友第一次看到3GPP TR 38.901就头疼,它不像老一代的ITU-R M.2135那样直接给几行公式,而是全篇表格、概率分布和生成流程。但既然要仿真5G毫米波,最好还是老老实实用它。原因很简单:传统信道模型大多是为6GHz以下频段设计的,路径损耗、穿透损耗、簇的分布都偏保守,直接搬到28GHz毫米波场景会失真。
TR 38.901覆盖0.5GHz到100GHz频段,支持UMa、UMi、RMa、InH等典型场景。它把信道参数分成大尺度参数和小尺度参数:大尺度包括路径损耗、阴影衰落、莱斯K因子、时延扩展、角度扩展;小尺度则细化到每条簇、每条射线。相比CDL模型,它多出了空间一致性、阻塞效应、氧气吸收等毫米波特性。也就是说,CDL是38.901的一个简化子集,适合算法验证;真正要评估系统级性能,还是得按38.901的完整流程来。
在实际项目中,我没有一开始就跑完整版38.901,而是先用Matlab自带的nrCDLChannel把NOMA和混合波束成形算法调通,确认接收机SIC那一套逻辑没问题,再切换到自主生成的UMa信道样本。这样能省掉大量调代码的时间,也避免把信道模型的问题和算法问题混在一起。
1.2 大规模MIMO-NOMA要解决的核心矛盾
大规模MIMO和NOMA放在一起,很多人第一反应是“这两个东西是不是重复了”。大规模MIMO用空间自由度区分用户,NOMA用功率域区分用户,表面上看确实有重叠。但在毫米波场景下,大规模MIMO的天线阵列很大,RF链路却很有限,波束数量和可同时服务的用户数都受限。NOMA的价值就在于:在同一个波束覆盖范围里,可以再叠加两个或更多用户,让一个时频资源块承载多路信号。
在功率域NOMA里,发射端对同一子载波上的多个用户发射叠加信号,信道条件差的用户分配更高功率,信道条件好的用户分配较低功率。接收端利用SIC技术,先解调功率大的信号,再从接收信号中减去它,再解调自己的信号。近用户信道好,所以要承担干扰消除的任务;远用户信道差,直接用高功率把近用户信号当噪声解调。
大规模MIMO-NOMA的典型配对方式是:把两个用户分到同一个波束覆盖范围内,一个靠近基站、一个靠近小区边缘,两者信道增益差异越大,NOMA增益越明显。如果两个用户信道太好,功率分配空间变小,NOMA反而可能不如正交多址OMA。因此仿真时不要随机乱配用户,最好先用信道增益差做一轮筛选。
1.3 混合波束成形和OFDM是怎么配合的
毫米波频段天线阵列通常有64到256个阵元,如果按全数字MIMO设计,每个天线都要配一条RF链路,成本和功耗都不可接受。混合波束成形把预编码拆成两级:模拟域用移相器网络做宽带波束指向,数字域在基带做窄带预编码。这样可以用远少于天线数的RF链路,比如8条或16条,同时支撑多流传输。
OFDM在这里的关键作用是降低均衡复杂度。毫米波大带宽会带来频率选择性衰落,如果单载波处理,均衡器复杂度会很高。OFDM把宽带信道切成一个个窄带子载波,每个子载波近似平坦,频域均衡只在对应子载波上进行。混合波束成形的设计也需要考虑OFDM的特点:模拟预编码对所有子载波共用,数字预编码可以逐子载波调整。这种“模拟宽带、数字窄带”的拆分,正是混合波束成形和OFDM配合的核心。
2. 关键模块设计与参数选择
2.1 信道模型实现要点
我在项目里的基础参数是这样设置的:载波频率28GHz,系统带宽100MHz,子载波间隔120kHz,FFT点数1024。基站端天线阵列采用8乘8的平面阵列,共64个天线阵元;RF链路数量设为8条。用户端每个设备为2天线,但只使用单流传输,因此接收天线可简化为1个有效端口。
按TR 38.901生成信道的流程可以概括为三步。第一步确定场景:我选的是UMa微小区,NLOS传播,用户在距离基站50到300米的环形区域内随机分布。第二步生成大尺度参数,包括路径损耗、阴影衰落、时延扩展、角度扩展等,这些参数都服从38.901里规定的分布,而且相互之间有相关性。第三步根据大尺度参数生成小尺度参数,包括簇的数量、每簇的功率、时延、到达角、离开角,最后合成信道冲激响应。
如果不想手工实现整套38.901,Matlab 5G Toolbox的nrCDLChannel是一个不错的入口。虽然它只提供CDL-A到CDL-E这几种标准化剖面,但底层参数和生成流程完全兼容38.901。代码可以这样初始化:
fc = 28e9; bandwidth = 100e6; channel = nrCDLChannel; channel.DelayProfile = 'CDL-D'; channel.CarrierFrequency = fc; channel.TransmitAntennaSize = [8 8 1 1 1]; % 8x8 UPA channel.ReceiveAntennaSize = [1 1 1 1 2]; % 2根接收天线 channel.TransmitArrayOrientation = [0; 15; 0]; % 下倾角15度 channel.SampleRate = 120e3 * 1024;需要注意,CDL-D剖面里没有大尺度衰落,它只提供多径强度和角度信息。因此如果你要对比不同用户间信道增益的差异,最好手动乘上路径损耗和阴影衰落。我在调试早期就直接忽略这个大尺度,导致两个NOMA配对用户的信道增益几乎一样,SIC怎么调都发挥不出优势。
2.2 用户配对与功率分配
NOMA系统里,用户配对直接决定性能上限。常规做法是让两个用户在同一个模拟波束方向上,一个信道强、一个信道弱。信道强的用户做SIC,信道弱的用户直接用高功率解调。配对指标最简单的就是信道增益差,增益差至少3到6dB,NOMA才有明显吞吐量优势。
功率分配我用的是分数阶功率分配FTPA,公式为:
[ P_k = P_{total} \cdot \frac{(h_k^2)^{-\alpha}}{\sum_{j=1}^{K} (h_j^2)^{-\alpha}} ]
这里α是需要人工调节的衰减因子,范围0到1。α越大,用户间功率差异越大。实测时α从0.2到0.6分档扫描,每个SNR点选最优α。对于两用户场景,我习惯直接写成功率系数:
P_total = 1; alpha = 0.3; h_norm = abs(H_user).^2 ./ mean(abs(H_user).^2); p_far = P_total * (1 / h_norm(2))^alpha / ... ((1 / h_norm(1))^alpha + (1 / h_norm(2))^alpha); p_near = P_total - p_far;注意这里我把远用户定义为信道增益低的一侧,所以分配功率更高。仿真时如果把p_near和p_far的分配关系写反,SIC流程会直接崩掉,误码率甚至会比OMA还差。
2.3 混合波束成形设计
混合波束成形设计分成模拟和数字两步。模拟部分我采用DFT码本扫描方式生成波束指向。基站64天线分8行8列,方位角扫描范围从-60度到60度,下倾角固定15度。对每个扫描角度,生成对应导向向量:
N = 64; d = 0.5; % 天线间距,单位波长 angle_az = -60:5:60; % 方位角扫描 angle_el = 15; % 下倾角 F_RF = zeros(N, length(angle_az)); for i = 1:length(angle_az) az = angle_az(i) * pi/180; el = angle_el * pi/180; F_RF(:, i) = exp(1j*2*pi*d*sin(az)*cos(el)*(0:N-1)')/sqrt(N); endF_RF是64乘25的矩阵,每一列对应一个模拟波束。实际系统会根据用户位置选择最匹配的波束索引,比如信道范数最大的那一列。注意每个RF链路只能选一个模拟波束,所以8条RF链路最多选8列,也可以多个RF链路指向同一波束来加强增益。
数字预编码在等效信道上做。假设某个子载波k对应的信道矩阵是H_k,尺寸为用户数乘64。先计算等效信道:
H_eq = H_k * F_RF_sel; % 用户数 x RF链路数然后做ZF或MMSE数字预编码:
RF_num = size(F_RF_sel, 2); reg = 1e-2; F_BB = H_eq' / (H_eq * H_eq' + reg * eye(RF_num));ZF预编码把用户间干扰置零,但天线空间自由度不足时矩阵奇异。加正则项回归到MMSE思路,仿真稳定性会好很多。最后合成并归一化总预编码矩阵:
F_total = F_RF_sel * F_BB; F_total = F_total / norm(F_total, 'fro') * sqrt(P_total);很多新手会漏掉最后一步功率归一化,导致发射信号功率随预编码矩阵波动,然后所有信噪比曲线对不上。这个坑一定要避开。
2.4 OFDM与帧结构参数
OFDM参数我选择120kHz子载波间隔,FFT点数1024,循环前缀长度128,对应采样率122.88MHz。100MHz带宽内有效子载波约810个,其余留作保护频带。对毫米波来说,120kHz间隔能适应28GHz载波上较大的相位噪声,同时也能在CP长度和符号开销之间取得平衡。
在发射端,每对NOMA用户在同一资源块上发射两个符号。对每个子载波,频域发信号为:
[ x_k = \sqrt{p_{far}} s_{far,k} + \sqrt{p_{near}} s_{near,k} ]
s_far和s_near都来自QPSK或16QAM调制。叠加后的信号再经过预编码矩阵映射到64根天线上,然后做IFFT、加CP、发射。
接收端先做OFDM解调,得到频域接收信号,再对每个子载波做均衡和SIC。因为NOMA是逐子载波操作的,所以OFDM的频域窄带特性让SIC的实现简单很多,不需要处理跨符号干扰。
3. Matlab仿真工程怎么落地
3.1 顶层代码结构与仿真流程
整套代码我建议分成四个文件:main主脚本、channel_generation函数、hybrid_beamforming函数、noma_sic_receiver函数。主脚本只负责参数设置、循环调用和结果汇总。
主脚本仿真流程可以这样组织:
- 初始化系统参数,包括载波频率、带宽、天线数、RF链路数、调制方式。
- 生成用户位置,计算大尺度路径损耗和阴影衰落。
- 按信道模型生成每个用户的基础信道矩阵H。
- 根据信道增益做NOMA用户配对,并计算功率分配系数。
- 对每个子载波设计混合预编码矩阵F_total。
- 生成QPSK/16QAM数据符号,做OFDM调制和NOMA功率叠加。
- 信道传输,加上高斯白噪声。
- 接收端每个用户在频域做均衡,执行SIC解码。
- 统计BER、频谱效率、吞吐量,并循环多个信道样本取平均。
蒙特卡洛循环次数我一般设500到1000次。如果信道生成太慢,可以先固定一个信道样本调试算法,确认所有模块工作正常后再开循环。这样调试效率会高很多。
3.2 信道生成的核心函数
自己写得最顺手的是把nrCDLChannel包装成“每次调用返回频域信道矩阵”的函数。因为OFDM仿真需要每个子载波一个信道矩阵,不能直接用nrCDLChannel的时域输出。大概思路是这样:
function H_freq = get_channel_frequency(channel, num_subcarriers, nTx, nRx) release(channel); channel.RandomStream = 'global stream'; [pathGains, sampleTimes, pathFilters] = channel(); % 通过时域冲激响应做FFT变换到频域 imp_resp = pathGains .* exp(1j*2*pi*fc*sampleTimes); % 简化示例 H_freq = fft(imp_resp, num_subcarriers, 2); H_freq = squeeze(H_freq); % 尺寸调整为 nSub x nRx x nTx end严格来说这个简化函数丢掉了信道滤波器的成型过程,对实验室算法验证是够用的。如果你追求更贴近3GPP标准,应该从pathFilters矩阵里提取完整的冲激响应再做FFT。但注意,在实际项目中我建议直接用Matlab提供的nrChannelModel或nrCDLChannel的info结构配合nrOFDMModulate,能省很多底层工作。
还没到毫米波规模的时候,别急着写几千行代码。先用小规模参数把流程跑通,比如16天线、4条RF链路、两个用户。等算法性能趋势正确了,再升级到64天线、8条RF链路。
3.3 混合波束成形与SIC解码的关键片段
混合波束成形放在逐子载波的循环里,会有性能问题。我一般先设计好模拟波束F_RF_sel,然后在每个子载波循环里只更新数字预编码F_BB。因为模拟波束是宽带共用,不需要反复计算。
数字预编码的细节在上文已经给出。现在重点说接收端SIC。假设某个子载波接收信号为y,等效信道为H_eq,两个NOMA用户中远用户功率高、近用户功率低。
远用户不需要做SIC,直接把y除以自身功率再解调。近用户需要先解调远用户信号,并根据信道估计重构出远用户的接收分量,再从y中减去。关键代码如下:
% 远用户解码 s_far_hat = demodulate(y / sqrt(p_far), modOrder); % 重构远用户发射信号(假设QPSK符号归一化) s_far_mod = modulate(s_far_hat, modOrder); y_sic = y - sqrt(p_far) * H_eq_far * s_far_mod; % 近用户解码 s_near_hat = demodulate(y_sic / sqrt(p_near), modOrder);这里H_eq_far是远用户在当前子载波上的等效信道。如果信道估计有误差,重构信号就会不干净,误差会传播到近用户。所以在仿真初期,我直接用理想信道估计先把SIC逻辑调通,再逐步加入信道估计误差,这样能明确区分出性能下降是来自信道估计还是SIC本身。
3.4 性能指标怎么算才合理
评价NOMA系统,不能只看误码率,还要看频谱效率。因为在同个资源块上叠加了两个用户,频谱效率应该按两个用户的速率之和除以占用带宽来算。
用户速率可以用香农容量公式算,也可以用实际调制编码算。我习惯看两种:一是理论频谱效率,便于和算法论文对比;二是实际吞吐量,更贴近工程。理论SINR计算时要注意NOMA的干扰结构:
远用户把近用户信号当作噪声,所以远用户SINR为:
[ \text{SINR}{far} = \frac{p{far} |h_{far} w_{far}|^2}{p_{near} |h_{far} w_{near}|^2 + N_0} ]
近用户做完SIC后,远用户信号被理想减去,所以近用户SINR为:
[ \text{SINR}{near} = \frac{p{near} |h_{near} w_{near}|^2}{N_0} ]
画频谱效率曲线时,横轴SNR建议按单用户发射功率定义,不然不同功率分配系数下横轴含义会混乱。我在项目里统一用每用户平均发射功率归一化,这样不同NOMA参数之间才可比。
4. 实测中常见的坑与排查
4.1 信道模型跑太慢怎么办
完整38.901信道生成非常耗时,尤其是多用户、多子载波、蒙特卡洛循环叠在一起。我第一次跑64天线、8用户、810子载波、500次仿真,笔记本直接跑了一个通宵。后来发现大部分时间都没有花在正事上,而是浪费在信道重复生成上。
解决思路有三个。第一,同一轮仿真内,用户位置和信道样本固定,只改变发射符号和噪声,统计错误概率时再用多轮信道平均。第二,用CDL-D或CDL-E代替完整UMa信道,验证NOMA和波束成形逻辑,只在最终性能验证时启用完整38.901。第三,如果用了并行计算,把信道生成函数加上parfor,但要确保随机数流在每个 worker 上是独立的,否则不同信道样本会重复。
4.2 混合波束成形数值不稳定
在做数字预编码时,ZF矩阵求逆经常遇到奇异问题,尤其是用户数接近RF链路数的时候。我遇到过一个12天线、4RF链路、4用户场景,H_eq的秩不足,F_BB算出来全是NaN。
解决办法是改用MMSE预编码,加一点正则化:
reg = 1e-3; F_BB = H_eq' * inv(H_eq * H_eq' + reg * eye(RF_num));另外输入模拟波束矩阵时要注意列归一化。如果F_RF每列功率不同,等效信道的尺度也有问题,后面所有功率分配都会偏。我习惯在仿真最开始就统一对F_RF做每列归一化,然后检查sum(abs(F_RF).^2)是否等于1。
4.3 SIC解码出错率飙升
SIC最怕误差传播。只要远用户解码错一个符号,近用户解调时信噪比就会急剧下降,误码率甚至会出现“地板效应”,再怎么增加SNR也降不下去。
排查时先看功率分配差距。两用户NOMA,远用户功率至少要比近用户高3dB,否则SIC第一步很容易出错。建议先用6dB功率差试跑,再去调α参数。
还要检查调制方式。如果远用户用16QAM,近用户用QPSK,远用户解调难度大,SIC性能自然差。实际系统里远用户通常用低阶调制,近用户用高阶调制。仿真时最好分别设置两用户的调制阶数。
4.4 问题速查表
| 现象 | 可能原因 | 排查方案 |
|---|---|---|
| 误码率随SNR下降不明显 | NOMA功率分配差距太小 | 增大α,或强制远用户功率提高3-6dB |
| 近用户误码率远高于理论值 | SIC误差传播 | 先检查远用户解调是否正确,再增加功率差 |
| 预编码矩阵出现NaN | H_eq秩亏 | 改用MMSE,加正则化系数 |
| 频谱效率混乱不可比 | 横轴SNR定义不统一 | 统一按每用户平均发射功率定义 |
| 蒙特卡洛仿真太慢 | 完整信道模型重复生成 | 固定信道样本,或临时用CDL替代 |
5. 这套平台还能往哪些方向扩展
5.1 从静态场景到移动性
TR 38.901支持空间一致性,能描述用户移动过程中的信道平滑变化。我目前的静态仿真里,用户位置固定、信道不随时间变,这只适合链路级算法验证。如果要做移动性研究,可以在每次信道生成时填入UE的移动轨迹,让大尺度参数和小尺度参数都随时间演化。这时候混合波束成形就要加波束跟踪模块,比如每几个OFDM符号重新扫描一次波束方向。NOMA用户配对也需要动态更新,因为用户信道增益差会随着位置变化而变化。
5.2 从两用户NOMA到多用户和RSMA
两用户NOMA是最简单的验证形态。实际系统里一个波束可能叠3到4个用户,SIC复杂度会随用户数指数上升。扩展方向之一是把固定功率分配换成迭代注水或比例公平调度,让功率分配和用户调度联合优化。另一个方向是研究速率拆分多址RSMA,它把用户信息分成公共部分和私有部分,在同一个资源块里用串行干扰消除处理,相比NOMA能更灵活地适应信道差异。Matlab代码里可以把发送端的信号叠加从两路扩展成多路,接收端SIC循环解码逐用户做。
5.3 从纯仿真到硬件验证
如果后面要做FPGA原型,Matlab仿真里的混合预编码矩阵可以直接导出为查找表。模拟波束码本里的移相器角度量化是重点,实际移相器只能提供比如6比特的量化相位。我在仿真里已经做了角度量化的占位函数,把F_RF的相位四舍五入到64个量化等级,这样可以提前评估量化损失。数字预编码部分则需要和定点数仿真配合,把浮点的F_BB转成定点数,这一步踩坑会很多,建议留足调试时间。
6. 经验总结与最后的实操建议
这套仿真平台最核心的收获不是某一条曲线,而是把3GPP TR 38.901信道、大规模MIMO-NOMA、混合波束成形和OFDM几件事串起来的方式。我自己在实际搭建中印象最深的是:信道模型和算法验证要分层。先用最简化的信道把接收机链路调通,再逐渐逼近真实信道模型,最后再回头看性能曲线。这样每一步都很稳,每一类问题都能明确归因,而不是所有模块堆在一起出了错都不知道从哪查起。
最后再分享一个小技巧:NOMA系统仿真的功率分配系数不要拍脑袋定,先在低SNR和高SNR各跑一条频谱效率曲线,看趋势是否单调。如果出现NOMA在低SNR反而不如OMA的情况,通常不是信道的问题,而是功率分配系数不合适。把α参数从0.1到0.9扫一遍,画出频谱效率和α的关系曲线,你会对这个系统的敏感度有非常直观的理解。这个技巧我每次做NOMA项目都会用,非常有效。