做了这么多年通信物理层,要说近十年编码领域最“稳”的选择,LDPC(低密度奇偶校验码,Low-Density Parity-Check)绝对算一个。从DVB-S2到Wi-Fi 5/6,再到5G NR的eMBB控制信道和数据信道,LDPC几乎成了无处不在的标准配置。很多人最开始接触LDPC都是抱着“这玩意儿性能接近香农极限”的期待来的,但真正上手做编解码器设计和仿真时,会被矩阵构造、译码迭代、定点量化这些东西反复折磨。这篇文章聊聊我做完一整套LDPC编解码器设计验证后的一些真实体会,包括编码器怎么从生成矩阵切换到校验矩阵方案、译码算法为什么最后选了归一化最小和而非完整置信传播、仿真链路怎么搭才能得到稳的误码率曲线,以及几个特别容易踩进去的坑。
如果你是通信专业的学生、刚接手LDPC相关模块的FPGA工程师,或者正在做5G/DVB系统的物理层方案选型,这篇文章应该能帮你少走不少弯路。
1. LDPC为什么能霸榜:稀疏校验矩阵的纠错逻辑
1.1 从Gallager到5G:一类被埋没二十年的好码
LDPC码最早是Gallager在1962年提出的,名字里的“低密度”指的就是校验矩阵里“1”的比例非常低。比如一个码长10000的规则LDPC码,行重3、列重6的话,校验矩阵里非零元素占比不超过千分之一。这种极度稀疏的结构,让它在长码长下能逼近香农限,又让迭代译码成为可能。但那个年代硬件条件完全撑不起这种大矩阵的迭代计算,所以沉寂了很久,直到九十年代末MacKay等人重新发现它,才在Turbo码的围剿里杀出一条路。
和Turbo码对比来看,LDPC的核心优势是译码并行度极高。Turbo的BCJR译码本质上是一条链式的软输入软输出过程,子译码器之间的顺序依赖很强;而LDPC的Tanner图里,同一层校验节点之间互不依赖,天然适合并行展开。这一点在FPGA上尤其珍贵,我见过同样码长下LDPC译码吞吐做到Turbo的三倍以上,并且逻辑资源还更省。
现代标准里用的LDPC基本都是结构化的QC-LDPC(准循环LDPC),比如5G NR里的基图BG1和BG2。QC-LDPC的校验矩阵由许多循环移位的单位阵或零矩阵拼接而成,只需要存储基矩阵和移位值,就能完整描述一个数千甚至数万比特的码,硬件实现时地址生成和存储冲突规避都容易处理得多。
1.2 接近香农极限的量化印象:误码曲线会告诉你
很多人问LDPC到底比其它码好多少,最直接的办法是看AWGN信道下BPSK调制的误码率曲线。以码率1/2为例,BPSK无编码时大约需要9.6dB的Eb/N0才能达到10^-5误码率;Turbo码在码长几千时能做到离香农限0.5dB以内;而结构良好的LDPC码在码长10000以上时,距离香农限0.3dB左右是完全可以实现的。香农限在哪里?BPSK(或QPSK)在AWGN下码率1/2的香农极限大约是0dB的Eb/N0,所以0.3dB就意味着一根直接逼近理论边界的曲线。
这个性能换算到系统里,就是实打实的链路预算节省。比如我曾经做过一个卫星通信仿真,把RS+卷积码级联方案换成LDPC方案,在同样误码率目标下,接收灵敏度提升了接近2dB,对发射功率和天线口径的影响非常大。这也是为什么DVB-S2会把LDPC作为核心FEC,5G NR直接把LDPC定为数据信道唯一编码方案。
1.3 长码与中短码的行列重设计,直接决定错误平层
LDPC不是随便生成一个稀疏矩阵就能用的,行重和列重决定了性能上限。规则LDPC(所有行重相同、所有列重相同)在无限码长下表现很好,但不规则LDPC在有限码长下往往更强,尤其是把列重为2的变量节点比例控制好后,可以在错误平层区域表现更好。
5G NR基图的设计就很有代表性:信息位部分列重较大(有的列重5到6),校验位部分列重较小(常为2或3),这样既保证了信息位的译码可靠性,又让整体矩阵保持足够稀疏。自己做设计时,如果只是教学验证,PEG(Progressive Edge Growth)算法构造出来的随机LDPC就够用;如果要贴近工程落地,建议直接按5G NR的QC-LDPC基图来搞,资料全、问题验证充分,后续做硬件也有参考。
2. 编码器结构设计的核心思路:不要碰G矩阵
2.1 经典生成矩阵方案的存储灾难
教科书上讲线性分组码,都是给一个生成矩阵G,然后c = u·G得到码字。对LDPC来说,这个路子理论上没问题,工程上却是灾难。LDPC的H矩阵是稀疏的,但通过高斯消元把H化成[P | I]形式后得到的G矩阵几乎一定是稠密的——每一行都有大量非零列。码长64800的DVB-S2 LDPC,稠密G矩阵意味着每次编码要做数以亿计的模二乘加运算,显然不适合硬件实现。
所以,凡是工程上可落地的LDPC编码器,都不会显式存储完整G矩阵,而是直接利用H的稀疏结构进行编码。
2.2 近似下三角编码(Richardson-Urbanke算法)是怎么省资源的
RU算法的核心思想是把H矩阵重排成近似下三角的分块形式:
H = [ A B T ] [ C D E ]
其中T是一个下三角方阵,E很窄。只做行交换和列交换,不改矩阵的代数性质,所以编码结果等价。然后,编码过程分成两步:
- 先利用稀疏矩阵的稀疏性求出特殊校验部分;
- 再利用T的下三角结构做回代,求出剩余校验比特。
这样做的好处是乘法和加法复杂度跟码长基本是线性关系,码长上万时也能控制在可接受的乘法器与LUT开销内。QC-LDPC结构下,矩阵的分块操作天然对齐到循环块上,优化后完全是“块级运算”,非常适合FPGA上的并行乘加阵列。
我在实际设计中选择的路线是:直接从5G NR基图MBMS或DVB-S2标准拿到矩阵,先做重排,再用C语言做整数级浮点模拟验证RU分解无误,最后才映射到硬件。重排这一步非常关键,标准给出的矩阵本身可能不是接近下三角的,不重排直接编码,稀疏性优势完全发挥不出来。
2.3 累加器结构:QC-LDPC编码的工程实现捷径
如果是基于QC-LDPC设计编码器,还有一条更工程化的路:很多标准矩阵的校验位部分都自带双对角或类双对角结构,编码时校验位可以像累加器一样逐块递推出来。5G NR的基图就是这种设计,编码时不需要完整的RU分解,只用把信息位按照基图里的行做异或累加,再通过已知校验块递推,就能一步步得到整个码字。
这种结构在硬件上极其友好,我当时的实现大概是这样:
- 信息位按基图块送入环形移位寄存器,完成各循环块与信息位的乘加;
- 校验位模块用一个累加器链,前一个校验块的结果参与下一个校验块的计算;
- 整个编码过程只需要一个主时钟周期级的流水设计,吞吐量做大几十Gbps没问题。
所以我的建议是:自研LDPC编码器,先看标准矩阵有没有双对角校验结构;没有的话再上RU分解,不要在稠密G矩阵上浪费时间。
3. 译码算法选型:从和积到最小和的那一步不是拍脑袋
3.1 对数域置信传播:完整贝叶斯推理的数学骨架
LDPC译码最经典的算法是置信传播(Belief Propagation),也叫和积算法。它做的事情,本质是在Tanner图上迭代传播“某个比特为0或1”的似然信息。为了方便硬件实现,所有消息都用对数似然比(LLR)表示,初始化时把信道输出y变成:
L_ch = 2y / σ²
σ²是噪声方差,对应BPSK调制下AWGN信道的对数似然比。
每次迭代分两组更新:
- 变量节点更新:一个变量节点的输出LLR,等于信道LLR加上除目标边之外所有相邻校验节点传来的LLR之和;
- 校验节点更新:校验节点的输出LLR等于所有相邻变量节点消息的某种非线性函数组合,核心公式是:
r = 2·artanh(∏ tanh(v_i/2))
这个tanh乘积是非线性的,数学上很漂亮,工程上非常难办。FPGA上做tanh和artanh的查找表,哪怕位宽只有6比特,也要消耗不少LUT和BRAM,而且关键路径长,时钟频率提不高。
3.2 最小和算法:一种“有度量的简化”
硬件工程师会怎么做?直接对校验节点更新公式做近似。因为tanh函数在正负区间单调,乘积的符号可以直接变成所有输入符号的异或,而幅度的乘积可以用最小绝对值来逼近,原因是当输入消息绝对值较大时,tanh接近±1,乘积幅度主要由绝对值最小的那个消息主导。这就得到了最小和(Min-Sum, MS)算法:
r_j = (∏ sign(v_i)) · min_i |v_i|
也就是说,校验节点不再做任何非线性函数运算,只比较绝对值大小、异或符号,硬件实现非常简单,只需要比较器和异或门。代价是性能损失,一般比完整BP差0.2到0.4dB,具体取决于码长和迭代次数。
为了把损失压回去,工程上常用归一化最小和(NMS),把校验节点的输出乘上一个小于1的修正因子,比如0.75,能把这0.2dB左右的损失找回大半。另一种是偏移最小和(OMS),在幅度上减去一个偏移量再截断到非负。两者我都实测过:NMS在5G NR基图BG2上表现更稳定,OMS在某些码率下的错误平层略微好一点。
3.3 分层译码:每次迭代都前进一步
普通BP迭代是“全场同时更新”,校验节点的新消息必须等到下一轮迭代才被变量节点使用。分层译码的思路不一样,它把校验矩阵按行分成若干子块,每处理完一个子块,更新的LLR立刻参与后续子块的计算。这样单轮迭代内的信息流通速度更快,收敛速度大约能翻一倍。
也就是说,同样的误码率目标,分层译码可能只需要普通BP一半的迭代次数。比如标准BP跑到10次收敛的,分层NMS大概5到6次就能到同等水平。硬件上的代价是:需要维护一个“后验LLR”存储体,每个子块处理时读写这个存储体更新外部信息,存在存储冲突的风险,尤其当子块之间的列有重合时,处理顺序要仔细设计。
仿真阶段,我强烈建议先跑通不分层的NMS,再上分层,因为分层的定点行为和非分层不完全一样,容易毛躁时根本分不清是算法问题还是架构问题。
4. 仿真平台的搭建:从浮点参考模型到定点逼近
4.1 先搭一个“闭眼可信”的浮点参考链路
LDPC仿真链路怎么搭最稳?我的顺序是:AWGN信道模型、BPSK调制、LDPC编解码、误码率统计,一条线跑通,不做任何并行优化,先把浮点参考曲线拿到。
具体说,Eb/N0和噪声方差的关系别搞错。BPSK下Es = Eb,R = K/N,那么:
σ² = 1 / (2·R·10^(Eb/N0/10))
很多人在低码率仿真中发现性能曲线不对,多半是这里换算错了。信道LLR初始化时,如果做BPSK硬判决符号±1,那么LLR = 2y/σ²,这个公式必须和上面的σ²配套使用。
仿真时记录BER和FER。FER比BER更敏感,因为一帧出错往往拖累多个比特,而LDPC译码器性能评估的最终指标大多是FER。稍微漏掉故障帧,帧误码率统计就不科学。
4.2 定点化位宽怎么选才不掉性能
浮点模型跑通以后,下一步就是定点化。这个过程的目标不是“完全复现浮点”,而是“定点损失控制在0.1dB以内”。
LDPC译码的关键定点参数有三个:
- 信道LLR位宽:一般5到6比特就够,因为初始化LLR的动态范围主要由SNR决定,不需要太宽;
- 内部消息位宽:变量节点出入消息和校验节点出入消息,通常用5到7比特,符号位加幅值位;
- 归一化最小和的修正因子:定点下建议不要用浮点小数,而是用移位近似,比如0.75可以用右移1位加右移2位实现。
我当时的做法是把浮点曲线和不同位宽定点曲线叠在一块儿看,选一个位宽组合,让高SNR端和低SNR端都不掉链子。实测下来,信道LLR 6比特、内部消息7比特、NMS因子0.75定点化,在码率1/2码长1944的802.11n LDPC上,与浮点BP相比损失在0.05dB左右。
注意,定点化不是越宽越好。位宽增加导致LUT翻倍,速度下降,而性能收益可能只有0.01dB,完全没有性价比。
4.3 误码率统计的帧数统计逻辑:赌场法则
蒙特卡洛仿真里最容易犯的低级错误,是统计帧数太少。做BER曲线时,每个SNR点至少要积累到30到100个错误事件,否则误码率估计的置信度太差,尤其是曲线低端,比如想要宣称BER=10^-6,如果只观察到两三个错误就停,那这个点根本站不住脚。
以码长1944、码率1/2为例,FER=10^-3意味着平均每1000帧错1帧,要积累到100个错误帧,需要仿真10万帧,也就是约1亿比特以上。如果每帧译码还要迭代10次,这个计算量不小。
所以我的习惯做法是:
- 先扫描大SNR范围找大概的“瀑布区”位置,每点跑很少帧数,比如1000帧;
- 锁定瀑布区后再加密SNR点,每点跑10万帧以上;
- 低SNR区可以粗一点,高SNR区为了确认错误平层必须把帧数拉满。
4.4 仿真加速手段:别只会开并行
很多人一觉得仿真慢就开Matlab并行池,其实更好的方式有以下几种:
- 用C或C++重写第一版定点模型,速度能比Matlab快十倍以上;
- 用固定点数对应多个SNR点同时跑,因为编码和信道噪声独立生成,比较省时间;
- 高SNR下用“重要采样”或“停止准则”做判定,如果译码器在最大迭代之前已经收敛,就提前跳出,省一半时间。
我后续的定点模型是用C写的,编译一次跑一晚上,能把靠近错误平层的长时间行为看得比较透。
5. 踩过的坑与排查链:环、平层和时序违例
5.1 基图里存在girth=4的环:为什么收敛会卡死
校验矩阵的Tanner图不能有太短的环,girth=4表示存在长度为4的小环,两个校验节点之间连着两个变量节点。这种结构会导致迭代过程中消息在环内反复自我强化,严重时译码器陷入虚假的“伪收敛”,误码率曲线出现明显的地板效应。
检查方法很简单:遍历所有校验矩阵的行对,看两行之间是否有超过1个共同的“1”位置。有的话,这两个行和对应两列就形成一个girth=4的环。设计矩阵时一旦发现girth=4,必须通过调整循环移位参数或置换列顺序来消除。PEG算法构造的矩阵天然能控制girth,这也是为什么工程上都优先用构造好的标准矩阵而非随便写个随机H。
5.2 错误平层为什么会高:列重的小尾巴
错误平层(error floor)是LDPC最磨人的问题。表面上看,高SNR段的BER/FER曲线应该一路往下掉,但实际总会出现一段趋于平缓,往往是码字中有少数“脆弱比特”在迭代中始终无法纠正。
这类脆弱比特基本都落在列重特别小的变量节点上。列重2的变量节点如果参与了一个或者多个坏环,就很容易出现在错误平层里。排查方法是:抓出所有错误帧,看未收敛的比特位置是否集中出现在某些固定的列位置;如果是,回到基图设计,把那些列的列重加上1,或者调整它们在基图中的连接关系。
5G NR基图设计时也处理过类似问题,BG1的校验位列重设为2,同时在交织和打孔阶段做了充分的保护,所以成品标准不需要你自己去调。但如果是自研矩阵,一定要在高SNR下做足统计,不要因为低SNR段曲线漂亮就收手。
5.3 定点迭代次数和时序违例的纠缠
FPGA上做LDPC译码,最大的难题往往是吞吐与时序的矛盾。迭代次数越多,误码性能越好,但延迟和功耗越高。时序违例通常出在两层:
- 循环移位网络的组合逻辑太多,导致关键路径过长;
- 存储体读写冲突,导致流水必须停顿,综合频率上不去。
我的排查经验是先把迭代次数砍到最小(比如4次),先让时序跑干净,再逐步加迭代次数并观察Fmax变化。最终选型往往是在性能和时序之间取一个平衡,比如迭代6次,Fmax反而比迭代8次时高20%,而FER差距不到0.1dB,那就坚决选6次。
另外,printf式的调试在FPGA上没用,建议在仿真里保存每一轮迭代的校验子(syndrome)数值,如果校验子归零但是后续又跳出,说明存储冲突或消息更新顺序出错了。校验子是LDPC译码器的“健康指示灯”,HW调试时直接把校验子的落点引到逻辑分析仪上观察,定位问题效率高很多。
5.4 数据速率估算与实际吞吐的差距
设计译码器时,常犯的错误是拿“时钟×并行度”去算最大吞吐,忽略分层译码时等待存储读写和外部迭代次数带来的折损。实际吞吐量应该用公式估算:
有效吞吐 = 码长 × 时钟频率 × 并行度 / (迭代次数 × 每次迭代耗费时钟数)
比如1944码长,时钟250MHz,并行度48(对应QC-LDPC的循环块大小),迭代6次,每次迭代20个时钟,算出来有效吞吐只有大约970Mbps。
这种估算意义在于提前识别方案差距。如果你目标是10Gbps,你会发现要么加大并行度到96或192,要么降低迭代次数,要么优化每次迭代的时钟数,而不是盲目拉高时钟频率。
我自己最终的设计是用并行度192、迭代6次,才在250MHz下勉强够到7Gbps左右,再想上就得考虑多译码核并行了。
6. 扩展到5G NR实际应用时要注意的“最后一公里”
6.1 速率匹配与HARQ对译码器的隐性需求
LDPC码在标准里往往不是“裸”编码,上游还有速率匹配和混合自动重传请求(HARQ)。5G NR的数据信道每次发送的码字可能是母码的截短或打孔版本,这会让译码器的初始LLR在某些位置变成0(对应未发送比特)。如果译码器不做特殊处理,这些0值LLR会严重干扰校验节点更新,导致首传性能显著劣化。
解决办法是输入缓冲时对未发送位置填一个“最大不确定性”的LLR值(通常接近0但不完全等于0),并在译码过程中把这些位置的可信度压低。仿真阶段,我建议把打的孔和截短比特都建模出来,不要只用理想完整码字跑性能验收。
6.2 需要特别注意信道LLR饱和
信道LLR初始化时,如果y刚好处于噪声较大的位置,LLR可能达到很大。无限制增大LLR数值,定点模型里就会溢出,性能会崩。硬件实现时通常会在信道LLR入口加一个饱和处理,超过最大可表示范围直接钳位。
仿真里也应该加入饱和模型,因为浮点模型里LLR可能达到几十甚至上百,而定点模型的LLR范围可能就是[-63, 63],这个差异如果不加处理,直接套用浮点性能目标,定点验证必然失败。
6.3 验证自己译码器的一个歪招儿:注入校验子
做译码器验证时,除了跑完整的编码-信道-译码链路,还可以用一个快速自检方法:直接构造一个已知正确的码字,然后人为翻转其中几个比特传入译码器,检查译码器能否正确纠正回来。这个测试虽然不如全链路仿真全面,但能快速暴露译码器内部状态更新的错误,尤其是校验节点和变量节点之间的消息方向搞反这种低级bug。
我在联调阶段经常用这种方法做回归,每次改完定点参数,先跑几十个注入错误样本,再跑全链路统计曲线。比一上来就跑百万帧出错老半天才发现问题要高效太多。
7. 最后的工程习惯建议
做LDPC编解码器设计和仿真,最大的风险往往不在算法本身,而在浮点模型、定点模型和硬件实现三者之间的“翻译失真”。我个人的习惯是:先维护一份可读性优先的浮点参考资料,再写一份与硬件一一对应的定点参考,最后才动RTL。每完成一步,都强制做对数差对比,宁可仿真跑慢一点,也绝不让错误积累到最后一步。
另外一个建议是,仿真平台的噪声种子管理要规范。可复现性在调试时太重要了,不固定随机种子,你很难判断一个性能变化到底是算法改动带来的还是噪声样本不同导致的。我所有仿真代码都会预留种子参数,算法对比时锁定同一组噪声。
LDPC这个领域,入门容易精通难。矩阵构造、编码技巧、译码算法、定点优化、系统联调,每一层都有大量细节。希望这些经验和思路能帮正在折腾LDPC的同学节省点时间。真踩到坑的时候,记得回到校验矩阵和校验子这两个最基础的工具上去找答案,它们往往比复杂的高级手段更可靠。