ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LDPC信道编解码实战:从校验矩阵到误码率曲线调优

LDPC信道编解码实战:从校验矩阵到误码率曲线调优

简介:LDPC码(低密度奇偶校验码)是一种性能接近香农限的线性分组纠错码,广泛用于5G通信、卫星通信、深空探测与数据存储等领域。该MATLAB程序资源面向通信工程、电子信息类专业学生和相关算法工程师,可直接在MATLAB环境运行,适用于LDPC编解码源码研读、课程设计或不同译码算法的性能对比。程序实现覆盖稀疏校验矩阵H的构造、生成矩阵G的推导与编码映射、Tanner图建模,以及基于信念传播(BP)算法的软判决迭代解码等完整环节;代码中还包括AWGN信道模拟与误码率统计逻辑,便于学习者在不同信噪比条件下观察纠错效果,将教材中的抽象公式转化为可调可测的运行结果。压缩包为标准rar格式,其中仅含1个m文件,包体大小约4KB,轻量小巧,不依赖额外工具箱,解压后即可运行学习。该资源已有155人浏览学习,对信道编码入门者、相关仿真报告撰写者以及希望深入理解LDPC译码细节的读者,具备清晰的参考与对照价值。

1. 拿到 LDPC.rar 之后:信道编解码到底在调什么

拿到一个“LDPC.rar_LDPC_信道编解码”这类压缩包的人,多半不是来学概念的,而是要把 LDPC 编码译码跑通,画出一条能交差的误码率曲线。LDPC 是低密度奇偶校验码,5G NR 的数据信道、WiFi 6、DVB-S2 都在用它做信道编解码。这个包能解决什么问题?它通常包含校验矩阵生成、编码函数、译码函数和仿真主脚本,帮你在给定信噪比下算出误码率。适合两类人:一类是刚接手物理层仿真,想知道代码怎么跑、参数怎么改的从业者;另一类是已经在调链路,苦于 BER 曲线在某一点翻车、却找不到原因的人。先说一个反直觉的结论:跑通这个包一般不难,真正的门槛在后面的译码收敛、错误平层和定点化退化上,这一篇就按这个顺序把这些点讲透。

2. LDPC 信道编解码原理与选型:校验矩阵、Tanner 图与逼近香农限的代价

2.1 稀疏校验矩阵 H 与生成矩阵 G:LDPC 约束关系与码参数设计

LDPC 是一种线性分组码,合法码字必须满足 H·c^T = 0 这个奇偶校验约束。H 是 M×N 的矩阵,N 是码长,M 是校验方程个数,信息位 K = N - M,码率 R = K / N。关键在 H 的“稀疏”:每行每列只有少量 1,这才叫低密度。有了稀疏性,译码时每个校验节点只和少量变量节点交换信息,迭代的计算量才可能从指数级降到线性级,否则码长一上万,任何迭代算法都跑不动。

Tanner 图把 H 画成二分图:左边 N 个变量节点,右边 M 个校验节点,凡是 H 第 i 行第 j 列为 1,就把第 j 个变量节点和第 i 个校验节点连起来。迭代译码的过程就是信息沿着这些边来回传递。图里一旦出现长度为 4 的环(两个变量节点和两个校验节点组成闭合回路),信息会在环上反复自我确认,译码器给出过度自信的错误判定。所以工程上构造 H 都会刻意把最小环长控制在 6 以上——这是 LDPC 里最典型的“看起来对但性能不对”的玄学现场,后面避坑章节会专门展开。

拿到 LDPC.rar 时,第一步是看它生成 H 的方式:是从 DVB-S2、802.11n 的基矩阵里读取,还是随机构造后做 4 环消除。基矩阵方式在码率码长上更可控;随机方式在长码时性能接近,但一旦出现错误平层,定位成本高很多。对做链路仿真的人来说,后者往往意味着要自己重新设计一套矩阵,工作量不在一个量级。

2.2 从 BP 到最小和:迭代译码逼近香农限的工程取舍

LDPC 逼近香农限靠的是置信传播(BP)译码。BP 的核心思想朴素:先给每个编码比特算一个信道软信息(LLR),然后在变量节点和校验节点之间来回传递外信息,每轮把其他节点带来的信息与自己的先验信息累加,直到所有校验方程同时满足或者迭代次数用尽。当码长足够长、Tanner 图足够稀疏时,BP 的译码性能接近最大似然译码,这就是它能在距香农限零点几 dB 范围内工作的原因。

但 BP 的校验节点更新公式里有双曲正切乘法,硬件和定点实现都很麻烦,所以工程里几乎都用最小和(Min-Sum)近似:把校验节点的外信息近似为所有输入 LLR 绝对值最小者乘以符号乘积。这个近似通常带来 0.1 到 0.3 dB 的性能损失,换来的实现代价下降远远不止一个数量级。我的习惯是先跑通浮点 BP 当基线,再切到最小和做定点化,这样一旦最小和比 BP 差得超过 0.5 dB,就能立刻判断是近似引入的问题还是代码写错。

主流的三种信道编解码方案对比如下:

维度LDPC 码Turbo 码Polar 码
典型译码方式BP / 最小和,并行性好MAP 迭代,串行交织SC / SCL 译码
码长灵活性基矩阵可裁剪,灵活依赖交织器,调整成本高按极化子信道构造
错误平层长码不明显,短码需优化基矩阵交织器设计不当时明显依赖 CRC 辅助
主要应用5G NR 数据信道、WiFi 6、DVB-S2LTE 时代主流、部分卫星链路5G NR 控制信道

选 LDPC 而不是 Turbo 或 Polar,多数时候是冲着并行译码和线性编码复杂度去的。这也是 LDPC.rar 这类代码包最常被复用的原因:拿一套可跑的矩阵和译码器,针对自己的码长码率裁剪参数,比从头推一套设计快得多。

2.3 LDPC.rar 里最常见的落点:5G NR、WiFi 6 与 DVB 链路仿真

你手里的压缩包,大概率落在这三个场景之一。5G NR 的 LDPC 用 BG1 和 BG2 两套基矩阵,BG1 面向大块数据传输、码率可到 8/9,BG2 面向小包和低码率,最低可到 1/5;WiFi 6 沿用 802.11n/ac 的 QC-LDPC 结构,码率在 1/2、2/3、3/4、5/6 之间选;DVB-S2 则是固定码长 64800 或 16200,码率档位更多。三者的共同点是都用准循环 LDPC(QC-LDPC),即用一个小基矩阵通过循环移位扩展成大矩阵,这样存储和并行译码都友好。

LDPC 信道编解码的工程落地,其实就是围绕这三套基矩阵做参数适配:Z 值决定码长,基矩阵决定码率,迭代次数决定时延和性能折中。LDPC.rar 里的代码如果能把这三套标准中的任意一套跑通,换到另一套的工作量就只是换基矩阵和 Z,译码器本身不用动。这也是为什么我建议你拿到代码后先确认它是按哪套标准组织的,再开始改参数。

3. 解开 LDPC.rar:文件结构与把仿真跑通的最小步骤

3.1 解压与目录识别:哪些是入口、哪些是参数、哪些可以暂时不看

LDPC.rar 不是标准命名,里面可能是完整工程,也可能只是一组散落的 .m 或 .cpp 文件。拿到手第一步别双击主脚本,先解压看目录结构。Linux 下解压命令是:

bash mkdir -p ldpc_prj && cd ldpc_prj 7z x ../LDPC.rar find . -type f | sort

Windows 下我习惯用 7-Zip 解压,然后立刻建一个 ber_output 目录,专门放仿真结果,避免原始脚本把数据直接写在根目录,跑多轮之后分不清哪一轮是哪一轮的输出。

常见包结构大致是这几类文件:

  • H_matrix_gen.m 或 genH.m:生成或读取校验矩阵;
  • ldpc_encode.m / encode_ldpc.m:编码函数;
  • ldpc_decode.m / bp_decode.m / minsum_decode.m:译码函数;
  • run_sim.m / main_sim.m / BER_sim.m:主仿真脚本;
  • README、params.m、config.m:说明或参数文件。

识别入口最快的办法是查函数调用关系。在目录里直接搜:

bash grep -n "H_matrix_gen(|ldpc_encode(|ldpc_decode(" *.m

哪个脚本同时调用了这三者,哪个就是主入口。没有 README 时,这一步比挨个打开瞎试快很多。然后打开主脚本确认三件事:用的 LDPC 是不是 QC-LDPC 基矩阵形式、调制是 BPSK 还是 QPSK、统计误码率时用的是信息比特还是编码后比特。这三件事直接决定你等下看到的曲线可不可信。

还有一类要注意的坑是运行环境。MATLAB 在 2019b 之后把一些 LDPC 相关函数改了接口,老压缩包如果用了 comm.LDPCEncoder 或者 dvbs2ldpc,新版本里可能报错或行为不同。先看文件头有没有版本注释,没有就把这些函数调用先注释掉,改成通用编码函数,避免一开始就被工具箱版本卡住。

3.2 跑通第一个 EbN0 仿真点的最小命令与参数核对

拿到主脚本后,不要直接跑整条 SNR 曲线,先只跑一个点验证链路通断。以 MATLAB 为例,最小骨架是这样:

matlab % 主仿真骨架:只跑一个 SNR 点,验证链路通断 EbN0_db = 2.0; % 目标信噪比,单位 dB nframes = 100; % 验证阶段帧数可以小 max_iter = 20; % 译码最大迭代次数

R = K / N; % 码率,K 和 N 来自校验矩阵维度 EsN0_db = EbN0_db + 10*log10(R); % 符号信噪比,必须乘码率 sigma = sqrt(0.5 * 10^(-EsN0_db / 10)); % 实 AWGN 噪声标准差

ber_total = 0; for frame = 1 : nframes msg = randi([0 1], K, 1); % 随机信息比特 codeword = ldpc_encode_system(H, msg); % LDPC 编码 x = 1 - 2 * codeword; % BPSK 映射:0->+1,1->-1 n = sigma * randn(N, 1); % 实高斯噪声 y = x + n; llr = 2 * y / (sigma^2); % BPSK 的 LLR 初始化 est = ldpc_decode(H, llr, max_iter); % 最小和/BP 译码 ber_total = ber_total + sum(est ~= msg); end ber = ber_total / (nframes * K); % 只统计信息比特

这段代码有三处容易出错。第一,EsN0 换算必须乘码率,不乘的话高码率下曲线整体右偏,看起来像译码器差 0.5 dB,其实只是噪声算错了。第二,BPSK 的 LLR 初始化是 2y/σ²,不是 y/σ²,少一个系数会让置信度和符号都出问题。第三,误码率分母必须是 K×nframes,统计的是信息比特,不能把编码后的 N 个比特算进去。

跑通的标准不是 BER 为 0,而是这个点上有错但在合理量级。BPSK、1/2 码率、2 dB 时 BER 大概在 1e-2 到 1e-1 之间,如果跑出来是 0.5 附近,说明译码输出和编码输入根本对不上,这个时候不要急着跑整条曲线,先回看译码器。如果跑出来是 NaN,先怀疑 LLR 里出现 inf,再看噪声方差是不是传成了向量。这一小步能帮你省掉后面至少一个小时的排查时间。

4. 编码端落地:构造 QC-LDPC 校验矩阵与 ldpc 编码器参数

4.1 用基矩阵和循环移位生成稀疏 H:Z、码长 N 与码率 R 怎么定

LDPC.rar 里如果自带 H 生成函数,我还是建议自己写一遍 QC-LDPC 的扩展函数,原因很实际:只有自己写过一遍,才能在遇到错误平层时判断到底是不是基矩阵设计的问题。QC-LDPC 的思想是把 H 分成小块,每个块是 Z×Z 的单位阵循环右移若干位,或者是全零矩阵。基矩阵 B 里第 i 行第 j 列的数值,就是移位位数,-1 代表全零块。

matlab function H = qc_expand(B, Z) % 将基矩阵 B 扩展为完整校验矩阵 H % B: mb×nb 基矩阵,元素为循环移位值,-1 表示全零块 % Z: 子矩阵大小,决定扩展后的码长 [mb, nb] = size(B); H = zeros(mbZ, nbZ); for i = 1 : mb for j = 1 : nb shift = B(i, j); if shift < 0 continue; % 全零块,跳过 end shift = mod(shift, Z); % 移位值按 Z 取模,防止越界 block = circshift(eye(Z), [0, shift]); % 单位阵循环右移 H((i-1)Z+1 : iZ, (j-1)Z+1 : jZ) = block; end end end

代码逻辑是双层循环遍历基矩阵每个元素,只有非负移位值才生成对角带;circshift(eye(Z), [0, shift]) 是生成循环移位单位阵最直接的方式。扩展后的 H 尺寸是(mb·Z)行、(nb·Z)列,码长 N = nb·Z,信息位 K = (nb - mb)·Z,码率 R = (nb - mb)/nb。这三个量就是 ldpc 编码器参数的核心。

需要强调,真实工程里 H 不会展开成这个稠密二维数组,而是存三元组索引,MATLAB 里用 sparse 矩阵。上面这段是方便看清结构,仿真里我建议改成 sparse 存储,否则 N=64800 时内存会先把你劝退。基矩阵从哪里来?常见做法有三个:从 DVB-S2 标准附表读(码长 64800/16200),从 802.11n 码表读,或者按 5G NR 的 BG1/BG2 选一组 Z 值然后自己填循环移位值。压缩包内自带的 H 一般就是某种基矩阵导出的,先把 Z 和码率确认清楚,再去动它,不要上来就换码表。

4.2 避免显式求 G:GF(2) 系统编码与替换方案

第一次写 LDPC 编码器时,最直接的想法是把 H 做高斯消元得到系统形式 Hsys = [P | I],然后生成 G = [I | P'],用 c = msg·G 编码。问题在于高斯消元会破坏 H 的稀疏性,得到的 G 是稠密矩阵。码长 64800 时保存 G 需要数十 GB,仿真直接跑不了。新手必踩,我自己也在这个位置翻过车。

如果 H 本身是系统形式 H = [A | B],其中 B 是 m×m 可逆方阵,那么编码可以避免生成 G,直接解 GF(2) 上的线性方程组求校验位。下面给出一个可靠的 GF(2) 求解函数和编码函数,适用于中短码验证:

matlab function p = gf2_solve(B, rhs) % 在 GF(2) 上解 B*p = rhs,B 为可逆方阵 n = size(B, 1); aug = mod([B, rhs], 2); for col = 1 : n pivot = find(aug(col:end, col), 1) + col - 1; % 找当前列第一个非零行 if isempty(pivot) error('B singular'); end aug([col pivot], :) = aug([pivot col], :); % 交换到当前行 rows = find(aug(:, col).'); % 所有该列非零的行 rows(rows == col) = []; aug(rows, :) = mod(aug(rows, :) + aug(col, :), 2); % 消去其他行该列 end p = aug(:, end); end

function c = ldpc_encode_system(H, msg) % H 需为系统形式 H = [A | B],B 可逆;输出码字 c = [p; msg] [m, n] = size(H); k = n - m; A = H(:, 1:k); B = H(:, k+1:end); s = msg(:); p = gf2_solve(B, mod(A * s, 2)); c = [p; s]; end

gf2_solve 的逻辑是标准高斯消元,但全部在 GF(2) 上做:加法就是模 2 加,消去一行就是把当前行加到目标行上。注意它要求 B 非奇异,否则会报错。ldpc_encode_system 把校验位解出来拼在信息位前面,输出系统码字。验证编码正确与否很简单:算一下 H·c^T 是否全 0,全 0 就说明编码器满足奇偶校验约束。

如果你的 H 不是系统形式,需要先做列置换,把可逆的 m 列挪到后面,并保存置换表。这个表在译码后要用来还原信息位,丢了她后续会头痛。长码(N 大于 10000)时不建议用这个直接法,改用近似下三角(RU)编码:把 H 重排后对校验位做前向替换,复杂度降到 O(N)。常见做法是先把短码链路跑通,再切 RU 做长码仿真,否则蒙特卡洛跑一整天也攒不够错误帧。

5. LDPC 信道编解码的高频踩坑:现象、根因与排查记录

这一章不是泛泛列注意事项,而是我踩过一轮后整理的排查顺序。遇到问题时按“现象→原因→解决”定位,比对着代码逐行看快得多。

5.1 高信噪比下 BER 出现地板,不再随 SNR 下降

现象:BER 曲线在 3 dB 之后突然变平,无论怎么提高 EbN0,误码率都停在 1e-4 左右下不去。很多人第一反应是迭代次数不够,把 max_iter 从 10 加到 50,曲线纹丝不动。

原因:多数是校验矩阵的最小环长不够。Tanner 图里存在 4 环或 6 环,迭代译码在环上反复自我强化,错误事件无法被周围节点的正确信息纠正。短码(N 小于 1000)尤其明显,因为短码的图结构可选择的余地本身就小。

解决:先检查 H 的围长。一个粗略的 4 环检查思路是枚举任意两个校验节点,看它们是否共用至少两个变量节点:

matlab % 粗略检查 4 环:两个校验节点共用的变量节点数 >= 2 for i1 = 1 : m for i2 = i1+1 : m common = find(H(i1,:) & H(i2,:)); if length(common) >= 2 fprintf('found 4-cycle between check %d and %d\n', i1, i2); end end end

这个 O(m²) 循环对中等码长可以接受,大码长要改成按稀疏索引做。确认有 4 环后,重新设计基矩阵里的循环移位值,或者直接换一套标准码表。5G NR 的 BG1/BG2 和 DVB-S2 的码表本身就是按大围长设计的,优先换标准码表,别自己调移位值。

5.2 编码一次要数秒,仿真根本跑不完

现象:码长 64800 时单帧编码耗时数秒,500 帧仿真直接要一天;N 降到 6480 时也只是勉强能跑。

原因:编码函数里出现了显式计算 G,或者对 H 做了 full(H) 后再求逆,算法复杂度 O(N²) 以上。这类代码在短码时看不出问题,码长一上去立刻暴露。

解决:在编码函数入口和出口各打一个 tic/toc,定位瓶颈在哪一行。如果看到 inv(G)、full(H) 这类调用,直接按第 4 章的思路换成 RU 编码,或者先用系统形式编码跑通中等码长。复杂度降下来之后,同一台机器上 N=64800 的编码耗时通常在毫秒级,不是秒级。这个差异不用仪器测,tic/toc 一打就清楚。

5.3 译码完全不收敛,BER 始终在 0.5 附近

现象:SNR 从 0 跑到 10 dB,BER 全是 0.5,译码器像没工作一样。这时候最容易怀疑译码函数写错,开始检查变量节点更新、校验节点更新,往往查半天没有结果。

原因:LLR 符号约定和 BPSK 调制映射反了。如果调制映射是 0 映射到 +1、1 映射到 -1,那么 LLR = ln(P(b=0)/P(b=1)) 在高斯信道下等于 2y/σ²,LLR 为正代表更可能是 0。一旦映射或符号搞反,译码器每一轮都把置信度加在错误的一侧,等于没译。

解决:写一个无噪声自测,编码后直接进译码器,0 噪声下必须 100% 正确:

matlab % 无噪声自测:LLR 用强置信度代替实际信道值 llr = 100 * (1 - 2*codeword); % ±100 表示确定无疑 est = ldpc_decode(H, llr, 20); assert(isequal(est(:), msg(:)), 'noise-free roundtrip failed');

如果断言失败,不要猜,去对照调制映射和 LLR 初始化公式,把符号统一掉。顺带把码字排列方式也确认一遍:如果你的编码器输出的是非系统码,但译码输出直接和 msg 比,那永远对不上。

5.4 浮点仿真正常,定点化后性能恶化超过 1 dB

现象:浮点最小和译码 BER 曲线很好,换到定点 C/C++ 或 HDL 模型后,同一 SNR 下 BER 差了一个数量级,甚至直接地板。

原因:通常是 LLR 量化位宽不足和截断方式太粗暴。译码过程中变量节点的 LLR 动态范围可以到 ±40,直接按 16 bit 幅值截断会把大 LLR 全部打到饱和值,精度全丢在增量上。

解决:LLR 存储用 Q4.11 或 Q5.10 格式,累加器用 Q8.8,每次更新做饱和截断而不是循环回绕。定点模拟里可以这样实现饱和:

matlab function y = sat_llr(x, bits) % 饱和截断到 [-2^(bits-1), 2^(bits-1)-1] thr = 2^(bits-1) - 1; y = min(max(x, -thr), thr); end

另外,最小和译码的归一化因子在浮点里取 0.75 附近通常最优,定点化时要重扫,α 在 {0.625, 0.75, 0.8125, 0.875} 各跑一版对比 BER,取拐点。如果最小和比浮点 BP 差 0.2 到 0.3 dB 是正常的,差超过 0.5 dB 就要回头检查实现。

5.5 仿真结果抖动大,相邻 SNR 点曲线不单调

现象:同一 SNR 跑两次,BER 分别是 2e-3 和 8e-4,曲线像锯齿。有人以为是随机种子问题,其实是统计量不够。

原因:帧数不够。高 SNR 下错误事件本来就稀疏,几百帧里只有个位数错误,统计方差自然大;也可能是误码统计时把编码后比特也算进去了,高 SNR 下校验位错误会放大 BER 数值。

解决:BER 小于 1e-3 时,错误帧数至少要攒到 20 到 50 再停,否则统计没有意义;只统计信息位。仿真脚本里可以加一个自适应终止条件:

matlab max_frame = 1000; target_err_cnt = 50; % 攒够 50 个错误比特就停 total_err = 0; frames_used = 0; for f = 1 : max_frame [~, err_cnt] = run_one_frame(H, sigma, max_iter); total_err = total_err + err_cnt; frames_used = f; if total_err >= target_err_cnt && f > 100 break; % 高 SNR 下不会无限跑下去 end end ber = total_err / (K * frames_used);

这套“自适应停帧”在高 SNR 点能省下大量无效仿真时间,也保证每个点的统计精度一致。

6. 验证 LDPC 编解码正确性:三个最小用例与蒙特卡洛提速技巧

6.1 三个最小验证用例:先证明链路对,再画曲线

我拿到任何 LDPC 代码,第一件事不是看解码器结构,而是按顺序跑三个最小用例。用例一是无噪声往返,编码后直接进译码器,用强置信 LLR 代替信道值,必须 100% 还原;用例二是单点 BER 量级检查,BPSK、1/2 码率、2 dB 时 BER 落在 1e-2 到 1e-1 区间,如果差一个数量级就说明某个参数换算出错;用例三是验证 H·c^T = 0,这一步能确认编码器输出确实满足奇偶校验约束,避免“编码器和译码器错在同一处”的幻觉。

这三个用例加起来运行时间不到几秒钟,却能把 90% 的接线错误挡在整条蒙特卡洛仿真之前。我曾经跳过用例二直接跑完整曲线,结果 2000 帧算完才发现 EsN0 换算少乘了码率,整条曲线全部作废。这个损失只能用血泪经验来形容。

6.2 蒙特卡洛提速的常见做法:并行帧与停帧统计

链路验证通过后,提速空间主要在三处:帧循环用 parfor 并行、LLR 初始化和统计操作全部向量化、最小和译码里只更新有变化的节点。parfor 是最直接的一步:

matlab % 并行帧循环:每帧独立,结果按帧存放 parfor f = 1 : nframes ber_loc(f) = run_one_frame(H, sigma, max_iter); end ber = sum(ber_loc) / (K * nframes);

注意 parfor 要求每帧完全独立,随机数生成要用可重现的流,否则换了并行池后结果对不上。矩阵维度不大时,parfor 开销可能反而更高,建议先用单核跑一小段测量,再决定要不要开并行。

我现在拿到任何 LDPC.rar 或类似代码包,第一步永远是解压、查函数调用关系、跑无噪声自测,这三件事做完才碰 SNR 曲线。这个顺序帮我避开了至少三次在错误配置上调参数的窘境。希望你照着这个路径走,能把时间花在译码性能调优上,而不是和接线错误搏斗。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进