Turbo码迭代译码原理与MATLAB实现:从Log-MAP到外信息交换

发布时间:2026/10/26 9:58:09
Turbo码迭代译码原理与MATLAB实现:从Log-MAP到外信息交换

简介:这份资源面向通信工程、电子信息类专业学生及信道编码方向的研究者,提供Turbo编码与译码技术的Matlab仿真实现,帮助读者在仿真环境中理解并行级联卷积码的编码结构、交织器作用以及迭代译码流程。资源包共1个文件,为docx文档,大小约17KB,内含可直接运行的Matlab脚本与参数说明,涵盖两个码率1/2的RSC编码器、尾部比特处理、随机交织、+1/-1调制与AWGN信道模拟,并支持Log-MAP、MAX-Log-MAP及SOVA等译码算法的选择与对比。脚本通过交互方式设定帧长、生成多项式、打孔方式、迭代次数与停止准则,便于评估不同信噪比下的误帧率性能。已有177人学习,适合作为信道编码课程实验、毕业设计或科研仿真的入门参考,帮助读者快速搭建Turbo编译码链路并观察迭代增益与算法复杂度之间的权衡。

1. 迭代译码:为什么两个“不完美”的译码器能逼近香农极限

Turbo 码最反直觉的地方在于:它的两个分量译码器单独拿出来看,性能都平平无奇,甚至因为递归卷积结构还带一点“自找麻烦”的毛病。可一旦让它们通过交织器交换软信息、来回迭代几轮,误码率曲线就会在某个信噪比附近陡然下坠,出现所谓的“瀑布区”。这份 MATLAB 实现把整条链路摊开在你面前:随机信息比特、两个码率 1/2 的 RSC 编码器、随机交织器、AWGN 信道、Log-MAP 或 SOVA 译码,参数全部可交互配置。搞懂它,你就搞懂了“软信息”到底是怎么在两次译码之间流动的,也就能理解为什么工程上宁愿多花几倍算力做迭代,也不肯退回单次译码。下面从编码器的状态机讲起,一路落到logmapo里那行max加修正项的写法。

2. 编码端:两个 RSC 编码器为什么一个收尾、一个不收尾

2.1 递归系统卷积码的状态转移

先看编码器本身。代码里g = [1 1 1; 1 0 1],[n,K] = size(g)得到n=2, K=3,于是m = K-1 = 2,状态数nstates = 2^m = 4。这两行生成多项式对应一个码率 1/2、约束长度 3 的 RSC 编码器:第一行1 1 1是反馈多项式,第二行1 0 1是前馈(校验)多项式。所谓“递归”,就是反馈支路把寄存器里的历史状态又送回输入端参与异或,这让编码器变成一个带记忆的有限状态机,而不是简单的移位寄存器。

系统码的含义是:输出的一路直接就是输入信息比特本身,另一路才是校验比特。这一点在译码端非常关键——L_e = L_all - 2*rec_s(1,1:2:2*L_total) - L_a这行里减掉的2*rec_s(...)正是系统比特带来的信道先验,必须扣除才能得到“纯外信息”。如果不理解系统码结构,这行减法看起来就像玄学。

递归结构带来的直接后果是:编码器的终止不能靠简单补零。因为反馈存在,输入 0 并不等于状态归零。所以第一个编码器要在信息比特后附加m个尾部比特,把状态“拽”回全零,这就是代码注释里说的 “First encoder is terminated with tails bits”。而第二个编码器接收的是交织后的比特,它没有自己的尾比特,处于“开环”状态——注释里 “second encoder is left open without tail bits of itself” 说的就是这件事。为什么允许它不收尾?因为收尾要额外传比特、降低码率,而 Turbo 码的迭代译码对第二个编码器的终态并不敏感,这点损失换来的码率收益更划算。

2.2 交织器:把突发错误打散成独立错误

[temp, alpha] = sort(rand(1,L_total))这一行生成了每帧随机的交织图案。rand产生随机数,sort返回排序后的值和原始索引,alpha就是那个索引排列。编码时信息比特按alpha重排后送进第二个编码器,译码时再用L_a(alpha) = L_e把外信息按同样图案搬回去。随机交织的好处是:如果第一个编码器产生的某个错误在信道上连续出现,经过交织后在第二个编码器看来就变成了分散的、近似独立的错误,两个分量码的纠错能力才能真正叠加。

注意这里是“每帧重新生成”交织器,而不是固定一个。仿真里这么做是为了让性能统计不依赖于某个特定交织图案的偶然性,属于蒙特卡洛仿真的常规做法。工程实现中通常用固定交织器并把它写进标准,因为收发两端必须严格同步同一张表。

2.3 打孔与码率的关系

puncture参数控制是否删余:rate = 1/(2+puncture)。不删余时码率 1/2,删余时 1/3。这里有个容易看反的地方——代码里puncture==0打印的是 “Punctured, code rate = 1/2”,puncture==1打印 “Unpunctured, code rate = 1/3”。从公式看,puncture=0时分母是 2,puncture=1时分母是 3,所以变量名和直觉相反:值为 0 反而对应“已删余”的 1/2 码率。读这份代码时如果按字面理解puncture,很容易在配置实验时把码率搞反,这是血泪经验级别的细节。

编码输出经encoderm打包后,直接叠加高斯噪声:r = en_output + sigma*randn(...),其中sigma = 1/sqrt(2*rate*en),en = 10^(EbN0db/10)。这里的L_c = 4*a*en*rate是信道可靠性度量,a=1对应 AWGN。rec_s = 0.5*L_c*yk把接收样点换算成对数似然比尺度,供译码器使用。参数之间的换算关系必须自洽,否则仿真出来的 BER 曲线会整体平移,看起来像算法出了问题,实际是信噪比标定错了。

3. Log-MAP 的核心:max 加一个修正项到底修了什么

3.1 从 ln(e^x+e^y) 到雅可比对数

Log-MAP 的全部精髓可以压缩成一个恒等式:ln(e^x + e^y) = max(x,y) + ln(1 + e^(-|x-y|))。代码注释里原样写了这一行。为什么需要它?因为 MAP 译码要在网格图上做前向、后向递推,每一步都要对多条分支的概率求和,而在对数域里“求和”就变成了上面这个运算。直接算ln(e^x+e^y)会溢出或下溢,取max又丢掉了较小一项的贡献。这个恒等式把求和拆成“取大值 + 一个只依赖差值的修正项”,修正项可以查表,既稳又准。

如果直接把修正项扔掉,只保留max(x,y),就退化成 MAX-Log-MAP。代码里dec_alg选 0 走logmapo(完整 Log-MAP),选 1 走sova0(SOVA,另一种软输出算法)。MAX-Log-MAP 的复杂度低很多,但外信息偏“硬”,迭代增益会打折扣,通常差零点几 dB。这份实现选择保留完整修正,是为了让读者看到性能上界。

3.2 前向-后向递推与分支度量

logmapo的输入是rec_s(该分量译码器看到的接收样点)、g(生成多项式)和L_a(来自另一个译码器的先验外信息)。内部做三件事:算分支度量、前向递推alpha、后向递推beta,最后合成每个比特的对数后验比。分支度量里同时包含系统比特项和校验比特项,系统比特项会和L_a相加——这就是“先验信息进入译码”的入口。

递推过程中每一步都要在进入同一状态的两条分支间做上面那个max + 修正运算。状态数只有 4,所以每比特的计算量是常数级,但整帧L_total个比特、每帧niter次迭代、两个分量译码器轮流跑,乘起来就是仿真慢的原因。代码里那句 “Please be patient. Wait a while to get the result.” 不是客套,是实话。

3.3 外信息的提取与交换

译码器输出的L_all是后验对数似然比,但它不能直接送给另一个译码器,否则会把系统比特的信道信息和先验信息重复计算两遍,导致“过度自信”。所以代码做了减法:

% 第一个分量译码器:扣除系统比特信道先验和本次输入先验,得到纯外信息 L_e = L_all - 2 * rec_s(1, 1:2:2 * L_total) - L_a; % 按交织图案把外信息交给第二个译码器作为先验 L_a = L_e(alpha);

2*rec_s(1,1:2:2*L_total)取的是系统比特位置上的信道观测(偶数索引步进 2),乘 2 是对数似然比尺度换算。减掉L_a是因为它已经作为先验参与过本次计算。剩下的L_e才是“本次译码新学到的、关于该比特的信息”,把它交织后送给第二个译码器,第二个译码器再重复同样的扣除动作,把新外信息送回第一个。如此往返niter次,每次都在前一轮的基础上增加一点独立证据,置信度逐步累积,这就是迭代译码“越算越准”的机制来源。

最后一轮结束后,xhat(alpha) = (sign(L_all)+1)/2用后验比的符号做硬判决。注意这里用的是L_all而不是L_e,因为判决要的是完整后验,不是外信息。

4. 仿真循环与停止准则:帧错误计数为什么比误比特率更靠谱

4.1 外层信噪比扫描与内层帧循环

结构是两层循环:外层for nEN = 1:length(EbN0db)遍历每个信噪比点,内层while nferr(nEN,niter) < ferrlim持续产生新帧,直到累计帧错误数达到ferrlim(默认 15)。每帧重新生成随机信息比特x = round(rand(1,L_total-m))、重新生成交织图案、重新过信道。errs和nferr按迭代次数分别记录,这样一次仿真就能画出不同迭代次数下的性能对比。

为什么用帧错误数而不是固定帧数做停止条件?因为在低信噪比下误码率很高,固定帧数很快就能统计到足够错误;而在高信噪比下误码稀疏,固定帧数可能一个错误都碰不到,统计结果毫无意义。以“攒够 15 个帧错误”为准则,能保证每个信噪比点的统计置信度大致相当,这是蒙特卡洛仿真的标准做法。代价是高信噪比点要跑很久,这也是仿真时间随EbN0db增大而暴涨的原因。

4.2 首帧可视化在验证什么

if nframe == 1那段画了三张图:原始比特、编码后比特、译码后比特。它的价值不在美观,而在快速自检——如果译码输出和原始比特肉眼可见地大面积不一致,说明参数配置或索引映射有错,不必等整轮仿真跑完才发现。我一般会先跑一个高信噪比、少迭代的配置看这三张图是否对齐,确认链路通了再去做性能扫描。这个习惯能省下大量“跑了一小时发现全错”的时间。

4.3 迭代次数与复杂度的权衡

niter默认 5。迭代次数增加,外信息累积更充分,BER 下降,但每增加一轮就多两次全帧 Log-MAP 递推,计算量线性增长。实际系统里迭代次数往往卡在 4 到 8 之间,再往上增益递减明显。代码把errs和nferr都存成1:niter的向量,正是为了让读者直观看到“第 1 次迭代到第 5 次迭代各差多少”,从而自己判断拐点在哪。

5. 避坑与排查:这份实现里最容易翻车的几处

5.1 交织索引方向搞反,BER 曲线整体抬高

现象:译码结果比预期差很多,但高信噪比下又能部分恢复。原因:L_a(alpha) = L_e和L_e(alpha)这类赋值依赖alpha的方向约定,编码端用alpha重排、译码端必须用同一方向还原,一旦把alpha和它的逆排列混用,外信息就被送到错误位置。解决:固定一帧、关掉噪声,检查编码前后和译码前后的比特是否逐位对齐,确认索引映射自洽后再开噪声。

5.2 信噪比标定与码率不匹配,曲线平移

现象:换puncture设置后,同一EbN0db下性能突变,且变化幅度不合常理。原因:L_c = 4*a*en*rate和sigma = 1/sqrt(2*rate*en)都含rate,码率变了但EbN0的定义基准没跟着核对,导致实际信噪比与标称值不符。解决:每次改码率后,用无噪声或极高信噪比跑一遍确认误码为零,再逐步降信噪比观察瀑布区位置是否符合理论预期。

5.3 尾比特长度与帧长参数混淆

现象:L_total设成信息比特数,结果编码器状态收不回来,高信噪比下仍有残留错误。原因:L_total是“信息 + 尾比特”的总长,信息比特实际是L_total - m,代码里x = round(rand(1,L_total-m))已经体现了这一点。若把L_total当纯信息长度用,尾比特就侵占了信息位。解决:明确L_total语义,需要 N 个信息比特时把L_total设为N+m。

5.4 停止准则设太小导致统计不可信

现象:高信噪比点跑得飞快,但 BER 曲线抖动大、不可复现。原因:ferrlim设得过小(比如 1 或 2),帧错误样本太少,统计方差大。解决:保持ferrlim在 10 以上,或对高信噪比点额外增加最小帧数下限,牺牲时间换置信度。

5.5 把 SOVA 当 Log-MAP 用,误判算法退化

现象:选dec_alg=1后性能明显变差,怀疑代码有 bug。原因:SOVA 本身就是次优的软输出算法,外信息精度低于 Log-MAP,性能差距是算法固有属性而非实现错误。解决:对比实验时固定其他参数只改dec_alg,把差距归因到算法而非配置,避免在错误方向上排查。

6. 设计取舍:为什么这份实现坚持“不量化、不近似”

这份代码在注释里明确写了 “Log-MAP algorithm without quantization or approximation is used”。这是一个刻意的取舍:它牺牲了运行速度,换来了一个可以作为“性能基准”的参考实现。工程里的 Turbo 译码器几乎不会这么写——定点化、查表近似、MAX-Log-MAP 甚至更激进的简化都会被用上,因为硬件资源和时延是硬约束。但做算法研究时,你需要一个“理论上限”来对照,否则无法判断某个近似到底损失了多少。

取舍维度本实现的选择工程常见做法代价与收益
修正函数完整 ln(1+e^-x-y)
数值表示浮点、不量化定点、位宽受限无量化误差,不反映硬件真实表现
交织器每帧随机固定、写入标准统计无偏,但不可直接移植
停止准则帧错误数帧数或时延上限统计可信,高信噪比耗时

我一般会这么理解这份代码的定位:它是一把尺子,不是一把锤子。你要验证自己的定点近似方案好不好,就拿它的 BER 曲线当参照;你要评估某个交织器设计,也拿它当基线。真正上手做系统时,反而要主动做减法——先把修正项换成查表,再把浮点换成定点,观察曲线偏移多少,偏移可接受就说明简化成立。

验证方法上,有个习惯值得坚持:任何改动之后,先在EbN0db取一个较高值、niter取较小值的配置下跑首帧三图对齐检查,确认链路没断,再放开做全扫描。这个顺序能把“配置错误”和“算法性能”两类问题分开,避免在错误的假设上浪费整晚的仿真时间。从那以后我每次动这类迭代译码的仿真脚本,都强制先走一遍无噪声对齐再开信道,希望帮到你。

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