☰
Turbo码原理到仿真实战:交织器与迭代译码全解析
2026/10/16 21:52:43 网站建设 项目流程

Turbo码这名字听起来就带劲儿,但很多人一看到交织器、软判决、迭代译码这些词就头皮发麻。实际上,只要把“外信息迭代”这条主线拎清楚,Turbo码的编译码过程并不比打仗时排兵布阵难多少。这篇文章就从一个普通开发者的视角,把Turbo码从编码结构、解码原理到仿真踩坑完整捋一遍,给那些想自己动手实现一套Turbo编译码Demo的人做个参考。

如果你正在搞信道编码仿真、物理层算法验证,或者纯粹对3G/4G里那个“扛把子”纠错方案好奇,这篇文章很适合你。我会尽量用工程语言来讲,原理部分给足公式推导思路,实操部分直接给可执行的步骤和坑点,争取你看完能自己上手搓一套出来。

1. 先搞明白Turbo码到底在解决什么问题

1.1 信道编码的使命

任何数字通信系统,数据在信道里传输时都会遇到各种干扰。无线信道尤其残酷,噪声、衰落、多径效应会把比特打得面目全非。信道编码做的事情,本质上就是在发送端给原始数据添加冗余,让接收端即使收到了部分错误比特,也能通过冗余信息把原始数据还原出来。

衡量编码性能最经典的工具是误码率曲线。在同样信噪比下,误码率越低,说明编码增益越高。Turbo码之所以能成为3G/4G乃至后续移动通信系统的关键纠错方案,就是因为它在中等码率下逼近了香农极限,而且译码复杂度相对可控。换句话说,同样的发射功率和带宽,用Turbo码能让通信链路在更差的环境下维持连接。

很多人第一次听说Turbo码,注意力全被“迭代译码”吸引。其实迭代只是Turbo码的外在表现形式,它真正的核心在于“软信息”的生成和交换。所谓软信息,指的是不仅仅是判断当前比特是0还是1,还要给出这个判断的可靠程度。传统硬判决只有0/1两个值,软判决则用一个浮点数表示对数似然比,正负号代表极性,绝对值大小代表置信度。Turbo码的厉害之处就在于把两个简单编码器通过交织器耦合起来,让它们互相给对方提供“外信息”,像两个人合作解谜,各自看到不同角度的线索,来回交换后把答案拼得越来越完整。

1.2 为什么香农极限那么难啃

香农告诉我们,在给定信噪比下存在一个信道容量上限,只要码率低于这个上限,就一定存在某种编码方案能让误码率无限逼近于零。问题是香农没告诉我们怎么构造这种编码,他只是证明了存在性。上世纪后半叶,工程师们设计了一堆分组码、卷积码,但离香农极限总有几dB的差距。

差距来自哪里?关键在于长码块下的性能瓶颈。要逼近香农极限,需要码字足够长,并且随机性好到像是“随机码”。但纯随机码的译码复杂度是天文数字,根本不可能工程实现。经典卷积码靠维特比算法解决了译码复杂度问题,但它的约束长度一旦加大,复杂度也随之暴涨,工程上只能维持在一个有限水平,性能因此受限。

Turbo码的巧妙之处在于,它用两个或更多个中等复杂度的卷积码并行级联,中间插一个伪随机交织器,用一种“伪随机长码”的方式逼近香农极限。每个分量编码器单独译码时复杂度不高,但通过迭代交换外信息,整体表现像一个超长约束长度的卷积码。这个思路在当时是革命性的,直接开启了迭代信号处理这个方向。

1.3 Turbo码的破局思路:打个比方

用一个日常场景来理解Turbo码的工作原理。假设你丢了一段文字,想通过两个朋友帮忙恢复。第一个朋友对上下文很熟悉,第二个朋友对用词习惯很熟悉。你把同一段错漏百出的文字用两种不同顺序分别交给他们。第一个朋友根据上下文规律给出“这句话哪些词应该是什么”的推测,第二个朋友根据用词习惯给出另一套推测。两人第一次给出的推测可能都不太靠谱,但只要把对方的推测当作额外线索重新审一遍,反复几次,两个人都能修正自己的判断,最终把文字恢复得八九不离十。

Turbo码就是这样一个“两个专家互相纠错”的架构。两个分量解码器分别处理一套系统比特和校验比特,中间的交织器让两套序列的相互位置关系重新洗牌,这样在一个解码器里容易连片的错误,在另一个解码器里被打散,从而使两个解码器能交替修正对方的“盲区”。盲区打散之后,迭代修正的效果非常显著。

2. 编码器:两个“直觉”拼出来的强力纠错

2.1 分量编码器:递归系统卷积码

Turbo码的每个分量编码器通常采用递归系统卷积码(RSC)。普通卷积码是前馈结构,输出除了和当前输入有关,还和之前几个时刻的输入有关。RSC的特殊之处在于它把输出反馈到输入端,形成一个带反馈的移位寄存器结构。反馈结构让编码器具备了“无限冲激响应”的特性,仅用较少的寄存器就能产生重量较重的码字,这对迭代译码特别有利,因为重量重的码字意味着更大的最小距离,能够抵抗低信噪比下的错误平层。

常见分量码是约束长度3或4的RSC,生成多项式在不同标准里略有差异。最经典的一对生成多项式是八进制表示下的13和15,也就是二进制形式[1 1 0 1]和[1 1 1 1]分别对应前馈和反馈多项式。这类分量码编译码复杂度低,但性能已经很好。约束长度再加大,性能提升有限,迭代译码的收敛速度反而变慢,工程上通常不会选太长。

编码时每个时刻输入一个信息比特,RSC会输出一个系统比特(就是原始信息比特本身)和一个校验比特。多个RSC产生的校验比特可以来自不同相位或不同交织顺序的信息序列,这样每个分量编码器对同一份信息的“看法”不同,解码器之间互相提供的新信息才足够新鲜。

2.2 交织器:不是乱排,是排兵布阵

交织器是Turbo码的另一半灵魂。它的作用是把信息序列的顺序打乱,生成一份“二手信息”给第二个分量编码器。千万别小看这个看似简单的打乱操作,交织器的设计直接决定了Turbo码的纠错性能和错误平层。

如果交织器设计得不好,两个分量码的最小距离会被“低重量码字”拖垮。所谓低重量码字,指的是输入信息序列中只有少量比特为1,但经过两个RSC编码后产生的校验序列重量都很低。这种码字最容易在低信噪比下被噪声整个打翻,而不会被迭代译码纠正回来。好的交织器会让这类低重量输入模式在两个分量编码器中至少有一个产生较高重量的校验序列。

工程上常用的交织器有伪随机交织器、S随机交织器和二次幂交织器。伪随机交织器简单,但有时候会产生“坏点”,即某些距离很近的位置被交织到距离也很近的位置。S随机交织器专门限制“最小交织距离”,确保相距T以内的输入位置交织后相距至少S,这样能显著改善低重量码字的分布。不过S随机交织器需要搜索生成,在长块下计算量也不小,但一次生成后可以固定下来,仿真时反复用就够了。

实操中,选择交织器时要关注一个指标:交织增益。交织深度越大,低信噪比下的性能越好,但同时译码等待时间边长,内存占用也变大。3G/4G系统里Turbo码通常处理几KB甚至更大的传输块,交织器长度跟着传输块走,所以物理层实现时交织器大多用硬件查表方式完成,因为实时生成大规模随机交织表太浪费逻辑资源。

2.3 打孔与码率调整

Turbo码最基本的编码输出是三路:系统比特、第一个分量编码器的校验比特、第二个分量编码器的校验比特。三路全输出的话,码率大约是1/3。实际系统经常需要更高的码率,比如1/2或2/3,这时候就靠打孔来实现。

打孔就是有选择性地丢弃一部分校验比特。由于系统比特关信息完整性,通常不被打孔,打孔只发生在两路校验比特上。打孔图案需要精心设计,尽量保证两个分量码的校验比特被均匀地丢弃一部分,否则某个分量码失去的校验信息太多,迭代译码时这一路外信息会变得很弱,整体性能会明显下滑。

举个例子,码率从1/3提高到1/2,需要丢弃三分之一的校验比特。最简单的方案是两路校验比特各自丢弃一部分,交错打孔。仿真时记得打孔位置必须在收发端完全一致,否则解码器根本没法把接收序列和编码输出对应上,错误率会直接崩到不可用。

2.4 一个完整编码流程的伪代码

下面给出一个最简Turbo编码流程的伪代码,方便对照理解整体结构:

def turbo_encode(u, interleaver, rsc1, rsc2): # u: 信息比特序列,长度为K # interleaver: 长度为K的交织映射表 # rsc1, rsc2: 两个分量编码器实例 p1 = rsc1.encode(u) # 第一路校验 u_inter = interleaver.permute(u) # 交织后的信息 p2 = rsc2.encode(u_inter) # 第二路校验 return u, p1, p2 # 系统比特 + 两路校验

实际系统中,编码输出还需要经过信道映射、调制,接收端则把解调出来的软信息送进解码器。这里我刻意省略了码率匹配和终止处理。真实标准里还会在编码器末尾加一些特殊处理来“收尾”编码器状态,让尾部状态已知化,这样解码性能会更好。

3. 解码器:迭代才是Turbo的灵魂

3.1 软信息与外信息的循环

Turbo解码器和编码器在结构上是镜像对称的。两个分量解码器,一个处理原始序,一个处理交织后的序。关键点在于,每个分量解码器不直接输出判决结果,而是输出对数似然比(LLR)。LLR的定义是:

对于某个比特b,LLR = ln( P(b=1 | 接收数据) / P(b=0 | 接收数据) )。

LLR绝对值越大,代表对应极性越可靠。解码时,后验LLR可以分解为三部分:信道值、先验值和外信息。信道值来自接收信号本身,先验值来自另一个分量解码器给出的信息,外信息才是这个分量解码器“新鲜产生”的增量信息。

迭代过程就是把一个解码器输出的外信息经交织/解交织后送到另一个解码器作为先验信息。每轮迭代,两个解码器交替利用对方提供的线索更新自己对每个比特的把握。刚开始迭代时增益提升很快,后续增益递减,最终趋于收敛。实际仿真里,迭代到8次到10次之后,误码率改善就很小了,继续迭代只增加延迟和计算量。

3.2 MAP / BCJR算法在干什么

分量解码器最经典的核心算法是BCJR算法,也叫最大后验概率算法,或者前向后向算法。它和维特比算法最大的区别在于,BCJR给出的不是一条最优路径,而是每个时刻每个状态的概率,最后再汇总每个比特的后验概率。

BCJR算法分三步。第一步:从前往后做前向递推,计算每个时刻到达各状态的概率;第二步:从后往前做后向递推,计算每时刻从各状态出发的概率;第三步:利用前后向概率计算每个比特的后验概率。整个过程涉及大量指数运算和乘法,但通过概率的归一化,数值稳定性可以维持得不错。

实现时还要注意分支度量。分支度量表示“当前时刻从某个状态转移到另一个状态,再结合接收符号来计算这一转移的似然”。它需要把系统比特、校验比特与接收符号一一对应起来,并把先验信息也乘进去。可以说,分支度量计算是BCJR里最繁复的地方,也是最容易写错的地方。

3.3 Log-MAP和Max-Log-MAP的简化

直接实现BCJR,要算一堆指数和对数,在仿真环境下问题不大,但在硬件或实时系统里太费资源。因此工程上几乎不用原始BCJR,而是用Log-MAP,把所有概率放到对数域计算,把乘法变成加法,把指数运算用max*操作来近似。

Log-MAP的最大好处是动态范围可控,不容易出现浮点下溢。为了进一步降低复杂度,还可以用Max-Log-MAP,把max*操作里的修正项直接丢掉。这样实现最简单,但会比Log-MAP损失大约0.2到0.5dB的性能。某些仿真项目为了快速验证系统级性能,会直接上Max-Log-MAP,毕竟代码量少、调试方便,性能差距在系统预研阶段完全能接受。

我在仿真里常做一个折中:先写Max-Log-MAP把链路调通,再在关键节点换成Log-MAP看性能差异,最后再评估是否需要补偿修正项。这样既能快速定位编码解码逻辑的错误,又能在最终结果上获得接近理论极限的性能。

3.4 迭代停止条件

迭代到底做几次?常见做法有固定次数迭代和自适应停止两种。固定次数实现简单,比如8次或10次直接掐断。自适应停止可以检测所有比特的硬判决结果是否在连续两次迭代之间发生变化,如果不变就提前停止,能省算力,但需要额外存储和判断逻辑,硬件实现上会增加复杂度。

做仿真时,我通常会先跑固定次数,比如10次,把误码率曲线画出来。如果发现性能不够,再逐个增加迭代次数看增益。不要一开始就无限迭代,因为Turbo码在低信噪比下也会出现“迭代振荡”,增加迭代次数不一定带来持续增益,在错误平层区域甚至可能不收敛。

4. 手搓实操:从零搭一个Turbo编译码Demo

4.1 仿真环境与参数设置

仿真语言我建议用Python做功能验证,重点在逻辑正确,不在性能。Python浮点运算方便,画图也快,适合第一版验证。等算法验证完,再考虑用C或硬件描述语言做效率优化。

仿真参数可以这样设:码块长度K取1024比特,分量码用约束长度4(寄存器数为3)的RSC,生成多项式用八进制15,反馈多项式用13。码率先用1/3,这样先避开打孔的干扰,把核心逻辑跑对,再逐步加打孔测高码率。

信道用AWGN,调制用BPSK,这样能把Turbo编解码本身的效果看得清清楚楚。仿真信噪比范围可以从0.5dB到3dB,每0.5dB一档,每档至少跑几千个码块才能统计出可靠误码率。第一次跑的时候可能比较慢,但耐心点,这正是形成性能曲线的基础。

4.2 编码端实现要点

编码端相对简单,但有一个地方特别容易踩坑:输入序列的长度必须和交织器长度严格一致。交织器表建议独立生成,并单独写一个函数做校验,检查交织后的长度和内容是否仍是原始集合的置换。万一交织器映射表写错,两个编码器拿到的就不是同一份信息,解码性能会直接崩掉。

编码的时候,每个分量RSC要维护好自身的状态寄存器。最稳妥的做法是把RSC封装成一个小类,包含状态转移函数和输出生成函数。这么做的好处是调试时能单独验证分量码的编码是否正确,还能顺便用维特比算法快速检验编码器输出和输入之间的关系,避免后期解码跑出奇怪结果时找不到根源。

还有一点值得注意:如果要做1/2码率或更高码率,建议先把1/3码率的编译码链路彻底跑通,确认误码率曲线符合预期后,再引入打孔。否则打孔逻辑和Turbo本身的bug混在一起,排查起来非常痛苦。

4.3 解码端实现要点

解码端最核心的数据结构是前向状态度量和后向状态度量两个数组。当K=1024时,状态数16,每个时刻两个数组各16个浮点数,内存开销并不大。但实现时要注意是“按帧存储”还是“按块滑动”的方式。简单仿真可以直接把整帧的前向度量存下来,然后做后向递推,最后生成每比特的软信息。

后递推时需要从帧尾开始。这时编码器尾状态的处理就很重要。如果编码器没有做尾归零,那帧尾的状态是未知的,后向递推初值就得设为所有状态等概率。实际标准里通常会在帧尾加几个拖尾比特来把状态归零,这样解码器可以从已知零状态开始反向跑,性能会好一点。工程上称为“收尾”,是最容易忽略的小细节。

分支度量的计算我觉得是整段代码里最容易出bug的地方。每个时刻要同时使用当前系统比特的接收软值和当前校验比特的接收软值,还要加上交织关系带来的耦合。很多人第一次写的时候,会把系统软值、校验软值和先验信息分别算好,再组合成转移分支度量。这个思路是对的,但必须注意所有值都要用对数域符号,先验信息是本轮迭代前面那个分量解码器输出后经过交织/解交织得到,千万不要拿混了。

4.4 跑通之后看什么:误码率曲线

链路跑起来之后,第一件要做的事不是看绝对误码率多低,而是看曲线趋势。横轴信噪比升高时,误码率应平滑下降,如果出现“地板效应”,很可能有低重量码字问题,这个锅多半要打在交织器设计上。

还要对比不同迭代次数下的曲线。比如跑1次迭代、4次迭代、8次迭代,画在同一张图上。横轴信噪比,纵轴误码率(对数坐标)。通常8次迭代的曲线会比1次低不少,但4次和8次之间差距明显收窄。如果多次迭代增益非常有限,那就要检查是不是外信息计算错了,或者交织器前后映射不对。

代码里建议加一个“逐比特软信息打印”的调试模式,输入一个极低噪声的接收序列,打印前几个比特的LLR变化过程。正常情况应该是系统比特的LLR符号和信息比特完全一致,且在迭代中越来越自信。这种逐层打印虽然啰嗦,但定位bug非常有效。

5. 踩坑记录与疑难排查

5.1 交织器设计失误导致性能崩塌

我踩过最惨的坑是交织器和解交织方向搞反了。仿真表面看起来链路能跑,但误码率比理论预期差好几个数量级。错误原因是在第二个分量解码器的迭代软信息累计上,先验信息需要经过“交织”再喂给第一个解码器,但我在某处用了解交织,方向一颠倒,迭代信息变成垃圾,性能直接崩溃。

排查方法是单独写一个交织一致性测试函数。输入一组顺序数组,交织后再解交织,必须得到完全相同的原始数组。并且还要在编码端验证,交织前的序列经过两路编码后,接收端按交织关系能精确对应上。这类方向问题靠肉眼不好找,一定要靠自检函数。

另一个常见问题是交织器长度不够随机,导致低重量码字比较多,误码率曲线出现明显平层。这时候可以换成S随机交织器,并统计最小交织距离。把最小交织距离从1提升到至少10,常能显著改善错误平层问题。

5.2 定点化与数值溢出问题

用浮点数跑仿真很顺利,但一旦想把算法往定点化方向改,麻烦就来了。BCJR里的前向后向度量在长帧下数值范围很大,直接做定点会导致溢出。解决办法是每步递推结束后对度量做归一化,把整体减去公共最大值。这只是平移操作,不会改变状态之间的相对关系,但能把数值控制在一个合理范围内。

Log-MAP里的max*修正项也需要考虑精度。如果定点位数不够,修正项干脆可以不要,直接使用Max-Log-MAP。硬件上常用这个方案,因为实现代价低,而且配合后期其他补偿手段,性能差距可以缩小到可接受范围。

如果做浮点仿真却出现NaN或Inf,建议在每轮迭代后检查LLR数值范围。一旦发现异常,多半是某个分支度量算错导致概率归零后取对数变成负无穷,或者先验信息被错误放大。逐步打印是解决这类问题的最快路径。

5.3 迭代不收敛或振荡

有些情况下,迭代次数增加后误码率反而变差。这通常是因为两个分量解码器之间软信息过度自信,导致早期决策被错误放大。解决办法是可以对先验信息乘一个小于1的阻尼系数,抑制迭代振荡。阻尼系数一般取0.5到0.9之间,需要做小规模参数扫描来确定。

迭代振荡的另一个来源是打孔失衡。如果一路校验被打掉太多,这路的解码器几乎得不到有效的校验信息,只能靠先验,结果每次迭代都只是“随声附和”,不会提供新信息。发生这种情况,要重新设计打孔图案,让两路校验被丢弃的比例尽量均衡。

还有一个容易被忽视的问题:交织器深度太小,比如只有几十比特时,Turbo码的迭代增益会非常有限。很多人用短块做验证,发现性能不佳就怀疑算法错了,其实只是交织增益还没显现。建议验证算法时至少用512比特以上的块长。

5.4 常见问题速查表

现象可能原因解决办法
误码率极高,接近无编码效果交织映射方向反了加交织/解交织一致性自检
迭代几轮后性能无变化外信息计算错误或没接上逐步打印软信息走向
高信噪比出现错误平层交织器不良,低重量码字多换S随机交织器,提高最小交织距离
定点化后性能崩塌前向后向度量溢出每步做减最大值的归一化
高码率时性能异常差打孔图案不均衡重新设计打孔方案
迭代增加反而变差过置信振荡对先验信息加阻尼系数

6. 工具箱与后续扩展

6.1 从模拟到硬件实现

理论验证通过之后,如果想做硬件实现,一个很自然的演进路径是:先把Python定点化代码改成C模型,再用硬件描述语言做RTL设计。Turbo码核心的迭代解码器是个资源大户,尤其是交织器查表和度量值存储。硬件设计里不会像仿真那样把整帧度量全存下来,而是采用滑窗方式,一边算前向一边吐软信息,或者用乒乓缓存做双窗口处理,以面积换延迟,以吞吐为目标。

硬件上最关键的设计约束是迭代速度和存储带宽。Turbo解码的一个迭代需要两个分量解码器各跑一遍前向后向,意味着每比特都要被多次访问。如果不做并行化,吞吐量很难上去。因此很多芯片方案里会放多个分量解码器并行工作,再把迭代次数控制在合理范围。

从工程成本上看,Turbo码在5G时代已经不是唯一的主角,低密度奇偶校验码逐渐成为长码块的首选。但Turbo码在中等码率和中短码块下依然有自身的价值,而且理解Turbo迭代思想,对你后续理解LDPC的迭代译码也有很大帮助。两者的“消息传递”思路非常相似,学会了Turbo,再上手LDPC会顺畅很多。

6.2 实用工具与建议

仿真时不需要一上来就写几万行代码。先用Python的numpy把BCJR算法的数组操作向量化,跑一个千比特级别的码块,速度完全够用。如果还想更快,可以用C扩展配合固定点定点仿真。

调试时我建议把每个分量解码器单独“黑盒”测试。比如只送一个分量码的校验序列,先不解交织,先确认单独解码能正常纠正简单错码,再接入Turbo迭代循环。这种分层调试比直接跑整个系统高效太多了。

用来画误码率曲线时,注意仿真点数要够多。低误码率下,如果只跑几千个码块,置信区间会很宽,画出来的曲线抖动很大。通常每个信噪比点至少要积累足够数量的错误帧,或者跑满码块数上限,比如一万帧。具体上限可以根据仿真时间调整,但一定要记录每个点的统计精度,否则后面看性能时容易误判。

最后再小小提醒一件事:Turbo码虽然性能好,但解码延迟和复杂度都不低。如果你在做系统设计,一定要在链路预算里把Turbo解码带来的处理时延考虑进去,尤其是在实时性敏感的协议栈或控制信道里,有时需要权衡码块大小和迭代次数。

我自己的体会是,Turbo码是那种“看十篇原理不如亲手跑一遍”的算法。只要你把1/3码率、BPSK、AWGN信道下的小系统跑通,再去看那些正式标准里的各种增强手段,比如打孔、交织优化、迭代停止、硬件滑窗,都会觉得顺理成章。纸上得来终觉浅,放下对数学符号的恐惧,先搭个最简链路,一切就都简单起来了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询