ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Turbo码MATLAB仿真:从RSC编码到Log-MAP迭代译码的完整实现

Turbo码MATLAB仿真:从RSC编码到Log-MAP迭代译码的完整实现 简介这份资源围绕Turbo码的MATLAB仿真实现面向通信工程和电子信息相关专业学生以及需要快速上手纠错编码仿真的研究者。压缩包共9个文件包含3个MATLAB脚本实现Turbo码编码、译码与主程序、2个fig仿真图、2份docx说明文档和2份PDF讲义整体约2.02MB内容精简但覆盖完整仿真链路。Turbo码由并行交织的递归系统卷积编码器和迭代译码构成包内代码可直接运行帮助理解PCCC编码结构、AWGN信道模拟、随机交织以及BCJR或Max-Log-MAP迭代译码过程通过绘制误码率曲线直观评估不同信噪比下的性能。资料已有1348人学习配套文档对Turbo码原理和MATLAB实现做了讲解适合用作课程设计或毕业设计的参考。只需要解压后按脚本顺序运行即可看到从编码到译码再到性能分析的完整流程便于结合理论验证仿真结果。1. 跑Turbo码MATLAB仿真之前先弄明白迭代译码在收敛什么Turbo码的增益本质上不是那两个卷积编码器给的而是两个软输入软输出译码器之间反复交换“外部信息”换出来的。同一个递归系统卷积码单独拿出来性能和一个普通卷积码差不多一旦加上交织器和迭代反馈Claude Berrou 当年在 1/2 码率下做到离香农限 0.7 dB靠的是这个迭代结构。很多人在 MATLAB 里照教材搭链路第一次跑出来的 BER 曲线却比理论差一大截多半不是编码写错了而是外部信息的交织映射、删余对齐、迭代初始先验这三处出了偏差。这篇文章走一条完整的落地路径先把 RSC、交织器、删余和 Log-MAP 迭代译码拆开再给一套可以直接跑的 MATLAB 最小链路最后落到参数调整和排错。适合熟悉数字通信基础、正在写课程设计或做链路验证的工程师。即便你已经写过 Turbo 码第四章和第五章的边界条件处理也应该有参考价值。2. 递归系统卷积码与交织器Turbo码编码器先解决的两个问题2.1 RSC与普通卷积码的差别反馈回路改变了坏码字的分布Turbo 码的分量码几乎总是递归系统卷积码Recursive Systematic Convolutional而不是普通前馈卷积码。RSC 的特征是有一条从寄存器输出回到输入的反馈回路因此编码器具有无限冲激响应。对一个信息序列普通卷积码的输入比特直接参与移位而 RSC 会先把输入和反馈模二加再送入移位寄存器。从码集的角度看一个 (2,1,2) RSC 和对应约束长度的普通卷积码有着相同的码字集合但“系统化”改变了信息位与校验位之间的映射方式。这样一个低权重输入序列比如 100..001经过反馈后会产生一个较长的校验序列低权重码字变少配合交织器那些会生成低码重的“坏输入模式”被随机化使得整体码字距离谱接近随机码。Turbo 码的误码平台error floor高低很大程度由这个性质决定。常用的是约束长度 3、记忆长度 2 的 RSC生成多项式取八进制 [7 5]反馈多项式1 D D²对应 [1 1 1]前向多项式1 D²对应 [1 0 1]。状态数为 4。这个配置是教科书里的标准选择网格规模小适合在 MATLAB 里逐状态写 Log-MAP跑起来也快。后面章节的代码都以这组多项式为基础。2.2 交织器设计S随机约束让外部信息不“自我印证”交织器在 Turbo 码里有两层作用。第一层是抗突发差错把信道里的连续错误打散到不同校验序列里第二层更本质它让两个分量译码器对同一组比特看到“相关但不相同”的校验约束。如果交织器设计得不好两个译码器交换的外部信息高度相关迭代就会很快饱和增益上不去。随机交织器实现最简单但存在一个问题距离很近的两个比特交织后可能仍然很近。S 随机交织器给了一个约束原始位置距离小于 S 的任意两个比特交织后位置距离必须大于等于 S。实际经验是 S 取 略小于 sqrt(N/2)N 为帧长。S 取得太大搜索会非常慢。生成 S 随机交织器的 MATLAB 代码function inter make_s_random_interleaver(K, S) % K: 帧长 % S: 最小距离约束 inter zeros(1, K); used false(1, K); for n 1:K for t 1:5*K m randi(K); if used(m), continue; end if n 1 inter(n) m; used(m) true; break; end start_idx max(1, n - S 1); if all(abs(inter(start_idx:n-1) - m) S) inter(n) m; used(m) true; break; end end if inter(n) 0 % 尝试多次后仍失败放松约束兜底 free_idx find(~used, 1); inter(n) free_idx; used(free_idx) true; end end end这段代码逐位填入交织位置每次随机抽一个候选位置检查它与前 S 个已填入位置的距离。5*K次尝试的限制避免了极端情况下死循环如果确实找不到就用一个未使用的空闲位置兜底保证函数必定返回合法排列。对 K1024、S22 的典型配置多数情况下不会触发兜底分支。注意这里生成的inter表示“第 j 个交织输出来自原始序列的第 inter(j) 个位置”。后面译码时的解交织映射为inv_inter(inter) 1:K这两行对应关系是 Turbo 码实现里最容易写反的地方。2.3 删余矩阵决定码率1/3母码怎么变成1/2不删余时发送系统位、第一路校验、第二路校验码率是 1/3。要拿到 1/2 码率常见做法是两路校验交替发送偶数时刻发第一路校验奇数时刻发第二路校验。用删余矩阵描述如下校验序列时刻1时刻2时刻3时刻4分量1校验 p11010分量2校验 p20101表中 1 表示发送0 表示删余。实施时只需要把删余后位置的 LLR 置 0因为接收端对未发送的校验比特不做任何假设LLR0 表示该比特为 0 和为 1 的概率相等。这个操作看似简单却是初版仿真性能差的常见原因。删余模式必须和两个译码器各自的时间轴对齐尤其是第二路译码器使用了交织后的系统 LLR而校验序列仍在原始时间索引上。后面第四章会给出带删余的完整修改方式。3. 用MATLAB从零搭Turbo码链路RSC编码器、AWGN信道和Log-MAP译码器3.1 仿真参数与链路框架这一章给一套可以直接跑的最小链路先跑 1/3 母码不删余目的是把迭代译码的主干走通。参数如下参数取值说明帧长 N1024 bit信息位长度生成多项式[7 5] 八进制约束长度 34 状态码率1/3系统位 两路校验全发调制BPSK符号映射 0→-11→1信道AWGN噪声方差按 Eb/N0 换算迭代次数6第一次迭代单独观察交织器S 随机S 取 22整体链路分四步编码、调制加噪、LLR 计算、迭代译码。编码器和译码器全部自己写不依赖 Communications Toolbox方便看到每个中间量。3.2 RSC编码器与网格构建编码器按反馈多项式和前向多项式逐比特推寄存器状态function parity rsc_encode(u, fb_poly, ff_poly) % u: 0/1 行向量 % fb_poly, ff_poly: 按 [D^0, D^1, ..., D^m] 排列 m length(fb_poly) - 1; mem zeros(1, m); L length(u); parity zeros(1, L); for k 1:L fb mod(sum(mem .* fb_poly(2:end)), 2); in_bit mod(u(k) fb, 2); parity(k) mod(sum([in_bit, mem] .* ff_poly), 2); mem [in_bit, mem(1:end-1)]; end endmem向量从左到右对应 D¹、D⁰每个周期先把反馈项算出来输入和反馈模二加后得到进入移位寄存器的比特in_bit该校验位由in_bit和当前寄存器内容按前向多项式加权得到最后把in_bit推进寄存器最高位。系统输出没有单独返回因为 RSC 的系统位就是输入本身。译码器需要一个描述状态转移的网格结构function trellis build_trellis(fb_poly, ff_poly) m length(fb_poly) - 1; Ns 2^m; trellis.next zeros(Ns, 2); trellis.out zeros(Ns, 2, 2); % (状态, 输入) - [系统位, 校验位] for s 0:Ns-1 for u 0:1 mem [bitget(s, 2), bitget(s, 1)]; % 高位是 D1 fb mod(sum(mem .* fb_poly(2:end)), 2); in_bit mod(u fb, 2); parity mod(sum([in_bit, mem] .* ff_poly), 2); next_mem [in_bit, mem(1)]; next_s next_mem(1) * 2 next_mem(2); trellis.next(s1, u1) next_s; trellis.out(s1, u1, 1) u; trellis.out(s1, u1, 2) parity; end end endbitget(s,2)取状态的高位bitget(s,1)取低位和编码器里mem的顺序保持一致。网格建好后trellis.next(s1, u1)给出从状态 s 输入 u 到达的下一状态trellis.out给出对应的系统位和校验位这些都是 Log-MAP 分支度量的输入。3.3 BPSKAWGN下的信道LLR计算BPSK 符号能量 Es1接收信号 y x n噪声方差 sigma² N0/2。按 Eb/N0 换算sigma² 1 / (2 × R × 10^(EbN0_dB/10))。信道对某个比特的 LLR 是 2y/sigma²。这个值直接作为译码器输入的软信息。N 1024; % 帧长 R 1/3; % 码率 EbN0_dB 1.5; S 22; fb_poly [1 1 1]; ff_poly [1 0 1]; inter make_s_random_interleaver(N, S); inv_inter zeros(1, N); inv_inter(inter) 1:N; % 解交织映射 trellis build_trellis(fb_poly, ff_poly); u randi([0 1], 1, N); p1 rsc_encode(u, fb_poly, ff_poly); p2 rsc_encode(u(inter), fb_poly, ff_poly); sigma2 1 / (2 * R * 10^(EbN0_dB/10)); Lc 2 / sigma2; ys (2*u - 1) sqrt(sigma2) * randn(1, N); yp1 (2*p1 - 1) sqrt(sigma2) * randn(1, N); yp2 (2*p2 - 1) sqrt(sigma2) * randn(1, N); L_sys1 Lc * ys; L_par1 Lc * yp1; L_sys2 L_sys1(inter); % 第二路的系统LLR按交织索引取值 L_par2 Lc * yp2;L_sys2 L_sys1(inter)对应第二路编码器的输入是原序列交织后的结果。噪声是独立同分布的交织后取到的 LLR 在统计上仍是独立高斯变量可以直接参与第二路分支度量计算。3.4 Log-MAP译码器前向、后向和外部信息Log-MAP 的核心是在对数域算分支度量 gamma、前向度量 alpha、后向度量 beta最后得到每个比特的后验 LLR。两个状态之间的分支度量gamma(s, s) 0.5 × (x_s × (L_sys(k) L_a(k)) x_p × L_par(k))其中 x_s、x_p 是 ±1 的调制符号L_a(k) 是来自另一个译码器的先验信息。省掉常数项不影响迭代方向实际实现通常省略。实现用 max* 函数处理两个数在对数域的求和比较function r logsumexp(v) v v(:); m max(v); if isinf(m) r m; return; end r m log(sum(exp(v - m))); endmax先取出最大值做归一化防止 exp 溢出。这是 Log-MAP 在长帧仿真中数值稳定的关键。译码器主体function L_ext logmap_decode(L_sys, L_par, L_a, trellis) N length(L_sys); Ns size(trellis.next, 1); gamma -inf(Ns, Ns, N); for k 1:N for s 1:Ns for u 0:1 ns trellis.next(s, u1); xs 2 * trellis.out(s, u1, 1) - 1; xp 2 * trellis.out(s, u1, 2) - 1; gamma(s, ns, k) 0.5 * (xs * (L_sys(k) L_a(k)) xp * L_par(k)); end end end alpha -inf(Ns, N1); alpha(:, 1) 0; % 截断实现初始状态均匀 for k 1:N for s 1:Ns vals -inf(1, Ns); for sp 1:Ns if gamma(sp, s, k) -inf vals(sp) alpha(sp, k) gamma(sp, s, k); end end alpha(s, k1) logsumexp(vals); end end beta -inf(Ns, N1); beta(:, N) 0; % 截断实现终态均匀 for k N:-1:1 for sp 1:Ns vals -inf(1, Ns); for s 1:Ns if gamma(sp, s, k) -inf vals(s) beta(s, k1) gamma(sp, s, k); end end beta(sp, k) logsumexp(vals); end end L_ext zeros(1, N); for k 1:N num -inf; den -inf; for s 1:Ns for u 0:1 ns trellis.next(s, u1); val alpha(s, k) gamma(s, ns, k) beta(ns, k1); if u 1 num logsumexp([num, val]); else den logsumexp([den, val]); end end end L_app num - den; L_ext(k) L_app - L_sys(k) - L_a(k); end end前向递推里alpha(s, k1)是所有能到达状态 s 的上一状态度量与分支度量之和的 max*后向递推对偶。外信息L_ext用当前比特的后验 LLR 减去系统 LLR 再减去先验 LLR这正好是另一个译码器下一步需要的“新信息”。这里使用了截断实现没有做格栅收尾tail-biting因此 alpha 初值和 beta 终值都设为所有状态均匀。对帧长 1024 的链路性能与标准实现差距很小但实现复杂度大幅降低。要精确复现 LTE 那种带尾比特的 Turbo 码第四章末尾会讲处理思路。3.5 跑通最小链路迭代调用与BER统计迭代译码时两个分量译码器交替输出外部信息nIter 6; L_a1 zeros(1, N); L_a2 zeros(1, N); for it 1:nIter [L_e1] logmap_decode(L_sys1, L_par1, L_a1, trellis); L_a2 L_e1(inter); % 交织后作为第二路先验 [L_e2] logmap_decode(L_sys2, L_par2, L_a2, trellis); L_a1_in L_a1; % 保留进入下一轮之前的先验 L_a1 L_e2(inv_inter); % 解交织后作为第一路先验 end L_APP1 L_sys1 L_a1_in L_e1; u_hat double(L_APP1 0); ber mean(u_hat ~ u);循环里L_a1_in L_a1这一行很容易被漏掉。硬判决用的后验 LLR 应该由系统 LLR、进入最后一次迭代的先验、最后一次迭代输出的外部信息三部分相加。如果直接用更新后的L_a1相当于多计了一次迭代的外部信息低信噪比区域会出现一个降不下去的误码平台。统计误码率时把编码、加噪、迭代译码这一段包进双层循环外层遍历 Eb/N0 的各个取值内层对每个信噪比跑几十到几百帧统计错误比特数除以总比特数。单帧跑通后先别急着画曲线把迭代从 1 调到 6观察每轮误码数是否单调下降这一步能最快确认链路主干没问题。4. Turbo码仿真的4个关键参数交织大小、迭代次数、删余模式与帧尾处理4.1 交织器大小与S值交织增益随帧长缓慢增长交织器越大低权重码字被完全打散的概率越高错误平台越低。把帧长从 512 翻到 2048在相同信噪比下通常能换来零点几个 dB 的编码增益同时错误平台明显下降。但代价是译码延迟和存储线性增长长帧下 logmap_decode 里的三个三重循环会让仿真时间暴涨。S 值也不是越大越好。S 超过 sqrt(N/2) 后S 随机交织器的搜索成功率迅速下降代码里的兜底分支被频繁触发此时交织器已经退化成接近随机。一个更实际的做法是固定帧长分别用 S10、S22、随机交织器各跑一遍比较第三次迭代后的外部信息幅度S 过小时外部信息在第三轮后就几乎不增长了。4.2 迭代次数与早停条件第二次迭代收益最大迭代译码的收敛速度呈明显的边际递减。第一次迭代相当于只用一路校验的软判决第二次迭代把另一路的约束引进来误码率通常下降一到两个数量级第三第四次继续下降但幅度逐次缩小到第六次以后多数帧的外部信息更新幅度已经很小继续迭代主要是在烧 CPU。实际操作中常见做法是设一个早停条件当两轮迭代之间所有比特的外部信息变化量小于某个阈值比如平均 |L_new - L_old| 0.01就提前跳出循环。阈值设得太大会损失 0.1 dB 左右性能太小则起不到加速作用。在 MATLAB 里跑长帧、多信噪比点时早停能把总仿真时间压缩 20% 到 40%。4.3 删余模式未发送校验位的LLR置零要放在正确位置从 1/3 改成 1/2 码率只需要改两个地方。第一Eb/N0 换算公式里的码率 R 从 1/3 改成 1/2第二给两路校验 LLR 施加删余掩码P1 repmat([1 0], 1, ceil(N/2)); P2 repmat([0 1], 1, ceil(N/2)); L_par1_dep Lc * yp1 .* P1(1:N); L_par2_dep Lc * yp2 .* P2(1:N);P1、P2在 0 的位置把校验 LLR 清零译码器对这些比特完全没有校验信息。这种交替删余是码率 1/2 下最直观的模式但自由距离不是最优。如果想要更好的距离谱删余周期要加长比如每 4 个时刻里发 2 个 p1、2 个 p2通过计算机搜索选择模式。一个容易踩的坑是删余和交织的顺序。第二路译码器的系统 LLR 是交织后的L_sys1(inter)但校验 LLR 不能跟着交织。因为 p2 本身就是第二路编码器在交织后输入下产生的校验序列它已经和交织后的比特对齐了。如果在删余时不小心把 p2 的 LLR 也做了交织分支度量和实际发送的校验序列就错位了误码率会直接掉到接近 0.5。4.4 帧尾的格栅收尾短帧下截断实现的性能损失不可忽略截断实现假设终止状态均匀帧长 1024 时损失很小但帧长压到 100 到 200 比特时尾部不确定造成的损失可能达到 0.3 dB 以上。工程上常用 tail-biting 解决即编码器的初始状态等于终态并且这个状态由输入序列决定无须额外发送尾比特。实现方法不复杂编码前先对所有可能的初始状态各跑一遍编码找到那个“编完 N 个比特后恰好回到自身”的状态再从该状态正式编码。代码如下function [parity, s_final] rsc_encode_from_state(u, fb_poly, ff_poly, init_mem) m length(fb_poly) - 1; mem init_mem; L length(u); parity zeros(1, L); for k 1:L fb mod(sum(mem .* fb_poly(2:end)), 2); in_bit mod(u(k) fb, 2); parity(k) mod(sum([in_bit, mem] .* ff_poly), 2); mem [in_bit, mem(1:end-1)]; end s_final mem(1) * 2 mem(2); end循环状态搜索不一定要对每帧都做。实际系统中这个状态由交织器和帧长唯一确定可以离线算好存表编码时直接查表复杂度可以忽略。注意 Tail-biting 下译码器的 alpha 初始化不能再用均匀分布更精确的做法是把前向递推多跑一圈让初始概率从递推结果中估计出来或者直接对 Log-MAP 用循环状态的 CVAcircular Viterbi初始化。5. 不画BER曲线先看外部信息一个高效的Turbo码实现验证技巧Turbo 码调试最怕的是链路跑完一整轮才发现误码率不对这时候很难定位是编码错、交织错还是译码错。更快的做法是在迭代循环里直接把外部信息的形态画出来。收尾阶段的 MATLAB 代码可以加一段监视逻辑monitor_flux zeros(nIter, 1); monitor_flip zeros(nIter, 1); prev_L_a1 zeros(1, N); for it 1:nIter [L_e1] logmap_decode(L_sys1, L_par1, L_a1, trellis); L_a2 L_e1(inter); [L_e2] logmap_decode(L_sys2, L_par2, L_a2, trellis); L_a1 L_e2(inv_inter); if it 1 monitor_flux(it) mean(abs(L_a1 - prev_L_a1)); monitor_flip(it) mean(sign(L_a1) ~ sign(prev_L_a1)); end prev_L_a1 L_a1; end figure; yyaxis left; semilogy(1:nIter, monitor_flux 1e-12, -o); yyaxis right; plot(1:nIter, monitor_flip, -s);monitor_flux度量相邻迭代之间先验信息更新的平均幅度monitor_flip度量符号翻转比例。一个正确的 Turbo 译码器这两条曲线都应当随迭代单调下降并趋于饱和先验更新的幅度逐渐变小符号翻转比例趋向 0。如果monitor_flip在第二次迭代后还在 0.3 以上振荡问题大概率出在交织和解交织映射上重点检查inv_inter(inter) 1:N这一行以及L_sys2的索引方式。另一个更直观的检查是看外部信息的直方图。迭代第一轮结束后histogram(L_a2, 100)应该是一个贴零的高峰因为这时候还没有多少信息到第三轮以后如果译码在收敛直方图会慢慢出现左右两个峰表示大量比特的外部信息正在被推向正确的判决方向。如果直方图始终是一个贴零的高斯形状说明两个译码器实际上没有在交换有效信息这时候去检查删余后的校验 LLR 是否在正确的时间索引上。把这段监视代码留在调试版本里后续换长帧、换更高阶调制、加删余模式时外部信息的变化趋势能比 BER 曲线更快告诉你实现有没有歪。本文还有配套的精品资源点击获取
返回列表