☰
SCMA的MATLAB仿真:码本设计、MPA检测与误码率曲线实践
2026/10/26 23:18:10 网站建设 项目流程

简介:这是一份SCMA(稀疏码分多址)的MATLAB仿真资源,适合学习5G非正交多址技术、开展算法研究与毕设仿真的通信专业学生和工程师。包内共11个文件,以9个m脚本为核心,覆盖编码、多址接入、信道模拟与接收端检测等环节,包含MPA、SD_MPA、DS_MPA等多种消息传递算法实现,另附README和说明文档便于理解代码结构。资源包约10KB,小巧完整,可直接运行和修改参数。已有274人学习下载,可用于快速验证SCMA的编码调制、迭代译码及误码性能,也可作为进一步优化编码器或降低译码复杂度的研究起点。

1. 拿到 scma-simulation.rar 之后:这套 SCMA 的 MATLAB 仿真到底在验证什么

SCMA(稀疏码多址接入)的 MATLAB 仿真代码,是无线物理层里少数几个"码本设计与多用户检测一体"的练手项目。类似 scma-simulation.rar 这类压缩包,通常装着一套可直接运行的链路:多个用户各自查自己的码本,符号在资源块上稀疏叠加,接收端用 MPA 做多用户检测,最后画出一条误码率曲线。它回答的问题很具体:非正交过载的增益从哪来,码本和检测算法各自贡献多少。适合做基带算法验证、准备非正交多址方向、或者刚接手协作通信仿真的工程师,先跑通,再把码本、信道、过载率挨个换成自己的配置,让别人的代码变成自己的实验台。

2. 从码本到叠加:拆开 SCMA 发射端的 MATLAB 代码

2.1 为什么码本才是 SCMA 的灵魂:稀疏性和过载的原理

SCMA 和传统 CDMA 有个本质区别:CDMA 的扩频码是把一个符号变成一串等幅度的码片,而 SCMA 把"比特到符号的映射"和"多用户复用"合并成了一个东西——码本。每个用户有一个 K×M 的码本矩阵,K 是资源块数,M 是码字个数,每列代表一个码字,一个码字承载 log2(M) 个比特。关键约束是每个码字只有 dv 个非零行,dv 通常取 2,其余 K-dv 行是 0。这个稀疏性让多个用户的码字可以在同一个资源块上叠加,而接收端能借助"每个资源上只有 df 个用户"这个结构把联合检测的计算量压下来。

J 个用户共享 K 个资源块,过载率就是 λ=J/K。经典的配置是 J=6、K=4,过载 150%;再激进一点是 J=4、K=2,过载 200%。当 λ>1 时,系统接入的用户数超过了正交资源能支撑的上限,这就是非正交多址的核心价值。你要在 MATLAB 里把这个说清楚,最好先抛开接收端,只跑发射端:把 6 个用户各自选的码字打印成四行,你会看到每个资源块上有 3 个用户的非零符号加在一起,而这 3 个用户分别是谁,是由因子图决定的。

很多刚接触这份代码的人容易犯一个错:把 SCMA 当成"扩频+叠加",然后去接收端找解扩模块。实际不是。SCMA 的检测是在码字级别做的,接收端拿到的是 K 维叠加符号,它要做的不是解扩,而是从被污染、被叠加的稀疏信号里反推出每个用户选了哪个码字。码本的稀疏结构决定了检测的可行性和复杂度,也决定了误码率上界。所以我把码本设计放在发射端代码的第一节讲,它是整个仿真里最值得反复改的东西。

2.2 码本参数怎么设:J=6、K=4、M=4 的经典配置

一份能跑出像样曲线的 SCMA 仿真,码本通常不是随机生成的,而是按固定流程构造:先生成一个母码本(多维星座),再通过相位旋转和资源映射给每个用户派生自己的码本。母码本的设计本身就是个优化问题。工程上最常见的做法是先不碰母码本设计,直接用一个已知性能好的码本文件,比如论文里给出的六用户四资源码本,把它保存成 codebook.mat,然后在主脚本里加载。你自己做验证时,这样最省事,也最容易和论文曲线对齐。

码本在 MATLAB 里的存放格式,我看到的大部分项目都用 1×J 的 cell 数组:cb{1} 是用户 1 的码本,每个 cb{j} 是 K×M 的复数矩阵。这里面最容易踩坑的是维度方向:有人存成 M×K,有人存成 K×M,导致后面所有索引都要翻过来。我一般会在脚本开头写一段断言,强制约定 K×M:

% 码本维度检查:统一约定 cb{j} 为 K x M,避免后续索引混乱 for j = 1:J assert(isequal(size(cb{j}), [K M]), ... 'cb{%d} 的维度应为 %d x %d,当前是 %d x %d', j, K, M, size(cb{j},1), size(cb{j},2)); end

这段检查 20 秒就能写完,但能省掉你后面排查矩阵维度报错的一小时。参数说明:K 是资源块数,M 是每用户码字数,log2(M) 就是每符号承载的比特数。M=4 意味着每个码字代表 2 比特,一个用户发一个符号就相当于发了 2 个比特。J 是用户数,在经典 6×4 配置里取 6。

另外两个参数是 dv 和 df。dv 是每个码字非零行数,决定用户占用几个资源;df 是每个资源上叠加的用户数,在规则因子图里 df=J×dv/K。6×4、dv=2 时 df=3,这是 MPA 更新时"遍历其他用户组合"的循环层数。你改过载率时,这两个数必须跟着改,否则因子图和码本不匹配。

2.3 发射端最小模板:从比特序列到叠加信号的 MATLAB 代码

发射端做的事可以压缩成三个动作:每个用户生成比特、按码本查表得到码字、在 K 个资源上叠加。下面是一段能跑通的最小发射端,只演示单个符号周期的映射:

function [x, bits] = scma_tx_single(J, K, M, cb) % 单符号周期发射端:6个用户各发1个码字,在K个资源上叠加 % 输入: cb - 1xJ cell,cb{j} 为 K x M 复数码本 % 输出: x - K x 1 叠加后的发射符号 % bits- J x log2(M) 发送比特矩阵,用于接收端统计误码率 x = zeros(K, 1); bits = zeros(J, log2(M)); for j = 1:J b = randi([0 1], 1, log2(M)); % 每个用户随机生成2比特 % 比特转码字索引:left-msb,比如 [0 1] -> 2 idx = 2*b(1) + b(2) + 1; % M=4 时索引范围 1..4 bits(j,:) = b; x = x + cb{j}(:, idx); % 稀疏码字直接叠加 end end

逻辑说明:每个用户只取自己码本的一列,加到公共的 K 维向量 x 上。因为码字是稀疏的,x 的每个分量只有 df=3 个用户的符号叠加,而不是 J 个都叠上来。这正是 SCMA 区别于 CDMA 的地方,也是后面 MPA 能降低复杂度的依据。如果你改动了 M,比如改成 8,那比特转索引那行要相应的改成 3 比特转 1 到 8,千万别再用 2*b(1)+b(2)+1 这种写死的表达式。

参数说明:J、K、M 三者的关系决定了过载率。这段代码里我没有做信道编码,也没有加导频,是最纯净的 SCMA 链路。实际仿真中,你会在外层加一个 for 循环跑成千上万个符号,并叠加噪声。发射端本身没有太多玄学,真正的难点在码本质量和接下来的接收端。你拿到一份陌生的 SCMA 代码时,我建议先只跑发射端,打印几个用户的码字索引,手工验证一下叠加结果,确认自己看懂了稀疏结构,再进入接收端,这一步能帮你后续排错时心里有底。

3. 跑通一条误码率曲线:SCMA 接收端 MPA 检测与主循环

3.1 为什么收端默认用 MPA 而不是最大似然:复杂度对比

接收端最理想的做法是最大似然(ML)检测:遍历所有用户所有码字组合,找一个联合后验概率最大的组合。但 SCMA 里 J 个用户各选 M 个码字之一,组合数是 M^J,6×4 配置下是 4^6=4096 种,看起来不多,但这是单个符号周期的。真实仿真常常要跑几十万符号,每个符号都做 4096 次遍历,再乘以 SNR 点数,总计算量会拖到没法调参。过载率到 200% 时组合数是 4^8=65536,基本就告别快速迭代了。

MPA(消息传递检测)利用的就是码本的稀疏性。每个资源块只连接 df 个用户,每个用户只占用 dv 个资源,消息只在因子图的边上传递,复杂度从 M^J 降到接近 M^df 量级。6×4、df=3 时,每个函数节点更新的遍历规模是 M^(df-1)=4^2=16,和 M^J 差了三个数量级。这就是为什么几乎所有 SCMA MATLAB 代码的接收端都写 MPA,偶尔有人做 ML 也只是为了拿最优结果当对照,验证 MPA 实现得对不对。你在看代码时,如果发现主循环里调了一个名字类似 scma_mpa 的函数,那基本就是整个接收端计算量最大的地方。

实现 MPA 有概率域和对数域两种。概率域代码直观好懂,但每步都要乘很多小于 1 的概率,数值下溢很快,高信噪比下尤其明显。对数域把乘法换成加法,动态范围好得多,代价是要处理 log-sum-exp。我看到的大部分成熟代码都用对数域。如果你手头拿到的 rar 里是概率域版本,建议先跑通,再看要不要改。

3.2 对数域 MPA 的核心更新代码

MPA 的核心是两类消息的交替更新:函数节点(资源块)向变量节点(用户)传递置信度,变量节点再回传。下面是一个对数域 MPA 的实现,使用 6×4 结构,约定每资源 df=3,每个用户 dv=2,M=4。我特意把循环写展开而不是用矩阵化,是为了让你看清消息更新的逻辑;真正要提速时,再把内层循环向量化。

function LLR = scma_mpa_log(y, N0, cb, F, maxIter) % 对数域MPA检测,适用于 K=4, J=6, dv=2, df=3 的经典SCMA % y: K x 1 接收符号 % N0: 噪声方差(每资源) % cb: 1xJ cell 码本 % F: K x J 因子图矩阵,F(k,j)=1 表示用户j占用资源k % maxIter:最大迭代次数,通常 5~8 次就收敛 % 返回: LLR - J x 2 比特对数似然比,符号>0判为1 [J, K] = size(F'); % 等效 J=size(F,2) M = size(cb{1}, 2); V2F = zeros(K, J, M); for iter = 1:maxIter F2V = zeros(K, J, M); % ---- 函数节点更新:资源k对连接用户j的码字m给出消息 ---- for k = 1:K conn = find(F(k,:)); % 占用资源k的用户编号,df=3 for u = 1:length(conn) j = conn(u); oth = conn([1:u-1 u+1:end]); % 同资源上的其他用户 for m = 1:M acc = -Inf; for c1 = 1:M for c2 = 1:M % df=3 时遍历另外2个用户的码字组合 sym = cb{j}(k,m) + cb{oth(1)}(k,c1) + cb{oth(2)}(k,c2); msg = -abs(y(k) - sym)^2 / N0; % 信道似然 msg = msg + V2F(oth(1),k,c1) + V2F(oth(2),k,c2); acc = logsumexp(acc, msg); end end F2V(k,j,m) = acc; end end end % ---- 变量节点更新:用户j汇总两个资源上的消息并归一化 ---- for j = 1:J nz = find(F(:,j)); % 用户j占用的资源,dv=2 for k = nz' otherRes = nz(nz ~= k); for m = 1:M V2F(k,j,m) = F2V(otherRes(1), j, m); % dv=2时只有一条来路 end V2F(k,j,:) = V2F(k,j,:) - max(V2F(k,j,:)); % 防溢出的归一化 end end end % ---- 汇总后验,计算比特LLR ---- LLR = zeros(J, log2(M)); for j = 1:J Lm = zeros(1, M); for k = find(F(:,j))' Lm = Lm + squeeze(F2V(k,j,:))'; end for b = 1:log2(M) p1 = -Inf; p0 = -Inf; for m = 1:M if bitand(m-1, 2^(b-1)) p1 = logsumexp(p1, Lm(m)); else p0 = logsumexp(p0, Lm(m)); end end LLR(j,b) = p1 - p0; end end end function z = logsumexp(a, b) % 对数域加法:计算 log(exp(a)+exp(b)),防止下溢 if a == -Inf, z = b; return; end if b == -Inf, z = a; return; end mx = max(a,b); z = mx + log(exp(a-mx) + exp(b-mx)); end

逻辑说明:函数节点更新那一步,资源 k 收到叠加信号 y(k),它不知道这堆能量来自哪几个用户的哪个码字。所以它对当前用户 j 的每个候选码字 m,穷举其他两个用户的所有码字组合,计算"如果发射端是这套组合,y(k) 出现的似然有多大",再把其他用户从变量节点发来的消息加权进来。这里遍历的是 df-1 个用户,而不是 J 个用户,复杂度低就低在这。变量节点更新则相反,用户 j 知道自己只用了两个资源,它在资源 k 上给函数节点回话时,只能引用另一个资源上的外部信息,不能自说自话。

参数说明:maxIter 取 5 到 8 就够,取太多只是白耗时间。N0 要和你 SNR 扫描对得上,否则整条曲线偏移。需要提醒的是,这份代码是我按 df=3 展开的版本,如果你改成 J=4、K=2 的 200% 过载结构,df 会变成 4,内层循环要加一层到三重循环,变量节点归一化逻辑不变,但 F2V 的索引要重新核对。另外 logsumexp 里对 -Inf 的处理必须保留,否则第一轮迭代就把消息算成 NaN。

3.3 主循环怎么搭:SNR 扫描、BER 统计与画图

发射端和接收端都有了,剩下的是把它们串进 SNR 扫描循环。常见的主循环结构是外层扫 SNR,内层跑若干帧符号,每帧独立随机生成比特,经过发射端、加噪声、MPA 检测,最后统计错误比特。下面是最小可运行的主脚本骨架:

% SCMA 主仿真脚本:AWGN 信道,6用户4资源,M=4 load('codebook.mat', 'cb'); % 预制码本,1x6 cell,每本 4x4 F = [1 1 1 0 0 0; 0 0 1 1 1 0; 1 0 0 0 1 1; 0 1 0 1 0 1]; maxIter = 6; ebnoVec = 0:2:14; ber = zeros(size(ebnoVec)); numFrame = 2000; % 每SNR点仿真帧数 for i = 1:length(ebnoVec) ebno = ebnoVec(i); % SNR与Eb/N0换算:每资源叠加df=3个用户,每码字2比特,dv=2 N0 = 1 / (10^(ebno/10) * (3/2) * log2(4)); errBits = 0; totalBits = 0; for f = 1:numFrame y = zeros(4,1); bitsAll = zeros(6,2); for j = 1:6 [x_j, b] = deal(zeros(4,1), randi([0 1],1,2)); idx = 2*b(1)+b(2)+1; x_j = cb{j}(:,idx); y = y + x_j; bitsAll(j,:) = b; end noise = sqrt(N0/2) * (randn(4,1) + 1i*randn(4,1)); y = y + noise; LLR = scma_mpa_log(y, N0, cb, F, maxIter); bHat = double(LLR > 0); errBits = errBits + sum(sum(bHat ~= bitsAll)); totalBits = totalBits + numel(bitsAll); end ber(i) = errBits / totalBits; fprintf('Eb/N0=%d dB, BER=%.2e\n', ebno, ber(i)); end semilogy(ebnoVec, ber, 'o-'); grid on; xlabel('Eb/N0 (dB)'); ylabel('BER');

逻辑说明:这段脚本每帧重新生成 6 个用户的比特,互不相关,因此各帧统计独立。噪声功率按 Eb/N0 换算,换算因子里那个 3/2 来自 df/dv(每资源叠加 3 个用户,每个用户符号能量平摊到 2 个资源),log2(4) 是每符号比特数。这个换算我后面还会再讲,它是整条曲线横轴对不对的关键。

参数说明:numFrame 取 2000 时每个 SNR 点有 2000×6×2=24000 个比特参与统计,BER 到 10^-4 量级还能看,再低就需要加大帧数或改用自适应停止。fprintf 打印进度是必需的,否则 SNR 点一多,你分不清是算得慢还是死循环。画图直接用 semilogy,BER 曲线在 0 dB 附近会陡降,这个视觉特征本身就是检测器是否工作正常的直观信号。

4. SCMA 仿真避坑:让误码率曲线翻车的五个常见问题

4.1 高信噪比下误码率出现地板

现象:BER 曲线走到某个 SNR 点后不再下降,拖出一条水平的尾巴,怎么加信噪比都压不下去。

原因:最常见的是消息传递过程中的数值下溢,尤其是概率域 MPA,在高 SNR 下所有概率都趋近 0 或 1,乘法结果直接归零。另一个可能是码本本身最小欧氏距离太小,存在两个码字在高信噪比下依然容易混淆,相当于系统上界被码本焊死了。

解决:先把概率域 MPA 换成对数域,用我上面贴的 logsumexp 版本,地板通常直接消失。如果换成对数域还压不住,就该怀疑码本:检查母码本里任意两个码字之间的最小距离,微弱的话换一组已知成熟码本对比。我自己的排查顺序是先确认检测器没问题,再怀疑码本,最后才怀疑信道模型。

4.2 矩阵维度报错:码本方向没约定好

现象:运行到 scma_mpa_log 里索引 cb{j}(k,c1) 时,MATLAB 报 Index exceeds matrix dimensions,或者矩阵尺寸不一致。

原因:八成是码本存成了 M×K,而代码按 K×M 写。这种错误在 rar 包换了好几个人手之后极易出现,前一个人转置过码本,后一个人没注意。再有一种是 F 矩阵维度写错,K×J 写成了 J×K。

解决:在代码入口加维度断言,这是最直接的手段。我通常会在主脚本里写好检查,cb 每个 cell 必须是 K×M,F 必须是 K×J。另外在加载 codebook.mat 后立刻打印 size(cb{1}),肉眼确认一次。这个检查看起来啰嗦,但 SCMA 仿真里码本方向搞反是最高频事故,没有之一。

4.3 对数域消息出现 NaN

现象:跑前几个 SNR 点都正常,到某个点突然整条曲线断掉,工作区里 V2F 或 F2V 出现 NaN。

原因:对数域里 -Inf 参与加法、log(0)、或者 exp 大数溢出都会产生 NaN。常见触发点是你把 V2F 初始化为全零,第一轮迭代的函数节点更新里,oth 用户消息全 0,加出来的 acc 可能变成很大的负数,再经过 logsumexp 里的 exp 和 log 组合,数值稍微没注意就溢出。

解决:变量节点更新后必须做减最大值归一化,这是数值稳健的底线。logsumexp 里对 -Inf 分支的处理也不能省。另外可以在迭代循环末尾加一句 assert(all(isfinite(V2F(:)))),一旦出现 NaN 马上定位到是哪一次迭代、哪一个资源,比最后查 NaN 快得多。

4.4 Eb/N0 换算差了几个 dB

现象:仿出来的 BER 曲线形态正常,但整体横轴和论文曲线对不上,系统看起来"亏"了 3 到 5 个 dB,或者莫名"赚"了几个 dB。

原因:SCMA 里一个资源块上有 df 个用户叠加,而每个用户符号的能量平摊到 dv 个非零维度上,横轴到底画 SNR 还是 Eb/N0,换算公式就绕不开这两个结构参数。很多代码直接把 SNR 当成 Eb/N0,或者只除 log2(M) 忘了 df/dv,曲线自然偏移。

解决:按前面主循环里的换算公式对齐。设 N0 是每个资源上的噪声方差,信号叠加后每个资源上的平均功率 P 由 df 个用户各贡献 Es/dv,所以 Es/N0 = (P/N0)×(dv/df)。再换算每比特:Eb/N0 = Es/N0 / log2(M)。dB 形式就是你横轴要画的 Eb/N0。改 df、dv 或 M 时,这个换算要同步改,否则换了过载率以后曲线横轴就不自洽了。工程上最稳妥的验证办法是把 M=1 的退化为 BPSK 单用户情形作为特例,曲线对齐 AWGN 理论界再回来做 SCMA。

4.5 迭代次数加了性能纹丝不动

现象:把 maxIter 从 5 加到 20,误码率几乎没变化,甚至偶尔变差。

原因:MPA 在 5 到 8 次迭代内通常已经收敛,再加外迭代只是空转。另一种可能是你的消息更新顺序写反了:变量节点用了本轮刚更新的值,等于把消息传回了自己,迭代变成自激振荡,收敛到错误分布。

解决:调试时在每个迭代步末记录 BER,画出"迭代次数 vs 错误符号数"的收敛曲线。正常应该看到前 3 次快速下降,之后平缓收敛。如果看到振荡,检查变量节点更新里是否引用了本轮同一边的 F2V。排查无误后,把 maxIter 固定在 6 或 8,能省下大量仿真时间。

5. 换码本、换信道、换过载:SCMA 仿真的三个进阶方向

5.1 用穷举 MAP 做正确性参照

MPA 写得对不对,最靠谱的验证不是对着论文曲线猜,而是把 MPA 和穷举 MAP 放在同一组输入下对比。J=6、K=4、M=4 时 MAP 每符号只需遍历 4096 种组合,作为验证工具完全跑得动。把 MPA 检测出的比特和 MAP 检测出的比特逐帧对比,两者一致率应该接近 100%,差异只应出现在低 SNR 的少数边界样本。我会在代码里保留一个 MAP 分支,用 switch 开关控制,平时不跑,改完 MPA 代码就打开对比一次,这比盯曲线快得多。

5.2 从 AWGN 换到 Rayleigh 衰落信道要补的两个模块

AWGN 只是起点。换到 Rayleigh 平坦衰落,每个资源块要乘一个独立的信道系数 h(k),接收方程从 y=x+n 变成 y=h.*x+n。发射端代码不用动,接收端要补两个地方:一是 MPA 里信道似然那行,从 -abs(y(k)-sym)^2/N0 改成 -abs(y(k)-h(k)*sym)^2/N0;二是主循环里对每个符号周期重新生成 h,并做功率归一化,保证 E[|h|^2]=1,否则横轴又偏。这个改动不大,但会让 BER 曲线整体抬高,因为衰落尾部的深衰落样本很难被 MPA 救回来。

5.3 高斯近似 MPA 与更高过载率

当你想把过载率推到 200% 甚至更高时,df 从 3 涨到 4,内层遍历组合数从 16 涨到 64,单帧仿真时间显著拉长。这个时候高斯近似 MPA 是下一步:把其他用户叠加的信号近似为高斯分布,只估计均值和方差,函数节点更新从遍历组合变成代数计算,复杂度降一个量级,性能损失在高 SNR 下通常可以接受。参数上要注意,df 变大后收敛需要的迭代次数也会略增,我的习惯是从 maxIter=8 起步重新看收敛曲线。

换码本时,验证边界要记住一点:过载率越高,码本的最小欧氏距离对 BER 的影响越敏感。GAM 这类近似算法用在高过载场景时,方差估计不准会直接反映在地板位置,不要指望它是万能药。

做这套仿真的时间里,我最大的教训是"先有对照,再谈优化"。从第一天就留好 MPA、MAP 两个检测器的对比开关,后面所有码本调整和信道扩展都被绑定在一个可信参照上,不会改了三个月发现基线本身就是错的。希望你从这份 SCMA 代码开始的调参路,也先把这个开关架好,希望帮到你。

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

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

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

立即咨询