简介:MU-MIMO_sim.rar 是一套面向无线通信专业学生与科研人员的多用户MIMO系统仿真MATLAB程序集,用于研究MIMO信道建模、预编码与多用户调度等核心问题。资源围绕瑞利衰落、多径衰落等信道模型展开,涵盖MMSE、MRT、零强迫等预编码算法,以及用户选择策略、吞吐量与误码率性能分析等模块,适合具备一定通信原理基础、希望从理论走向实现的读者。压缩包共229个文件,约1.65MB,以94个mat数据文件、51个m脚本、38个asv备份、14个c源码及12个mexw32与2个mexw64编译文件为主,另含少量log、h、cpp与fig图形文件,结构完整、便于逐模块研读。目前已有390人学习下载。通过运行与调试这些代码,读者可直观理解多用户MIMO的信道差异、用户间干扰抑制及系统容量评估方法,在理论与工程实践之间建立有效连接。
1. 从一份 MU-MIMO_sim.rar 说起:多用户 MIMO 到底在仿真什么
如果你手上正好有一个叫MU-MIMO_sim.rar的压缩包,或者你正在用 MATLAB 做多用户 MIMO 信道建模,那你大概率卡在同一个地方:单用户 MIMO 的代码满地都是,多用户一上来就懵——用户之间怎么区分?预编码矩阵怎么塞进链路?信道容量曲线为什么和论文对不上?这个标题背后其实是一整套「多用户 MIMO 系统级仿真」的活儿:从信道建模、预编码、功率分配,到容量计算和误码率验证。它适合通信方向的研究生、做基带算法的工程师,以及需要快速复现论文曲线的从业者。下面我按自己搭这套仿真的顺序,把能跑通的路径和踩过的坑一次讲清楚。
2. 多用户 MIMO 信道建模:从 i.i.d. 到相关信道怎么选
2.1 为什么不能直接套单用户瑞利信道
单用户 MIMO 里,信道矩阵H就是一个Nr×Nt的复高斯矩阵,每个元素独立同分布,生成一行代码就完事。但多用户场景下,H变成了K个用户的堆叠:第k个用户的信道是H_k,维度Nr_k×Nt。这时候如果还按 i.i.d. 生成,用户之间完全独立,仿真出来的和速率会偏乐观,因为现实中基站侧天线相关性、用户空间相关性都会让性能掉一截。
我一般会分三层来建模:大尺度衰落(路径损耗+阴影)、小尺度衰落(瑞利或莱斯)、空间相关性(Kronecker 模型)。大尺度决定用户信噪比的差异,小尺度决定快衰落,相关性决定 MIMO 增益能不能吃满。少了任何一层,容量曲线在低 SNR 和高 SNR 段的斜率都会不对。
2.2 用 Kronecker 模型生成相关信道矩阵
Kronecker 模型的核心是把发射端相关矩阵R_t和接收端相关矩阵R_r做克罗内克积,再乘上 i.i.d. 矩阵。下面这段是我常用的生成函数,支持每个用户独立配置天线数和相关性系数:
function H = generate_correlated_channel(Nt, Nr, K, rho_t, rho_r) % Nt: 基站发射天线数 % Nr: 每个用户的接收天线数(标量,假设所有用户相同) % K: 用户数 % rho_t, rho_r: 发射/接收端相关系数 (0~1) % 返回 H: Nr*K x Nt 的堆叠信道矩阵 H = zeros(Nr*K, Nt); for k = 1:K % 生成发射端相关矩阵(指数相关模型) Rt = zeros(Nt, Nt); for i = 1:Nt for j = 1:Nt Rt(i,j) = rho_t^abs(i-j); end end % 生成接收端相关矩阵 Rr = zeros(Nr, Nr); for i = 1:Nr for j = 1:Nr Rr(i,j) = rho_r^abs(i-j); end end % Kronecker 模型:H_k = Rr^(1/2) * G * Rt^(1/2) G = (randn(Nr, Nt) + 1j*randn(Nr, Nt)) / sqrt(2); Hk = sqrtm(Rr) * G * sqrtm(Rt); H((k-1)*Nr+1:k*Nr, :) = Hk; end end逻辑说明:sqrtm是矩阵平方根,保证Rr^(1/2)和Rt^(1/2)的乘积再乘自身转置等于原相关矩阵。rho_t和rho_r取 0 时退化成 i.i.d. 瑞利信道,取 0.5 以上时相关性明显,容量会下降 15% 到 30%。参数上,Nt常见取 4、8、16,Nr取 1 或 2,K取 4 到 10。注意sqrtm对大规模矩阵较慢,如果Nt超过 64,建议改用 Cholesky 分解加滤波,速度能快一个数量级。
2.3 路径损耗和阴影衰落的叠加方式
大尺度部分我习惯用简化模型:PL(d) = 128.1 + 37.6*log10(d),d单位公里,这是 3GPP 常见的宏站路损公式。阴影衰落用对数正态,标准差 8 dB。每个用户独立生成,然后折算成线性信噪比缩放因子,乘到小尺度信道上。
d = 0.1 + 0.4*rand(K,1); % 用户距离 100~500 米 PL = 128.1 + 37.6*log10(d); % 路径损耗 dB shadow = 8*randn(K,1); % 阴影衰落 dB gain_dB = -PL - shadow; % 总大尺度增益 gain_lin = 10.^(gain_dB/10); % 线性增益 % 对每个用户的信道块乘以 sqrt(gain_lin(k)) for k = 1:K H((k-1)*Nr+1:k*Nr, :) = sqrt(gain_lin(k)) * H((k-1)*Nr+1:k*Nr, :); end这里gain_lin直接乘在信道上,等价于接收功率的缩放。注意别把 dB 和线性值搞混,我见过有人把gain_dB直接乘上去,结果容量曲线直接飞到 10^12 量级,血泪经验。
3. 预编码与用户调度:把多用户干扰压下去
3.1 线性预编码选 ZF 还是 MMSE
多用户 MIMO 下行最常用的两种线性预编码:迫零(ZF)和最小均方误差(MMSE)。ZF 完全消除用户间干扰,但会放大噪声,在低 SNR 时性能差;MMSE 在干扰和噪声之间折中,低 SNR 更稳。我一般两个都实现,对比着看。
预编码矩阵W的维度是Nt×K(每个用户单流)。ZF 的公式是W = H^H (H H^H)^{-1},然后按列归一化。MMSE 是W = H^H (H H^H + σ²/ P * I)^{-1},其中σ²是噪声功率,P是总发射功率。
function W = precoding_zf(H, P) % H: Nr*K x Nt 堆叠信道 % P: 总发射功率 [~, Nt] = size(H); K = size(H,1) / (size(H,1)/size(H,1)); % 这里假设每个用户单天线 % 更稳妥的写法:外部传入 K W = H' * inv(H*H'); % 列归一化,保证每列功率为 P/K for i = 1:size(W,2) W(:,i) = W(:,i) / norm(W(:,i)) * sqrt(P/size(W,2)); end end上面代码里K的计算我写了个占位,实际用时建议把K作为参数传进来,避免用户天线数不为 1 时出错。归一化那一步很关键:不归一化的话,预编码后总功率会随信道条件数剧烈波动,容量曲线会出现不合理的尖峰。MMSE 版本只需把inv(H*H')改成inv(H*H' + noise_var/P * eye(size(H,1))),noise_var是噪声方差。
3.2 用户调度:选谁一起服务
多用户 MIMO 不是用户越多越好。基站天线Nt=4时,同时服务超过 4 个用户,ZF 的零空间不够用,干扰压不干净。我一般用贪心调度:先算所有用户的信道范数,选最大的那个,然后每次选一个和已选用户信道相关性最低的,直到选满Nt个或达到用户数上限。
function selected = greedy_scheduling(H, K, Nr, Nt) % H: Nr*K x Nt % 返回选中的用户索引 selected = []; candidates = 1:K; % 第一个选信道增益最大的 gains = zeros(K,1); for k = 1:K Hk = H((k-1)*Nr+1:k*Nr, :); gains(k) = norm(Hk, 'fro')^2; end [~, first] = max(gains); selected = [selected, first]; candidates(candidates == first) = []; while length(selected) < min(Nt, K) && ~isempty(candidates) best_corr = inf; best_idx = -1; for c = candidates Hc = H((c-1)*Nr+1:c*Nr, :); % 计算与已选用户的最大相关性 max_corr = 0; for s = selected Hs = H((s-1)*Nr+1:s*Nr, :); corr = abs(trace(Hc' * Hs)) / (norm(Hc,'fro')*norm(Hs,'fro')); max_corr = max(max_corr, corr); end if max_corr < best_corr best_corr = max_corr; best_idx = c; end end selected = [selected, best_idx]; candidates(candidates == best_idx) = []; end end调度带来的增益在用户数多的时候非常明显:K=10、Nt=4时,贪心调度比随机选 4 个用户,和速率能高 40% 以上。注意相关性用trace(Hc'*Hs)是一种简化,更严格的做法是用子空间夹角,但计算量大,实际仿真里上面这种够用。
3.3 功率分配:注水还是等功率
如果做的是容量仿真,注水算法是理论最优。但多用户下注水要在预编码之后做,而且每个用户等效信道增益不同。我一般先等功率跑通,再换注水对比。等功率就是每个用户分P/K,简单不容易错。注水需要先算等效信道H_eff = H*W,然后对H_eff的奇异值做注水。
function p = waterfilling(lambda, P) % lambda: 等效信道增益(奇异值平方) % P: 总功率 lambda = lambda(:); N = length(lambda); % 二分法找水位 mu_low = 0; mu_high = max(lambda) + P; for iter = 1:100 mu = (mu_low + mu_high) / 2; p = max(mu - 1./lambda, 0); if sum(p) > P mu_high = mu; else mu_low = mu; end end p = max(mu - 1./lambda, 0); endlambda是等效信道矩阵奇异值的平方,1./lambda是等效噪声放大因子。注水在低 SNR 时会把功率集中到强子信道,高 SNR 时接近等功率。实际仿真里如果lambda有零元素,要加个极小值保护,否则除零报错。
4. 容量计算与误码率验证:曲线对不上的排查思路
4.1 和速率容量的两种算法
多用户 MIMO 下行和速率容量,在完美 CSI 和线性预编码下,常用公式是R = Σ log2(1 + SINR_k),其中SINR_k是第 k 个用户预编码后的信干噪比。另一种是脏纸编码(DPC)容量上界,作为理论天花板对比。
function R = sum_rate(H, W, P, noise_var, K, Nr) R = 0; for k = 1:K Hk = H((k-1)*Nr+1:k*Nr, :); wk = W(:, k); % 有用信号功率 sig = abs(Hk * wk)^2; % 干扰功率 interf = 0; for j = 1:K if j ~= k interf = interf + abs(Hk * W(:,j))^2; end end sinr = sig / (interf + noise_var); R = R + log2(1 + sinr); end end注意noise_var要和预编码归一化时的功率假设一致。我见过有人预编码归一化用了P/K,但容量计算时noise_var没对应缩放,结果曲线整体平移。排查方法:先跑K=1单用户,看容量是否等于log2(1+SNR*|h|^2),对上了再上多用户。
4.2 误码率仿真:ML 检测和线性检测的差距
容量是理论值,误码率才是能验证系统能不能用的指标。多用户下行,用户端做检测。线性检测(ZF/MMSE)复杂度低但性能差,ML 检测最优但复杂度指数级。我一般先跑 MMSE 检测,再跑 ML 做对比。
function ber = simulate_ber(H, W, P, noise_var, K, Nr, mod_order, nbits) % 简化:每个用户单流,QPSK 或 16QAM % 生成发送符号 M = 2^mod_order; tx = randi([0 M-1], K, nbits); tx_sym = qammod(tx, M, 'UnitAveragePower', true); % 预编码 x = W * tx_sym; % 过信道 y = H * x + sqrt(noise_var/2)*(randn(size(H,1), nbits) + 1j*randn(size(H,1), nbits)); % MMSE 检测 G = (W'*H'*H*W + noise_var*eye(K)) \ (W'*H'); rx_sym = G * y; rx = qamdemod(rx_sym, M, 'UnitAveragePower', true); ber = sum(rx(:) ~= tx(:)) / numel(tx); endmod_order取 2 是 QPSK,取 4 是 16QAM。UnitAveragePower保证符号平均功率为 1,和预编码功率假设一致。MMSE 检测矩阵G里的noise_var*eye(K)是正则项,noise_var估计不准时性能会掉,实际系统里需要信道估计辅助。
5. 避坑与排查:多用户 MIMO 仿真里最容易翻车的 5 个点
5.1 容量曲线随 SNR 增长但斜率不对
现象:和速率曲线在低 SNR 正常,高 SNR 时斜率明显小于min(Nt, Nr*K)。原因:预编码矩阵没有按列归一化,或者归一化时用了总功率P而不是P/K,导致高 SNR 时功率分配错误。解决:检查W每列的norm是否等于sqrt(P/K),并确认容量计算里的noise_var和归一化功率匹配。
5.2 ZF 预编码在用户数等于天线数时噪声爆炸
现象:K=Nt时,ZF 预编码后某些用户 SINR 极低,误码率接近 0.5。原因:H*H'接近奇异,求逆放大噪声。解决:改用 MMSE 预编码,或加正则化项H*H' + ε*I,ε取0.01*P量级。也可以减少调度用户数,留一个天线余量。
5.3 信道相关矩阵sqrtm报错或结果非正定
现象:sqrtm返回复数或报「矩阵必须为正定」。原因:相关系数rho设置过大或矩阵维度不匹配,导致Rt或Rr非正定。解决:确保rho在[0, 1)之间,且Rt和Rr用指数相关模型时对角线为 1。如果仍报错,改用chol分解加chol的转置,效果等价且更稳。
5.4 用户调度后容量反而下降
现象:加了贪心调度,和速率比随机选用户还低。原因:调度算法里相关性计算用了trace但没取模,或者信道增益排序时把大尺度衰落和 small-scale 混在一起。解决:调度前先对大尺度增益做归一化,或者用等效信道H_eff = H*W的范数排序。另外检查selected索引是否和H的块对应,(k-1)*Nr+1:k*Nr这个切片很容易写错。
5.5 误码率曲线在高 SNR 出现错误地板
现象:SNR 超过 20 dB 后 BER 不再下降,卡在 10^-3 左右。原因:检测端用了 ZF 但没做干扰消除,或者调制阶数太高而预编码残留干扰没压干净。解决:换 MMSE 检测,或降低mod_order到 2 先验证链路。如果必须用高阶调制,考虑非线性预编码(如 THP),但复杂度会上去。
6. 进阶技巧:用蒙特卡洛加速和并行计算把仿真时间压下来
多用户 MIMO 仿真最耗时的不是单次容量计算,而是蒙特卡洛循环——每个 SNR 点要跑几千次信道实现。我一开始用for循环硬跑,Nt=8, K=8, 1000 次要等十几分钟。后来改成parfor并行,时间直接砍到原来的 1/4 到 1/6。
snr_list = 0:2:20; n_iter = 2000; R_avg = zeros(length(snr_list), 1); parfor i = 1:length(snr_list) snr = snr_list(i); noise_var = 1 / (10^(snr/10)); R_tmp = 0; for iter = 1:n_iter H = generate_correlated_channel(8, 1, 8, 0.3, 0.3); % 加路径损耗 d = 0.1 + 0.4*rand(8,1); PL = 128.1 + 37.6*log10(d); gain_lin = 10.^((-PL - 8*randn(8,1))/10); for k = 1:8 H(k,:) = sqrt(gain_lin(k)) * H(k,:); end selected = greedy_scheduling(H, 8, 1, 8); H_sel = H(selected, :); W = precoding_zf(H_sel, 1); R_tmp = R_tmp + sum_rate(H_sel, W, 1, noise_var, length(selected), 1); end R_avg(i) = R_tmp / n_iter; endparfor要求循环内变量不互相依赖,这里每个 SNR 点独立,正好适合。注意parfor里不能用rand的全局流,否则每次结果不一样,建议在循环内用rng(iter)固定种子,或者用RandStream独立流。另外n_iter取 2000 时,曲线已经比较平滑,取 5000 以上会更稳但时间线性增长。
还有一个技巧:把信道生成和容量计算拆成两个函数,用profile看哪个占大头。我实测下来sqrtm和inv占 60% 以上时间,后来把sqrtm换成chol的转置,inv换成左除\,整体又快了 30%。这些细节在论文里不会写,但跑大规模仿真时能省不少电费。
最后说个习惯:每次改完参数,先跑K=1单用户验证链路,再跑K=2看干扰,最后上K=Nt。跳过中间步骤直接上满用户,出了问题根本不知道是哪一层。希望帮到你。
本文还有配套的精品资源,点击获取