极化码CA-SCL译码工程实战:原理、参数调整与避坑指南 简介围绕极化码与CA-SCL解码器这份压缩包提供了完整的MATLAB与C实现聚焦Polar码与级联逐次取消列表CA-SCL解码算法面向无线通信研究者、5G编码技术学习者以及需要开展极化码仿真的工程师。资源共16个文件包含12个.m脚本覆盖编码、SCL解码、CRC24校验、AWGN信道模拟、路径度量与似然计算等核心环节2个.cpp源文件用于算法效率优化或MEX编译配合1个.mexw64已编译文件可直接在MATLAB环境调用另附1个.log运行日志便于排错参考整体压缩包仅19KB。目前已有216人学习下载。通过这套代码读者可以逐行研读编码器、解码器和路径管理模块理解信道极化、列表解码中路径概率更新与剪枝的具体实现也可利用C与MEX文件进行性能优化或二次开发。对于关注列表大小L与回溯深度TB影响的算法研究者这套代码还提供了灵活的修改入口并紧密衔接第五代移动通信中的极化码应用是深入学习CA-SCL解码器的实用素材。1. 认识 CA-SCL从极化码到能落地的译码方案拿到一个名为 polar.zip 的代码包里面是 polar code 的 CA-SCL 译码实现——这是不少刚接触信道编码的工程师会遇到的第一道坎。你以为照着论文把 SC 译码写出来就算完了结果误码率曲线在高信噪比下出现一个平得吓人的平台加功率也压不下去。问题不在信道而在译码器单路径的 SC 只要在一个比特上判错错误就会顺着递归结构往下传。CA-SCL 的思路是保留 L 条候选路径再用 CRC 当裁判选出正确的那条它把极化码从「论文算法」变成了「能进系统的方案」。这篇文章从原理讲到实现把信息位选择、列表大小、CRC 多项式这些参数怎么定、坑在哪一次说清适合正在复现论文、写仿真或者准备把极化码放进 FPGA 的从业者。2. 从 SC 到 CA-SCL为什么加了 CRC 的列表译码才是实用方案2.1 信道极化与子信道可靠性信息位和冻结位怎么来的极化码的底层逻辑是信道极化把 N 个独立的二进制输入信道做递归的合并与分裂当 N 足够大时一部分子信道容量趋近于 1另一部分趋近于 0。这个「趋近」不是平均分配而是两极分化。设计编码器时我们把信息比特放在容量接近 1 的高可靠子信道上把收发双方都已知的固定比特通常是 0放在低可靠子信道上这些固定比特就是冻结位。衡量子信道可靠性最经典的指标是巴氏参数 Z(W)它表示该信道上最大似然判决错误概率的上界。Z 越接近 0信道越可靠。理论上取 Z 最小的 K 个子信道放信息位就行。但巴氏参数只在二进制删除信道BEC下有漂亮的闭式解实际系统跑的是 AWGN 信道这时候再用巴氏参数选位置性能和理论预期会差一大截。工程上更常用的是高斯近似把每个子信道上的对数似然比LLR分布近似成高斯分布然后递归递推每个子信道的均值。高斯近似对设计 SNR 敏感设计 SNR 定得高选出来的信息位更激进定得低更保守。这个设计 SNR 是后面仿真对不上参考曲线时最容易被忽略的变量。2.2 SC 译码的递归结构与错误传播单路径的天花板SCSuccessive Cancellation译码按比特序号从 0 到 N-1 顺序估计。估计第 i 个比特时把前面已经硬判出来的 u_hat[0..i-1] 当作已知条件从接收 LLR 中递归算出当前比特的 LLR。这个过程依赖因子图上的两个基本运算f 函数和 g 函数。import numpy as np def f_node(left_llr, right_llr): # f 函数把两个子节点的 LLR 合并成父节点的 LLR # 对应因子图中的异或分支用 min-sum 近似 return np.sign(left_llr) * np.sign(right_llr) * np.minimum(np.abs(left_llr), np.abs(right_llr)) def g_node(left_llr, right_llr, u_left): # g 函数已知左子节点硬判结果后把父节点 LLR 传给右子节点 # u_left 是左子节点的估计值0 或 1 return (1 - 2 * u_left) * left_llr right_llrf 函数做的事情是「合并」两个子节点通过异或关系生成父节点父节点的可靠度取决于两个子节点里更不可信的那个所以取绝对值小的那个符号由两个子节点共同决定。g 函数做的事情是「补偿」左子节点已经判出来了右子节点的 LLR 要用左子节点的判决结果对父节点 LLR 做一次修正。这两个函数是整个极化码译码的地基后面 SCL 的所有路径分裂都建立在它们之上。SC 的问题在于硬判。每个比特只保留一个结果一旦某一位判错这个错误会通过 g 函数进入后续所有比特的 LLR 计算错误像滚雪球一样往下传。高信噪比下这种错误传播不会消失只会在 BER 曲线上形成一个平台。这个平台不是靠加 SNR 能压下去的必须换译码策略。2.3 SCL 与路径度量用 L 条路换一次后悔药机会SCLSuccessive Cancellation List的改动看起来很小每个信息位不再只保留一个硬判结果而是同时尝试 0 和 1 两个分支路径数翻倍等路径数超过预定的列表大小 L就按路径度量Path Metric, PM排序砍掉最差的路径只留 L 条。冻结位不分裂直接置 0。译码结束后从 L 条路径里挑 PM 最小的一条作为输出。路径度量是每条候选路径的惩罚分越小代表越可信。工程上最常用的是对数域的近似更新当前比特的硬判结果和 LLR 符号一致时 PM 不变不一致时 PM 加上该 LLR 的绝对值。def pm_update(pm, llr, bit): # 更新路径度量用 min-sum 近似 ln(1 exp(-x)) hard 0 if llr 0 else 1 return pm (0.0 if bit hard else abs(llr))这个近似在 LLR 绝对值大时非常准LLR 绝对值小时偏乐观。它意味着 SCL 的路径排序在低信噪比下不够精确这也是后面提到「增大 L 性能反而变差」的伏笔。SCL 的复杂度是 O(L * N * logN)L 从 4 提到 16仿真时间基本线性上涨所以列表大小不是越大越好要在性能和算力之间找平衡点。2.4 CRC 辅助选路给列表译码装一个裁判没有 CRC 的 SCL 最终选的是 PM 最小路径。但 PM 只是局部度量的累计PM 最小不代表这条路径一定正确尤其在列表大小不够大的时候。CA-SCL 的 CA 是 CRC-Aided做法是在编码时把 CRC 校验比特附加在信息比特后面译码结束后对 L 条路径的候选信息序列逐一做 CRC 校验选「通过校验」的路径里 PM 最小的那一条。如果 L 条全都不通过才回退到 PM 最小路径。CRC 在这里扮演的是全局裁判PM 衡量的是「每一步像不像」CRC 衡量的是「整条序列对不对」。SCL 负责生成候选集CRC 负责从候选集里挑真货。CRC 长度 r 的选取是个权衡r 太短漏检率高错误路径容易混进来r 太长会吃掉信息位额度相同码率下有效信息变少。K128 时用 8 bit 或 16 bit 都比较常见8 bit 在仿真阶段够用16 bit 在系统级设计里更稳妥。CRC 的生成多项式收发两端必须完全一致这个一致性问题会在避坑章细说。3. 手写一个 CA-SCL 译码器最小工程、参数与仿真验证3.1 拿到 polar.zip 这类工程包先看什么常见做法是这类极化码工程包里会有这么几块极性变换与编码模块、信息位选择模块、SC/SCL 译码模块、CRC 模块以及一个仿真入口脚本。我一般不会一上来就读核心算法那样容易在递归索引里迷路。先跑仿真入口用默认参数跑出一条 BER 或 BLER 曲线确认这个工程本身是闭环的然后从译码器的接口往里看弄清楚输入是信道 LLR 还是硬判决输出是信息位还是带冻结位的完整码字。这两个接口只要有一个理解偏差后面所有调试都会白费。如果你是第一次自己搭工程不是读别人的包建议按「编码器 → 信道 → SC 译码 → SCL → CRC」的顺序逐步加模块。先让无列表的 SC 跑通再上 SCL最后加 CRC。一步到位写 CA-SCL出问题了根本分不清是列表管理错了还是 CRC 对不齐。3.2 第一组参数怎么定N256、K128、L8仿真阶段我建议从 N256、K128 开始也就是码率 0.5。N 太小比如 128时极化不充分CA-SCL 相对 SC 的增益不明显N 太大比如 1024时纯 Python 仿真一个 Eb/N0 点要跑很久调参效率太低。列表大小 L8 是性能与复杂度的常用折衷先跑基线再往 16 或 32 调方便观察增益趋势。CRC 用 8 bit生成多项式用 0x07CRC-8/ATM 风格长度 8 bit 对 128 bit 信息位来说漏检率足够低。参数取值说明码长 N2562 的幂仿真速度和极化效果折中信息位 K128码率 0.5列表大小 L8基线值后续调 16/32 对比CRC 长度8 bit多项式 0x07收发一致设计 SNR0 dB高斯近似选位时使用调制BPSK先别上高阶调制设计 SNR 这个参数容易误解。它不是在仿真的每个 SNR 点重新选信息位而是在整个仿真过程中固定不变。设计 SNR 选 0 dB意味着信息位选择针对 0 dB 附近优化如果你的目标工作点是 2 dB把设计 SNR 也设成 2 dB 往往能得到更好的性能。这个操作在参考代码里常被藏起来导致别人曲线比你好。3.3 信息位选择用高斯近似算可靠性巴氏参数在 BEC 信道下好用AWGN 下不精确。高斯近似把每个子信道的 LLR 分布近似为高斯分布递推均值和方差。这里给出工程上常用的近似实现核心是 φ 函数的闭式近似。def phi_approx(x): # 高斯近似里的辅助函数 phi(x)x 为 LLR 均值 # 该近似在 x 较小时略偏大整体趋势正确仿真够用 x np.maximum(x, 1e-12) return np.exp(-0.4527 * x**0.86 0.0218) def gaussian_approx_reliability(n, design_snr_db): # 返回长度为 n 的可靠性度量数组值越大越可靠 snr 10 ** (design_snr_db / 10) mu np.zeros(n) mu[0] 2 * snr # 初始 LLR 均值BPSK 下 2/σ^2σ^21/(2*snr) for step in range(int(np.log2(n))): half 1 step for j in range(0, n, 2 * half): for i in range(half): a mu[j i] b mu[j half i] # f 分支两个子信道合并可靠性按 phi 函数折减 mu[j i] phi_approx(1 - (1 - phi_approx(a)) * (1 - phi_approx(b))) # g 分支一个子信道已知另一个可靠性近似相加 mu[j half i] a b return mu这段递推对应的是极化码因子图的蝶形结构。f 分支代表异或合并两个同样可靠的信道合并后可靠性会下降g 分支代表条件分支已知一半信息后另一半的可靠性会提升。选位时把返回的 mu 数组从大到小排序前 K 个下标作为信息位其余作为冻结位。注意排序方向取反会直接把仿真结果干到「像没编码一样」这是踩坑高发点。3.4 译码器主循环路径分裂与排序下面是一个教学版的 CA-SCL 译码器类。_bit_llr是递归因子图节点计算的封装教学版里我建议先用 SC 的递归实现顶上确认整体流程正确后再做优化。路径复制的开销比你想象的大过早优化反而难排查。class CASCLDecoder: def __init__(self, n, k, crc_poly, crc_len, list_size, design_snr_db0.0): self.n n self.k k self.L list_size self.crc CRC(crc_poly, crc_len) # 独立实现的 CRC 类 reli gaussian_approx_reliability(n, design_snr_db) order np.argsort(reli)[::-1] # 可靠性从高到低 self.info_pos set(order[:k]) self.frozen_pos set(order[k:]) def decode(self, llr_in): # llr_in: 长度为 n 的信道 LLRBPSK 下由 2*y/sigma^2 得到 paths [{pm: 0.0, u: np.zeros(self.n, dtypenp.int8)}] for i in range(self.n): candidates [] for p in paths: llr_i self._bit_llr(p[u], i, llr_in) if i in self.frozen_pos: # 冻结位固定为 0LLR 硬判为 1 时需要惩罚 p[pm] 0.0 if llr_i 0 else abs(llr_i) p[u][i] 0 candidates.append(p) else: for b in (0, 1): pc dict(p) pc[u] p[u].copy() pc[u][i] b hard 0 if llr_i 0 else 1 pc[pm] 0.0 if b hard else abs(llr_i) candidates.append(pc) candidates.sort(keylambda x: x[pm]) paths candidates[:self.L] # 按 PM 从小到大逐条做 CRC 校验优先返回通过校验的路径 for p in sorted(paths, keylambda x: x[pm]): info_bits p[u][list(sorted(self.info_pos))] if self.crc.check(info_bits): return info_bits[:self.k - self.crc_len] # 全部失败则回退 PM 最小路径这在低 SNR 时很常见 best min(paths, keylambda x: x[pm]) return best[u][list(sorted(self.info_pos))][:self.k - self.crc_len] def _bit_llr(self, u_hat, idx, llr_in): # 这里应实现极化码因子图的递归 LLR 计算 # 工程要点索引需要按生成矩阵的蝶形结构做比特逆序映射 # 教学版先返回 SC 递归实现的结果后续再缓存优化 raise NotImplementedError(请按标准 SC 递归树补全)代码里的路径管理是核心。每比特先把所有候选路径展开排序后截断到 L 条。这里的排序用的是 Python 内置 sortL 小的时候没问题L 到 32 以上、N 到 1024 以上排序就成了性能瓶颈后面会说怎么优化。冻结位不分裂是刻意的冻结位是收发双方约定好的已知比特分裂只会浪费列表容量。CRC 校验放在最后逐条按 PM 顺序试这个顺序保证了「通过校验的路径里 PM 最小」。3.5 最小仿真脚本Eb/N0 与噪声方差的换算仿真最常翻车的地方不是译码器而是 SNR 换算。BPSK 调制下编码比特经过映射 0→1、1→-1 后叠加高斯白噪声。接收端算 LLR 的公式是 2y/σ²但 σ² 到底怎么取取决于你定义的是 Eb/N0 还是 Es/N0。def bpsk_awgn_llr(codeword_bits, eb_n0_db, rate, rng): # BPSK 映射0 - 1, 1 - -1 # Es/N0 Eb/N0 10*log10(rate)因为每个编码比特携带 rate 个信息比特 es_n0_db eb_n0_db 10 * np.log10(rate) sigma2 10 ** (-es_n0_db / 10) x 1 - 2 * codeword_bits.astype(float) noise np.sqrt(sigma2) * rng.randn(len(codeword_bits)) y x noise llr 2 * y / sigma2 return llr注意 rate 必须显式传给函数。很多参考代码默认 rate1用在无编码 BPSK 上没错但用在码率 0.5 的极化码上相当于噪声方差算小了一半仿真曲线会比理论上差 3 dB 左右。判断换算对不对有个简单办法把 rate 改成 1 跑无编码 BPSK 的 BER应该能对上理论值 erfc(sqrt(Eb/N0))。如果这个基线对不上译码器写得再对都没用。仿真时每个 Eb/N0 点至少跑 1000 帧N256 的码字下纯 Python 能在一分钟内跑完一个点低于 1000 帧BLER 曲线会抖得没法看。4. CA-SCL 避坑实战五条让仿真翻车的常见问题与排查路径4.1 CRC 多项式用错误码平台下不去现象BLER 曲线在中等 SNR 处掉到 1e-2 左右就再也不往下走形成一个新的平台。提高列表大小也没用。原因最常见的是收发两端 CRC 多项式不一致。比如编码端用查表法实现多项式 0x07译码端却用按位异或的 0x9B或者 CRC 校验位在编码时被放在了冻结位里信息位索引对不上导致译码端从错误的位置取校验位。CRC 的实现细节多查表法、按位法、初始值、输出异或值任何一项不一致都会让校验形同虚设。解决把 CRC 模块单独抽出来做一次闭环自测。先随机生成一段信息比特追加 CRC 后立刻校验确认 check(append(msg)) 恒为 True再人为翻转一位确认 check 返回 False。这一步过了再接进译码器。多项式一旦固定收发两端都用同一份查表不要边写边换。4.2 信息位选择方向取反BER 高得像没编码现象仿真结果 BER 在 0.1 附近徘徊和目标差了三个数量级而且无论怎么调 L 都不变。原因选位代码里argsort默认从小到大如果直接取前 K 个选的是最不可靠的 K 个子信道。码字几乎全错但 SC 和 SCL 都还在正常跑所以表现不像崩溃更像「没编码」。解决在选位后加一个断言打印信息位索引对应的可靠性均值人工确认前几个位置是不是接近最大值。更稳的做法是统一用一个函数返回order选位时写order[:k]并注释「可靠度从高到低」。信息位和冻结位集合同时发给编码端和译码端编码端用同一份配置别在两侧各写一份选位逻辑不然早晚有一侧写反。4.3 增大 L 性能反而变差路径度量精度不够现象L 从 8 提到 32BLER 不降反升或者曲线出现抖动。原因路径度量 PM 用的是 float32 累加LLR 绝对值大时 PM 会变得很大小路径之间的差值被浮点精度吃掉。更麻烦的是 PM 更新用 min-sum 近似在 LLR 小的区域偏乐观L 增大后这些不精确的相对排序会影响剪枝决策。解决先给 LLR 做饱和度裁剪比如限制在 ±20 以内防止单点异常值主导 PM再把 PM 换成 float64 跑一遍对比。如果 float64 下性能正常就是精度问题。对数域精确 PM 用ln(1exp(-|LLR|))替换abs(LLR)也能改善但计算量会涨仿真阶段不如直接上 float64 省事。4.4 仿真速度慢到没法调参路径复制和排序是瓶颈现象一个 Eb/N0 点跑十几分钟数据点还没跑几个一天就过去了。原因路径管理写得粗糙。每比特分裂时用dict(p)加copy()深拷贝整个 u_hat 数组L16、N256 时每比特要拷贝 32 次、每次 256 字节累加起来很可观。排序用的是全量 sort其实只需要保留前 L 条没必要把 2L 个元素排得严丝合缝。解决用预分配的路径池路径的 u_hat 用写时复制COW而不是深拷贝只有被保留的路径才真正复制。排序改成部分选择或者用堆只取前 L 个最小值。另一个大杀器是节点缓存——很多 LLR 在不同路径之间是共享的不需要每条路径都重算整棵因子树。先把路径复制改掉通常能把仿真时间降一个数量级。4.5 曲线和参考代码对不上Eb/N0 定义差异现象别人的 CA-SCL 曲线在 1.5 dB 到 1e-3你的曲线要 2.5 dB 才到。L、N、K、CRC 全都一样就是差 1 dB。原因十有八九是噪声方差换算里没带码率。还有一种情况是设计 SNR 用得不一样参考代码设计 SNR 取 2 dB你取 0 dB信息位选择不同同一条曲线自然对不上。解决把换算公式统一写成sigma2 1 / (2 * rate * 10^(eb_n0_db/10))并在代码注释里写明。先用无编码 BPSK 验证理论 BER确认噪声模型无误再用 SC 译码L1跑一条基线确认信息位选择和因子图实现正确最后才上 CA-SCL。这条链路走完剩下的差距基本可以归因于设计 SNR 或 CRC 细节不至于瞎调。5. 把 CA-SCL 调得更好用自适应列表、节点剪枝与奇偶校验第一个值得上的技巧是自适应列表大小Adaptive SCL。CRC 在这里不只是译码结束时的裁判还可以当作提前终止的信号。做法是把 L 从 1 开始翻倍增长先用 SC 译码L1跑一次做 CRC 校验不通过就把 L 翻倍重译直到通过或到达预设上限。低 SNR 时可能需要跑到 L16 甚至 32高 SNR 时经常 L1 就过了平均复杂度远低于固定 L8 的 SCL。这个技巧对硬件实现尤其友好译码延迟不固定但平均功耗和吞吐都能优化。注意 L 要翻倍而不是每次加 1因为 SCL 的路径分裂天然是 2 的幂结构。第二个技巧是节点剪枝。极化码的因子图里存在大量 rate-0 节点全是冻结位和 rate-1 节点全是信息位。对 rate-0 节点所有比特已知不需要分裂路径对 rate-1 节点LLR 符号直接给出判决结果也不需要逐比特展开。教学版代码里逐比特跑完整个树工程版会在递归时先判断节点类型直接跳过这些冗余计算。N256、R0.5 时尾部一大段子码接近 rate-1剪枝后能省掉将近一半的节点操作而且对 BLER 没有任何损失。第三个技巧是加奇偶校验外码也就是 PC-CA-SCL。CRC 只在译码结束时起作用中间过程如果错误路径一直带着走到末尾才被 CRC 否决。奇偶校验的思路是在信息位序列里每隔几个比特插入一个奇偶校验位取值是前面若干信息位的异或让每一条路径在一开始就背上局部校验约束错误路径会更快积累 PM 惩罚。常见做法是每 4 个信息位后插 1 个 PC 位PC 位约束的窗口长度取 4 或 5。这个技巧在 L 较小时增益明显配合自适应列表效果更稳。我自己现在拿到一个新极化码工程第一步永远是跑 L1 的 SC 基线用无编码 BPSK 校准噪声模型再逐步加列表和 CRC。这个习惯帮我少翻了很多车也让我在曲线对不上时能快速定位是信道模型的问题还是译码器的问题。CA-SCL 不是黑匣子把参数一个个拆开验证你也能把 polar.zip 里的代码调成自己的曲线。希望帮到你。本文还有配套的精品资源点击获取