Turbo码原理到仿真实战:交织器与迭代译码全解析
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的定义是对于某个比特bLLR ln( P(b1 | 接收数据) / P(b0 | 接收数据) )。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编译码Demo4.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 解码端实现要点解码端最核心的数据结构是前向状态度量和后向状态度量两个数组。当K1024时状态数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信道下的小系统跑通再去看那些正式标准里的各种增强手段比如打孔、交织优化、迭代停止、硬件滑窗都会觉得顺理成章。纸上得来终觉浅放下对数学符号的恐惧先搭个最简链路一切就都简单起来了。