如果你稍微留意过身边的通信设备,会发现一个很有意思的现象:Wi-Fi 5/6、5G 手机、卫星电视机顶盒、万兆以太网口、甚至你正在用的 SSD 主控,内部都在跑同一种纠错算法——LDPC(Low-Density Parity-Check,低密度奇偶校验)码。一个 1962 年就被提出的老码,为什么在最近十几年突然成了各种链路里近乎“默认选项”的存在?答案是一个让人无法拒绝的数字:LDPC 的性能可以逼近香农极限。换句话说,在同样的信噪比下,它能做到比其他经典方案更低的误码率,或者反过来,在同样的误码率要求下,用更少的发射功率就能通信。
这篇文章我会把 LDPC 从编码到译码的完整链路拆开讲清楚:校验矩阵和 Tanner 图到底在表达什么,编码为什么不能用最朴素的高斯消元硬算,译码算法中的 BP 和 Min-Sum 每一步在做什么,以及真正把它落地到硬件和仿真环境时,大家最容易踩的坑在哪里。适合刚接触信道编码的初学者,也适合已经在看 3GPP 协议或写浮点模型验证的人。我尽量少堆公式,但该有的推导一步都不会省。
1. 从“逼近香农极限”说起:LDPC 解决的真实问题
1.1 信道编码为什么重要,以及传统方案的瓶颈
任何一条物理链路,无论是空口辐射、铜线传输还是闪存颗粒读回,本质上都是往一个不完美的通道里写数据。噪声和干扰会让某些比特翻转,而信道编码做的事情,就是发送端在原始数据后面加一段结构化的冗余,接收端利用这段冗余把错误纠正回来。这个“冗余”加得越高效,能容忍的噪声就越大。
在 LDPC 火起来之前,业界用了很多年卷积码和它的级联方案。Turbo 码在上世纪九十年代一度非常亮眼,逼近香农极限的性能让它在 3G/4G 时代大放异彩。但 Turbo 有个天生缺陷:译码延迟很大,因为交织器会引入一个块级别的处理时延;同时在高吞吐场景下,它需要大量的迭代计算,功耗和面积都不好看。卷积码本身性能上限偏低,Viterbi 译码虽然稳定,但增益有限,级联 RS 码之后系统复杂度又上去了。
LDPC 的优势恰恰在于:它采用一种全然的并行迭代结构,计算密集型操作被拆成大量独立小任务,天然适合硬件并行做高吞吐。而且随着码长增加,它的性能可以非常接近 Shannon 界——在 AWGN 信道、长码块条件下,LDPC 甚至能做到离香农极限不到 0.1dB 的距离。
1.2 LDPC 从被遗忘到被重新发现的过程
1962 年 Gallager 在他的博士论文里提出了 LDPC 码,思路非常直接:如果校验矩阵足够稀疏,那么迭代译码的时候,每个校验方程之间几乎没有交叠干扰,错误信息可以被“逐个击破”。但那个年代没有足够强的芯片来处理迭代运算,这套思想很快就没人理了。
直到 1996 年,MacKay 和 Neal 重新挖掘出它,学界才发现它和 Turbo 码一样可以逼近极限,而且拥有更低的错误平层。再后来,准循环 LDPC(QC-LDPC)让编码器和译码器的电路实现变得可行,于是 DVB-S2 率先采用,IEEE 802.11n 也把它定为标准。到了 5G NR,数据信道直接以 LDPC 为核心编码,和极化码分工:控制信道用极化码,数据信道用 LDPC。这个格局背后不是偶然,而是 LDPC 在长码块、高吞吐、混合自动重传请求(HARQ)场景下综合胜出的结果。
2. 校验矩阵与 Tanner 图:LDPC 的骨架语言
2.1 校验矩阵 H 的稀疏性到底意味着什么
LDPC 是一种线性分组码。假设信息位长度是 K,编码后码字长度是 N,那么校验位长度 M = N - K。码字 c 必须满足:
H · c^T = 0
这里 H 是一个 M × N 的矩阵,元素只有 0 和 1。所谓“低密度”,指的就是 H 中 1 的数量非常少,每一行、每一列只有个位数的 1,其余全是 0。如果 H 是一个全稠密的矩阵,那它描述的就不是 LDPC,而是一般的线性码,译码复杂度会指数级上升。
为什么稀疏性这么重要?因为译码本质上是求解一组有约束的方程。如果每个方程里涉及的变量很少,那么每个方程单独看都很“简单”;合在一起迭代,信息能够在图结构中逐步传播。一旦矩阵密度变高,变量之间严重耦合,迭代很容易在一个错误的局部解上收敛。
工程上,稀疏性直接影响硬件存储。H 矩阵如果 10000 行 × 20000 列,全存下来是两亿个比特,但如果每行只有 6 个 1,那就只用存大约 12 万个索引,稀疏表达的成本完全不同。
2.2 Tanner 图与度分布,看码结构像看地图
H 矩阵可以画成一张二分图,叫 Tanner 图:左边一类节点叫变量节点,对应码字的 N 个比特;右边一类节点叫校验节点,对应 H 的 M 行方程。H 矩阵第 i 行第 j 列如果为 1,就在第 j 个变量节点和第 i 个校验节点之间连一条边。
一个变量节点连接的边数叫做它的“度”。校验节点也一样。所有变量节点的度构成的分布,直接决定了一个 LDPC 码能好到什么程度。比如经典的规则 LDPC 可以写成 (dv, dc),意思是每个变量节点度数为 dv,每个校验节点度数为 dc。dv=3、dc=6 是一个很经典的选择,码率大约 1 - 3/6 = 1/2。在 AWGN 信道下,规则码就能提供不错的性能。
但更强大的码往往是非规则的:不同的变量节点具有不同度数。为什么要这样?可以这样理解:度数高的变量节点和许多校验方程相连,它在迭代中能更快收集外部信息,相当于“更信任它”;度数低的节点虽然信息少,但不容易被噪声带偏。通过密度演进(Density Evolution)工具,可以优化出一个最优度分布,让迭代译码过程中错误概率下降最快。一般来说,变量节点度高的节点比例大一些,能显著提升低信噪比下的性能,但要注意避免过高,否则错误平层会变差。
Tanner 图的另一个重要概念,是所有环的长度里的最小值 girth。如果图中存在一个长度为 4 的环,意味着某两个变量节点和某两个校验节点相互连接,形成两条独立通路。迭代时,两股消息会在环内互相加强,形成一种“自我确认”效应,让错误的置信度也被放大,译码性能严重下滑。因此设计好的 LDPC 矩阵,通常要求 girth 至少到 6,更好的是 8。这也是为什么随机生成的稀疏矩阵不能直接入用的原因——你必须做环长检测。
2.3 QC-LDPC:从抽象矩阵到可实现的电路
直接用一个任意稀疏矩阵做工程,会遇到三个问题:第一,矩阵本身需要大量存储;第二,变量节点和校验节点的连接关系不规则,导致硬件布线极其复杂;第三,编码时如果矩阵没有结构,计算复杂度难以降低。QC-LDPC 的出现就是来解决这些问题的。
QC-LDPC 的核心思想是:把 H 矩阵分块组织,每个子块是 Z × Z 的循环移位单位阵,或者 Z × Z 的全零阵。一个循环移位单位阵可以通过把单位阵的每一行向右移动固定偏移量得到。这样一来,整个 H 矩阵只需要存储每个子块的偏移量即可,而且译码时对同一个 Z 组的校验方程可以并行计算,天然支持高并行度。
5G NR 里的 LDPC 就是 QC-LDPC。协议里定义了两套基图(Base Graph):BG1 用于大传输块和高码率场景,BG2 用于小传输块和低码率场景。基图是基础的较小矩阵,每个元素都代表一个 Z×Z 循环移位块,Z 叫做提升因子。发送端需要根据实际的传输块大小选择一个 Z,把基图“扩展”成真正使用的 H 矩阵。这个机制使得同一套硬件可以通过参数配置,适配从几十个字节到几千个字节的数据块。
3. 编码端:如何用线性复杂度完成纠错编码
3.1 最朴素的高斯消元编码,为什么在工程上走不通
给定校验矩阵 H,最直接的编码方法是:把 H 化成形如 [P | I] 的形式,然后得到生成矩阵 G = [I | P^T],编码时计算 c = u · G。因为 G 要满足 H · G^T = 0,系统位直接摆放原始信息比特,校验位通过生成矩阵计算。
听起来很简单,但代价非常明显:经过高斯消元之后,P 矩阵往往不再稀疏,它可能变成一个几乎全密的矩阵。如果信息位 K = 4096,那么 P 就是 4096 × 4096 的一个稠密矩阵,一次编码要做几千万次比特运算。对于高吞吐场景,这几乎等于直接把编码器推到功耗和面积的绝路。更糟糕的是,消元过程中 H 的稀疏结构被完全破坏,你无法用任何并行电路去加速。
所以,工程上做 LDPC 编码基本不会走这条路。真正有效的方法,是利用 H 本身的结构,或者干脆设计编码友好的 H。
3.2 Richardson-Urbanke 近似线性编码的直觉
Richardson 和 Urbanke 在 2001 年提出了一种经典方法,基本思路是:不把 H 消成完全系统形式,只把它消成一种“近似下三角”的形态,即左上角很大一块保持稀疏,只留右下角一个较小的区域需要稠密计算。
具体来说,把 H 重排列成如下分块形式:
H = | A B | | C D |
其中 A 是较大的稀疏方阵,对应的变量参与编码中的中间变量计算;B 和 C 保持稀疏;D 是较小的矩阵。编码时,信息位已知,先利用 A 的稀疏性快速解出一组中间变量,然后再通过 D 求解校验位。由于 D 的尺寸比较小,即使对它做稠密运算,总计算复杂度也只是近似线性的,通常可以做到 O(N) 或 O(N log N)。
这种方法的核心思想,是“把稠密的部分限制在一个尽可能小的区域里”。它不需要设计者重新构造 LDPC 码,只要现有的 H 矩阵结构具备满足要求的近似下三角形状,就可以用。虽然现在很多标准和自研码都直接采用更加结构化的 QC-LDPC 编码方案,但 RU 算法的思想仍然值得了解,因为当你拿到一个任意的稀疏 H 矩阵时,它是最通用的编码工具。
3.3 5G NR 基图与编码速率匹配的工程逻辑
5G NR 的 LDPC 编码并不需要做复杂的矩阵消元,因为协议定义的基图本身带有可用于编码的结构。BG1 基图尺寸是 46 行 × 68 列,BG2 是 42 行 × 52 列,其中信息列数分别对应不同的最大信息位长度。实际信息比特长度 K 不支持任意值时,协议采用填充比特(filler bit)补齐到 K 的整数倍对应的列数。
真正重要的是速率匹配机制。LDPC 编码之后产生一个系统位加校验位的序列,但发射机不一定全部发送。协议采用循环缓冲区,从某个起始位置开始,按照特定顺序连续取比特发射;重传时从缓冲区的另一个位置取更多比特。这种增量冗余的方式和 HARQ 天然契合:第一次发送尽量少、码率尽量高,如果解码失败,再发送更多校验比特,接收端把这些合并起来用更低码率译码。
编码器的硬件实现也很有意思:由于 QC-LDPC 每个 Z 块内部的校验位计算是并行的,编码器可以设计成多个 Z 通道同时运算,每个通道内部做移位异或。在 5G 的典型配置下,一个中等规格的 FPGA 或者 ASIC 就能做到数十 Gbps 的编码吞吐。
4. 译码算法:从置信传播到最小和
4.1 LLR 域的和积算法推导
LDPC 译码的主流算法是置信传播(Belief Propagation),也叫和积算法(Sum-Product Algorithm)。它的工作基础是:接收端从信道拿到每个比特的软信息,通常用对数似然比 LLR 表示:
L_i = log(P(b_i=0 | y_i) / P(b_i=1 | y_i))
在 BPSK 调制、AWGN 信道下,如果发送映射是 0 -> +1,1 -> -1,那么 L_i = 2 y_i / σ²。LLR 大于 0 说明该比特偏向 0,绝对值越大置信度越高。
迭代译码的过程,就是在 Tanner 图上不断地传递两种消息:
变量节点传给校验节点的消息,等于自己的信道初始 LLR 加上除了目标校验节点以外,所有其他相邻校验节点传回的外部信息之和。这个操作的直觉是:变量节点综合“信道证据”和“邻居的校验意见”,形成自己对这个比特的当前看法,但传给某个校验节点时,要刻意排除那个校验节点自己刚传来的信息,防止自我强化。
校验节点传给变量节点的消息,是所有相邻变量节点把自己的值作为独立观测时,校验方程能够推断出的信息。严格的计算公式为:
mc→v = 2 arctanh( Π_{v' ∈ N(c) \ {v}} tanh(m_{v'→c} / 2) )
这个公式看起来有点吓人,但它的含义其实非常优雅:把一个消息先映射到“概率空间”(通过 tanh),在这个空间里,多个独立证据合起来判断某个比特是否满足校验方程,相当于做乘法;再把乘法结果映射回 LLR 域(通过 arctanh)。
经过若干轮迭代,每个变量节点把信道信息和所有校验节点传回的信息加起来,得到后验 LLR,对它取符号就得到判决。为了保证收敛后的结果是合法码字,通常在每一轮迭代结束都要做一次硬判决,并计算校验子 H·c^T 是否全零。一旦校验通过,立即停止迭代,这能省掉大量无效计算。
4.2 Min-Sum 近似:为什么敢省掉 tanh 和 arctanh
和积算法的问题在于 tanh 和 arctanh 在硬件里非常昂贵,即使是软件仿真,每条边消息都算一遍也会拖慢速度。但看校验节点更新公式的结构,会发现一个重要事实:多个因子的乘积,数值大小实际上由绝对值最小的那个因子主导,其他因子的贡献主要体现在符号上。
把 tanh 和 arctanh 全部省掉,校验节点消息的幅值直接用相邻变量节点消息幅值的最小值来近似:
mc→v ≈ ( Π sign(m_{v'→c}) ) · min_{v' ≠ v} |m_{v'→c}|
这就是最小和算法(Min-Sum)。它的计算量比和积算法低一个数量级,没有乘法器、没有查表,只要做比较和异或。代价是性能会有损失,在规则码、典型配置下大约差 0.2 到 0.4 dB。不过工程上几乎不会直接使用朴素 Min-Sum,而是用两种改进方案:
归一化最小和(Normalized Min-Sum):把 Min-Sum 的输出消息乘一个小于 1 的归一化因子 α,典型值在 0.75 到 0.8 之间。这个因子补偿了用 min 近似替代精确乘积时带来的幅值高估。偏移最小和(Offset Min-Sum):把幅值减去一个固定偏移 β,小于 β 的消息直接置零,原理是压制那些置信度太低的“可疑消息”。
我在实际项目里测试过,归一化因子对性能非常敏感,选不好甚至会不如朴素 Min-Sum。推荐的做法是用 0.8 起步,然后以 0.05 的步进做完整链路仿真,找到在目标误块率处的最优值。
4.3 迭代调度策略:泛洪、分层与归一化修正
消息更新的顺序叫调度策略。最基础的是泛洪(Flooding)调度:每一轮迭代里,先并行更新所有变量节点消息,再并行更新所有校验节点消息。这种做法逻辑清晰、容易并行,多核处理器和 FPGA 上都能跑,但收敛速度一般,要达到目标误码率往往需要比较多的迭代次数。
分层调度(Layered Scheduling)是一个很实用的改进思路:把校验节点按某种顺序逐批更新,每更新完一批,立刻把它的影响反馈到变量节点,后续校验节点再更新时,就能用上更新的消息。这个“即时反馈”机制让信息在图网络中传播得更快。实测中,相同的迭代次数下,分层调度的收敛速度大约是泛洪调度的两倍;换句话说,原本需要 10 次迭代才能达到的性能,分层调度可能 5 到 6 次就达到了,这对降低译码延迟非常有价值。
但分层调度在硬件里有一个甜蜜的负担:它要求变量节点消息存储支持同一周期内多个不同地址的读写,否则就会出现内存冲突,反而拖慢时钟频率。常见的解决办法是把变量节点的消息存储拆成多个 bank,同时用基图的循环移位结构设计避免同一周期内访问同一个 bank。如果你的矩阵不是 QC-LDPC,分层的收益会被存储冲突吃完,这也是为什么标准化的 LDPC 几乎全是 QC-LDPC 的原因之一。
5. 工程落地中的经典坑:量化、短环与错误平层
5.1 定点量化对译码性能的影响
浮点仿真跑得很漂亮的 LDPC 译码器,一旦转成定点实现,性能往往掉得让人心疼。关键原因是消息在迭代中会不断更新、饱和、截断,任何一步的精度损失都可能被迭代放大。
我常用的设置是:信道初始 LLR 用 6 bit 表示,其中 1 bit 符号位、5 bit 幅值;迭代过程中校验节点消息用 5 bit 幅值,变量节点消息由于要累加多个外部信息,用 8 bit。饱和值不能拍脑袋定,如果初始 LLR 的幅值范围通常在 ±15,变量节点累加后可能到 ±63,所以至少需要使用 7 bit 幅值才不会让高置信度的正确消息被截断。
一个很常见的坑是信道 LLR 的缩放系数没有对。理论上 L_i = 2 y_i / σ²,但实际系统经过 AGC、信道估计之后,噪声方差估计不准的话,这个系数就偏了。实践中我通常先做一次噪声方差估计,然后对归一化系数做一个小范围扫描。你可能会觉得奇怪,为什么一个放大系数对迭代译码影响这么大?因为 Min-Sum 是基于消息幅值比较的,全局缩放不会改变比较结果,但和积算法里 tanh 和 arctanh 是非线性的,缩放偏差会扭曲置信度分布。
5.2 环路(cycle)与错误平层:一个实测案例
很多人在仿真中会发现一个现象:信噪比提高到一定程度后,误码率曲线不再陡峭下滑,而是出现一个平台,像是踩在了一个地板上,上不去。这就是所谓错误平层(error floor)。
我遇到过最典型的一次,是设计一个码率 0.89 的高码率 LDPC,浮点仿真中错误平层出现在误码率大约 1e-7 的位置,怎么加迭代次数都不下去。后来把 H 矩阵的四环分布画出来,发现问题集中在几个特定的短环组合上:某些变量节点度数只有 2,且它们参与的环长度只有 6,这些节点在迭代中很容易形成 trapping set——一组变量节点的硬判决卡在某个错误状态,即使反复迭代也无法逃离。
解决错误平层的方法有几个:第一是设计阶段避免低度数节点组成小环,特别是度数为 2 的节点,它们在迭代中消息来源少,容错能力差;第二是增加迭代次数上限,虽然不能从根源上消除平层,但能把平台位置压得更低;第三是级联一个外码,比如闪存控制器里普遍的做法是在 LDPC 外面再叠一个 BCH 码,用 BCH 去纠正 LDPC 残留的极少量错误。
另外,Min-Sum 算法的错误平层通常比和积算法更早出现,这是归一化因子不足和低幅值消息过早截断共同造成的。如果目标误码率在 1e-8 以下,建议在仿真阶段就把定点模型跑起来,别总拿浮点结果去估算。
5.3 提前停机准则与吞吐率平衡
迭代译码的功耗和吞吐,很大程度取决于“平均迭代次数”而不是“最大迭代次数”。如果每次都跑满最大迭代,白白浪费功耗;如果停机太早,性能又会下降。提前停机(early termination)是译码器设计的必修课。
最简单的准则是校验子检测:每轮迭代结束,裁决出硬判决比特,乘以 H 矩阵,如果校验子全零,就停止迭代。但在高信噪比区域,往往很早就能通过校验子检测;而在低信噪比区域,大部分数据包都会一直跑到最大迭代次数,无法通过提前停机省时间。
另一个工程上实用的准则是“相邻两轮迭代的硬判决结果不再变化”:如果输入消息还在更新,但判决输出已经稳定了,说明继续迭代大概率也没用。我测试过,这个准则能减少约 10% 到 15% 的平均迭代次数,且不影响性能,延迟基本无损。但要注意,实现它会增加额外的硬判决比较逻辑,你需要权衡存储和比较开销是否值得。
如果你做的是 5G NR 场景,还要考虑 HARQ 的影响:初传失败后,重传到达时,接收端通常会把本次的信道 LLR 和上一次译码过程中保留的变量节点外部信息合并,再用新的迭代起始点重新译码。这个过程里,提前停机准则同样适用,但要注意第一次迭代时旧消息不能直接当作信道信息处理,否则会造成过度置信。
6. 我的实现与测试经验总结
6.1 仿真工具链与浮点到定点的标准流程
我的 LDPC 验证流程一直遵循一套从“纯浮点模型”到“定点模型”再到“RTL/FPGA 原型验证”的三步走策略。千万不要跳过浮点模型直接开始写硬件代码,否则一个算法选择错误,返工代价极高。
第一步,用 MATLAB 或 Python 搭建浮点链路。Python 里我习惯用 ldpc 的自研脚本配合 numpy 做矩阵操作,码字用随机数生成,信道用 AWGN,译码分别实现和积和最小和两套,方便性能对比。建议把环长检测、度分布统计、错误平层扫描这些脚本也一次性写好,后面会反复用到。
第二步,把译码器改成定点模型。定点模型必须和浮点模型逐步对比:先固定信道 LLR 的量化参数,观察变量节点消息的分布范围,再确定内部消息位宽。通常要对比三轮:短码长、中码长、长码长,分别观察误码率和误块率曲线。我给自己定过一条规矩:定点模型相对于浮点模型的性能损失,控制在 0.1 dB 以内才允许进入硬件设计。
第三步,在 FPGA 上做原型验证。个人经验是:不要一开始就做全并行,先用一个“时间分片+少量并行单元”的架构跑通功能,再逐步提升并行度。全并行结构的数据对齐、存储冲突、时序收敛,任何一个问题都够纠结两周。
6.2 关于性能对照和参数调优的个人建议
如果你拿到一个现成的 LDPC 代码,想把它用到自己的系统里,一定不要只看官方文档里的那组仿真曲线。不同信噪比定义、不同调制方式、不同码率、不同块长,都会大幅影响可比性。我建议你在自己的链路里跑一遍参考码和候选码,并记录几组固定对比点:最高码率、最低码率、中间码率;长块和短块;BPSK 和 16QAM。
参数调优时,我最开始会固定迭代次数为 20,然后单独扫归一化因子 α,找到误块率最低的点;接着扫描偏移量 β;最后把迭代次数从 4 到 30 挨个跑一遍,看有没有性能饱和。还有一个容易被忽略的点是码字映射关系:发送端 0/1 映射成 BPSK 符号时,到底是 0 映射 +1 还是 -1,会直接影响接收端 LLR 的正负号约定。这个看似愚蠢的细节,真的会让你的译码器在第一次上链路时输出全零反而导致校验失败,别问我怎么知道的。
另外,如果你需要在一些特殊场景下自定义 LDPC,可以尝试用 PEG(Progressive Edge-Growth)算法构造稀疏矩阵。PEG 的核心是逐条添加边,并且每次都选择能最大化当前局部最小环长度的变量节点和校验节点对。用 PEG 构造出来后,再用 QC 结构进行循环移位扩展,实测能稳定做出 girth 为 8 的中短码。
说了这么多,LDPC 最迷人的地方其实不是那堆公式,而是它把两个看起来矛盾的目标同时做到了极致:结构足够简单,简单到每一条边都可以独立计算;结构又足够复杂,复杂到百万级码长的误码纠错能逼近理论极限。做编译码器和写普通软件完全是两种心态,你需要面对的不只是逻辑正确,还有资源约束、时序边界和那一点点永远差着的信噪比。希望这篇文章能帮你把这条路上的几个大坑提前填平,让你把精力花在真正有意思的优化上。