简介:面向无线通信与编码理论研究者,资源包提供极化码与CA-SCL(级联逐次取消列表)解码器的完整MATLAB实现,贴合5G新空口信道编码学习需求。包含编码、解码主流程,以及CRC校验、路径度量、BPSK调制、AWGN信道等配套函数,可帮助理解信道极化原理与列表解码中路径维护、分支判决、回溯机制等关键环节。资源共16个文件,其中12个m脚本覆盖系统仿真、信源生成、编译码与误码统计等模块,2个C++源文件用于似然比计算等核心运算,另附编译后的mexw64文件便于直接运行,压缩包仅19KB。日志文件表明资源经过实际运行验证。已有214人学习下载,适合正在学习5G信道编码、或希望复现CA-SCL算法性能的研究者与工程师。通过研读代码并结合列表大小、回溯深度等参数,可直观观察不同设置对误码率的影响,也可在此框架上修改CRC多项式或译码策略,为算法优化提供可扩展基础。
1. Polar码配合CA-SCL,解码链路里最终的一环
Polar码在理论上用SC解码就能达到信道容量,但这个结论默认码长足够大。实际仿真里码长停在256、512或1024的时候,SC解码对中间可靠性信道的判决经常出错,误帧率曲线和最大似然界之间会拖出1~2 dB的差距。SCL解码的思路是在每个信息比特处保留多条候选路径,让错误决策不至于立即杀死整条路径;CA-SCL在这基础上再叠一层CRC约束,在候选路径里选那条能通过CRC校验的,作为最终输出——这正是3GPP NR里Polar码配套的主流接收方案。不少polar code资源包把编码器写得完整,SCL解码部分却只有两行伪代码,导致你自己动手时根本不知道路径度量怎么更新、冻结位怎么处理。这篇文章把polar code的编码构造、CA-SCL的路径维护和参数选择一次讲透,最后给出一套可以在本地直接跑仿真曲线的最小代码骨架,适合正在搭Polar码仿真链路或准备把解码器写成硬件逻辑的通信工程师。
2. Polar码的编码构造:信道极化与信息位选择
2.1 生成矩阵是克罗内克幂,编码器本质是异或网络
Polar码的编码器并不需要真正保存一个 $N \times N$ 的生成矩阵。它建立在这样一个小矩阵上:
$$ G_2 = \begin{bmatrix} 1 & 0 \ 1 & 1 \end{bmatrix} $$
对两个比特 $u_0, u_1$ 做编码,输出是 $[u_0 \oplus u_1, \ u_1]$。这个二输入二输出的变换就是最基本的极化单元。码长取 $N = 2^m$,整个生成矩阵 $G_N$ 是 $G_2$ 的 $m$ 次克罗内克幂,编码运算可以完全展开成蝶形异或网络。
下面这段Python实现了无比特反转版本的Polar编码,输入长度必须是2的幂:
import numpy as np def polar_encode(u: np.ndarray) -> np.ndarray: n = u.size x = u.copy() step = 1 while step < n: for j in range(0, n, 2 * step): # 蝶形运算:前半段与后半段逐位异或 x[j:j + step] ^= x[j + step:j + 2 * step] step <<= 1 return x # 示例:N=4 的编码 u = np.array([1, 0, 1, 1], dtype=np.int8) c = polar_encode(u) print(c) # [1, 1, 0, 1]编码的循环次数是 $\log_2 N$,每一轮步长翻倍。内层循环等价于把长度为step的左半边和右半边做异或,右半边保持不动。注意这里x[j:j+step] ^= x[j+step:j+2*step]是原地向量化写法,它同时修改左侧元素,再用修改后的值参与下一步,恰好对应 $G_N = G_{N/2} \otimes G_2$ 的递归结构。这里要求dtype是整型,如果输入是浮点LLR值就需要先硬判决。
polar_encode只负责把长度为N的输入序列变换成码字。输入序列里哪些位置放信息位、哪些位置放冻结位,在调用这个函数之前就要确定。这引出一个核心问题:信道极化的结果,是把N个等效子信道分成“可靠”和“不可靠”两组,信息位要放在可靠的那一组上。
2.2 用极化权重排序:N=16时信息位具体落在哪里
在AWGN信道下计算每个子信道容量,严格做法是密度演进或高斯近似,但工程上更常用极化权重(Polarization Weight)来快速排序。权重公式是:
$$ P_i = \sum_{j=0}^{n-1} B_j \cdot 2^{j \cdot \frac{1}{4}} $$
其中 $i$ 是子信道序号,$n = \log_2 N$,$B_j$ 是 $i$ 的第 $j$ 个二进制位。权重越大,说明这个子信道越可靠。以 $N=16$ 为例,按 $P_i$ 从大到小的顺序,前8个子信道如下:
| 子信道序号 | 二进制位组合 | 极化权重 $P_i$ |
|---|---|---|
| 15 | 1111 | 5.285 |
| 14 | 1110 | 4.285 |
| 13 | 1101 | 4.096 |
| 11 | 1011 | 3.871 |
| 12 | 1100 | 3.096 |
| 10 | 1010 | 2.871 |
| 9 | 1001 | 2.682 |
| 7 | 0111 | 3.603 |
如果码率 $R = 0.5$,即 $K=8$ 个信息位,这8个序号就是要映射的信息位位置,剩下的位置全部填0作为冻结位。这里值得注意,第7号子信道权重为3.603,明明大于第10号的2.871,排序里却排在后面,因为权重排序要按从上到下的整体次序看,我这里表格只取了总排序的前8个,实际用程序排序后取索引即可。
实际工程里,信息位集合还要避开CRC位置。CA-SCL的CRC位占的也是信息位中的几个位置,所以真实数据比特数是 $K - L_{crc}$。信息位集合通常取最可靠的 $K$ 个子信道,然后把CRC位放在这些位置里最可靠的一段。
2.3 编码前必须先插入CRC,CRC长度和位置都有讲究
CA-SCL里的CRC不是在解码端临时补的,它必须在编码端就参与Polar编码。具体流程是这样:先把 $K - L_{crc}$ 个数据比特输入CRC生成器,得到 $L_{crc}$ 位校验比特,把这两段拼接成长度为 $K$ 的序列,填入信息位位置,冻结位按0填充,最后调用polar_encode。
CRC生成器用线性反馈移位寄存器实现,以CRC8为例:
def crc8(data_bits: np.ndarray, poly: int = 0x2F) -> np.ndarray: crc = 0 for b in data_bits: crc ^= int(b) << 7 for _ in range(8): if crc & 0x80: crc = ((crc << 1) ^ poly) & 0xFF else: crc = (crc << 1) & 0xFF # 返回8位校验比特 return np.array([(crc >> (7 - k)) & 1 for k in range(8)], dtype=np.int8)这里的poly = 0x2F是常用CRC8多项式的十六进制表示,最高位固定隐含。CRC位拼接在数据位之后,放入信息位集合时放在最可靠的那几个位置。为什么放在最可靠的位置?因为SCL解码越靠后的比特可利用的路径数越少,错误概率越高,把CRC放在可靠位置能提高最终校验通过的路径比例。CRC长度取8到16比较常见,太短会造成校验冲突,即错误路径也碰巧通过了CRC;太长会挤占数据位,降低有效码率。
3. CA-SCL解码:路径度量、列表剪枝与CRC终选
3.1 SC解码的LLR传递拆成f和g两种运算
SC解码是在编码因子图上做的置信传播。对每一级蝶形运算,从信道收到的LLR向左传播,判决值向右回传。两个核心公式用最小和近似表达:
- f运算(合并两个LLR):
$$ f(a, b) = \text{sign}(a) \cdot \text{sign}(b) \cdot \min(|a|, |b|) $$
- g运算(利用已知左侧比特更新右侧LLR):
$$ g(a, b, \hat{u}_l) = b + (1 - 2\hat{u}_l) \cdot a $$
其中 $\hat{u}_l$ 是左侧已经判决的比特值。解码从最右侧的信道LLR开始,逐级向左传播,到达叶子节点后做硬判决,再把判决结果向右回传,逐级计算出完整的信息比特序列。
这个递归结构是SCL的基础。SCL与SC的差别在于:SC在叶子节点只保留一个判决;SCL在叶子节点同时保留“判0”和“判1”两条扩展路径,然后根据路径度量裁剪。
3.2 路径分裂与路径度量更新规则
每条路径都有自己的LLR中间值和已经判决的比特序列。当SCL解码器处理某一个信息位时,对当前路径分裂成两条候选:当前位判0和判1。路径度量PM衡量这条路径的可靠程度,更新规则是:
- 如果扩展出的比特与当前硬判决一致,PM不变;
- 如果与硬判决不一致,PM增加 $|\text{LLR}|$。
更准确地说,硬判决是 $\hat{u} = 0$ 当 $\text{LLR} \geq 0$,否则 $\hat{u} = 1$。若扩展比特 $u=0$ 但 $\text{LLR}<0$,路径的PM就要加上 $|\text{LLR}|$;反之亦然。PM越小,路径越可靠。这个累加式保证PM实在所有候选路径上的一个单调惩罚值,解码结束时直接取最小PM的路径是SCL的默认输出。
冻结位的处理要单独说明:冻结位是编码端已知的固定值,解码时不应分裂,直接把该值扩展到所有存活路径上。如果SCL在冻结位上生成分支,等于把错的路径也保留了,浪费列表容量。常见实现里对冻结位会直接跳过分裂步骤。
路径分裂后,总路径数可能达到 $2L$,需要按PM排序,只保留PM最小的 $L$ 条。这一步就是列表剪枝。
3.3 CRC校验参与终选:从“最小PM”改成“通过CRC的最小PM”
SCL解码到最后一个信息位时,手里有 $L$ 条完整路径。不加CRC的普通SCL直接挑PM最小的那条;CA-SCL则先对每条路径提取信息位序列,算CRC,选出能通过CRC校验的路径中PM最小的一条。做CRC校验时要把路径中映射到信息位位置的比特取出来,按原始顺序还原成数据位+CRC位的拼接,再对数据位部分重新计算CRC,与路径里的CRC位对比。
这里有一个关键的工程细节:CRC校验不一定要留到全部K个信息位解完。有些实现会在每扩展完一个CRC位时立即检查CRC,通过校验的路径保留,未通过的直接淘汰。这个方法能提前剪掉错误路径,给后面的比特留出更多列表容量,但代价是实现复杂度高,而且CRC位的判决仍然有错误风险。最常见的做法仍是全部解完再统一CRC。
CA-SCL解码器的复杂度主要被列表大小 $L$ 控制。SC单独的解码复杂度是 $O(N \log N)$,SCL的复杂度近似 $O(L \cdot N \log N)$。列表扩容一倍,延迟和功耗都显著上升,这也是为什么工程里L不会设得特别大的原因。
4. 复现:写一个N=256的最小CA-SCL解码器闭环
4.1 信道模型:BPSK调制下的LLR计算
仿真链路用BPSK调制加AWGN信道。编码后的比特0映射为+1,比特1映射为-1,接收信号 $y$ 是发射符号加上均值为0、方差为 $\sigma^2$ 的高斯噪声,噪声方差由信噪比决定:
$$ E_b/N_0 = \frac{1}{2 R \sigma^2} $$
LLR可以直接算出来:
$$ \text{LLR} = \frac{2y}{\sigma^2} $$
注意 $\sigma^2$ 必须按码率修正。R = K/N,在固定 $E_b/N_0$ 下码率越高,噪声方差越大。很多初版实现把 $\sigma^2$ 按无码率修正算,仿真出来的FER曲线会整体偏移0.5 dB以上。
4.2 SCL路径维护的Python核心循环
完整SCL解码器涉及因子图递归,代码量较长。下面这段只关注路径分裂与剪枝,它是整个SCL最核心的数据结构部分,配合递归LLR传播就能组装出完整解码器:
def scl_extend_paths(paths, L, llr_tensor, bit_pos, is_frozen, frozen_bit): """对位置 bit_pos 做路径扩展与剪枝。 paths: 列表,每个元素是 (pm, partial_bits, internal_state) llr_tensor: 当前该比特对应的LLR值,长度与路径数一致 """ new_paths = [] for path_idx, (pm, bits, state) in enumerate(paths): if is_frozen: # 冻结位不分裂,所有路径一致扩展 new_paths.append((pm, bits + [frozen_bit], state)) continue llr = llr_tensor[path_idx] hard_bit = 0 if llr >= 0.0 else 1 # 分裂成判0和判1 for b in (0, 1): if b == hard_bit: pm_new = pm else: pm_new = pm + abs(llr) new_paths.append((pm_new, bits + [b], state)) # 按PM升序排序,保留前L条 new_paths.sort(key=lambda t: t[0]) return new_paths[:L]路径分裂逻辑在这个函数里是完整的。llr_tensor的下标对应路径索引,也就是说每条存活路径在该比特位置都有自己的LLR值,这与SCL实现里每条路径保存独立中间状态的要求一致。bits保存该路径到目前为止的完整判决序列,解码结束后对它提取信息位做CRC校验。state是路径的因子图内部节点值,实际解码时需要随每条路径复制一份,这是SCL内存开销最大的部分。
这里特别强调llr_tensor[path_idx]的取值:它必须来自当前路径自己的递归节点,不能用一个共享LLR数组同时喂给所有路径。这也是许多半成品实现最容易写错的地方——把路径当成共享变量,扩展时互相覆盖。
一个可用的时隙分配是:列表L取8,CRC取16位,CRC多项式用0x1021(CRC16-CCITT),数据和CRC一起占用全部128个信息位位置。代码层面把编码好的K位序列填入信息位向量后调用polar_encode。
4.3 三个最容易写错的位置:PM符号、冻结位、LLR缩放
PM更新看起来就是一行加法,实际调bug能折腾一晚上。第一个坑是PM增量的符号。按本节的公式,决策与硬判决不一致时PM加上绝对值,但很多资料会写成 $PM \mathrel{+}= -|\alpha|$,因为LLR定义方向不同。LLR符号定义一改,PM的加减也要跟着倒;没有统一约定前不要混用。
第二个坑是冻结位不能参与分裂。有的实现为了简化循环,把冻结位统一按0处理,但忘记给非零冻结位留扩展路径。如果某些冻结位是1,解码器却永远判0,编码构造就失去意义。正确做法是构造信息位向量时冻结位全部置0,解码端is_frozen直接跳过分裂。
第三个坑是LLR未按噪声方差缩放。LLR公式里的 $\sigma^2$ 漏掉,路径度量和理论推导对不上,FER曲线会高出一大截。查这个问题的办法是单独打印一帧的LLR统计值,观察方差是否接近 $2/\sigma^2$。
参数速查表:
| 参数 | 推荐值 | 影响方向 |
|---|---|---|
| N | 256 | 码长越大,极化越充分 |
| K | 128 | 码率R=0.5,信息位个数 |
| L | 8 | 列表越大,性能越好但复杂度线性涨 |
| CRC长度 | 16 | 太短会误通过,太长挤占数据位 |
| 调制 | BPSK | 仿真最简,换QPSK可等效两路BPSK |
5. 验证与调优:用L=1作为基准,先证明实现没写歪
拿到一套新写的CA-SCL解码器,第一件事不是急着把L调到16,而是先做退化自检。把列表大小L设为1时,SCL退化成普通SC解码。如果你的实现正确,关闭CRC辅助逻辑后,FER曲线应该和标准SC解码器的曲线完全重合。这一步通过,说明LLR传播、路径度量更新、冻结位处理三条主线都没问题。
第二步做CRC开关对比。同一份代码里,把CA-SCL的终选逻辑从“CRC通过+PM最小”改成“纯PM最小”,此时性能应该比L=8的普通SCL还要差一些。这是CA-SCL的核心增益来源。如果两条曲线重合或CA-SCL反而更差,多半是CRC位没有正确放进信息位集合,解码时CRC占的位置被当成数据位解读了。
第三步看列表增益的规律。L从1、2、4、8、16增大,FER改善逐渐收敛。在 $E_b/N_0 = 2.5$ dB附近、N=256、码率0.5的场景,大致增益是:L=2比L=1有约0.3 dB增益,L=8比L=2约0.2 dB,L=16比L=8不到0.1 dB。每条仿真曲线至少跑2000帧错误事件再平均,否则只看单帧就判断算法好坏没有意义。
验证完毕后的调优重点放在CRC多项式选择和信息位集合的微调上。CRC多项式换成0x2F还是0x1021,在不同码长下的误检概率差异并不大,优先选实现成本低、表格查起来快的那个。信息位集合用极化权重还是高斯近似排序,在N=256时差距很小,但N超过1024后建议用密度演进得到的可靠性序列,曲线尾部会更干净。把CRC位分布到信息位中的不同位置试几十帧,也能找到对当前码率更友好的配置。
解码器全部完成后,把编码器输出固定一帧,重新初始化随机种子,对比不同L值的解出的信息位是否一致。这个自检能快速暴露列表剪枝排序的不稳定问题,特别是PM出现相等值时,排序算法是否稳定会直接改变最终选路。
本文还有配套的精品资源,点击获取