☰
极化码CA-SCL译码工程实战:原理、参数调整与避坑指南
2026/10/13 8:44:57 网站建设 项目流程

简介:围绕极化码与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 译码的递归结构与错误传播:单路径的天花板

SC(Successive 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_llr

f 函数做的事情是「合并」:两个子节点通过异或关系生成父节点,父节点的可靠度取决于两个子节点里更不可信的那个,所以取绝对值小的那个,符号由两个子节点共同决定。g 函数做的事情是「补偿」:左子节点已经判出来了,右子节点的 LLR 要用左子节点的判决结果对父节点 LLR 做一次修正。这两个函数是整个极化码译码的地基,后面 SCL 的所有路径分裂都建立在它们之上。

SC 的问题在于硬判。每个比特只保留一个结果,一旦某一位判错,这个错误会通过 g 函数进入后续所有比特的 LLR 计算,错误像滚雪球一样往下传。高信噪比下这种错误传播不会消失,只会在 BER 曲线上形成一个平台。这个平台不是靠加 SNR 能压下去的,必须换译码策略。

2.3 SCL 与路径度量:用 L 条路换一次后悔药机会

SCL(Successive 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 太长会吃掉信息位额度,相同码率下有效信息变少。K=128 时用 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 第一组参数怎么定:N=256、K=128、L=8

仿真阶段我建议从 N=256、K=128 开始,也就是码率 0.5。N 太小(比如 128)时极化不充分,CA-SCL 相对 SC 的增益不明显;N 太大(比如 1024)时纯 Python 仿真一个 Eb/N0 点要跑很久,调参效率太低。列表大小 L=8 是性能与复杂度的常用折衷,先跑基线,再往 16 或 32 调,方便观察增益趋势。CRC 用 8 bit,生成多项式用 0x07(CRC-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,σ^2=1/(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_db=0.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 的信道 LLR,BPSK 下由 2*y/sigma^2 得到 paths = [{'pm': 0.0, 'u': np.zeros(self.n, dtype=np.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: # 冻结位固定为 0,LLR 硬判为 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(key=lambda x: x['pm']) paths = candidates[:self.L] # 按 PM 从小到大逐条做 CRC 校验,优先返回通过校验的路径 for p in sorted(paths, key=lambda 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, key=lambda 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 内置 sort,L 小的时候没问题;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 必须显式传给函数。很多参考代码默认 rate=1,用在无编码 BPSK 上没错,但用在码率 0.5 的极化码上,相当于噪声方差算小了一半,仿真曲线会比理论上差 3 dB 左右。判断换算对不对有个简单办法:把 rate 改成 1 跑无编码 BPSK 的 BER,应该能对上理论值 erfc(sqrt(Eb/N0))。如果这个基线对不上,译码器写得再对都没用。仿真时每个 Eb/N0 点至少跑 1000 帧,N=256 的码字下纯 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 提到 32,BLER 不降反升,或者曲线出现抖动。

原因:路径度量 PM 用的是 float32 累加,LLR 绝对值大时 PM 会变得很大,小路径之间的差值被浮点精度吃掉。更麻烦的是 PM 更新用 min-sum 近似,在 LLR 小的区域偏乐观,L 增大后这些不精确的相对排序会影响剪枝决策。

解决:先给 LLR 做饱和度裁剪,比如限制在 ±20 以内,防止单点异常值主导 PM;再把 PM 换成 float64 跑一遍对比。如果 float64 下性能正常,就是精度问题。对数域精确 PM 用ln(1+exp(-|LLR|))替换abs(LLR)也能改善,但计算量会涨,仿真阶段不如直接上 float64 省事。

4.4 仿真速度慢到没法调参:路径复制和排序是瓶颈

现象:一个 Eb/N0 点跑十几分钟,数据点还没跑几个,一天就过去了。

原因:路径管理写得粗糙。每比特分裂时用dict(p)加copy()深拷贝整个 u_hat 数组,L=16、N=256 时每比特要拷贝 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 译码(L=1)跑一条基线,确认信息位选择和因子图实现正确;最后才上 CA-SCL。这条链路走完,剩下的差距基本可以归因于设计 SNR 或 CRC 细节,不至于瞎调。

5. 把 CA-SCL 调得更好用:自适应列表、节点剪枝与奇偶校验

第一个值得上的技巧是自适应列表大小(Adaptive SCL)。CRC 在这里不只是译码结束时的裁判,还可以当作提前终止的信号。做法是把 L 从 1 开始翻倍增长:先用 SC 译码(L=1)跑一次,做 CRC 校验,不通过就把 L 翻倍重译,直到通过或到达预设上限。低 SNR 时可能需要跑到 L=16 甚至 32,高 SNR 时经常 L=1 就过了,平均复杂度远低于固定 L=8 的 SCL。这个技巧对硬件实现尤其友好:译码延迟不固定,但平均功耗和吞吐都能优化。注意 L 要翻倍而不是每次加 1,因为 SCL 的路径分裂天然是 2 的幂结构。

第二个技巧是节点剪枝。极化码的因子图里存在大量 rate-0 节点(全是冻结位)和 rate-1 节点(全是信息位)。对 rate-0 节点,所有比特已知,不需要分裂路径;对 rate-1 节点,LLR 符号直接给出判决结果,也不需要逐比特展开。教学版代码里逐比特跑完整个树,工程版会在递归时先判断节点类型,直接跳过这些冗余计算。N=256、R=0.5 时尾部一大段子码接近 rate-1,剪枝后能省掉将近一半的节点操作,而且对 BLER 没有任何损失。

第三个技巧是加奇偶校验外码,也就是 PC-CA-SCL。CRC 只在译码结束时起作用,中间过程如果错误路径一直带着走,到末尾才被 CRC 否决。奇偶校验的思路是在信息位序列里每隔几个比特插入一个奇偶校验位,取值是前面若干信息位的异或,让每一条路径在一开始就背上局部校验约束,错误路径会更快积累 PM 惩罚。常见做法是每 4 个信息位后插 1 个 PC 位,PC 位约束的窗口长度取 4 或 5。这个技巧在 L 较小时增益明显,配合自适应列表效果更稳。

我自己现在拿到一个新极化码工程,第一步永远是跑 L=1 的 SC 基线,用无编码 BPSK 校准噪声模型,再逐步加列表和 CRC。这个习惯帮我少翻了很多车,也让我在曲线对不上时能快速定位是信道模型的问题还是译码器的问题。CA-SCL 不是黑匣子,把参数一个个拆开验证,你也能把 polar.zip 里的代码调成自己的曲线。希望帮到你。

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

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

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

立即咨询