
简介这份MATLAB仿真代码包面向无线通信、信号处理方向的学生与研究者围绕大规模MIMO系统建模与性能评估展开适合在理论学习之后通过可运行脚本加深对多天线通信机制的理解。压缩包共11个文件以7个.m脚本和4个.fig结果图为主整体约29KB脚本承担信道建模、检测与误码率计算等仿真流程图形文件则保存了误码率随收发天线数、接收天线数变化等性能曲线便于直接对照结果。内容覆盖QPSK调制解调、ZF/MMSE及其连续干扰消除、MRC接收、范数最小化检测以及不同天线配置下的BER对比能够帮助读者复现空间复用与分集带来的性能差异并观察检测算法在不同信噪比条件下的表现。已有4107人学习可作为课程实验、毕业设计前期验证和算法对比的参考底稿配合脚本注释逐步修改参数较快建立对MIMO仿真流程的整体认识。1. 大规模MIMO仿真跑不通先看清楚这套MATLAB程序到底算了什么一位做课程设计的同学拿到一套大规模 MIMO 系统仿真的 MATLAB 程序双击主脚本天线数从 64 改成 128曲线立刻跑到 0.5 附近不再下降另一位把它当参考实现结果和论文里的图差了 3 dB。这两个现象的根源都不是代码写错而是没弄清这套脚本的计算链路它按「参数初始化 → 信道生成 → 导频发送与信道估计 → 上行检测 / 下行预编码 → 误码率与和速率统计」这条流水线切分文件每一环都有自己的归一化约定任何一环漏掉或重复做归一化结果就会异常。这套 MIMO MATLAB 程序覆盖的是 TDD 制式下的多用户大规模 MIMO基站侧几十到几百根天线服务若干个单天线用户上下行共用同一段相干带宽内的信道。它能解决三件事——验证你自己推的检测器 / 预编码公式对不对、快速扫天线数与用户数的性能拐点、给论文或毕设出一组能复现的 BER 与和速率曲线。适合已经懂一点线性代数与通信原理、但被「改一个参数全盘崩」卡住的本科生、研究生和做算法预研的工程师。2. 流水线与信道建模先搞清楚每个文件在算什么拿到一套仿真程序最省时间的做法不是从头读代码而是先画出它的数据流。规模确定之后天线数 M、用户数 K、导频长度 τ、调制阶数所有文件的职责就固定了剩下的都是实现细节。2.1 一条从比特到误码率的流水线这类程序一般按下面的职责切文件命名可能各有不同但流水线位置是固定的。照着表去对照你手上的目录五分钟就能定位到要改的那一行。环节典型职责输入输出最容易改错的地方参数初始化定义 M、K、τ、调制、SNR 扫描范围、蒙特卡洛次数无结构体 params噪声方差与 SNR 的换算信道生成生成 M×K 小尺度衰落 H 与大尺度衰落 βparamsH、β相关矩阵维度搞反导频与估计发送正交导频做 LS 或 MMSE 估计H、β、τHhat导频功率没算进噪声方差上行检测从接收向量 y 恢复符号y、Hhats_hatMMSE 正则项漏了下行预编码由 Hhat 生成 M×K 预编码矩阵 WHhatW漏掉 1/M 功率归一化性能统计累积误比特、算和速率各环节输出BER、sum raterng 没固定或 parfor 里重复绘图曲线对比统计结果图横轴用线性还是对数刻度一个经验判断如果程序里只有一个大脚本、几百行堆在一起改起来会很痛苦因为信道、估计、检测的变量名会互相覆盖。常见做法是至少把信道生成和检测器拆成独立函数主脚本只负责循环和统计这样换检测器时不用动循环结构。2.2 三种信道模型i.i.d. 瑞利只是起点新手最容易把 i.i.d. 瑞利当成「大规模 MIMO 信道」然后发现仿真出来的和速率好得离谱。原因是独立同分布假设下用户信道完全正交多用户干扰趋近于零这只有在天线数趋于无穷、且散射极其丰富时才近似成立。真实系统里基站侧天线阵列有物理尺寸限制天线间存在相关性用户之间也存在空间相关性能会明显下降。常见做法是准备三套模型按验证目的切换第一套是 i.i.d. 瑞利H (randn(M,K)1j*randn(M,K))/sqrt(2)。它的价值是作为理论上界用来验证检测器和预编码的代码本身写没写对——如果连 i.i.d. 下 MMSE 都跑不出正常曲线那一定是实现问题不是信道问题。第二套是 Kronecker 相关模型H Rr^(1/2) * Hiid * Rt^(1/2)其中基站侧相关矩阵用指数相关模型构造Rt(i,j) rho^abs(i-j)rho取 0.30.7 之间比较典型对应天线间距从半波长到几个波长。注意维度Rr是 K×K 的用户侧相关Rt是 M×M 的基站侧相关乘反了矩阵直接报错但如果 M 和 K 恰好相等就不会报错只会悄悄把结果算错这是很难查的一类坑。第三套是几何模型单环或稀疏散射用导向矢量构造a(theta) exp(-1j*2*pi*d*(0:M-1)*sin(theta))/sqrt(M)每个用户给一个中心角加角度扩展把若干条路径叠加。这套模型能反映「用户角度分离度」对性能的影响做预编码对比时最有说服力。function [H, beta] gen_channel(M, K, cfg) % GEN_CHANNEL 生成多用户大规模MIMO上行信道 % M : 基站天线数 % K : 单天线用户数 % cfg : 结构体, 含 model / rho / d / as / dmax % H : M x K 小尺度衰落, 每列功率归一化为1 % beta: K x 1 大尺度衰落系数(含路损与阴影) % 大尺度衰落: 参考距离路损 对数正态阴影 d cfg.dmax * rand(K,1) 10; % 用户到基站距离, 单位米 pl (d/10).^(-3.0); % 简化路损指数3.0 shad 10.^(randn(K,1)*8/10); % 8dB 对数正态阴影 beta pl .* shad; switch lower(cfg.model) case iid H (randn(M,K) 1j*randn(M,K)) / sqrt(2); case corr % 基站侧指数相关矩阵, rho 越大相关性越强 idx 0:M-1; Rt cfg.rho .^ abs(idx - idx); Rt Rt 1e-9*eye(M); % 加小量防止 chol 失败 Hiid (randn(M,K) 1j*randn(M,K)) / sqrt(2); H chol(Rt, lower) * Hiid; % 只对基站侧加相关 case geo % 单环模型: 每用户 L 条路径, 角度在中心角附近展开 L cfg.L; H zeros(M,K); for k 1:K th cfg.theta(k) cfg.as*randn(L,1); A exp(-1j*2*pi*cfg.d*(0:M-1)*sin(th.) ) / sqrt(M); g (randn(L,1) 1j*randn(L,1)) / sqrt(2*L); H(:,k) A * g; end otherwise error(未知信道模型: %s, cfg.model); end % 逐列归一化, 保证小尺度功率为1, 功率差异全部交给 beta H H ./ sqrt(sum(abs(H).^2, 1) / M); end这段代码有三个参数值得留意。cfg.rho控制空间相关性从 0.3 调到 0.8 会看到和速率明显下降这正好能说明为什么大规模 MIMO 在 correlated 场景下要靠用户调度而不是单纯堆天线。cfg.as是角度扩展单位是弧度典型值 0.10.3。最后的逐列归一化很容易被删掉——一旦删掉geo模型下每列功率随A的构造方式浮动后面按beta分配功率的前提就不成立了曲线会忽高忽低看起来像「玄学」。提示chol在相关矩阵接近奇异时会报错加1e-9*eye(M)是最省事的后悔药如果你的rho接近 1最好直接检查矩阵条件数。2.3 为什么默认走 TDD估计开销决定架构这套程序的骨架是 TDD不是随意选的。大规模 MIMO 的核心矛盾是基站要拿到 M×K 维的信道如果走 FDD下行 CSI 要靠终端反馈反馈量随 M 线性增长天线数一多就没法落地。TDD 利用上下行信道的互易性上行发一段导频基站估出来直接用于下行预编码开销只和用户数 K 有关与天线数 M 无关——这正是大规模 MIMO 能成立的前提。代价是导频长度 τ 必须不小于 K正交导频而 τ 又会占用相干块内的资源。相干块长度 T 固定时用于数据传输的符号数是 T−τ所以和速率不是单调随 K 增长的用户多了导频开销也大。这套程序里 τ 一般直接取 K做扩展时可以扫 τ 从 K 到 2K看小区间干扰带来的性能变化。如果你的仿真结果里和速率随 K 一直涨、没有拐点那基本可以确定是漏了导频开销或者导频没有复用。3. 参数配到位导频、估计与蒙特卡洛循环流水线里最影响结果可信度的不是检测器而是信道估计和循环结构。估计器写错检测器再对也没意义循环写错曲线会平滑得不真实。3.1 参数表先定这几个数参数符号常用取值说明基站天线数M32 / 64 / 128 / 256少于 16 体现不出大规模特性用户数K4 / 8 / 10受导频长度限制导频长度τ K 或 2K正交导频最小为 K相干块长度T100200 符号决定导频开销比例调制方式—QPSK / 16QAM高阶调制需要更长的蒙特卡洛SNR 范围—-10:2:20 dB每接收天线上的信噪比蒙特卡洛次数Nmc1e31e4误码率 1e-4 量级至少 1e5 比特路径损指数α3.03.8影响用户间大尺度差异M 和 K 的比例关系比绝对值更重要。M/K 小于 4 时MRC 和 MMSE 的差距很明显M/K 大于 10 之后线性检测器就接近最优了。做对比实验时固定 K 扫 M比固定 M 扫 K 更容易看出渐进特性。3.2 导频设计与 LS/MMSE 估计上行导频阶段K 个用户同时发送长度为 τ 的正交导频基站收到 M×τ 的矩阵。若导频矩阵Phi满足Phi*Phi tau*I常见用 DFT 矩阵或 Hadamard 矩阵截取LS 估计就是Hhat_ls Yp*Phi/tauMMSE 估计则要带上大尺度先验function Hhat est_channel(Yp, Phi, beta, tau, N0) % EST_CHANNEL 上行导频信道估计 % Yp : M x tau 接收导频矩阵 % Phi : K x tau 导频矩阵, 满足 Phi*Phi tau*I % beta: K x 1 大尺度衰落(先验方差) % N0 : 每接收天线的噪声方差 % Hhat: M x K 估计结果 G Phi * Phi; % K x K, 正交导频下为 tau*I Hls Yp * Phi / tau; % LS 估计 % MMSE: 逐用户带先验的正则化, 避免直接对 MK x MK 大矩阵求逆 Hhat zeros(size(Hls)); for k 1:size(Phi,1) denom G(k,k) N0/beta(k); % 用大尺度衰落做先验 Hhat(:,k) Yp * Phi(k,:) / denom; end end参数上最关键的是N0/beta(k)这一项。beta是用户的大尺度衰落系数它决定了估计器对哪个用户更「信任」——近端用户信噪比高估计更准远端用户先验弱估计误差大。很多人写 MMSE 估计时把这一项写成常数结果远端用户的误码率曲线完全不符合预期。LS 估计则不需要先验实现简单但在低 SNR 下误差明显更大做算法对比时通常两个都跑用 LS 当基线。导频矩阵Phi用单位 DFT 矩阵的前 K 行乘sqrt(tau)就行注意归一化因子如果Phi*Phi不等于tau*I上面的denom里G(k,k)就要换成实际值否则估计结果会系统性地偏大或偏小。3.3 蒙特卡洛循环SNR 放外层还是内层循环顺序错了结果会骗人。正确做法是 SNR 放最外层、蒙特卡洛放内层、每个蒙特卡洛样本内重新生成小尺度信道但保持大尺度衰落不变。理由是大尺度衰落变化慢对应「用户位置不变、只经历快衰落」的短时仿真如果你每个样本都重新撒用户位置得到的是长时间平均和速率的方差会小很多但这与大多数论文的设定不一致对比时会显得你的曲线「太理想」。rng(20240101); % 固定随机种子, 保证可复现 SNRdB -10:2:20; ber zeros(size(SNRdB)); for is 1:numel(SNRdB) snr 10^(SNRdB(is)/10); N0 1/snr; % 每接收天线噪声方差, 信号功率归一化为1 err 0; nbit 0; for imc 1:cfg.Nmc [H, beta] gen_channel(M, K, cfg); % 快衰落每样本重生成 % ...导频发送、估计、检测、判决... err err sum(s_hat ~ s); nbit nbit numel(s); end ber(is) err / nbit; end两个细节N0 1/snr的前提是信号功率已归一化到 1如果前面信道没有逐列归一化这里就要乘上beta否则横轴标的是「标称 SNR」实际不是。另外误码统计的样本量要够QPSK 在 SNR10 dB、MMSE 检测下误码率大概在 1e-3 量级Nmc1000、每个样本 200 个符号只有 2e5 比特误差条会很明显想画出平滑曲线至少要把每个 SNR 点的错误比特控制在几百个以上。注意rng只能放在最外层一次。如果放在parfor内部每个 worker 会被重置成同一个种子所有并行样本变成同一份数据曲线会平滑得不像真的。4. 上行检测与下行预编码把公式写成矩阵运算检测器和预编码是这套程序里最能体现「改一行看一条曲线」的部分也是最容易在归一化上出错的部分。4.1 上行检测MRC、ZF、MMSE 的差别在哪上行接收向量y H*s n三种线性检测器都是在y左乘一个矩阵检测器权重矩阵复杂度适用场景MRCHhatO(MK)极低 SNR、抗噪声优先ZF(Hhat*Hhat)^-1*HhatO(K^3 MK^2)高 SNR、干扰主导MMSE(Hhat*Hhat N0*I)^-1*HhatO(K^3 MK^2)全 SNR 折中MRC 在用户数增多时会因为用户间干扰出现「误差平台」SNR 再高也降不下去——这是新手最常踩的现象。ZF 在低 SNR 下放大噪声曲线反而比 MRC 差。MMSE 在两端都不最差是默认选择。function s_hat detect_ul(y, Hhat, type, N0) % DETECT_UL 上行线性检测 % y : M x 1 接收向量 % Hhat : M x K 估计信道 % type : mrc | zf | mmse % N0 : 每接收天线噪声方差 switch lower(type) case mrc W Hhat; case zf W (Hhat*Hhat) \ Hhat; case mmse % 正则项必须用噪声方差, 不是固定的 0.01 W (Hhat*Hhat N0*eye(size(Hhat,2))) \ Hhat; otherwise error(未知检测器: %s, type); end s_hat W * y; endMMSE 的正则项N0*eye(K)是最容易被写死的地方。有人为了方便直接写0.1*eye(K)跑出来的曲线在低 SNR 段看着还行高 SNR 段会提前出现误差平台因为正则项压住了有用信号。正确做法永远是用当前的噪声方差它随 SNR 循环变化。另外\比inv()数值上更稳K 上百时差别明显。4.2 下行预编码MRT 与 ZF 的功率归一化下行预编码就是上行的对偶s_tx W*xW是 M×K。MRT 取Hhat共轭ZF 取伪逆的共轭转置选完必须做功率归一化让每列或整个矩阵的发射功率等于 Pfunction W precoder_dl(Hhat, type, P) % PRECODER_DL 下行线性预编码 % P : 总发射功率, 归一化后 sum(|w_k|^2) 之和 P K size(Hhat,2); switch lower(type) case mrt W Hhat; case zf W Hhat * ((Hhat*Hhat) \ eye(K)); % Hhat*(Hhat*Hhat)^-1 otherwise error(未知预编码: %s, type); end % 每用户功率归一化: 让每个 w_k 的功率为 P/K for k 1:K W(:,k) W(:,k) / norm(W(:,k)) * sqrt(P/K); end end归一化方式决定曲线的绝对位置。如果按「整体功率」归一化W W*sqrt(P/norm(W,fro)^2)各用户分到的功率不均等远端用户拿不到功率和速率会偏低按「每用户功率」归一化每个用户固定拿 P/K才是大规模 MIMO 论文里常见的设定。两种都对但混用就没法比较。另外norm(W(:,k))在 K 很大时接近 sqrt(M)所以 ZF 预编码在低 SNR 下每用户有效信号功率会被「吃掉」一部分这是 ZF 下行不如 MRT 的原因也是正则化 ZF加 alpha*I存在的理由。4.3 和速率别把估计误差漏掉算和速率时如果直接用估计出来的Hhat当真实信道算 SINR得到的是「完美 CSI 上界」曲线会比实际好一大截。严谨做法是用 use-and-then-forget 下界把估计误差当成额外的干扰项% 下行每用户 SINR 下界 (MRT预编码为例) signal zeros(K,1); interf zeros(K,1); for k 1:K signal(k) abs(Hhat(:,k)*W(:,k))^2; interf(k) sum(abs(Hhat(:,k)*W).^2) - signal(k) N0; end sinr signal ./ interf; rate mean(log2(1 sinr)); % 单位 bit/s/Hzinterf里减去自身那一项是必须的否则每个用户的干扰项里都含自己的信号。这个减法看起来 trivial实际是排查「和速率随 SNR 不增反降」时的第一检查点。如果要做含估计误差的曲线就在interf里再加上估计误差方差项beta_k - var(Hhat(:,k))对应的功率数值上表现为高 SNR 段和速率饱和——这正是导频污染和大规模 MIMO 有限精度的真实表现。5. 避坑五个让 BER 曲线变得很奇怪的坑5.1 误码率停在 0.5 不下降现象不管 SNR 怎么加BER 都在 0.5 附近看起来像在随机猜。原因调制符号的功率没有归一化。QPSK 取exp(1j*pi/4*(2*bit-1))时幅度是 1但 16QAM 用qammod出来的平均功率不是 1若噪声方差按信号功率 1 设定实际 SNR 就被抬高或压缩判决门限完全错位更常见的是接收端判决时用了错误的星座图顺序。解决统一用qammod(...,UnitAveragePower,true)或者自己写星座表后除以sqrt(mean(abs(s).^2))。解码端用qamdemod配同样的参数别一边归一化一边不归一化。5.2 曲线整体比理论差 3 dB现象MMSE 检测的 BER 曲线形状对但整体右移 3 dB 左右。原因噪声方差的定义搞混了。N0是「每接收天线」的噪声方差而很多理论公式里的Es/N0是按「每用户发射符号能量」定义的。大规模 MIMO 上行有 K 个用户同时发射总发射功率是 K 倍如果 SNR 扫描时按总功率归一化、噪声方差却按单用户算就会出现固定偏移。解决在参数文件里写死一行注释说明 SNR 的定义并且在循环里只在一个地方计算N0其他地方一律引用这个变量不要在函数内部重新推。5.3 MMSE 在高 SNR 段提前饱和现象SNR 超过 15 dB 后 BER 不再下降形成平台。原因正则项被写成了常数或者信道估计用的是 LS 但检测时按完美 CSI 算。前者是正则项压制信号后者是估计误差成了不可消除的干扰。解决正则项改成N0*eye(K)如果用的是估计信道就在循环里同时保留真实 H 用于算 SINR 下界别用Hhat假装是真值。高 SNR 段的平台高度还能反过来验证你的估计器性能是很有用的一步。5.4 并行循环后曲线异常平滑现象加了parfor之后1000 次蒙特卡洛的曲线比之前 10000 次的还平滑。原因rng被放在parfor内部每个 worker 重置成同一颗种子所有样本的数据完全相同相当于只跑了一次。解决rng放最外层parfor内部用rng(seed imc)做流独立或者改用RandStream按 worker 分片。5.5 天线数一加性能反而变差现象M 从 64 调到 128和速率不升反降。原因预编码矩阵漏了1/M或1/sqrt(M)的功率归一化。ZF 预编码的列范数随 M 变化不重新归一化就等于偷偷加大了发射功率但接收端仍然按原功率算 SINR噪声项相对变大另一种情况是大尺度衰落beta没有随 M 重新分配。解决无论 M 是多少归一化步骤必须无条件执行在大规模 MIMO 里还要记住beta是用户位置决定的跟 M 无关别把它写成 M 的函数。6. 进阶加一个多小区场景再验证你的曲线是对的单小区跑通之后最值得加的一件事是多小区导频污染。它是大规模 MIMO 里唯一「加天线也消不掉」的干扰相邻小区复用了同样的导频序列基站估计出来的信道里混进了别家用户的成分天线数趋于无穷时这个误差依然存在。代码改动不大在信道生成之后给每个用户叠加干扰小区的信道即可% L 个小区, 每小区 K 用户, 复用同一组正交导频 H zeros(M, K, L); for l 1:L H(:,:,l) gen_channel(M, K, cfg); % 第 l 个小区到本基站的真实信道 end % 本小区基站收到的导频: 本小区 干扰小区 Yp zeros(M, tau); for l 1:L Yp Yp sqrt(beta_l(l)) * H(:,:,l) * Phi; % beta_l 为跨小区大尺度衰落 end Yp Yp sqrt(N0/2)*(randn(M,tau)1j*randn(M,tau)); Hhat est_channel(Yp, Phi, beta, tau, N0); % 估出来的已含污染加完这一段你会看到和速率随 M 增大先升后饱和饱和点由干扰小区的信号强度决定。这正是导频污染该有的样子。如果你的曲线还是一路涨到 256 根天线不带停的那说明污染没加进去或者beta_l被设成了 0。验证方法我一般用两把尺子。第一把是极限检查把rho设 0、N0设极小、K1此时 MMSE 检测的 BER 应该和理论 QPSK 曲线几乎重合偏差超过 0.5 dB 就是实现有问题。第二把是单调性检查固定其他参数单独扫 M 从 16 到 256MRC、ZF、MMSE 三条曲线应收敛到同一点收敛速度 MMSE 最快如果哪条曲线不收敛说明对应的归一化漏了。剩下的就是参数扫描和出图用表格把每组参数对应的图号记下来别靠文件名认。血泪经验是这套程序我每次改动哪怕只改一个归一化因子也强制先跑一遍K1, M8的最小用例确认 BER 和理论对得上再去跑 128 天线的大场景。因为大场景跑一次要十几分钟跑完才发现曲线形态不对重来的成本太高。希望这套流程能帮你少走几遍弯路也希望你能顺手把它用到自己的场景里。本文还有配套的精品资源点击获取