LDPC 这块我实际接触得不算晚但真正让我把误码率仿真当成一回事去认真研究的还是当年第一次面对“稀疏校验矩阵到底怎么构造”这个问题的时候。很多人跑 LDPC 仿真直接用现成矩阵或者随机矩阵一换码长、换码率性能曲线就完全不对劲。后来我沿“大衍数”这条思路去做校验矩阵的确定性构造在 MATLAB 里把不同译码迭代次数、码率、码长的影响都对比了一遍才发现这里面的门道比单纯跑一个ber循环要多得多。这篇文章就把这套思路整理出来。内容围绕“基于大衍数构造稀疏校验矩阵的 LDPC 误码率 MATLAB 仿真”展开适合正在做信道编码课设、通信系统仿真或者刚接触 LDPC 但不知道矩阵怎么下手的研究生和工程师。我会把矩阵构造、译码链路、变量对比和仿真调试一起讲清楚每一段都是可以直接复现的经验。1. 这个仿真到底在解决什么问题1.1 误码率是衡量 LDPC 的“考试分数”我们在做 LDPC 仿真时最终拿出来的指标往往是误码率BER曲线偶尔也会看误信率BLER。误码率统计的是所有传输比特中出错的比特占比而误信率统计的是整帧数据中出现错误的帧占比。前者关注比特级质量后者关注分组传输质量。对 LDPC 这种块编码来说两个指标都很有参考价值但很多人一上来就只画 BER其实不够全面。用一句话概括BER 曲线是校验矩阵和译码算法综合实力的体现。同一种译码算法下两张不同的校验矩阵有可能一张在 1.5 dB 附近就进入“瀑布区”另一张到 3 dB 还在缓慢下滑。差异的来源不是 MATLAB 代码写得好不好而是矩阵本身的围长、度分布、环结构这些底层性质起作用了。1.2 校验矩阵 H 是 LDPC 的“基因”LDPC 的核心是一个稀疏校验矩阵 H大小为 m×n。n 对应码字长度m 通常对应校验位的数量。信息位长度 kn−m码率 Rk/n。矩阵的每一行代表一个校验方程每一列代表一个码字比特参与的校验关系。稀疏的含义就是 H 中“1”的数量非常少。如果矩阵是稠密的译码时校验节点和变量节点之间就会互相牵连消息传递算法的独立性假设会被破坏性能会非常差。因此构造 H 的关键在于在保证稀疏性的同时让“1”的位置分布足够均匀并且尽量避免短环尤其是长度为 4 的环。这里就引出了“大衍数”的价值。大衍数本质上是一种同余递推思想源自古人处理一次同余组问题的“大衍求一术”。现代的确定性 LDPC 矩阵构造里很多方法都在用模运算生成“1”的位置索引。用同余递推的方式确定每一行中 1 的列坐标可以让矩阵既保持稀疏又带有一定的代数结构降低随机搜索带来的不确定性和“运气成分”。1.3 “大衍数”在这里到底起了什么作用我会这样描述“基于大衍数构造”的核心逻辑它不是某一类固定的矩阵而是一类利用同余取模规则生成稀疏校验矩阵的构造方法。具体实现时通常先确定行重 d_c 和列重 d_v然后用形如col (a * row_index b) mod n的规则生成位置再通过合理选择 (a,b) 来避免同一列里出现重复的行坐标。这种方法和纯随机矩阵相比最大的好处是确定性强、可复现。换一组参数矩阵的性能方向大致可控而随机生成矩阵哪怕只是换一个随机种子BER 曲线就可能抖动尤其在中等码长场景下非常明显。所以如果你想做“不同码率、不同码长”的系统性对比用确定性构造比随机构造稳得多。2. 大衍数构造稀疏校验矩阵的设计思路2.1 一种可行的构造规则构造 LDPC 校验矩阵确定参数是第一步。以码率 R1/2、码长 n512 为例校验位 mn−k256矩阵维度就是 256×512。如果设置列重 d_v3那么矩阵中总共有 512×31536 个“1”。均匀分配到 256 行行重就是 1536/2566。接下来生成坐标。下面这段 MATLAB 风格的伪代码展示了我常用的构造方式% 参数设置 n 512; m 256; dv 3; dc 6; % 大衍数同余递推生成列坐标 H zeros(m, n); for col 1:n for j 1:dv % 根据大衍数递推计算行索引 row mod(col * a(j) b(j), m) 1; % 防止同一列出现重复位置 while H(row, col) 1 row mod(row 1, m) 1; end H(row, col) 1; end end其中 a(j) 和 b(j) 就是与大衍数相关的同余参数。实际构造时可以选取一组互素的参数例如让 a(j) 分别是某个基础序列的取值避免列内冲突。防冲突的while循环是一个必需操作因为同余运算天然可能把不同列的“1”映射到同一行的同一个位置。这个构造过程的时间复杂度不高MATLAB 里几百毫秒就能生成一张 256×512 的矩阵。这里我故意没有把矩阵生成写成复杂的随机搜索就是为了保证可复现性。2.2 短环问题矩阵性能的隐形杀手LDPC 译码基于 Tanner 图上的消息传递图中如果存在长度为 4 的环两个校验节点之间就有两条不同的路径连接同一对变量节点消息在环里来回打转外部信息容易被重复计算导致误码率性能明显下降。长度为 6 的环影响稍小但如果数量过多也会让译码收敛变慢。构造矩阵时至少应该做到“无 4 环”最好能做到“无 6 环”。怎么检查 4 环最简单的方法是看任意两列之间是否出现超过一个共同行位置。在 MATLAB 中可以用一行代码完成% 计算任意两列的共同非零行数 overlap H * H; % 如果 overlap 中除对角线外还有大于 1 的元素说明存在 4 环如果发现 4 环就要调整同余参数或者做行列置换。确定性构造的调整比随机构造容易得多这也是我推荐它的原因之一。2.3 码率与矩阵维度的换算逻辑码率由矩阵维度直接决定。R1−m/n所以固定 n512m128R0.75矩阵行重和列重的比例也要相应调整固定 m256n1024R0.75要想得到 R1/3 的低码率码就需要 m≈2n/3校验节点增加矩阵行数变多。在对比不同码率时有一个容易忽略的点列重如果固定为 3行重会随码率变化。比如 n512R1/2m256则 dc(n×dv)/m6。当 R3/4m128dc(512×3)/12812。行重变大意味着每个校验方程包含的变量节点变多单次校验更新能获取的信息量变大但同时校验约束的可靠性可能下降这种权衡会在 BER 曲线中体现出来。理解了这层关系再去看仿真结果就不会只停留在“高码率性能差低码率性能好”的粗糙结论上。3. 基于 MATLAB 的仿真链路搭建3.1 整体流程一条完整的“消息回路”LDPC 误码率仿真并不复杂但链路必须完整缺一块就会得到完全错误的曲线。我习惯把仿真拆成五个环节编码、调制、加噪、译码、统计。仿真参数的核心形式是比特 SNREb/N0。在 AWGN 信道下BPSK 调制时噪声方差需要根据码率换算N0 1 / (10^(EbN0_dB/10) * R) sigma sqrt(N0 / 2)这里的 R 必须是码率不能直接用信噪比计算噪声。我见过不少人在这一步出错直接把 Eb/N0 当 Es/N0 用导致所有曲线整体偏移而且偏移量恰好就是 10log10(R)。3.2 编码侧由 H 得到生成矩阵 G在 MATLAB 中做 LDPC 编码最直接的方法是把校验矩阵转化成系统形式的生成矩阵。对 H 做高斯消元把它化成[P | I]的形式然后生成矩阵就是G [I | P]。但这里有个现实问题不是所有 H 都能被完整化成这种系统形式因为 H 可能不是满秩的。一个实用的处理思路是构造 H 时先把最后一列或几列设计成单位阵的一部分或者编码时直接采用“基于 H 的迭代编码”方法。对入门仿真来说我更推荐先做高斯消元如果发现秩不足就微调同余参数重新生成矩阵。编码后的码字需要满足约束mod(H * coded_bits, 2) zeros(m, 1)这个校验关系必须成立否则说明编码端和校验矩阵不匹配后边的译码再怎么优化都没有意义。3.3 对数域和积译码的核心公式译码算法我用的是对数域和积算法Log-SPA它比概率域实现更稳定公式也不难。假设信道输出为 yBPSK 映射比特 0→1、比特 1→−1信道对数似然比初始为L_ch 2 * y / sigma^2变量节点向校验节点传递的消息初始等于 L_ch。每一次迭代中校验节点更新公式为L(r_mn) 2 * atanh( prod( tanh(L(q_mn)/2 ) , 对 n 属于与 m 相连且 n≠n 的集合 ) )变量节点更新公式为L(q_mn) L_ch sum( L(r_mn) , 对 m 属于与 n 相连且 m≠m 的集合 )硬判决时对所有与变量节点 n 相连的校验节点消息求和再加上信道消息L_total L_ch sum( L(r_mn) , 对 m 属于与 n 相连的集合 )L_total 0判为 0否则判为 1。判完之后立刻做校验子检查mod(H * hard_bits, 2)如果全零说明译码成功可以提前退出迭代。不提前退出会让“迭代次数”这个变量的对比变得没有意义因为所有仿真都会一直跑满最大迭代。3.4 迭代次数怎么“传”进译码器这里说的“传”不仅是函数参数还包括脚本设计。我习惯把迭代次数定义成一个数组比如[1 2 3 5 10 20 50]外层循环遍历 Eb/N0内层循环遍历迭代次数或者反过来。关键是每一组迭代次数都要用完全相同的信道噪声样本这样曲线之间的差异才纯粹来自迭代次数的影响。如果每换一个迭代次数就重新生成随机噪声曲线会叠加随机抖动对比效果非常差。用 MATLAB 里的rng固定种子并在每轮仿真开始前重置是保证公平对比的一种简单做法。4. 迭代次数、码率、码长三个变量如何改变误码曲线走势4.1 迭代次数先看收敛再看极限迭代次数对 BER 的影响有两个明显阶段。在低迭代次数阶段比如 1 次到 5 次BER 曲线会随着迭代次数增加而明显下降尤其在高信噪比区域下降幅度非常可观。原因很好理解迭代次数太少消息还没有在网络里充分传播很多校验节点的信息没有被利用起来硬判决的置信度不够。迭代次数再增加比如从 20 次到 50 次曲线改善就非常有限了。这是译码收敛的本质特征——和积算法会在一定迭代后收敛到一个固定点再做更多迭代并不会带来额外增益。所以仿真“不同迭代次数”时你会看到一族逐渐靠拢的曲线。这里有一个实践要点选迭代次数不要只看误码率最低点还要看平均迭代次数。如果 20 次迭代和 50 次迭代性能几乎一样25 次左右就是性价比很高的选择尤其是在长码长、大数据量仿真时能省下大量时间。4.2 码率高码率和低码率的真实代价码率对比的结论看起来直白低码率性能好高码率性能差。但关键在于理解为什么以及代价在哪里。低码率意味着冗余多。比如 R1/3 的码字里每 1 个信息比特对应 3 个传输比特信道噪声经过冗余校验后有更大的纠错空间瀑布区出现的 SNR 更低。但代价是频谱效率下降。相同带宽下低码率码的有效传输速率只有高码率码的几分之一。高码率码比如 R5/6传输效率高但为了维持纠错能力必须保证矩阵的行重足够大或者列重足够大。如果直接沿用低码率的列重参数高码率矩阵的短环概率会增加性能会迅速恶化。从仿真曲线上看高码率曲线会比低码率曲线向右移动移动量通常在 1~3 dB。如果你的目标是“在有限 SNR 下达到某个 BER”码率选择就是一个频谱效率和功率效率的折中问题。4.3 码长长码为什么强以及强到什么程度LDPC 的一个显著特性是码长越长性能越逼近 Shannon 限。从理论上解释长码对应更大的 Tanner 图消息传递过程中的随机波动更小译码器能获取更充分的外部信息。实际仿真中把码长从 256 增加到 1024在相同码率和相同迭代次数下误码率曲线通常会明显左移。不过长码的仿真代价也很高矩阵变大、译码迭代变慢、BER 统计需要更多帧样本才能稳定。在统计 BER 时有一个常见误区为了省时间每个 SNR 点只传 100 帧。码长 256 时100 帧就是 25600 个比特高误码率区域这个数据量还勉强够看码长 1024 时如果你想看 10^-4 以下的 BER至少需要传几千帧否则曲线会像毛刺一样剧烈抖动。5. 仿真结果怎么读从误码曲线反推校验矩阵质量5.1 典型曲线形态瀑布区、错误平层和拐点把不同变量对比出来的 BER 曲线放在一张对数坐标图上通常会看到三段高误码率的“平缓区”、迅速下坠的“瀑布区”、以及接近底部时可能出现的“错误平层”。平缓区对应低 SNR信道噪声太大译码器只能勉强工作BER 下降非常缓慢。瀑布区是 LDPC 最迷人的部分BER 会指数级下降。错误平层则说明译码性能到达一个极限通常由矩阵中的短环、度分布不均或者特定子图结构导致加迭代次数往往也改善不了。分析矩阵质量时我最关注两个数值瀑布区出现的位置以及错误平层的高度。瀑布区位置越靠左说明矩阵的渐近性能越好错误平层高度越低说明矩阵在高 SNR 区的可靠性越高。5.2 多组变量对比图的几种摆法如果想把三种变量的对比在一篇文章或报告中展示清楚我建议不要全挤在一张图里。迭代次数对比用固定码率和码长画 4~5 条曲线在一张图码率对比用固定迭代次数和码长码长对比用固定迭代次数和码率。这样每张图只讲一件事读者看起来也不容易混乱。如果想把矩阵质量也放进对比图还可以在图中加入对应的随机矩阵仿真曲线作为基准。同一套译码算法下如果大衍数构造矩阵的瀑布区比随机矩阵明显左移说明构造方法确实带来了增益。5.3 用仿真结果定位矩阵问题仿真结果不好时很多人第一反应是改译码代码但我建议先怀疑矩阵。如果曲线很难看先检查矩阵中是否存在大量 4 环。计算H*H的除对角线外最大值如果超过 1就说明有 4 环。然后检查行列重分布列重的方差如果很大说明某些列承担的校验关系太多或太少会导致变量节点之间的置信度不均衡。还有一个很隐蔽的问题矩阵的秩不足。H 如果不是满秩码字的实际码率会比理论值低仿真出的性能曲线和理论码率对不上。用rank(H)一查就能看出来。我遇到过不少“性能莫名其妙比别人差 2 dB”的仿真最后都是矩阵秩的问题。6. 仿真工程里最该注意的细节与我摔过的坑6.1 噪声方差公式别记混前面提过一次这里值得单独再讲因为这个错误太常见且太致命。BPSK 调制下Eb/N0 换算噪声方差的标准公式是sigma2 (1) / (2 * R * 10^(EbN0_dB / 10))如果忘记乘码率低信噪比区域似乎影响不大但到高信噪比区域曲线会整体偏移 10log10(R) dB。R1/2 时偏差约 3 dBR1/3 时偏差约 4.77 dB这个偏差足以让仿真结论完全错误。建议在代码开头写一个自检对 Eb/N00 dB理论噪声方差是否符合预期并打印出来。6.2 用全零码字验证译码器LDPC 是线性码全零码字是合法码字。仿真时可以先固定发全零序列跳过编码阶段直接进入调制、加噪、译码。如果译码器在无差错情况下能把全零码字正确恢复出来说明译码逻辑基本正确。这种方法调 bug 非常高效。如果译码器有逻辑错误比如变量节点更新符号反了全零码字会很快报错。等全零码字通过后再切换成随机信息比特用mod(H * coded_bits, 2)验证编码器正确性再开始正式仿真。6.3 短环检查的速度优化如果你要对比的码长很大比如 n4000 以上直接用H*H检查 4 环会非常慢因为矩阵乘法涉及大量浮点运算。可以用稀疏矩阵方式H sparse(H); overlap full(H * H);稀疏运算会快一些。如果还嫌慢就做逐列比较对每一列找出非零行索引两两求交集交集元素大于 1 就存在 4 环。这个方案在列重比较小的时候效率很高因为每列只有 d_v 个非零元素。6.4 长码仿真的随机种子和内存问题长码仿真时生成高斯噪声样本的随机数消耗非常大。为了可复现rng(0)写在仿真脚本入口。但要注意循环内如果每帧都重置种子会生成重复噪声导致统计结果偏差正确的做法是在整个 Eb/N0 循环外设置一次种子循环内让它顺序生成。内存方面H 矩阵如果存成 double 的稠密矩阵n8192 时会占 8192×4096×8 字节大约 268 MB虽然也能用但没必要。直接用sparse存储校验矩阵译码器里的消息矩阵再单独用 double 数组这样内存压力会小很多。6.5 高信噪比下 BER 统计的耗时预估BER 仿真最耗时的阶段是高信噪比区域。比如想测 10^-5 量级的 BER至少需要传 30 万到 50 万个比特才能统计到几十个错误比特。如果不做提前终止这个过程的计算量非常惊人。一个实际建议是“分层仿真”先用较少的帧数跑完整条曲线确定瀑布区的大致位置再针对瀑布区附近的几个 SNR 点增加帧数把曲线画光滑。这样既省时间又能保证关键区域的准确性。这套做法在做多种码率和码长对比时尤其实用。7. 一套可以直接套用的 MATLAB 仿真骨架最后给出一套精简但完整的仿真骨架。它不是论文级别的大代码而是能帮你快速上手、验证矩阵和译码算法的最小闭环。%% LDPC BER 仿真骨架 clear; clc; rng(0); % 矩阵参数 n 512; m 256; dv 3; R (n - m) / n; % 用大衍数规则生成校验矩阵 H generate_LDPC_H(n, m, dv); % 译码参数 max_iter 20; EbN0_dB 0:0.5:4; target_err 50; max_frames 2000; ber zeros(size(EbN0_dB)); for i 1:length(EbN0_dB) EbN0 EbN0_dB(i); sigma2 1 / (2 * R * 10^(EbN0/10)); sigma sqrt(sigma2); n_err 0; n_bit 0; frame 0; while n_err target_err frame max_frames frame frame 1; % 随机信息比特并编码 info randi([0 1], 1, k); % 这里省略求解 G 矩阵的过程实际中直接调用 sys_encode(info, H) % BPSK 调制 x 1 - 2 * code; % AWGN 噪声 y x sigma * randn(size(x)); % 译码函数内部含最大迭代次数控制 code_hat log_spa_decode(y, H, sigma2, max_iter); n_err n_err sum(code_hat ~ code); n_bit n_bit length(code); end ber(i) n_err / n_bit; end semilogy(EbN0_dB, ber, o-); grid on; xlabel(Eb/N0 (dB)); ylabel(BER);骨架里的生成矩阵求解函数没有展开但代码结构是清晰的。你可以把它补全为“高斯消元--转成系统码--编码”的标准流程。实际跑数据时我会额外记录每帧的平均迭代次数这样不仅能画出 BER 曲线还能画出一张“迭代收敛曲线”看看不同 SNR 下译码器是提前收敛还是耗尽迭代次数。这个信息对判断矩阵质量和迭代次数设置非常有用。8. 我在做这类仿真时的几点体会把整个流程走下来之后我对基于大衍数构造 LDPC 校验矩阵这件事的看法是它有价值但不要把它神话。这种方法真正的优势是确定性和可复现性方便做系统性变量对比而纯粹追求极致性能它未必能超过精心设计的 5G NR LDPC 或经过优化搜索的随机构造矩阵。做仿真的人往往会陷入一个误区总觉得曲线不够好就不断堆迭代次数或者换个更大的矩阵。实际上很多性能损失早在矩阵构造阶段就注定了。先把无 4 环、行列重均匀这两件事做好再谈算法优化否则后面的功夫都白费。如果你正准备做 LDPC 相关课题我个人建议从这三个变量对比里挑一个先入手。先固定码率和迭代次数只研究码长对 BER 的影响顺利完成之后再逐步放开另外两个变量。这个思路能让你避免一开始就陷入多变量同时变化的混乱也能更快建立对 LDPC 性能曲线的直觉。最后别忘了每个 SNR 点都要保留原始统计结果记录错误比特数和总比特数而不要只存一个 BER 值否则后面想补充分析某个数据点时就得重跑仿真又得等上几个小时。