LDPC 码的仿真调试起来,比很多人想象中要麻烦。麻烦的不是 BP 译码本身,而是从校验矩阵到对比实验的那一整条链路:矩阵怎么构造才够稀疏、短环怎么避开、迭代次数和码率码长对误码率的影响怎么分开看。这篇文章要讲的,是一套我用 Matlab 完整跑通的仿真方案,核心是基于大衍数构造稀疏校验矩阵,然后做 LDPC 误码率仿真,重点对比不同译码迭代次数、不同码率和不同码长下的表现。
项目标题里最容易被误解的应该是“大衍数”三个字。它不是某种神秘算法,而是从中国古代数学思想里借来的同余构造思路,落到代码里就是一组取模运算和等差偏移,用来生成确定性、无短环、高度稀疏的校验矩阵。后面的正文我会先把方案设计拆开,再给关键代码和参数表,最后把三组对比实验的典型趋势和排查经验一起讲清楚,适合正在做信道编码仿真、或者被随机校验矩阵和性能地板折磨的同学参考。
1. 仿真方案拆解:三组变量背后到底在比什么
1.1 为什么选迭代次数、码率、码长这三个维度
先回答一个最基础的问题:为什么标题里要拿这三组变量做对比,而不是只跑一条误码率曲线。
迭代次数直接控制 BP 译码器消息传播的轮数。LDPC 的软判决译码本质上是一种图上的迭代消息传递,迭代次数少,消息还没有充分扩散开,高层级的纠错能力体现不出来;迭代次数多,低信噪比下可能会出现消息振荡甚至错误固化。这个变量影响的是“译码器的收敛代价”,也就是你愿意花多少计算量去换性能。
码率则决定了冗余度和信息传输效率之间的权衡。码率越低,校验位越多,纠错能力越强,但每个信息比特占用的带宽资源也越多;码率越高,系统吞吐量越高,但性能会相应下降。对比不同码率,实际上是观察“同一套构造方法在不同冗余度下还能不能保持良好性能”。
码长是最有意思的维度。LDPC 码是一种线性分组码,码长越短,有限长效应越明显,距离香农限越远;码长越长,译码性能越逼近理论极限,但同时仿真复杂度大幅上升。你不可能只靠增加迭代次数来“弥补”短码长的性能损失,所以码长这个维度必须单独看。
这三个维度放在一起,恰好覆盖了 LDPC 系统设计中最核心的三角关系:性能、复杂度、码率效率。仿真时把它们分开扫描,你才能真正理解一个校验矩阵设计是好是坏,以及它在什么条件下会失效。
1.2 大衍数在这个仿真里扮演什么角色
很多同学一开始接触 LDPC 仿真,习惯用完全随机的矩阵生成方式,或者用现成工具包里的矩阵。随机矩阵的问题在于:每次运行结果不同,短环出现的概率大,而且一旦误码率曲线出现“地板效应”,很难定位到底是译码器问题还是矩阵问题。
大衍数构造在这里的核心价值是确定性。它通过一组同余规则来放置校验矩阵中的非零元位置,也就是“1”的位置,让矩阵在保持稀疏的同时,尽量避免短环,尤其是避免最容易破坏迭代译码的 4 环。用了这种方式以后,矩阵可以被参数精确描述,仿真结果可以复现,排障时也能把变量控制得干干净净。
从性能角度看,大衍数构造还能让校验矩阵的行重、列重保持相对均匀,这让变量节点和校验节点在图上的“地位”比较一致,不至于出现某些节点负载过重、导致迭代收敛特别慢的问题。
1.3 这套方案适合什么场景
如果你正在做信道编码方向的课程设计、搭建误码率仿真平台、或者做 LDPC 码在某种通信场景下的性能预研,这套方案可以直接参考。它能帮你把误码率仿真流程跑通,并且给出合理的矩阵构造工具。
因为方案里用的是 Matlab,代码结构也比较线性,哪怕你之前只接触过 BCH 码或卷积码仿真,没有太多 LDPC 经验,也可以按步骤把整套链路搭起来。重点不是让你背代码,而是理解每一步在干什么、为什么要这么干。
2. 核心原理:稀疏校验矩阵和大衍数构造
2.1 为什么 LDPC 码必须“稀疏”
LDPC 码的译码复杂度与校验矩阵中的非零元素个数直接相关。校验矩阵里大部分元素是 0,只有少量 1,这样在迭代译码的每一步里,每个校验节点只需要处理少数几个变量节点的消息,每个变量节点也只需要把消息传给少数几个校验节点,整体计算量才可控。
可以打一个比方:如果你维护一个城市交通网络,每个路口只连接附近几条路,那么交警疏通拥堵的速度一定比所有路口全部互联要快得多。稀疏校验矩阵就是“每个路口只连几条路”的结构,密度过高,消息传递会变成全网广播,计算复杂度不可接受,而且会出现大量短环,导致译码性能急剧劣化。
所以“稀疏”和“无短环”是 LDPC 码性能的两条腿。大衍数构造的设计目标,正是同时兼顾这两点。
2.2 大衍数构造的直观理解与最小示例
大衍数构造的思想,本质上是用数论里的同余关系去“安排”校验矩阵中每一行非零元的列位置。矩阵大小是 M 行 N 列,N 是码长,M 是校验位的个数。每一行的非零元不能太多,否则行太重;每一列的非零元也不能太多,否则列太重。大衍数构造的常见做法是把列位置看成一组模运算的结果:行号每变化一次,非零元的列坐标按某个与块长互素的步长循环偏移。
我举个最小规模的例子,方便理解规则。假设矩阵有 6 行 6 列,行重和列重都设置为 3。第一行的非零元放在第 1、3、5 列。如果步长取 2,那么第二行的非零元就会平移到第 3、5、1 列。计算到底就是mod(旧列号 + 步长, 列块数)。这样循环下去,每一行仍然是 3 个非零元,每一列也会均匀出现 3 个非零元,矩阵整体非常规整。
这个示例只是为了说明“等差偏移 + 取模”这种规则是怎么工作的。实际仿真中,矩阵规模要比这大得多,步长和分块数也需要经过一次小规模搜索,确保生成的矩阵行列重均匀、且两两列之间的共同非零行数不超过 1。
2.3 构造参数与码率的绑定关系
设计 LDPC 校验矩阵时,行重和列重不能随便拍脑袋定,它们与码率之间存在硬约束。对正则 LDPC 码来说,设列重为 γ,行重为 ρ,校验矩阵是 M 行 N 列,那么总非零元数满足:
M × ρ = N × γ
于是校验位比例 M/N 等于 γ/ρ,码率 R = 1 - γ/ρ。
这个公式非常实用。例如想让码率约等于 0.5,可取列重 γ=3,行重 ρ=6;想让码率约等于 0.75,可取列重 γ=3,行重 ρ=12。这样设计出来的矩阵天然满足码率目标,不需要事后做复杂的裁剪。
我在仿真里通常用下面这组参数作为基线:
| 码率 | 列重 γ | 行重 ρ | 含义 |
|---|---|---|---|
| 0.5 | 3 | 6 | 每个变量节点连接 3 个校验方程,每个校验方程包含 6 个变量 |
| 0.75 | 3 | 12 | 冗余减少,校验方程包含的变量节点变多 |
行重变大后,单个校验方程包含的比特数变多,BP 译码里校验节点更新时需要考虑的因子也更多,计算量会略有上升。这是高码率必然付出的代价。
2.4 与随机构造和 PEG 算法的差异
我还想提一下大衍数构造和另外两种常见构造方式的区别。一种是完全随机生成稀疏矩阵,另一种是用 PEG 算法逐列添加边来保证围长。PEG 算法性能确实好,但实现逻辑复杂,而且每次运行结果不固定。
大衍数构造的优势在于“规则可复现”,同一组参数在任何机器上生成完全一样的矩阵,调试时非常方便。缺点是对参数选择比较敏感:如果步长与列数之间存在公因子,构造出来的矩阵可能出现行列重严重不均衡,甚至出现 4 环。这一点需要在实际构造时做一次冲突检测,我会在下一节给出对应的检查方法。
3. Matlab 实现全流程:从校验矩阵到误码率曲线
3.1 第一步:生成大衍数稀疏校验矩阵
我在 Matlab 里的做法是,先把参数定义清楚,然后用一层循环按同余规则填非零元,最后做一次冲突检测和校正。
% 参数定义 R = 0.5; % 目标码率 N = 512; % 码长 gamma = 3; % 列重 rho = 6; % 行重,R = 1 - gamma/rho = 0.5 M = round(N * (1 - R)); % 校验位个数 A = zeros(M, N); step = 7; % 步长,要求与 N 互素 for row = 1:M base = mod(row, N / rho); % 每行起始位置随行号变化 for k = 0:(rho - 1) col = mod(base + k * step, N) + 1; A(row, col) = 1; end end这段代码生成的是一个每行恰好 rho 个非零元的矩阵。虽然列重不保证完全等于 3,但通过合理选择步长和分块参数,列重分布会非常接近 3。生成之后必须做两件事:检查列重分布、检查 4 环。
% 检查列重 colWeight = sum(A, 1); fprintf('列重范围: %d ~ %d\n', min(colWeight), max(colWeight)); % 检查4环:任意两列内积不超过1 flag = 0; for i = 1:N for j = i + 1:N if sum(A(:, i) .* A(:, j)) > 1 flag = 1; end end end if flag == 0 disp('没有检测到4环'); else disp('存在4环,需要调整步长或分块参数'); end这个 4 环检测虽然是用双重循环写的,只适合确认矩阵性质时用,跑一次没问题,N 到 1024 也还能接受。如果 N 继续加大,可以换成基于列坐标集合的哈希方式,速度会快很多。
注意:实际跑仿真之前,建议把 H 矩阵转成 sparse 存储,即
H = sparse(A)。后面译码算法里要用到大量矩阵运算,满矩阵在 N=1024、M=512 时虽然不影响运行速度,但到更大码长时内存消耗会明显上升。
3.2 第二步:编码、调制与信噪比换算
有了校验矩阵 H 之后,编码环节需要求出生成矩阵 G。最直接的办法是高斯消元把 H 变换成[A | I]的形式,然后得到 G =[I | A']。
% 高斯消元求生成矩阵 Hsys = mod(rref(double(full(H))), 2); % 提取校验部分并组合生成矩阵 % 注意这里需要先对 H 做列交换,保证前 M 列可化为单位阵实际编码时,信息位向量 u 是 1×K,K = N - M。码字 c = u × G 的每个分量再做模 2 运算,就得到 1×N 的码字。
调制方式用最简单的 BPSK:把 0 映射成 +1,把 1 映射成 -1,即x = 1 - 2 * c。然后叠加高斯白噪声,得到接收信号y = x + noise。
这里面有一个特别容易踩坑的点:信噪比换算。误码率曲线横轴通常用 Eb/N0,即每个信息比特对应的信噪比。但因为发送的是整个码字,每个码字里有 K 个信息比特,发送 N 个符号,所以符号信噪比 Es/N0 与 Eb/N0 之间要乘一个码率因子:Es/N0 = Eb/N0 + 10×log10(R)。
例如想测 Eb/N0 = 3dB、码率 R=0.5 的点,实际 Es/N0 = 0dB。在用 Matlab 生成噪声时,噪声方差按sigma2 = 0.5 * 10^(-EsN0_dB / 10)来算,然后noise = sqrt(sigma2) * randn(1, N)。很多初学同学直接用 Eb/N0 生成噪声,会导致仿真曲线整体偏移 3dB 左右,这是最常见的问题。
译码器初始化需要的对数似然比 LLR 在 BPSK + AWGN 信道下有闭式解:
Lch = 2 × y / sigma2
这个 LLR 直接作为变量节点的初始消息送入 BP 译码器。
3.3 第三步:和积译码主循环
和积译码器的核心是变量节点与校验节点之间来回传递置信消息。我给出一个精简骨架:
% llr: 初始信道消息, H: 稀疏校验矩阵, maxIter: 最大迭代次数 [rows, cols] = find(H); % 建立消息索引表 % varMsg: 变量节点传给校验节点的消息 % checkMsg: 校验节点传给变量节点的消息 for iter = 1:maxIter % 1. 校验节点更新 % 对每个校验节点,收集所有相邻变量节点消息, % 用 tanh 规则计算外信息 % 2. 变量节点更新 % 每个变量节点把信道消息与相邻校验消息求和, % 注意自己到校验节点的那条边消息不能加回来 % 3. 硬判决 xHat = double(llrTotal < 0); % 4. 伴随式校验 syndrome = mod(H * xHat, 2); if all(syndrome == 0) break; end endtanh 规则是 BP 译码的核心公式,Matlab 里用tanh和atanh实现时要注意数值稳定:当输入接近 ±1 时,atanh会趋向无穷。工程上通常给消息加一个符号下限,或者用sign*log映射到对数域来求外信息,也就是对数域和积算法。我在代码里实现的是对数域版本,比直接用 tanh 规则稳得多,尤其在迭代次数较高的时候,数值差异会直接影响误码率曲线是否出现异常抬升。
停止迭代的标准是“伴随式为全零向量”,也就是说当前硬判决结果满足所有校验方程。如果达到最大迭代次数但仍然不满足,就把这帧当作译码错误,统计进入误码率的分母。
3.4 仿真框架的循环组织
对比三组变量,代码组织上我建议写成“参数表驱动”的形式,而不是复制粘贴多个测试脚本。
configList = { struct('N', 512, 'R', 0.5, 'maxIter', 5) struct('N', 512, 'R', 0.5, 'maxIter', 20) struct('N', 1024, 'R', 0.5, 'maxIter', 20) struct('N', 512, 'R', 0.75, 'maxIter', 20) }; for idx = 1:length(configList) cfg = configList(idx); % 依次执行:构造矩阵、编码、加噪、译码、统计BER end每一组配置跑完后,把[EbN0_dB, ber]的结果存成一个 mat 文件或直接 append 到 cell 里,最后统一画图。这样后面想调整信噪比范围时,不需要重新跑所有配置,能省下非常多时间。
画图时建议用semilogy,Y 轴用对数坐标,不同配置用不同的 Marker 区分,这样三条曲线之间的差距会非常清楚。
4. 三组对比实验的典型结果与解读
4.1 迭代次数:曲线先陡降后饱和
迭代次数对 LDPC 码性能的影响,通常呈现这样的规律:迭代次数从 1 增加到 10 左右,误码率曲线改善非常明显;继续增加到 20、50,曲线的改善幅度迅速收窄,甚至在高信噪比区域几乎没有区别。
原因在于 BP 译码本质上是在图上做置信传播,消息需要经过多轮传播才能跨越足够远的校验节点和变量节点。迭代次数太少时,图中距离较远的节点之间无法有效传递信息,所以性能很差。但当迭代次数足够大时,所有能有效参与纠错的路径都已经“饱和”,消息不再有新的信息增益,曲线就进入平台期。
仿真时选择迭代次数,我建议先跑一个小范围扫描,比如 5、10、20、50。如果 20 次和 50 次的曲线几乎重合,就可以把最大迭代次数锁定在 20,没必要为了多一点点增益把仿真时间翻倍。
不过要特别注意,低信噪比区域有时候会出现迭代次数增加、误码率反而略微变差的现象。这通常是消息振荡导致的。下一章我会专门聊振荡问题。
4.2 码率:冗余与净编码增益的权衡
对比不同码率时,必须统一到 Eb/N0 上比较才有意义。如果直接用 Es/N0 作为横轴,高码率码字因为信息位占比高,天然会占便宜,但这种比较不能反映系统实际的信息传输效率。
在我跑过的仿真里,码率 0.5 的曲线通常会明显优于码率 0.75 的曲线,在误码率 1e-4 附近大约有 1~2dB 的差距。这是因为低码率有更多校验位,纠错能力更强。但代价也显而易见:同样带宽下,实际传输的信息量少了一半。
高码率并不是没有价值。在带宽受限系统中,你更关心单位频谱能传多少信息比特。这时候高码率带来的吞吐增益可能比低误码率更重要。所以码率这个维度的对比,本质上是看“净编码增益”和“带宽效率”的取舍,而不是简单地说哪个更好。
4.3 码长:有限长效应与逼近香农限
码长对比的结果通常也比较典型:码长从 256 增加到 1024,误码率曲线会向低信噪比方向移动,瀑布区变得更加陡峭。短码长时有明显的地板效应,也就是曲线在某个误码率附近不再继续下降,这是有限长码字距离特性和图结构决定的。
用一句话概括:长码靠的是“统计平均”,码字长了以后,低码重码字出现的概率被摊薄,距离谱性质更好,因此更逼近香农限。
码长加长带来的问题是仿真时间几乎线性增长。因为每个信噪比点要发送的帧数变多,每帧编码和译码的复杂度也在上升。我个人的做法是:验证实现正确性时用 N=256,跑正式曲线时用 N=512,最后只对最优参数组跑一次 N=1024 的长码验证。这样能极大缩短开发周期。
5. 实测排障:误码率调不下来的几个常见原因
5.1 4 环与性能地板的排查
如果误码率曲线在中高信噪比区域出现明显的“地板”,也就是曲线不再继续下降,首先要检查的就是校验矩阵里是否存在 4 环。4 环会让两个变量节点之间形成直接的消息封闭循环,迭代译码时消息容易互相强化,导致一部分错误固化解不出来。
检查方法就是我在 3.1 节给出的列内积检测。如果发现 4 环,可以调整步长参数,或者把步长与分块数的互素条件验证一遍。大衍数构造里,步长与码长 N 不互素时几乎必然出现结构性问题,这一点比矩阵稀疏度本身更容易导致性能劣化。
还有一个值得注意的盲区:有时候 H 本身没有 4 环,但因为编码时高斯消元得到的生成矩阵隐含在码字结构中,实际参与译码的错误图样里出现了低码重码字,也会形成地板。这种情况需要结合码字的距离谱来看,仿真排查时可以先降低迭代次数,如果地板位置不变化,说明问题多半不在译码器而在码结构。
5.2 迭代振荡和错误平台
迭代次数很高时误码率反而抬升,很多人第一反应是代码写错了。其实更常见的原因是消息振荡。BP 译码在循环因子图上不是总能收敛的,消息会在两组极性之间来回弹跳,导致硬判决结果不断翻转。
处理办法有两个。第一个是限制最大迭代次数,避免振荡区域扩散,这也是为什么很多 LDPC 系统的最大迭代次数设为 10~30 而不是 100。第二个是给消息更新加阻尼因子,也就是把新旧消息做加权平均,例如新消息 = 0.75 × 当前计算消息 + 0.25 × 旧消息。阻尼因子不需要太高,0.7 到 0.8 之间一般就能明显抑制振荡。
我实测下来,阻尼因子对高码率、长码长的组合尤其有效。低码率短码长时,振荡现象本来就不严重,加阻尼反而会让收敛速度变慢,所以这个参数最好按对比组分开设置。
5.3 仿真时间太长怎么办
LDPC 误码率仿真是出了名的耗时大户。每个信噪比点如果要跑到 1e-5 量级,可能需要上千帧甚至上万帧。如果没有优化策略,一组配置跑一整天是很正常的事。
我一般按三段式来控制时间。第一步,用小信噪比范围和最少帧数,比如每点 200 帧,快速确认整条链路没有报错;第二步,用中等帧数,比如每点 1000 帧,跑出粗略趋势,判断哪些信噪比区间值得细化;第三步,只对瀑布区附近的信噪比点增加帧数到 5000 以上,抬高区间的点不用重复跑。
还有非常重要的一点:Matlab 里要固定随机种子,也就是在脚本开头执行rng(2024)。这样每一次仿真都使用同一组噪声序列,不同配置之间的性能差异不会因为随机噪声的涨落而抖动。否则你会发现同一组参数跑两次,误码率曲线差得比对比组之间的差距还大,根本没法分析。
5.4 参数对照速查表
| 现象 | 可能原因 | 排查方向 | 常见解法 |
|---|---|---|---|
| 曲线整体右偏 3dB 左右 | 信噪比换算错误,用 Es/N0 代替 Eb/N0 | 检查噪声方差计算 | 噪声方差乘以码率因子 R |
| 高信噪比出现地板 | 校验矩阵存在 4 环 | 列内积检查 | 调整大衍数构造步长 |
| 迭代次数增加但性能变差 | 消息振荡 | 查看不同迭代次数曲线交叉现象 | 加阻尼因子或限制迭代次数 |
| 列重严重不均匀 | 构造参数与 N 不互素 | 打印列重分布范围 | 更换步长,重新做冲突检测 |
| 短码长曲线抖动剧烈 | 帧数不够,随机涨落大 | 查看同参数重复实验 | 固定 rng,增加瀑布区帧数 |
| 译码失败率很高但 BER 很低 | 统计了错误码字中的少数比特 | 确认误码率统计口径 | 同时统计 FER 与 BER,互相验证 |
这张表是我调试过程中积累下来最常用的一组排查线索。遇到异常曲线时,先对号入座,效率会比盲改代码高很多。
6. 一点实操补充:数据记录与实验节奏
最后再分享一个实验管理上的小技巧。三组变量交叉对比时,配置组合会非常多,如果每一组单独跑完再手动记录,很容易搞混。我习惯把每个仿真结果做进一个结构体数组里,字段包括码长、码率、最大迭代次数、信噪比点向量、BER 数组、FER 数组,还有一个很关键的elapsedTime字段,用来记录这次仿真耗时。
后续分析时直接从这个结构体里取数,画图、整理对比表格都方便很多。更重要的是,我再也不需要在“这条曲线是哪个参数组合”上花时间回忆。做科研实验或者课程设计,数据管理能力往往比写代码能力更影响效率。
仿真的节奏上,我强烈建议先跑最短码长、最少帧数、最小迭代次数的组合。这样做不是为了出结果,而是为了验证整条链路有没有问题。链路验证通过后,再逐步加长码长、增加迭代次数、扩大信噪比扫描范围。
LDPC 的矩阵构造和 BP 译码是有很多细节的,任何一个环节出错,最终误码率曲线都会给你一个“莫名其妙”的结果。但只要矩阵生成规则清晰、信噪比换算正确、统计口径统一,这套基于大衍数构造的仿真框架就能稳定地支撑对比实验,也希望这篇记录能帮你少走一些弯路。