ARTICLE DETAIL

资讯详情

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

MATLAB实现Turbo码编码译码:从RSC分量码到迭代译码全解析

MATLAB实现Turbo码编码译码:从RSC分量码到迭代译码全解析 Turbo码这个东西通信专业的人肯定不陌生。C. Berrou在1993年提出它的时候直接把信道编码推进到了逼近香农极限的时代3G、4G、甚至5G的某些控制信道里都有它的影子。但原理课上听老师讲是一回事真正要自己在MATLAB里把Turbo码编码、译码整套链路跑通又是另一回事。这篇博文就是我们自己动手实现Turbo码编码译码的一次完整探索从RSC分量码、交织器设计到迭代译码里的Log-MAP算法每一步都拆开讲清楚代码也给出可直接参考的版本。不管你是做毕业设计、需要搭一个通信仿真链路还是想弄懂Turbo码内部到底在迭代什么这篇文章都能给你一个落地的抓手。1. Turbo码的核心思路与MATLAB实现路径1.1 为什么Turbo码能逼近香农极限Turbo码最本质的思想是把一个复杂的长码分解成两个或多个结构简单的短码中间用交织器把信息比特的顺序打乱然后在译码端通过迭代的方式让两个译码器不断交换软信息最终逼近最大似然译码的性能。这里有个很容易被忽略的点Soft信息软信息的交换才是Turbo码的精髓。传统卷积码用Viterbi译码输出的是硬判决比特Turbo码的译码器输出的是每个比特的对数似然比LLR不着急判决而是把我不确定的信息传给下一个译码器。这就好比你跟同伴一起回忆一件事各自列出一份有把握清单两个人反复核对彼此哪条比较确定、哪条没把握几轮下来比一个人瞎猜要靠谱得多。交织器的作用也在这里。它把两个分量码的约束关系错开让相关性低的信息互相补充。如果两个译码器面对的是同一顺序的比特迭代就变成了重复劳动交织之后一个译码器的突发错误在另一个译码器看来是分散的纠错能力就上来了。1.2 为什么用MATLAB来实现Turbo码选择MATLAB写Turbo码不是因为它在运行速度上有什么优势其实纯脚本循环很慢而是因为它做仿真链路太方便了矩阵和向量化的表达天然契合交织器这种重排操作一条索引语句就搞定不需要像C语言那样写一堆指针循环。随机数生成、误码率统计、星座映射都是内置函数BPSK/QPSK调制、AWGN信道加噪一行代码就能完成。调试可视化方便中间任何一步想看看LLR分布、交织前后的比特序列直接plot一下就行。我自己实际项目里最终的Turbo码产品是用FPGA实现的但算法验证、性能摸底、参数寻优全是在MATLAB里先做透的。先跑通浮点模型再去定点和硬件实现这个流程放到今天依然是性价比最高的。1.3 最小Turbo码系统的组成要实现一个能跑的Turbo码仿真链路至少需要这些模块随机信息源生成足够多的随机比特用于统计误码率。Turbo编码器由两个RSC分量码、一个交织器、一个删余器组成。调制与信道BPSK调制AWGN信道归一化噪声方差。Turbo译码器两个软输入软输出的MAP译码器外加交织/解交织器迭代交换外信息。误码率统计模块对比发送比特和译码输出比特累计错误数。这五个模块串起来就是一个完整的最小闭环。下面我从编码器开始逐个模块写实现细节。2. Turbo编码器实现从结构到代码2.1 分量码选择为什么必须是RSC卷积码Turbo码的两个分量码几乎无一例外都采用递归系统卷积码RSC而不是教科书里最常讲的非递归卷积码。两者虽然自由距离特性可以一样但递归结构会生成一个无限冲激响应式的码序列这让它和交织器配合时能产生更大的有效自由距离这是Turbo码性能的关键。一个经典的RSC分量码是八进制生成多项式(7,5)也就是反馈多项式1 D D^2 - 八进制7 前向多项式1 D^2 - 八进制5约束长度K3记忆长度2。对应生成矩阵可以用MATLAB的poly2trellis函数得到trellis poly2trellis(3, [7 5]);这个trellis结构里包含每个状态分支的输入、输出、下一状态等信息后面写维特比和MAP译码都要用到它。建议新手先打印出来看一眼理解一下状态转移到底是怎么组织的后面写递推才不会乱。2.2 RSC编码器的MATLAB实现我自己写RSC编码器不喜欢一上来就调用convenc而是先用手写状态机确保我是真懂了跑通之后再用封装函数提速。核心逻辑是这样的function out_bits rsc_encode(in_bits, trellis, tail_flag) % 简化版RSC编码器约束长度3 % 输入: in_bits 待编码比特序列 % trellis poly2trellis结构 % tail_flag 是否输出结尾归零比特 % 输出: out_bits 编码后比特系统比特 校验比特 % 寄存器初始状态设为0 % 注意RSC的反馈结构当前输出 输入XOR反馈值 % 这里以生成多项式 [7 5] 为例反馈多项式即第一个输出分支 len length(in_bits); out_sys zeros(1, len); % 系统比特 out_par zeros(1, len); % 校验比特 reg [0 0]; % 两个寄存器的状态 for k 1:len % 反馈输入 当前比特 XOR 第二个寄存器反馈 feedback bitxor(in_bits(k), reg(2)); % 第一个抽头对应D^1即reg(1) % 八进制7的二进制111表示抽头 [1 1 1]也就是reg(1)、reg(2)都反馈 input_to_reg feedback; % 系统输出就是输入比特本身 out_sys(k) in_bits(k); % 校验输出 输入 XOR 各抽头 out_par(k) bitxor(in_bits(k), bitxor(reg(1), reg(2))); % 状态更新 reg(2) reg(1); reg(1) input_to_reg; end out_bits [out_sys; out_par]; end这段代码对应的是反馈多项式抽头系数为[7]、前向输出抽头为[5]的情况。参考poly2trellis(3,[7 5])实际八进制7对应的反馈抽头是[1 1 1]也就是说当前位置有抽头反馈。如果直接用convenc做编码还能省掉手写状态机的时间但是必须注意RSC的递归结构不能用简单卷积命令替代需要配合反馈多项式做修正否则编码结果完全不对。2.3 交织器的设计别小看这个打乱操作交织器决定了两个分量码之间的相关性它的质量直接影响Turbo码的错误平台。常见的选择有三种块交织把比特按行写入矩阵按列读出。实现最简单但性能最差。随机交织用随机置换打乱顺序randperm一行搞定。S-random交织限制任意两个距离小于S的比特交织后距离也至少为S。性能最好但构造稍复杂。在MATLAB里随机交织是最常见的工程选择len_interleaver 1024; rng(42); % 固定随机种子保证收发端一致也保证仿真可复现 interleaver_perm randperm(len_interleaver); deinterleaver_perm interleaver_perm; % 解交织用逆映射 [~, inv_perm] sort(interleaver_perm);注意这里最容易踩的坑收发两端的交织器必须完全一致包括随机种子和初始化顺序。有些同学在仿真时没有固定种子发送端用randperm生成了一个置换接收端又用randperm重新生成了一次结果编码端和译码端各自拿到不同的交织映射性能直接崩掉还以为是算法写错了。如果你需要构造S-random交织器也可以自己循环生成简单流程是不断生成随机数检查新位置和已选位置中距离小于S的元素是否满足约束不满足就舍弃满足就加入。2.4 删余与码率调整Turbo码的原始输出包含系统比特、分量码1的校验比特、分量码2的校验比特三路码率是1/3。实际系统中为了传输效率通常要做删余把码率提高到1/2。最常见的删余模式是奇时刻保留分量码1的校验比特偶时刻保留分量码2的校验比特。系统比特全部保留。这样总输出比特数 系统比特 校验比特总数的一半 2N码率正好1/2。% 假设编码后得到 sys_seq, par1_seq, par2_seq % 删余模式[1 0; 0 1] 表示第一路奇保留第二路偶保留 % 注意这里是按数据块操作不是按帧循环用索引更高效 par1_pos 1:2:len; par2_pos 2:2:len; interleaved_par2 par2(interleaver_perm); % 先交织再删余 coded_stream repelem([],1); % 临时占位 % 实际就是把 sys全部、par1奇数位、par2偶数位按序拼接这里有个容易迷糊的地方分量码2的校验输出是在交织后的比特流上编码得到的所以在组装发送序列时分量码2的校验比特已经是交织后顺序的校验了。在译码端解交织时对这份校验也要做对应的顺序处理。还有结尾归零比特一般不做删余或者单独处理否则最后一个状态无法归零会造成尾部误差在短块时尤其明显。3. Turbo译码器实现迭代的核心引擎3.1 译码目标对数似然比与迭代结构Turbo译码器的输入是信道接收到的软值序列输出是每个信息比特的后验LLR。LLR的定义是L(d) ln( P(d1) / P(d0) )LLR的符号决定了硬判决为正判1为负判0。绝对值大小代表置信程度。迭代译码的过程就是两个分量译码器不断更新这个LLR的过程。在MATLAB里最直接的理解方式是把两个MAP译码器看成两个黑盒每个黑盒吃进去三个东西先验LLR来自上一个译码器的外信息信道软值对应系统比特和校验比特的接收值另一路校验软值来自另一个译码器的校验信息吐出来的是后验LLR。后验减去先验和信道值就是所谓的外信息Extrinsic Information这个外信息要传给另一个译码器当作新的先验。3.2 MAP/BCJR算法的四步递推BCJR算法是Turbo译码的基础核心是前向后向递推共四步第一步初始化分支度量gamma。每个时刻k每条分支从状态s到状态s的度量由三部分相加该比特的先验LLR、系统比特的信道值、校验比特的信道值。在AWGN信道下信道值为2*r*y/σ^2的形式r是接收信号幅度的归一化值。第二步前向递推alpha。从k1开始从头到尾递推每个时刻每个状态的概率alpha_k(s) logsumexp(alpha_{k-1}(s) gamma_k(s, s))第三步后向递推beta。从kN开始从尾到头递推beta_k(s) logsumexp(beta_{k1}(s) gamma_{k1}(s, s))第四步计算输出LLR。把所有d1对应的分支路径的alphagammabeta做logsumexp减去所有d0路径的logsumexp就得后验LLR。MATLAB实现的骨架如下% 假设已经得到信道相关值 ch_sys、ch_par、先验先验llr for k 1:N for s 1:num_states for input_bit 0:1 next_s trellis.nextStates(s, input_bit1); out_sys trellis.outputs(s, input_bit1); % 实际要换算为±1 gamma(k, s, next_s) 0.5 * prior_llr(k) * (2*input_bit-1) ... ch_sys(k) * sys_val ch_par(k) * par_val; end end end % 前向递推注意归一化防止数值上溢 for k 2:N alpha(k, :) logsumexp(alpha(k-1, :) gamma(k, :, :), 2); alpha(k, :) alpha(k, :) - max(alpha(k, :)); end % 后向递推同理 % 最终LLR llr_post logsumexp(alpha(k,:) beta(k1,:) gamma_k1, 2) ... - logsumexp(alpha(k,:) beta(k1,:) gamma_k0, 2);这里最关键的是logsumexp的实现它用来避免直接在概率域相乘造成的数值下溢。数学上logsumexp就是取最大值加上log(1exp(-差值))在浮点上很稳。3.3 迭代过程外信息怎么传停止准则是什么迭代译码器结构如下分量译码器1接收原始系统软值、分量1校验软值、来自译码器2解交织后的先验外信息输出后验LLR1。后验LLR1减去输入的先验外信息和系统信道值得到外信息1。外信息1经过交织器变成分量译码器2的先验输入。分量译码器2接收交织后的系统软值、分量2校验软值、先验外信息输出后验LLR2。后验LLR2减去输入的先验外信息和交织后的系统信道值得到外信息2。外信息2经过解交织器送回分量译码器1。回到第1步。外信息的计算公式里最容易出错的是减去的信道值用哪个顺序。在译码器1中系统信道值用原始顺序在译码器2中系统信道值必须用交织后顺序。这个顺序一错外信息就混入了重复的自身信息迭代会发散。停止迭代有两种思路固定迭代次数简单粗暴一般4到8次就能覆盖90%以上的性能增益。提前停止对后验LLR做硬判决如果连续两次迭代硬判决结果一致就认为收敛提前退出。这对低信噪比下节省计算量很有用。我自己常用固定迭代次数8次作为默认跑实验时同时统计提前停止能省多少轮次两者对比评估。4. 参数选择与性能调优的实战经验4.1 关键参数对照表这里是Turbo码仿真中最值得花时间调的一组参数我整理成一张表方便查看参数常见取值性能影响调优建议分量码生成多项式(7,5)、(13,15)、(23,37)自由距离直接影响错误平台高度长帧用大约束长度短帧用(7,5)就够交织器长度100~10000越长性能越好但延迟变大做毕业设计选512或1024即可交织器类型随机、S-randomS-random错误平台更低中短帧优先S-random迭代次数4~10超过8次后增益趋缓仿真默认8次删余模式1/3或1/2码率码率越高冗余越少性能下降对比不同码率曲线时控制变量调制方式BPSK/QPSK影响LLR映射先跑BPSK验证链路以约束长度3的(7,5)码、块长1024、1/2码率为例BPSKAWGN信道下的性能通常在1.2dB左右能达到BER 10^-4与未编码BPSK约9.6dB才达到同等误码率相比优势巨大这正是Turbo码逼近香农限的直接体现。4.2 完整仿真链路与误码率曲线验证一套完整的MATLAB仿真脚本主循环大致长这样EbN0_dB 0:0.5:2.5; num_trials 100; ber_results zeros(size(EbN0_dB)); for idx 1:length(EbN0_dB) bit_errors 0; total_bits 0; EbN0 10^(EbN0_dB(idx)/10); % BPSK编码比特能量归一化注意1/2码率下Eb是信息比特能量 N0 1 / (EbN0 * code_rate); noise_std sqrt(N0 / 2); for trial 1:num_trials info_bits randi([0 1], 1, len_info); coded turbo_encode(info_bits, trellis, perm); % BPSK调制0-11--1 tx 1 - 2 * coded; rx tx noise_std * randn(size(tx)); % 注意噪声要折算出每个符号的信噪比 decoded_bits turbo_decode(rx, trellis, perm, N0, iter); bit_errors bit_errors sum(decoded_bits ~ info_bits); total_bits total_bits len_info; end ber_results(idx) bit_errors / total_bits; end semilogy(EbN0_dB, ber_results, o-); grid on;实测时要特别注意噪声方差的计算。很多新手在这里直接把N0设成10^(-EbN0/10)没有考虑码率的影响。正确做法是EbN0是每个信息比特的能量比上噪声密度编码后每个信道符号的能量是Ec Eb * code_rate加噪时要用Ec对应的幅度。用BPSK时Ec1则N0 1 / (EbN0 * code_rate)。这个细节不对整条BER曲线能偏好几dB。4.3 常见问题与排查技巧实录这部分是我实际跑Turbo码仿真时踩过的坑每一个都花了不少时间排查直接列成问题排查表现象可能原因排查方法BER在0dB以上仍然高达0.1完全无纠错效果交织器收发不一致或LLR极性反了先固定rng检查交织映射是否一致打印译码后LLR均值看符号是否正常迭代次数增加BER反而变差外信息计算公式里混入了重复的信道信息检查后验LLR减去的先验和系统信道值顺序确保只减一次误码率低信噪比正常高信噪比出现平台交织器不够随机或迭代次数太少换成S-random交织器增加迭代到10次看是否改善编码后的序列长度不对删余索引后校验比特拼接错了拆开打印三段输出分别验证与直接编码结果的一致性数值NaN或Inf前向后向递推没有做归一化在每轮alpha/beta递推后减去最大值还有一个比较隐蔽的坑尾比特tail bits没有正确处理权值。Turbo编码器在完成数据比特编码后状态必须归零所以最后会有三个尾比特。这些尾比特对应的系统比特在校验时不应该参与误码率统计因为信息比特数量不包含它们。如果在译码时把尾比特一起当成信息比特输出统计的误码率会虚高甚至会因为尾部状态约束缺失让最后几个比特的LLR算错。我通常的做法是编码时尾比特单独存放译码时只对前N个信息比特做判决统计。另外用logsumexp时两个数值差太大会导致exp(-较大值)下溢为0这其实是安全的因为在log域下log(10)0等价于取最大值不会出错。但如果你把logsumexp里写成log(sum(exp(x)))这种直接方式就会溢出或下溢到log(0)-inf整个算法崩溃。5. 性能验证与个人实操体会5.1 一组有参考价值的仿真结果我拿块长1024、随机交织、迭代8次、(7,5)分量码、1/2码率、BPSKAWGN跑了一组结果大约在这些量级Eb/N0 (dB)BER0.0约1.2e-20.5约2.0e-31.0约2.5e-41.5约1.5e-52.0约5e-7样本不够时可能统计不到错误在这个码长和码率下理论上的香农限大约在0.2dB左右Turbo码离香农限还有1dB出头的距离。这不奇怪短块随机交织的Turbo码本来离限就有差距把块长加到4096、用S-random交织、迭代12次还能再逼近零点几dB。仿真时每个点至少统计100个错误比特再停止否则曲线尾部的抖动会很大。5.2 我的几点实操体会第一点是一定要分模块验证。别等整个系统写完了才去调错那样你面对的是一片混沌。我的做法是先单独验证RSC编码器输入一段固定序列手算出校验比特跟代码输出做对比再验证交织器把交织前后的位置关系画出来检查最后才把编码器、信道、译码器全连起来。第二点是迭代次数不是越多越好。8次之后外信息增益很有限但计算量是线性增长的。而且在高信噪比下迭代3到4次就已经收敛了后面几次除了整形数值之外几乎没变化。做工程时用提前停止准则能省下不少时间。第三点是LLR值的分布很能说明问题。如果译码正常随着迭代增加LLR的绝对值会越来越大直方图会从集中在0附近逐渐摊开呈现出两峰分布。如果LLR一直徘徊在0附近说明外信息没有有效积累基本可以确定是先验或信道值的极性反了或者信道软值归一化错了。5.3 后续可以怎么扩展Turbo码这套架子搭好之后扩展方向很多。想研究LTE标准里的Turbo码就把分量码改成(13,15)、交织器改成QPP交织想提升性能就用S-random交织加更大的块长想考察实际通信链路就把BPSK换成QPSK或16QAM把AWGN换成衰落信道想面向FPGA实现可以进一步把浮点改成定点Log-MAP简化成Max-Log-MAP观察定点位宽对BER的影响。再分享一个小技巧Turbo译码的计算瓶颈在前向后向递推MATLAB里要提速尽量把状态转移矩阵预先算好用矩阵操作代替嵌套循环代码能快一个数量级。手写个小的C-MEX加速递推内核就能撑起上万比特块的实时仿真吞吐这时候再大规模扫参数时间成本就完全可控了。Turbo码不是那种看一遍原理就能写对的算法但一旦你亲手把编码器、交织器、迭代译码整套跑通再回头去看LTE里的各种标准细节会清晰很多。如果卡在某个环节按上面表格里的排查思路走一遍比盲猜代码要有效得多。
返回列表