
我最初接触TPCTurbo乘积码是在一个卫星链路前向纠错方案的选型调研里当时和它对比的是LDPC码以及RS级联方案。做了一轮Matlab仿真后我的结论很明确TPC在逼近香农极限这件事上确实不是LDPC的对手但它结构规矩、译码延迟可控、码率调整灵活非常适合“要可靠又不能把硬件复杂度和功耗顶上天”的工程场景。这篇博文不打算复读教科书推导而是把我用Matlab把TPC从编码到软硬判决译码完整跑通的源码结构、选型逻辑和踩坑点梳理出来重点讲清楚硬判决和Chase-Pyndiah软判决译码各自怎么落地以及它们之间到底差多少性能。适合正在写TPC仿真、做通信系统课设或者工程预研的朋友参考。1. 为什么是TPC一个“够用且可硬件化”的纠错码选择1.1 从乘积码结构看TPC的本质TPC的全称是Turbo Product Code核心结构是二维乘积码也就是把两个线性分组码在行和列两个方向上交叉编码。信息比特先排成一个k2行k1列的矩阵每一行用第一个分量码C1编码变成k2行n1列然后每一列再用第二个分量码C2编码最终得到n2行n1列的码字矩阵。你可以把它理解成一份“纵横双向都要通过校验”的表格只看一行它必须是一个合法的C1码字只看一列它又必须是一个合法的C2码字。接收端译码时先按行纠错再把行的纠错结果交给列译码器接着列的结果又传回行译码器如此反复迭代这也就是“Turbo”这个名字的来源——不是级联而是在两个维度之间反复交换信息。这个结构最大的好处是“由短码构造长码”。分量码本身可以很短比如64比特、128比特但乘积之后总码长能做到几千甚至上万比特。较短的码字意味着译码器可以用比较简单的代数译码甚至查表方式实现硬件上不像LDPC的迭代置信传播那样需要大规模节点更新网络。1.2 分量码选型扩展汉明码与BCH码怎么权衡分量码选对了TPC项目就成功了一半。二维乘积码的性能下限基本由两个分量码中最弱的一个决定而最大迭代增益又取决于分量码的纠错能力和码率。我实际用过的分量码方案大体分三类分量码参数(n,k)最小距离纠错能力码率典型定位扩展汉明码(32,26)4纠1检20.8125低复杂度、高速场景扩展汉明码(64,57)4纠1检20.8906高码率、硬件友好BCH码(63,51)5纠20.8095经典折中方案BCH码扩展(64,51)6纠2检30.7969软判决增益更明显我自己的仿真配置里最常用的是两类一类是分量码用扩展汉明码(32,26)甚至(64,57)乘积码码率能做到0.89左右非常“经济”但硬判决能力弱纠错主要靠迭代和外信息另一类是分量码用BCH(63,51)再加一个全校验位扩展成(64,51)乘积后总码率约0.635迭代增益和误码平台特性都比汉明码分量好不少。选型时容易忽略的一点是分量码的码长要尽量短。一方面Chase软判决算法里最费时的步骤是对每个候选翻转模式调用一次分量码代数译码码字短了译码器延迟才压得住另一方面分量码太长之后二维乘积码的最小距离虽然变大但迭代次数带来的额外增益会迅速饱和工程上的性价比会明显下降。这也是为什么TPC系统里很少看到分量码超过128比特的原因。2. 编码端实现二维乘积码的Matlab构造流程2.1 信息位填充与校验位交叉编码端的实现思路非常直接先按行编码再按列编码。我第一次写的时候就吃了矩阵方向顺序的亏——Matlab默认按列优先存储信息位排成矩阵时一旦弄混行和列后面整个仿真链路都会错得莫名其妙。下面这个函数是我在仿真里用的编码核心信息输入是一个二进制矩阵行分量码和列分量码通过函数句柄传入function [codeword, parity] tpc_encode(info, rowEnc, colEnc, k1, n1, k2, n2) % info: k2 x k1 的二进制信息矩阵 % rowEnc/colEnc: 分量码编码函数句柄接收行向量返回完整码字 % codeword: n2 x n1 的二进制码字矩阵 rowEncoded zeros(k2, n1); for i 1:k2 rowEncoded(i, :) rowEnc(info(i, :)); end colEncoded zeros(n2, n1); for j 1:n1 colEncoded(:, j) colEnc(rowEncoded(:, j)); end codeword colEncoded; parity colEncoded((k2 1):end, (k1 1):end); end需要注意两个细节。第一行编码完成后的矩阵是k2行n1列列编码的对象是“每一列”也就是把行编码后的列向量再次送入列分量码编码器。第二全局校验位其实只有右下角那一块是“纯校验”如果把矩阵整体看成一个码字它每个边界上的行和列都满足各自分量码的校验关系。2.2 从“先按行”到“先按列”顺序为什么重要理论上先按行编码再按列编码和先按列再按行最终得到的码字集合是完全等价的——因为行列编码操作可交换。但这个结论只对完整二维矩阵成立当你做缩短、截断或者码率适配时编码顺序就会影响约束关系。实际工程里通常统一约定先行后列。原因是行译码器在迭代时最先拿到的是信道上最原始的软信息行译码输出紧接着送到列译码器这种顺序在实现上更自然也方便和硬件流水线对齐。我在Matlab里就踩过一次坑一开始图省事先按列填充再按行编码结果列译码器拿到的输入顺序和编码器预期不一致迭代第一轮就出现校验位扩散错误查了一整天才定位到是填充顺序问题。2.3 编码正确性的自检方法编码这类模块建议在跑整条链路之前单独做一次自检而且要在“解析结构”层面验不只是看误码率低。我常用的检查方法有三个把编码输出矩阵的每一行重新编码验证是否仍然满足行分量码校验把每一列重新编码验证列分量码校验随机翻转单个比特再对翻转后的矩阵跑一次硬判决迭代译码看能否在行列交替之后恢复出原始信息。第二个方法尤其重要因为单比特翻转是最基本的错误事件如果这一步恢复不了说明编码结构或者译码器本身就有问题不需要急着上软判决。我在初版实现里就因为这个自检发现了行列索引错位——单比特翻转后第一轮行译码能纠正但第二轮列译码又把另一个位置改错了典型的维度索引不匹配。3. 硬判决译码BCH代数译码的工程化写法3.1 Berlekamp-Massey加钱搜索的简化流程硬判决译码说穿了就是代数译码器的那套流程先计算伴随式再用Berlekamp-Massey算法迭代求错误位置多项式最后用钱搜索逐位定位错误并翻转。对于BCH码或扩展汉明码这一步在Matlab里有两种做法效率和对源码的理解深度差别很大。第一种是直接用通信工具箱里的对象比如comm.BCHDecoder或者旧的bchdec函数。优点是代码量小硬件行为也模拟得比较像缺点是当分量码是“缩短码”或者带扩展校验位时需要自己做额外处理。第二种是我在讲源码时更建议的把分量码解码器单独封装成函数内部调用代数译码逻辑。这里给一个用通信工具箱对象的写法工程上非常常见decoder comm.BCHDecoder(CodewordLength, 63, MessageLength, 51); % 输入是列向量输出是信息位 decBits decoder(hardBits);需要注意的是comm.BCHDecoder默认只输出信息位而TPC行译码之后列译码器需要的是“整个合法码字”不是单纯的信息位。所以我一般在译码输出信息位之后再补一次编码恢复完整码字这一步虽然是多余计算但它让迭代结构清晰了不少也避免了因为只传信息位导致列校验失效的隐蔽bug。3.2 硬判决迭代译码行列交替与提前终止硬判决迭代译码的思路很朴素先对每一行做代数译码把每行纠正成合法码字然后拿纠正后的行码字去更新整个矩阵再对每一列做代数译码列译码完成后再回到行如此往复。每完成一轮行列译码错误图样里的错误个数通常会减少。我在实现时会在每个迭代周期后统计“有多少行和列仍然不满足校验”如果行列都全部满足就提前退出不再继续。从工程角度看硬判决迭代主要做两件事一是把行列之间的校验关系用起来二是让“小概率超过分量码纠错能力”的错误通过另一维的信息得到修正。它的缺点是二值化带来的信息损失非常显著一个比特只要判决错它对后续译码的破坏就和普通错误完全一样没有任何“置信度”上的缓冲。这也是硬判决和后面软判决性能差距的核心来源。3.3 硬判决的固有瓶颈硬判决的问题要从信息论角度理解接收信号原本包含幅度或概率信息但硬判决只保留0/1相当于把连续信道变成了二进制对称信道这部分软信息损失在低信噪比区域尤其致命。普通代数译码对“错误个数”是严格敏感的超过纠错能力就进入“译码失败”模式不会给你一个更好的软输出。更麻烦的是硬判决下的错误传播是非对称的。行译码把一个位置纠错了列译码基于这个错误再进行纠正可能又产生新的错误集合。在误码率10^-3到10^-5这个区间硬判决的译码错误模式往往呈现“成片突发”的特点这对上层系统很不友好。所以我自己的习惯是硬判决只用来验证编码链路正确性或者作为软判决算法的初始化底版真正看性能时一定上软判决。4. 软判决译码Chase-Pyndiah算法的四个关键模块4.1 候选码字集合构造软判决译码的核心思路是Chase算法既然代数译码只能纠正固定数目错误那我就生成一组候选码字从中挑一个最合理的。具体做法把每一行的软值序列按幅度排序幅度绝对值最小的p个位置就是“最不可靠位”对这些位做2^p种符号翻转每个翻转组合对应一个待译码的二进制序列再全部送入分量码代数译码器。这个步骤在Matlab里实现起来很干净function cand chase_candidates(r, p) % r: BPSK软值行向量取值1/-1附近 % cand: 2^p x length(r) 的候选软值矩阵 absR abs(r); [~, idx] mink(absR, p); % 最不可靠的p个位置 flipPattern de2bi(0:2^p-1, p, left-msb); % 预计算所有翻转组合 cand repmat(r, 2^p, 1); cand(:, idx) cand(:, idx) .* (1 - 2 * flipPattern); % 翻转等价于取负 end这里p的选取直接决定了复杂度p4就有16个候选p6就是64个每个候选又要跑一次代数译码。我实测下来对于分量码纠错能力t2的情况p取4到5通常已经够用了继续加大p对性能提升很有限但译码时间几乎翻倍。这个“边际收益递减”的拐点是软判决调参里非常值得记住的经验。4.2 竞争码字与对数似然比的逼近有了候选码字集合之后先挑一个欧氏距离最小的码字d作为当前判决码字。然后为了给下一轮迭代提供软信息需要把“判决的自信程度”也传出去。具体做法是找“竞争码字”c也就是在某个比特位置j上和d相反且距离r最近的候选码字。竞争码字存在时软比特可以用两个码字到接收值的距离差来近似如果存在位置j的竞争码字c则LLR(j) ≈ ( |r - c|^2 - |r - d|^2 ) * d(j) / (2σ²)其中d(j)是±1。这个公式本质上是把后验概率里的正态指数项做了“胜者通吃”的近似d离得越近、c离得越远LLR绝对值越大说明这个判决越可靠。竞争码字不存在时说明在当前候选集合里找不到一个“合理且反方向”的码字这时不能直接给0否则会破坏迭代的软信息分布。我在代码里用一个和迭代次数挂钩的经验置信度β代替β随迭代逐渐增大。这个处理方式虽然粗糙但经过Pyndiah提出后在工程上被证明稳健性很好几乎不会让迭代发散。4.3 外信息更新与加权系数α软判决译码真正“Turbo”起来的一步是外信息反馈。每次行或列译码结束后我根据LLR计算出外信息w然后更新下一维译码器的软输入r_new(j) r原始(j) α * w(j)这里有几个容易踩的坑。第一r原始指的是信道接收软值不是上一轮迭代后的值否则多轮迭代后外信息会“滚雪球”式地自我膨胀码字会逐渐偏离真实信道信息第二α不能一开始就给满否则低信噪比区域的外信息噪声会让译码器发散。Pyndiah原始论文里建议α从0逐步递增我在实现里使用的典型序列如下第1次迭代α 0.0第2次迭代α 0.2第3次迭代α 0.3第4次迭代α 0.5第5次迭代α 0.7第6次及以后α 1.0早期迭代不信任外部信息后期逐步放开这个思路和低密度校验码里的阻尼置信传播本质上是同一类思想防止不可靠信息过早污染整体判断。4.4 停止条件与残差校验软判决迭代也不能一直跑下去。我见过不少初学同学把迭代次数设到20次以上结果不仅仿真时间爆炸误码率偶尔反而上升。原因是TPC迭代收敛到一定程度后剩余错误已经不满足“单个分量码可纠正”的形态继续迭代只是在外信息之间来回搬动错误实际上是在原地打转。我用的停止条件有两个满足任意一个就退出达到预设的最大迭代次数通常6到10次足够当前矩阵的所有行和列都通过分量码校验。第二个条件听上去很完美但要小心“伪码字”陷阱——一个矩阵即使每一行每一列各自都满足校验作为二维码字整体未必合法。所以在条件2生效后我还会额外做一次完整重新编码比对确认恢复出的信息位能再次编码出完全一致的行列校验位才认为译码成功。5. Matlab源码结构从单函数到仿真链路5.1 模块划分与关键接口定义Matlab做TPC仿真最忌讳的是把所有功能揉在一个巨型脚本里。我自己习惯把整个链路拆成四个模块每个模块一个文件接口尽量单一函数名输入输出职责tpc_encode信息矩阵、分量码参数码字矩阵、校验位编码端tpc_decode_hdd硬判决矩阵、最大轮数信息位估计、校验统计硬判决迭代译码tpc_decode_sd软值矩阵、p、迭代数信息位估计、外信息记录Chase软判决迭代译码sim_tpc_main信噪比列表、帧数、配置结构体BER曲线、误帧数仿真主入口这么拆分的好处是当你从硬判决切到软判决时编码端完全不需要动只需要把tpc_decode_hdd换成tpc_decode_sd仿真主循环的改动量非常小。我在做扩展汉明码分量和BCH分量两种方案的对比实验时这套结构省了大概一半的重复工作量。5.2 性能优化预计算翻转集合与矩阵化Matlab跑TPC仿真最大的瓶颈就是循环。分量码译码器本身不复杂但每一行每一列都要调用帧数一多就非常慢。有几个优化手段我实际操作下来性价比很高。第一个是预计算翻转集合。Chase算法里的de2bi(0:2^p-1, p)完全可以在循环外算好固定p值后整帧复用。第二个是尽量把“逐行译码”写成矩阵批量操作。如果分量码是扩展汉明码每个码字只有32或64比特完全可以在GF(2)上用查表译码把一个矩阵的所有行同时译完。第三个是用parfor来并行跑不同信噪比点或者不同帧通信工具箱的编解码对象在parfor里通常能正常工作但要留意随机数流的独立管理。如果跑纯MATLAB循环实在吃不消可以把分量码的伯利坎普-马西迭代和钱搜索写成C-MEX整体速度能提升一到两个数量级。不过大多数验证性仿真用上面的预计算加矩阵化就够了。5.3 一个能跑的仿真主循环下面这段主循环代码是我做BER仿真时用的简化框架直接沿用了前面说的模块接口。关键点是编码输出是0/1二进制序列送入BPSK调制时映射成±1接收端拿到的是加了高斯噪声后的连续软值。% 配置参数 cfg.n1 64; cfg.k1 51; cfg.n2 64; cfg.k2 51; cfg.p 4; cfg.maxIter 6; cfg.numFrames 500; snrList 4:0.5:8; berList zeros(size(snrList)); for sidx 1:length(snrList) totalErr 0; totalBits 0; for f 1:cfg.numFrames info randi([0 1], cfg.k2, cfg.k1); txBits tpc_encode(info, rowEncBCH64, colEncBCH64, ... cfg.k1, cfg.n1, cfg.k2, cfg.n2); bpsk 2 * double(txBits(:)) - 1; rx awgn(bpsk, snrList(sidx), measured); rxSoft reshape(rx, cfg.n2, cfg.n1); % 软值矩阵 % 软判决译码返回信息位估计 estInfo tpc_decode_sd(rxSoft, cfg); totalErr totalErr sum(estInfo(:) ~ info(:)); totalBits totalBits numel(info); end berList(sidx) totalErr / totalBits; end注意这里每一帧都重新生成随机信息比特没有做“固定数据跨信噪比重传”的统计法所以帧数不够时BER曲线尾部会有抖动。如果目标误码率是10^-5量级至少要跑到足够多的错误事件数而不是固定帧数后直接看统计。6. 软硬判决实测对比与避坑清单6.1 实测性能我的仿真配置与结果我用两套配置做了软硬判决对比。配置A是分量码扩展汉明码(32,26)乘积码总参数(1024,676)码率约0.66配置B是分量码BCH(63,51)加全校验位变成(64,51)乘积码总参数(4096,2601)码率约0.635。调制方式都是BPSK软判决使用Chase-p4最大迭代6次硬判决也是6次行列迭代但每次做纯代数硬纠。配置码率硬判决迭代BER≈1e-4时的Eb/N0软判决迭代BER≈1e-4时的Eb/N0相对增益A扩展汉明(32,26)0.66约7.2dB约4.6dB约2.6dBBBCH(64,51)0.635约7.5dB约4.4dB约3.1dB这两组绝对数值来自我当时的仿真配置不同的信道模型、停止条件和随机种子会带来零点几dB的差异但趋势非常稳定软判决迭代比硬判决迭代在BER10^-4附近普遍多拿到2到3dB的编码增益而且分量码本身的纠错能力越强这个增益越明显。我观察到的另一个规律是硬判决迭代在分量码纠错能力较弱时很容易出现“错误平台”——信噪比继续升高误码率却下降得非常缓慢。软判决在同样条件下虽然也存在平台但出现的位置明显更靠后。如果你的系统允许一定的译码延迟软判决的收益几乎是“白拿”的。6.2 隐蔽的坑符号映射、LLR符号与数据对齐我梳理了这次实现里最让我头疼的几个坑按优先级排序第一BPSK映射和软值符号。很多人习惯直接把0/1送进译码器但在软判决里必须明确发送端0映射成-11映射成1接收端的软值必须关于0对称。如果映射方向反了Chase候选集合的翻转操作就会变成“往错误方向翻”性能会莫名其妙地劣化好几个分贝。检查这个问题的办法很简单不发噪声直接把无错软值送入译码器看译码能否原样通过。第二译码输出到底是信息位还是完整码字。我在3.1里提过comm.BCHDecoder默认输出信息位而TPC列译码需要完整行码字。如果直接拿信息位去填矩阵列校验永远对不上迭代会在前几轮就挂掉。保险做法是译码输出信息位后再编码一次得到完整码字参与行列交替。第三迭代软输入的“基底”用错了。4.3节提到的r原始始终是信道接收值不能替换成上一轮的r_new。这个坑表现得很隐蔽前两轮迭代误码率看着正常第三轮开始性能飙升式恶化几乎可以断定是外信息积累过量。第四随机数管理和统计口径。用awgn函数时每次调用都会重置全局随机流如果主循环里忘记处理随机种子单帧结果就会不同导致BER曲线抖得非常离谱。我做仿真时统一用rng锁定全局种子并且一个信噪比点连续跑多帧最后把所有帧的错误累加再除总比特数而不是逐帧求平均。6.3 我给新手的调参建议最后分享一个我自己的上手指南。如果你刚要跑通TPC软判决我不建议一上来就追最优性能而是按下面这个顺序调试先固定一个简单的分量码比如扩展汉明码(32,26)把编码和硬判决迭代跑通再加软判决p先设成2迭代3次确认性能比硬判决至少高一截p加到4迭代数加到6观察性能提升分量码再换成BCH(64,51)整体重复一遍。每一步都要确认前一步没问题再往前走。只调一个参数一次只改一维这是排掉TPC这类多模块嵌入式差错特性问题的最快路径。至于“三重迭代还是六重迭代更好”我的经验是工程上迭代超过8次后延迟和功耗的增长已经超过误码率的改善6次是一个比较稳妥的默认值。