☰
Turbo码MATLAB实现:迭代译码与外信息交换全解析
2026/10/15 8:00:24 网站建设 项目流程

简介:面向通信系统与信道编码方向的研究人员和学生,本资源提供了Turbo编码译码技术在Matlab环境下的完整仿真实现过程,覆盖并行级联卷积码编码、随机交织、AWGN信道传输以及Log-MAP/MAX-Log-MAP迭代译码等核心环节。压缩包内仅含1个docx文档,大小约17KB,内容精炼,包含带详细注释的Matlab代码与参数配置说明,便于直接阅读和移植使用。已有176人学习下载,常被用于无线/卫星通信课程设计及编码性能对比实验。文档还保留了完整的交互式仿真流程,用户可自行选择译码算法(Log-MAP或SOVA)、设定帧大小、生成多项式、是否打孔、迭代次数及停止准则帧错误数,并观察不同Eb/N0下的译码结果。借助该资源,可快速搭建Turbo编码译码验证平台,深入理解迭代译码原理和算法复杂度差异,为后续通信系统设计优化提供参考。

1. turbo编码译码在MATLAB里实现,最难的不是编码器,而是让两条译码路径把外信息接上

turbo编码译码在MATLAB中的实现,很多人在编码器上只花一小时,却在译码器上磨一整天。原因是用在低信噪比下逼近香农限的可靠性传输上,turbo码的增益主要靠迭代译码时两个分量译码器交换外信息累积出来,一旦交织映射方向接反、外信息符号写错或LLR的初始化不对,性能甚至比不编码还差。本文给出能直接运行的RSC编码器、Log-MAP译码器和完整BER仿真链路,同时把参数为什么这样设、常见翻车点在哪里讲透。适合做信道编码课程设计、无线物理层仿真验证,或想彻底搞懂迭代译码内部机制的开发者。本文不涉及某份特定源码包的解读,全部按多数从业者采用的经典方案自行实现。

2. 编码器建模:从RSC分量码到交织器再到删余拼接

2.1 RSC分量码:反馈多项式决定编码器会不会“循环”

turbo码编码器的主流结构是两个递归系统卷积码分量码,中间夹一个交织器。系统码指输出比特中直接包含信息比特,递归指编码器内部存在反馈,反馈多项式决定了编码器的记忆深度和距离特性。为什么不用普通卷积码而强调递归系统卷积码?普通卷积码的脉冲响应有限长,产生不了turbo码需要的低重量码字的“随机化”扩散效果;递归结构的反馈会让单个错误比特的影响持续传播,配合交织器才能把误码打散。约束长度3的(2,1,3)RSC是最常用的入门分量码,反馈多项式取[1 1 1](八进制7),前向多项式取[1 0 1](八进制5),状态数只有4个,编译码和调试都足够简单。

我建议不要直接用通信工具箱的poly2trellis,而是自己写一个RSC编码函数。工具箱生成的trellis结构虽然规范,但输出比特顺序与多项式排列的关系对初学者不透明,一旦后面要手写Log-MAP译码器,你还得回头去查状态转移表,不如从一开始就让编码器结构和状态转移逻辑完全暴露在代码里,后面译码器直接复用同一套状态定义。下面是自己写RSC编码器的核心代码:

function [parity, final_state] = rsc_encode(u, fb, ff, init_state) % u: 行向量,信息比特,取值为0/1 % fb: 反馈多项式系数,例如 [1 1 1] 表示 u + s1 + s2 % ff: 前向多项式系数,例如 [1 0 1] 表示 u + s2 % init_state: 初始寄存器状态 [s1 s2],默认 [0 0] if nargin < 4 init_state = [0 0]; end u = u(:)'; N = length(u); s1 = init_state(1); s2 = init_state(2); parity = zeros(1, N); for k = 1:N fb_in = mod(u(k) + fb(2)*s1 + fb(3)*s2, 2); parity(k) = mod(ff(1)*u(k) + ff(2)*s1 + ff(3)*s2, 2); s2 = s1; s1 = fb_in; end final_state = [s1 s2]; end

这个函数里fb_in是反馈支路的输入,等于当前信息位与两个寄存器状态的模2和;parity(k)是校验位;每次编码完一个比特后寄存器右移,反馈值进入第一个寄存器。这个移位顺序必须与译码器里的状态转移表完全一致,否则编码端和译码端的网格对不上,LLR算出来全是乱的。我一般把final_state同时返回,方便后面做尾比特处理。

2.2 交织器选型:随机交织和S-random交织的取舍

交织器是turbo码的第二根支柱。它把第二个分量码的输入顺序打乱,使得一个分量译码器在译码时遇到的“突发错误”在另一个分量译码器眼里是独立分布的。交织深度越大、打散效果越好,turbo增益通常越明显。MATLAB里最简单的做法是randperm(N)生成一个随机置换,但要注意两点:第一,随机交织必须固定种子才能复现实验结果;第二,普通随机交织对短帧效果不稳定,有时候某一帧恰好打成局部聚集,性能就突然掉一块。

工程上更稳妥的是S-random交织器。它要求每个随机生成的位置与已选中的前j个位置的距离都不小于某个值d,从结构上强制相邻信息比特在交织后至少隔开d个位置。生成算法不复杂,一个带约束的randperm循环即可,代价是帧长和约束参数不合适时可能陷入死循环。我的建议是:入门和验证阶段直接用固定种子的randperm,性能曲线做出来后如果发现抖动明显,再换S-random。交织器的逆映射也要一次算好,MATLAB里用索引赋值一行就能得到:

N = 1024; rng(2024); % 固定随机种子,保证实验可复现 interleaver = randperm(N); % 交织映射 rev = zeros(1, N); rev(interleaver) = 1:N; % 逆交织映射

这里interleaver(k)表示原序列的第interleaver(k)个比特放到交织序列的第k个位置;rev是它的逆映射,用于译码迭代时把第二个分量译码器的外信息映射回原序。调试时一定要检查rev(interleaver)是否等于1:N,这个映射关系一旦写反,迭代译码完全无法收敛,误码率会高到像没做任何纠错一样。

2.3 删余矩阵与码率拼接:1/3码率怎么改成1/2

默认的turbo码发送信息位、第一个校验序列、第二个校验序列三部分,码率是1/3。如果直接全部发送,带宽开销比较大,所以工程上常通过删余把码率提高到1/2。所谓删余就是按规则丢掉一部分校验比特,接收端在译码前把缺失位置按符号0补回去。下面这个函数演示了如何打包和删余,输出的是一个按发送顺序排列的比特序列:

function tx = turbo_encode(info, interleaver, fb, ff, punc_mode) % punc_mode: 0 表示全码率1/3,1 表示码率1/2 p1 = rsc_encode(info, fb, ff); p2 = rsc_encode(info(interleaver), fb, ff); if punc_mode == 0 tx = [info; p1; p2]; tx = tx(:)'; return; end % 码率1/2: 每两个信息比特组成一组 % 发送顺序为 u1, u2, p1_2, p2_1 % 即第一个信息比特带第二路的校验,第二个带第一路的校验交替输出 N = length(info); tx = zeros(1, 2*N); u_pair = reshape(info, 2, N/2); p1_pair = reshape(p1, 2, N/2); p2_pair = reshape(p2, 2, N/2); tx(1:4:end) = u_pair(1, :); tx(2:4:end) = u_pair(2, :); tx(3:4:end) = p1_pair(2, :); tx(4:4:end) = p2_pair(1, :); end

删余模式选择需要小心。上面的模式交替保留p1的偶数位和p2的奇数位,保证两个分量译码器拿到的校验位数量大致均衡;如果某个分量码的校验位删得太狠,对应译码器的外信息质量就会变差,迭代增益明显下降。接收端解析时需要按照同样的分组规则把校验位拆回两条序列,缺失位赋0,在实际实现里这一步是最容易出错的地方。我一般会先写一个“编码-补零-重新排列”的辅助函数,专门用于验证发送端和接收端的删余模式是否互为逆过程。

3. 译码器实现:Log-MAP迭代译码的完整计算流程

3.1 状态转移表和分支度量的预处理

Log-MAP译码器的输入是系统位和校验位的信道软信息,即LLR序列,而计算的核心是网格上的前向后向递推。第一步要先把状态转移关系和分支度量算清楚。对约束长度3的RSC,状态是[s1 s2],共4个状态,每个状态下输入0或1各有一条转移边,并伴随一个校验输出比特。为了不依赖工具箱,我用一个独立函数把转移表和校验输出表构建出来:

function [trans_tab, parity_tab] = build_rsc_trellis(fb, ff) % trans_tab(s+1, u+1): 在当前状态s输入u后的下一状态 % parity_tab(s+1, u+1): 对应的校验输出比特 num_states = 4; trans_tab = zeros(num_states, 2); parity_tab = zeros(num_states, 2); for s1 = 0:1 for s2 = 0:1 s = s1*2 + s2 + 1; % 状态编号 for u = 0:1 fb_in = mod(u + fb(2)*s1 + fb(3)*s2, 2); par = mod(ff(1)*u + ff(2)*s1 + ff(3)*s2, 2); trans_tab(s, u+1) = fb_in*2 + s1; parity_tab(s, u+1) = par; end end end end

这里的状态编号用了s1*2 + s2,与rsc_encode里的寄存器顺序一一对应。构建完成后,建议打印出来和手写的转移网格对照一遍,确认trans_tab和parity_tab的对应关系没有做反。这一步做扎实,后面的Log-MAP递推就只是套公式了。

3.2 前向度量与后向度量的递推:Log-MAP核心代码

Log-MAP的核心是BCJR算法在对数域的实现。前向度量α从网格头部开始递推,后向度量β从网格尾部开始递推,每一时刻每个状态都要记录一个对数度量值。为了避免数值溢出,所有运算都在对数域完成,加法用logsum操作代替。分支度量γ的表达式是u * L_a(k) / 2 + (sys_llr(k) * xs + par_llr(k) * xp) / 2,其中xs和xp是编码输出比特映射成的±1。下面是一个可直接运行的Log-MAP译码器实现:

function L_e = log_map_decode(sys_llr, par_llr, L_a, trans_tab, parity_tab) % sys_llr: 系统位信道LLR, 即 Lc * y_s % par_llr: 校验位信道LLR, 即 Lc * y_p % L_a: 先验LLR,第一次迭代为全0 % 返回外信息 L_e (不包括信道值和先验) N = length(sys_llr); num_states = 4; alpha = -inf * ones(N+1, num_states); alpha(1, 1) = 0; % 初始状态为状态0 beta = zeros(N+1, num_states); % 无尾比特时末端状态等概率 % beta(N+1, 1) = 0; 其它 -inf; % 若做了尾比特则用这一行 for k = 1:N for s = 1:num_states for u = 0:1 ns = trans_tab(s, u+1); xs = 2*u - 1; xp = 2*parity_tab(s, u+1) - 1; gamma = u*L_a(k)/2 + (sys_llr(k)*xs + par_llr(k)*xp)/2; alpha(k+1, ns+1) = logsum(alpha(k+1, ns+1), alpha(k, s) + gamma); end end end for k = N:-1:1 for s = 1:num_states beta(k, s) = -inf; for u = 0:1 ns = trans_tab(s, u+1); xs = 2*u - 1; xp = 2*parity_tab(s, u+1) - 1; gamma = u*L_a(k)/2 + (sys_llr(k)*xs + par_llr(k)*xp)/2; beta(k, s) = logsum(beta(k, s), beta(k+1, ns+1) + gamma); end end end L_total = zeros(1, N); for k = 1:N num = -inf; den = -inf; for s = 1:num_states for u = 0:1 ns = trans_tab(s, u+1); xs = 2*u - 1; xp = 2*parity_tab(s, u+1) - 1; gamma = u*L_a(k)/2 + (sys_llr(k)*xs + par_llr(k)*xp)/2; if u == 1 num = logsum(num, alpha(k, s) + gamma + beta(k+1, ns+1)); else den = logsum(den, alpha(k, s) + gamma + beta(k+1, ns+1)); end end end L_total(k) = num - den; end L_e = L_total - L_a - sys_llr; end function r = logsum(a, b) % 对数域加法,避免数值下溢 if a == -inf r = b; return; end if b == -inf r = a; return; end r = max(a, b) + log1p(exp(-abs(a - b))); end

这段代码我一般照着抄之后会再检查三个地方。第一,alpha初始化只有状态0为0,其它为负无穷,这对应编码器从全零状态开始;第二,beta末端在无尾比特时全部置0,如果编码器加了尾比特就必须改成只有状态0为0;第三,外信息要从总LLR里减掉信道值和先验,三者的符号要一致。把logsum换成max就是Max-Log-MAP,性能会损失约0.3到0.5dB,但计算量小很多,适合初次验证链路逻辑时使用。

3.3 外信息更新与迭代收敛条件:Turbo译码主循环

两个分量译码器靠外信息接力,方向是:第一个译码器输出外信息后交织,作为第二个译码器的先验;第二个译码器输出外信息后逆交织,作为下一轮第一个译码器的先验。代码里用interleaver和rev两个索引直接完成映射,避免显式循环:

function [L_total, L_e1] = turbo_decode(sys_llr, par1_llr, par2_llr, ... interleaver, rev, trans_tab, parity_tab, max_iter) % 迭代译码主循环 N = length(sys_llr); L_a1 = zeros(1, N); for it = 1:max_iter L_e1 = log_map_decode(sys_llr, par1_llr, L_a1, trans_tab, parity_tab); L_a2 = L_e1(interleaver); % 交织 L_e2 = log_map_decode(sys_llr(interleaver), par2_llr, ... L_a2, trans_tab, parity_tab); L_a1 = L_e2(rev); % 逆交织 end L_total = sys_llr + L_a1 + L_e1; end

迭代次数不是越多越好。通常3到6次迭代就能获得大部分增益,继续增加只会让外信息绝对值不断膨胀,甚至出现过拟合式的性能回退。收敛判断可以用一个非常简单的方法:连续两轮迭代后信息比特的硬判决结果不变,就提前终止。具体做法是每次循环结束后保存double(L_total < 0),与上一轮比较。信噪比很低时这个方法能省下不少仿真时间,信噪比高时本来一两次迭代就收敛了,作用有限,但对调参阶段的反复仿真帮助很大。

4. 把编码器和译码器接成完整链路:最小可运行Demo与参数设定

4.1 关键仿真参数:帧长、信噪比、迭代次数怎么定

把整个链路跑通之前,参数要先定下来。下面这张表是我在教学和验证中常用的默认配置,照着设基本不会出方向性错误,之后可以逐个参数调整观察性能变化。

参数推荐值说明
信息帧长 N1024太短交织增益不明显,太长仿真耗时
RSC多项式fb=[1 1 1],ff=[1 0 1]约束长度3,4状态
码率1/3 全码率先把链路跑通再试删余
迭代次数63次与6次差距明显,超过6次收益低
信噪比范围0到2.5 dB观察误码率从高到低的整段曲线
统计策略至少50个误码误码个数太少,曲线抖动大

帧长设为1024主要是平衡交织增益和仿真时间。帧长降到200以下时,turbo码的增益会被交织不足和尾比特开销吃掉大部分;帧长升到4096后,单帧译码时间明显变长,但性能提升有限,更适合在曲线基本稳定后用来确认最终性能。

4.2 主程序脚本:编码、BPSK调制、AWGN信道、译码统计

主程序的逻辑很直接:随机生成信息比特,编码,BPSK映射,加高斯白噪声,再解调译码,统计误码。这里有一个容易踩的细节是LLR缩放因子Lc的计算。对BPSK和码率R,Lc = 4 * R * EbN0,其中EbN0是线性值。噪声方差sigma2 = 1 / (2 * R * EbN0),两者必须配套使用,否则译码器的输入软信息幅度与实际信噪比不匹配,性能会差很多。

% turbo_link_demo.m clear; clc; rng(1); N = 1024; fb = [1 1 1]; ff = [1 0 1]; rng(2024); interleaver = randperm(N); rev = zeros(1, N); rev(interleaver) = 1:N; [trans_tab, parity_tab] = build_rsc_trellis(fb, ff); EbN0_dB = 0:0.5:2.5; max_iter = 6; target_errs = 100; max_frames = 500; ber = zeros(size(EbN0_dB)); for idx = 1:length(EbN0_dB) EbN0 = 10^(EbN0_dB(idx)/10); R = 1/3; Lc = 4 * R * EbN0; sigma2 = 1 / (2 * R * EbN0); errs = 0; total_bits = 0; frames = 0; while errs < target_errs && frames < max_frames info = randi([0 1], 1, N); tx = turbo_encode(info, interleaver, fb, ff, 0); x = 2 * tx - 1; % BPSK映射 n = sqrt(sigma2) * randn(size(x)); % AWGN y = x + n; y_mat = reshape(y, 3, N); sys_llr = Lc * y_mat(1, :); par1_llr = Lc * y_mat(2, :); par2_llr = Lc * y_mat(3, :); [L_total, ~] = turbo_decode(sys_llr, par1_llr, par2_llr, ... interleaver, rev, trans_tab, parity_tab, max_iter); hat = double(L_total < 0); errs = errs + sum(hat ~= info); total_bits = total_bits + N; frames = frames + 1; end ber(idx) = errs / total_bits; fprintf('Eb/N0 = %.1f dB, BER = %.3e\n', ... EbN0_dB(idx), ber(idx)); end

这段脚本里turbo_encode在码率1/3模式下直接输出[info; p1; p2]串成的序列,所以接收端reshape(y, 3, N)可以把三行分别取出来。target_errs设为100是为了让每个信噪比点的误码统计足够可信;如果只用固定帧数统计,高信噪比下可能一帧都不错,误码率直接跳到0,曲线无法比较。

4.3 运行结果怎么判断正常:先看趋势再看绝对数值

跑通之后第一件事不是调参,而是判断结果是否正常。最简单的判据有两个:一是误码率曲线是否随信噪比单调下降,且斜率在1dB之后明显变陡;二是0.5dB到1.5dB区间的绝对值是否符合预期。第一次完整跑完,如果看到BER在1.5dB附近已经到10的负4次方量级,说明整个链路基本正确;如果BER卡在10的负2次方上下不来,通常不是噪声问题,而是译码器某个细节写错了。

要快速定位问题,可以把迭代次数临时改为1,观察此时单次Log-MAP译码的性能是否接近软判决Viterbi的水平。如果1次迭代就比未编码还差,问题大概率在LLR符号、交织方向或状态转移表;如果1次迭代正常但多次迭代不收敛,问题在外信息反馈的极性上。这个分步排查法比直接对着整条链路猜要高效得多。

5. 避坑清单:迭代增益消失、外信息爆炸、仿真跑不动的5个真实原因

5.1 现象:迭代次数增加,误码率不降反升

原因:外信息符号或缩放没控制好,迭代过程进入了正反馈振荡。外信息本身是软信息,理论上应当逐步收敛;但如果某个符号写反,每一轮迭代都会把错误方向的“信心”放大,最终硬判决跟着错。解决:先检查L_e = L_total - L_a - sys_llr是否减对了项,再检查L_a2 = L_e1(interleaver)的方向是否有误。还不收敛的话,给外信息乘一个0.7左右的缩放因子,能缓解过度自信的问题。

5.2 现象:外信息绝对值越滚越大,几百轮后LLR溢出成Inf

原因:Max-Log-MAP或Log-MAP在低信噪比下输出的外信息方差本身就偏大,迭代中如果没有任何约束,绝对值会一路膨胀。这是turbo译码器的固有特征,也是工程里常用缩放因子的原因之一。解决:要么限制每轮外信息的最大值,比如截断到±30;要么在连续两轮判决一致后提前停止迭代,避免多余的自我确认。

5.3 现象:帧长缩短后,turbo增益几乎消失

原因:交织器长度不足导致两个分量码之间的相关性无法被充分打散,同时短帧下没有尾比特时的状态不确定性占比也更高。解决:短帧场景不要用全码率扫参,先把帧长提到512或1024验证链路,确认无误再压缩。如果必须短帧,就要接受几个dB的性能损失,必要时改用S-random交织器弥补一部分损失。

5.4 现象:误码率曲线出现平台,低信噪比能纠错、高信噪比反而不动

原因:删余模式下两个分量译码器拿到的校验位严重不均衡,或者接收端补零位置错误导致某个校验序列形同虚设。解决:用前面提到的打包函数重新检查删余模式,确保两条校验序列的删余比例接近1:1;补零位置的LLR必须严格置0,不能塞进随机噪声值。

5.5 现象:仿真慢到没法调参,扫一组信噪比要跑一整晚

原因:在高信噪比下继续用低信噪比点的统计策略,为了凑够误码数跑了大量无差错帧。解决:先只在0到1.5dB区间把链路调通,确认无误后再向高信噪比推进;同时开启提前停止,连续两轮硬判决不变就跳出迭代循环。对于临时验证,可以把target_errs从100降到30,曲线会抖一些但调参效率高很多。

这些坑我基本都踩过一遍,最折磨人的一次是交织方向写反,跑出来的误码率曲线比未编码还差,当时怀疑了信道模型、怀疑了随机数种子,最后打印出交织前后序列才发现只是反了。A同学后来复现同一套链路时也卡在删余补零上,所以说遇到反常结果,先怀疑映射和符号,再怀疑数学。

6. 进阶调优:用三板斧验证译码器有没有写对,再放心改参数

第一板斧,固定噪声样本检查外信息方向。取一个很随机的信息帧,固定信道种子,在迭代前后打印前20个比特的L_total符号与实际信息比特的对应关系。如果外信息符号与信息比特的一致性明显高于50%,说明方向正确;如果高度负相关,说明+和-映射反了。这个检查在整条链路上几乎不花时间,却能排除掉最隐蔽的符号错误。第二板斧,把迭代次数设为1,对比软判决Viterbi的参考曲线,单次Log-MAP的BER应当略优于Viterbi但量级相近;差太多说明状态转移表或LLR缩放有问题。第三板斧,固定种子下对比迭代1次、3次、6次的误码率曲线,正常情况应当是前两次迭代的增益最明显,之后趋于平稳;如果1次和6次几乎没有差别,说明外信息根本没有在两条分量码之间流动起来。

这三板斧都通过之后,就可以放心调整交织器长度和迭代次数了。我通常的做法是:先保持迭代6次不变,扫帧长从256到2048看增益趋势,找到吞吐与性能的折中;再把帧长固定在选定的值上,扫迭代次数1到8次,确认3到6次之间的增益差是否符合预期。仿真时把每次信噪比点的随机种子固定,不同参数之间的对比才不会因为噪声起伏而失真。这套验证流程也适用于把Log-MAP换成Max-Log-MAP或线性近似译码算法,所有改动都要回到“外信息方向正确、收敛趋势明确”这两条底线上来检查。

turbo编码译码的MATLAB实现看起来公式多、代码长,真正难住人的从来不是BCJR递推本身,而是编码器和译码器各自定义的那套状态、映射和LLR符号是否完全对齐。我自己从第一版能跑到稳定出曲线,浪费的时间主要就是耗在交织逆映射和校验位补零这两个毫不起眼的环节上。把这个教训写成这套实现和检查方法,希望帮到你,也希望你跑通之后再去改参数时,能少走一次弯路。

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

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

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

立即咨询