
简介这套MATLAB仿真程序实现了LDPC码与64-QAM调制结合的数字通信系统面向通信工程专业学生、科研人员及需要开展信道编码仿真的工程师可用于分析LDPC编译码在64-QAM信道下的纠错性能。程序包含完整的LDPC编码器与迭代译码器采用软判决消息传递算法并配有64-QAM的调制映射与解调模块可通过脚本内置参数调整码率、信噪比等条件对比高斯白噪声或衰落信道下的误码率曲线帮助理解信道编码与高阶调制的协同作用。压缩包共7个文件其中5个m脚本负责主仿真与核心算法实现2个mat文件存放校验矩阵和生成矩阵包体仅13KB结构简洁便于直接运行和二次修改。已有250人学习下载适合作为理解LDPC码、软判决译码及高阶调制系统性能的入门与进阶参考。1. LDPC 与 64QAM 仿真程序先搞清这笔预算花在哪拿到一个 LDPC 译码器 IP 或者正在写物理层毕业设计时最容易被问的一句话就是你的 LDPC 编码配合 64QAM 调制在 AWGN 信道下到底能跑到多少误码率。所谓 LDPC 64QAM 仿真程序就是把这个问题的答案在 MATLAB 里老老实实跑出来随机信息比特经过 LDPC 编码映射成 64QAM 符号过 AWGN 信道再软解调成对数似然比 LLR送进译码器最后统计 BER。它能帮你回答三个问题给定 Eb/N0 链路余量够不够、译码算法性能差多少、换成不同码率或迭代次数会怎样。适合通信专业学生、物理层预研工程师以及想做 FPGA 验证前先拿到参考曲线的人。别急着找现成代码先把链路模型和参数量纲搞对后面才不会被一条假曲线误导。2. 系统模型与模块划分为什么 64QAM 配 LDPC以及链路由谁驱动2.1 为什么 64QAM 配 LDPC 是 5G 的典型组合64QAM 每个符号携带 6 个比特频谱效率高但对信噪比要求也高LDPC 是逼近香农限的线性分组码5G NR 的数据信道和 DVB-S.2 都把它作为纠错主力。两者配合时接收端必须把解调器输出的不是硬判决 0/1而是软信息 LLR译码器才能发挥出纠错潜力。硬判决会让 64QAM 到 LDPC 之间损失 2 dB 甚至更多这也是整个仿真里「软解调」存在的意义。对于 5G LDPC 场景调制阶数从 QPSK 到 256QAM 都会出现但 64QAM 是一个性价比很高的中间档星座规模适中软解调计算量可控又能真实反映高码率下的性能趋势。仿真时先用 64QAM 和 AWGN 链路把算法跑通再去扩展衰落信道和更高阶调制是最稳妥的推进方式。2.2 一条仿真链路分成五段信号流与量纲整个基带仿真链路可以拆成五段信号形态和长度是我在调试时第一个要盯住的随机信息比特流data长度等于信息位长度 k。LDPC 编码后的码字cword长度等于码长 n。64QAM 调制后的复符号tx每 6 个码字比特变成 1 个符号符号数 n/6。过信道后接收符号rx加了复高斯噪声。软解调的 LLR 序列长度回到 n送进译码器。以 DVB-S.2 短帧 n16200、码率 2/3 为例信息位 k10800编码后 16200 bit64QAM 符号数正好是 2700。这个整除关系不是巧合选参数时尽量让码长是调制阶数的整数倍否则还需要做比特填充徒增麻烦。量纲上要注意的是功率归一化。调制端用平均能量为 1 的星座噪声方差再根据目标 Es/N0 换算这样断点排查时能直接比较信号功率和噪声功率的大小不会出现星座图整体漂移或者 LLR 幅度小到译码器无法判断的情况。2.3 两条译码路线内置译码器与自写 BP 解码MATLAB 里做 LDPC 译码有两条路线。第一条是用通信工具箱内置的comm.LDPCDecoder直接指定校验矩阵 H、算法类型和最大迭代次数速度快、数值稳定适合批量出 BER 曲线。第二条是自己写信念传播 BP 译码我通常写成 min-sum 近似版本因为它在对数域里把校验节点的 tanh 运算简化成了取最小值和符号相乘代码量小也能为后续移植 C 或 FPGA 打底。两条路线都要保留。内置译码器用来验证整条链路对不对自写 BP 用来理解消息传递细节以及将来脱离 MATLAB 环境做定点化。很多老代码还在用gf域做译码那是在 R2021b 之前的常见做法现在新版本 MATLAB 不推荐了直接操作稀疏校验矩阵更直观。3. 用 MATLAB 搭通 LDPC 64QAM 链路发射、软解调、译码三块代码3.1 发射端DVB-S.2 校验矩阵与 64QAM 映射发射端的核心是两件事按校验矩阵完成 LDPC 编码再把码字比特映射到星座点。下面这段代码是主脚本的参数区和发射部分我用 DVB-S.2 短帧作为默认配置。% ldpc_64qam_sim.m —— LDPC(2/3) 64QAM over AWGN % 依赖 Communications ToolboxR2021b 及以上 clear; clc; rng(1); rate 2/3; % DVB-S.2 码率 n 16200; % 短帧码长 k n * rate; % 信息位长度 10800 bitsPerSym 6; % 64QAM 每符号比特数 EbN0dB 4:0.5:7; % 扫描的 Eb/N0 范围 maxIter 20; % 译码最大迭代次数 nFrames 30; % 每个信噪比点的仿真帧数 H dvbs2ldpc(rate, n); % 返回稀疏校验矩阵注意第二个参数指定短帧 ldpcEnc comm.LDPCEncoder(H); ldpcDec comm.LDPCDecoder(H, ... Algorithm, Min-sum, ... MinSumNormalizationFactor, 0.75, ... MaximumIterationCount, maxIter, ... OutputValue, Information part); data randi([0 1], k, 1); % 随机信息比特 cword ldpcEnc(data); % LDPC 编码输出 n x 1 symIdx bit2int(cword, bitsPerSym); % 每 6 bit 一组MSB-first tx qammod(symIdx, 64, UnitAveragePower, true); % 64QAM 星座映射这段代码需要注意的是dvbs2ldpc(rate, n)的第二个参数它决定返回长帧 64800 还是短帧 16200 的校验矩阵。如果漏写默认是长帧后面bit2int分组的长度和蒙特卡洛耗时都会翻倍。bit2int按 MSB-first 方式把比特整理成十进制符号索引这与qammod的格雷映射顺序是一致的不需要额外调换比特顺序。qammod开了UnitAveragePower星座图平均功率归一化为 1这是后面噪声方差换算的前提。如果没有这个参数星座能量是 42同样信噪比下噪声方差要跟着放大 42 倍很多新人在这里翻车。3.2 AWGN 信道与 64QAM 软解调LLR 是译码的生命线信道部分就是加复高斯白噪声。难点不在加噪在于把 Es/N0 换算成噪声方差以及写对软解调函数。这里先把换算写在主脚本里EsN0dB EbN0dB(snrIdx) 10*log10(bitsPerSym) 10*log10(rate); noiseVar 10^(-EsN0dB/10); % 复噪声总功率 sigma2 noiseVar / 2; % 每维噪声方差实部虚部各占一半 rx tx sqrt(sigma2) * (randn(size(tx)) 1j*randn(size(tx)));64QAM 软解调我一般不用枚举 64 个星座点的暴力做法而是利用格雷映射的可分离特性实部承载前 3 个比特虚部承载后 3 个比特。这样每个维度只需要在 8 个一维星座点上做 max-log 近似计算量小逻辑也清晰。function llr soft_demod_64qam_gray(rx, sigma2) % 64QAM 格雷映射 max-log 软解调 % rx: 复接收符号列向量 % sigma2: 每维噪声方差 % 返回列向量长度numel(rx)*6每符号 6 个 LLR顺序与 bit2int 一致 x real(rx(:)); y imag(rx(:)); N numel(x); const1D [-7 -5 -3 -1 1 3 5 7] / sqrt(42); % 一维 8-PAM平均功率归一化 grayIdx [7 6 5 4 0 1 3 2]; % 8 个一维点的格雷标签 bitLabel dec2bin(grayIdx, 3) - 0; % 8x3 逻辑矩阵列 1 是最高位 llr zeros(N, 6); for m 1:3 bitOn bitLabel(:, m) 1; bitOff ~bitOn; distX (x - const1D).^2 / sigma2; % Nx8 距离矩阵 d1x min(distX(:, bitOn), [], 2); d0x min(distX(:, bitOff), [], 2); llr(:, m) d0x - d1x; % 正 LLR 表示 bit 为 1 distY (y - const1D).^2 / sigma2; d1y min(distY(:, bitOn), [], 2); d0y min(distY(:, bitOff), [], 2); llr(:, m3) d0y - d1y; end llr llr(:); endmax-log 近似的本质是用最大项代替求和公式推导后得到LLR (d0_min - d1_min) / sigma2。这里 d0 是距离比特取 0 的星座点集合d1 是距离比特取 1 的集合所以代码里是d0x - d1x顺序不能写反否则 LLR 极性整体反转译码结果会全错。噪声方差 sigma2 用的是每维方差也就是复噪声总功率的一半。原因是实部虚部分开计算距离时各自只面对一个实数维度的噪声。如果误把总噪声功率除进去LLR 幅度会整体缩小一半译码性能会损失 2 dB 左右但曲线形状看起来还是正常的非常容易被忽略。3.3 自写 min-sum 译码用 80 行看清 BP 内部内置译码器适合出曲线但为了讲清楚 BP 在干什么我还是保留了一个自写 min-sum 版本。它按边来组织消息每条边连接一个校验节点和一个变量节点变量节点消息初始化为信道 LLR然后在校验节点和变量节点之间来回传递。function [bits, iter] bp_decode_min_sum(H, llrIn, maxIter, alpha) % H: MxN 稀疏校验矩阵 % llrIn: Nx1 信道 LLR正值表示 bit 为 1 % alpha: min-sum 归一化因子默认 0.75 if nargin 4, alpha 0.75; end [M,N] size(H); [rId, cId] find(H); % 每条边对应的校验行和变量列 E numel(rId); vnEdges cell(M,1); % 每个校验节点的边索引 cnEdges cell(N,1); % 每个变量节点的边索引 for e 1:E vnEdges{rId(e)}(end1) e; cnEdges{cId(e)}(end1) e; end % 初始化变量到校验消息为信道 LLR v2c zeros(E,1); for e 1:E v2c(e) llrIn(cId(e)); end for it 1:maxIter % ---- 校验节点更新min-sum ---- c2v zeros(E,1); for r 1:M edges vnEdges{r}; mag abs(v2c(edges)); sg sign(v2c(edges)); [min1, idx] min(mag); mag(idx) inf; min2 min(mag); signProd prod(sg); for e 1:numel(edges) if edges(e) edges(idx) c2v(edges(e)) alpha * signProd * sg(e) * min2; else c2v(edges(e)) alpha * signProd * sg(e) * min1; end end end % ---- 变量节点更新 ---- v2cNew zeros(E,1); for c 1:N edges cnEdges{c}; totalExt sum(c2v(edges)); for e 1:numel(edges) v2cNew(edges(e)) llrIn(c) totalExt - c2v(edges(e)); end end v2c v2cNew; % ---- 硬判决与早停 ---- llrTotal llrIn; for c 1:N edges cnEdges{c}; llrTotal(c) llrIn(c) sum(c2v(edges)); end bits double(llrTotal 0); if mod(H * bits, 2) 0 iter it; return; end end iter maxIter; end这条实现里最容易写错的点是校验节点更新中符号的处理。标准 min-sum 对某条边 e 输出的消息幅度是该行所有其他边消息幅度的最小值符号则是其他所有边消息符号的乘积。代码里signProd * sg(e)看似多乘了一次自己的符号实际上正好把符号积中含 e 的那一份抵消掉得到的就是「除 e 以外」的符号积。变量节点更新时每个变量节点的总 LLR 等于信道 LLR 加上所有入边 c2v 消息之和但回传某条边的 v2c 时要扣掉该边自己刚刚收到的 c2v避免消息自我循环。掉进这个坑的表现是译码迭代次数总是打满且不收敛后面第五章会专门说。这个版本用 cell 循环组织消息适合码长几千以内的验证。直接拿它跑 16200 码长的蒙特卡洛会很慢所以我平时出曲线用内置译码器自写 BP 只用于算法验证和定点化移植。3.4 整机脚本与 BER 统计把三段接起来把发射、信道、软解调、译码组合起来就是完整的仿真循环。每个 Eb/N0 点跑若干帧统计总错误比特数最后除以总发送比特数得到 BER。for snrIdx 1:length(EbN0dB) EsN0dB EbN0dB(snrIdx) 10*log10(bitsPerSym) 10*log10(rate); noiseVar 10^(-EsN0dB/10); sigma2 noiseVar / 2; bitErr 0; for f 1:nFrames data randi([0 1], k, 1); cword ldpcEnc(data); symIdx bit2int(cword, bitsPerSym); tx qammod(symIdx, 64, UnitAveragePower, true); rx tx sqrt(sigma2) * (randn(size(tx)) 1j*randn(size(tx))); llr soft_demod_64qam_gray(rx, sigma2); decBits ldpcDec(llr); bitErr bitErr sum(data ~ decBits); end ber(snrIdx) bitErr / (k * nFrames); fprintf(Eb/N0 %.2f dB, BER %.4g\n, EbN0dB(snrIdx), ber(snrIdx)); end semilogy(EbN0dB, ber, o-); grid on; xlabel(Eb/N0 (dB)); ylabel(BER);comm.LDPCDecoder的OutputValue设为Information part输出直接就是信息位可以直接和发送的 data 比较。如果改成Whole codeword需要知道校验矩阵中信息位的位置才能取出来容易吃亏。这套接口从 R2021b 开始就是主流用法新版 MATLAB 依旧兼容如果是从老教程里翻到gf域写法建议直接换掉。4. 参数怎么设码率、迭代次数、LLR 缩放与噪声方差换算4.1 关键参数表与一组可直接起步的配置仿真参数不是拍脑袋定的它们之间有强耦合。我常用的起步配置如下参数默认值说明码率 rate2/35G NR 和 DVB-S.2 都常用降低到 1/2 可换取约 0.5~1 dB 增益码长 n16200短帧仿真速度和性能都均衡64800 更贴近实际系统但耗时高调制阶数64QAM每符号 6 bit星座点数 64译码迭代20继续加大收益递减超过 50 基本只增加耗时min-sum 归一化因子0.75在 0.5~0.9 之间调整用于补偿 min-sum 的幅度偏差Eb/N0 扫描范围4~7 dB步进 0.5 dB码率 2/3 的 64QAM 在 5~6 dB 附近 BER 快速下降每点帧数30先看趋势再决定是否加大到 100 以上一组跑出来的典型趋势是Eb/N0 在 4.5 dB 时 BER 约 1e-2 到 1e-3到 6.5 dB 时可以降到 1e-5 以下。如果曲线斜率明显变缓优先查 LLR 的噪声方差换算而不是怀疑译码迭代不够。4.2 Eb/N0 到噪声方差的换算最容易被带偏的量纲这条换算我每次写新仿真都要重新推导一遍因为它直接决定曲线的横纵坐标对没对上。先明确这里用的是 Es/N0即每个调制符号能量与噪声功率谱密度之比。由于每个 64QAM 符号携带 6 个编码比特其中信息比特数是6 * rate所以 Es/N0 和 Eb/N0 的关系是EsN0dB EbN0dB 10*log10(bitsPerSym) 10*log10(rate);再假设星座平均功率为 1则复噪声总功率为10^(-EsN0dB/10)实部和虚部各分到一半也就是代码里的sigma2 noiseVar / 2。这里最容易犯的错是把bitsPerSym和rate漏掉一个或者把rate的位置放反结果曲线整体平移 1~2 dB看起来像模像样实际上横坐标已经失真。用 BPSK 做对照实验也能发现这个问题64QAM 的香农极限比 BPSK 高不少如果在 0 dB 附近就出现低误码率那一定是换算少乘了 log10 项。4.3 min-sum 的归一化因子与迭代次数怎么调min-sum 译码在校验节点把真实 BP 的 tanh 求和近似成了取最小值近似结果是消息幅度偏大所以需要乘一个小于 1 的归一化因子 alpha。alpha 通常在 0.5 到 0.9 之间工程上先取 0.75再和内置 BP 译码对比曲线微调。迭代次数的影响是边际递减的。10 次迭代能解决大部分错误20 次趋于收敛50 次以上通常不再有误码率提升。要判断迭代次数是否够可以打开comm.LDPCDecoder的迭代终止通知端口统计一个信噪比点上的平均迭代次数。如果平均只有 5 次说明大部分码字早停迭代上限设 20 很安全如果普遍打到上限还没有收敛优先检查 LLR 极性或噪声方差而不是盲目加大迭代。4.4 蒙特卡洛帧数想要多稳得先算这笔账BER 仿真的置信度由错误比特数决定不是由帧数决定。目标是 1e-4 的误码率想看到相对平滑的曲线最好累计到 100 个错误比特以上。按照 k10800意味着大约需要 100/(108001e-4) 约 93 帧也就是帧数要往 100 靠。但在 30 帧的起步配置里如果看到 0 个错误比特只能说明真值低于 1/(1080030) 约 3e-6不能直接画成 0否则对数坐标下曲线会断掉。实际做法是低信噪比点跑 20 到 30 帧高信噪比点把帧数加大到 100 甚至 200。如果每个点都跑 200 帧16200 码长配合 64QAM 的蒙特卡洛会非常耗时所以我习惯先用 30 帧扫出趋势再对曲线陡降区间的两三个点加大帧数精修。5. 避坑指南LDPC 64QAM 仿真里最容易翻车的五个细节5.1 曲线整体往右偏噪声方差量纲错了现象仿真曲线看起来形状正常但整体比理论预期右偏 1~2 dB换个码率偏差也跟着变。原因Es/N0 换算少了10*log10(rate)或10*log10(bitsPerSym)其中一项。很多老脚本直接把 AWGN 信噪比当作符号信噪比使用对 64QAM 来说是致命的量纲错误。解决重新核对从 Eb/N0 到 noiseVar 的每一步换算并用无编码 BPSK 或者内置译码器在 0 dB 附近的性能作为标定点。也可以打印每个信噪比点下rx的信号功率和噪声功率验证比值是否等于目标 Es/N0。5.2 LLR 极性反了解出来全是反码现象无噪声单帧测试时译码输出与发送码字完全不同BER 接近 0.5而且校验方程怎么都不满足。原因软解调函数里d0x - d1x写成了d1x - d0x或者 LLR 正负约定与译码器不一致。MATLAB 内置译码器默认接受「正值表示比特 1」的 LLR自写函数必须严格保持一致。解决第一步做无噪声单帧自检把noiseVar设成 0 或极小数确认soft_demod_64qam_gray输出的 LLR 符号与data一致第二步检查H * cword模 2 是否为 0确认编码端正确第三步再把假设翻过来测一次确认译码器约定确实是正为 1。5.3 自写 min-sum 迭代不收敛消息更新没有排除自己现象自写 BP 译码总是跑满最大迭代次数硬判决后的校验和一直不为 0而且迭代过程中 LLR 绝对值越来越大出现发散迹象。原因变量节点更新时把当前边自己的 c2v 消息也算进了回传消息里。消息在变量节点和校验节点之间反复循环放大最后数值爆炸。这是自写 BP 里最常见也最隐蔽的符号级错误。解决在变量节点更新时严格使用llrIn(c) totalExt - c2v(edges(e))而不是llrIn(c) totalExt。调试时还可以把迭代次数设成 2 到 3 次打印每轮每个变量节点的总 LLR对比排除自身前后的数值差异。5.4 内置译码器和自写 BP 性能差 0.5 dBmin-sum 缺了归一化现象自写 min-sum 译码的 BER 曲线比内置 BP 或理论值差 0.3 到 0.5 dB在陡降区域尤其明显。原因min-sum 比真实 BP 的校验节点消息偏大直接使用等于把外部信息权重放大译码朝着错误方向加速收敛。常见表现是性能略差但不至于完全不能工作容易被当成码率差异忽略。解决给校验节点消息乘一个 0.5 到 0.9 的归一化因子从 0.75 起步。如果对比内置comm.LDPCDecoder的 min-sum 模式它也有MinSumNormalizationFactor属性把两边设成同一个 alpha 再对比曲线。5.5 高信噪比下 BER 画不出线0 误码怎么处理现象semilogy 曲线在某个 Eb/N0 点之后突然少了一个点或者曲线断掉看起来像程序报错。原因高信噪比下仿真帧数不足以统计到任何错误BER 算出来是 0对数坐标无法表示 0画图时这个点就被丢弃。解决把高信噪比点的帧数加大到 100 以上优先保证累计错误比特数达到几十个如果仍然为 0就在图中标注「该点未检出错误」不要把 0 硬画到 1e-8 之类的位子。工程曲线里0 误码点下方能给出的只是一个上界不是精确误码率。6. 进阶从 AWGN 到衰落信道与 BER/FER 验证方法6.1 把 AWGN 换成平坦衰落信道LLR 不再只有一个 sigma2AWGN 链路跑通后下一步通常是平坦衰落信道。此时接收符号为rx h .* tx noise噪声方差不变但信号幅度被信道系数 h 影响。如果直接对rx做软解调星座点位置会被 h 缩放LLR 全部失真。常见做法是先做迫零均衡把接收符号除以 h再把每维噪声方差改写成sigma2 ./ abs(h).^2。由于每个符号的衰落不同你的软解调函数要支持逐符号的sigma2向量而不是一个标量。h (randn(nSymbols,1) 1j*randn(nSymbols,1)) / sqrt(2); % 每符号衰落 rx h .* tx sqrt(sigma2) * (randn(size(tx)) 1j*randn(size(tx))); rxEq rx ./ h; llr soft_demod_64qam_gray(rxEq, sigma2 ./ abs(h).^2);这个改动很小但会让 LLR 的可靠性随深衰落位置明显变化曲线比 AWGN 情况右移 7~10 dB而且会出现错误平层。如果你做的是 OFDM 系统还会涉及子载波级 CSI原理是同一套。6.2 用校验方程做 FER 统计替 BER 把关BER 低不代表每个错误帧只错一个比特有时一个错误帧里错了几十个比特。工程里更关心的是帧错误率 FER因为它直接对应能否触发重传。译码后的硬判决bits如果是完整码字用校验矩阵做一次模 2 乘法就能判断这一帧是否正确parity mod(H * bits, 2); ferFrame any(parity); % 校验不为 0 则该帧错误这个判断比逐比特比较更可靠因为它不依赖信息位位置也不怕comm.LDPCDecoder的输出格式差异。建议把 BER 和 FER 同时统计FER 曲线通常在 BER 曲线的右侧约 0.5 dB 处两条线一起看才能判断译码器是否真的在正常收敛。我做这类仿真时有个习惯每跑完一个新配置先不画曲线而是打印「无噪声自检、校验和、平均迭代次数」三个数字任何一个不对就回头查 LL R 极性和噪声方差。这套动作看似笨实际帮我省掉过好几轮整曲线返工的时间。LDPC 加 64QAM 的组合本身不难难的是所有量纲和比特顺序都恰好对得上。希望这篇笔记能帮你少走一遍我当年走过的路。本文还有配套的精品资源点击获取