ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

5G极化码译码实战:SC/SCL/CA-SCL原理与Python仿真避坑指南

5G极化码译码实战:SC/SCL/CA-SCL原理与Python仿真避坑指南

简介:这份资源面向5G物理层通信的学习者与开发者,聚焦极化码(Polar Code)的编码与译码实现,适合已具备一定通信原理与MATLAB基础、希望深入理解5G NR控制信道编码方案的中高级读者。压缩包共19个文件,以12个.m脚本和4个.mat数据文件为主,另含1个.cpp源文件、1个mexw64动态库及1个pdb调试文件,整体约56KB,涵盖信道加噪、生成矩阵构造、CRC校验、SC译码与信道转移等模块,脚本与数据配套,便于直接运行验证。目前已有427人学习下载。读者可借助其中的编码生成、CRC辅助校验与逐位取消译码等实现,理解极化变换、可靠比特选择与软信息迭代的核心机制,并在此基础上尝试Fast-SSC或Bit-Flipping等低复杂度改进,是研究5G极化码译码算法与性能优化的实用参考。

1. 极化码译码到底难在哪:从一份 5G 链路仿真包说起

如果你最近在找 5G 物理层链路的仿真代码,大概率会撞上极化码(Polar Code)译码这一关。控制信道里它扛着信令,广播信道里它保着系统消息,标准里写得明明白白,可真到自己动手跑一份polar_code_decode的仿真包时,很多人会卡在同一个地方:编码能对上,调制能对上,一到译码就 BER 居高不下,或者干脆跑不出结果。这不是你数学不好,而是极化码译码本身有几个反直觉的坑——SC 串行逐位判决、LLR 的符号约定、冻结位集合的构造,任何一处对不上,整条链路就是黑匣子。这篇笔记就围绕一份典型的 5G 极化码编译码仿真包,把译码这条链路从原理到可复现的代码一步步拆开,适合正在做物理层链路仿真、想跑通极化码端到端 BER 曲线的工程师和研究生。

2. 极化码译码的三种主流算法与选型逻辑

2.1 SC、SCL、CA-SCL 到底差在哪

极化码的核心思想是信道极化:把 N 个独立信道经过线性变换后,一部分变成近乎无噪的“好信道”,另一部分变成近乎全噪的“坏信道”。信息位放在好信道上,冻结位放在坏信道上。译码的本质就是利用这个结构去恢复信息位。

最基础的 SC(Successive Cancellation)译码是逐位串行判决:从第 1 位到第 N 位,每一位都依赖前面所有已判决的位。它的复杂度是 O(N log N),很漂亮,但问题是——一旦前面某一位判错,后面全跟着错,错误传播非常严重。在短码下 SC 的性能离 ML 译码差得远。

SCL(Successive Cancellation List)就是为解决这个问题来的。它不再只保留一条路径,而是同时保留 L 条候选路径,每步扩展后按路径度量(PM)排序,保留最优的 L 条。L 越大性能越好,但复杂度和内存也线性增长。L=8 是常见折中,L=32 在短码下已经非常接近 ML。

CA-SCL(CRC-Aided SCL)是在 SCL 基础上加了一层 CRC 校验。译码结束后,从 L 条路径里选那条 CRC 通过的作为输出。这一步提升非常明显——在 5G 控制信道常用的码长和码率下,CA-SCL 的 BLER 能比纯 SCL 再降一个数量级。5G 标准里 Polar 码的译码就是基于 CA-SCL 的。

选型上我的建议很直接:做链路级仿真验证算法,从 SC 起步把链路跑通;要做性能对比或逼近标准,直接上 CA-SCL,L 取 8 起步,CRC 用 24 位。别在 SC 上死磕性能,它就不是干这个的。

2.2 冻结位集合怎么定:可靠性序列的构造

译码之前必须先确定哪些位置是冻结位、哪些是信息位。这个由可靠性序列决定。5G 标准里用的是一套固定的极化权重(PW)序列,不依赖具体 SNR,直接查表就行。很多仿真包里会带一个reliability_sequence数组,长度 1024,前 K 个位置放信息位,其余冻结。

如果你自己从零构造,常用的方法是高斯近似(GA)或密度进化(DE),但计算量大。工程上直接用 5G 标准给出的 PW 序列最省事,也最不容易出错。注意:不同码长 N 对应的序列是嵌套的——N=512 的序列是 N=1024 序列的子集,这个性质在实现时要利用好,不然每次换码长都重新算,效率很低。

2.3 一份典型仿真包的模块划分

一份结构清晰的极化码仿真包,通常包含这几个模块:

模块功能关键输入关键输出
可靠性序列确定信息位/冻结位码长 N冻结位索引集合
编码器极化变换 + 比特映射信息比特、冻结位编码后码字
调制BPSK/QPSK 映射编码比特符号序列
信道AWGN 加噪符号、SNR接收符号
解调计算 LLR接收符号、噪声方差LLR 序列
译码器SC/SCL/CA-SCLLLR、冻结位译码比特
性能统计BER/BLER 计算译码比特、原始比特误码率曲线

这个划分的好处是每一块可以独立替换和验证。比如你想换译码算法,只动译码器模块就行,其他不用碰。我一般会先把编码器+调制+信道+解调这条“正向链路”单独跑通,确认 LLR 的符号和尺度没问题,再接入译码器。这样出问题的时候能快速定位是链路问题还是译码问题。

3. 用 Python 从零跑通极化码 SC 译码

3.1 编码器的递归实现

极化码编码本质是矩阵乘法 x = u · F^⊗n,其中 F = [[1,0],[1,1]],⊗n 是 n 次克罗内克积。直接构造大矩阵做乘法在 N=1024 时内存吃不消,工程上用递归蝶形运算。

import numpy as np def polar_encode(u, frozen_mask): """ 极化码编码:递归蝶形实现 u: 信息比特序列(长度 N),冻结位位置填 0 frozen_mask: 布尔数组,True 表示该位置是冻结位 """ N = len(u) x = u.copy() # 将冻结位强制置零 x[frozen_mask] = 0 step = 1 while step < N: for i in range(0, N, step * 2): for j in range(i, i + step): x[j] ^= x[j + step] step *= 2 return x

这段代码的逻辑是:每一轮把相邻两个块做异或,块大小从 1 逐步翻倍到 N/2。frozen_mask保证冻结位在编码前就是 0,这是标准要求。参数上唯一要注意的是u的长度必须是 2 的幂,不是的话要么补零要么报错,别让它静默出错。

3.2 LLR 计算与 SC 译码核心循环

SC 译码的输入是解调后的 LLR 序列。BPSK 下 LLR = 2y/σ²,其中 y 是接收符号,σ² 是噪声方差。这个符号约定必须和译码器内部一致,否则全盘皆错。

def sc_decode(llr, frozen_mask): """ SC 译码:逐位串行判决 llr: 解调后的对数似然比序列,长度 N frozen_mask: 冻结位掩码 返回:译码比特序列 """ N = len(llr) u_hat = np.zeros(N, dtype=int) # 存储中间 LLR 的树结构 llr_tree = np.zeros((int(np.log2(N)) + 1, N)) llr_tree[0] = llr.copy() # 递归计算左孩子 LLR(f 函数) def f_func(a, b): return np.sign(a) * np.sign(b) * np.minimum(np.abs(a), np.abs(b)) # 递归计算右孩子 LLR(g 函数) def g_func(a, b, u): return b + (1 - 2 * u) * a # 简化版:按层展开的迭代实现 # 实际工程中常用递归或栈模拟,这里给出核心逻辑 n = int(np.log2(N)) # 自底向上计算 LLR 树 for level in range(n): width = N >> (level + 1) for i in range(width): a = llr_tree[level][2 * i] b = llr_tree[level][2 * i + 1] llr_tree[level + 1][i] = f_func(a, b) # 逐位判决(简化示意,完整实现需要回溯) for i in range(N): if frozen_mask[i]: u_hat[i] = 0 else: u_hat[i] = 0 if llr_tree[n][i] >= 0 else 1 return u_hat

这里要说明的是,上面给出的是 SC 译码的简化框架,真实的 SC 译码需要在 LLR 树上做深度优先遍历,左子树算完 f 函数后判决,再用判决结果算右子树的 g 函数。完整实现大概 80-100 行,核心就是 f 和 g 两个函数加上递归遍历。f_func是 min-sum 近似,比精确的 log-domain 计算快很多,性能损失很小,工程上首选。

参数方面,llr的尺度很关键。如果你的 LLR 幅值整体偏小,f 函数的 min 操作会频繁取到小值,导致判决不可靠。建议在解调后检查一下 LLR 的均值和方差,正常 AWGN 下 LLR 的绝对值均值应该随 SNR 增大而增大。

3.3 从 SC 扩展到 SCL:路径管理和 PM 排序

SCL 在 SC 基础上维护 L 条路径。每条路径有一个路径度量 PM,初始为 0。每步扩展时,对每条路径分别假设当前位为 0 和 1,计算新的 PM,然后从 2L 条候选里保留 PM 最小的 L 条。

def scl_decode(llr, frozen_mask, L=8): """ SCL 译码:保留 L 条路径 返回:最优路径的译码结果 """ N = len(llr) # 路径列表:每条路径包含 (u_hat, pm, llr_state) paths = [(np.zeros(N, dtype=int), 0.0)] n = int(np.log2(N)) for bit_idx in range(N): new_paths = [] for u_hat, pm in paths: if frozen_mask[bit_idx]: # 冻结位只能取 0 new_paths.append((u_hat, pm)) else: # 尝试 0 和 1 两个分支 for bit_val in [0, 1]: # 计算该分支的 LLR(需要完整的树遍历) # 这里用简化 PM 更新示意 llr_val = llr[bit_idx] # 实际应从 LLR 树获取 if bit_val == 0: new_pm = pm + max(0, -llr_val) else: new_pm = pm + max(0, llr_val) new_u = u_hat.copy() new_u[bit_idx] = bit_val new_paths.append((new_u, new_pm)) # 按 PM 排序,保留 L 条 new_paths.sort(key=lambda x: x[1]) paths = new_paths[:L] # 返回 PM 最小的路径 return paths[0][0]

这段代码省略了 LLR 树的完整遍历,重点在路径管理逻辑。实际实现中,每条路径需要独立维护自己的 LLR 状态和部分和(partial sum),内存开销是 L × N × log N。L=8、N=1024 时大概几十 MB,可以接受。L=32 就要注意内存了。

PM 的更新公式这里用的是简化版:如果判决位和 LLR 符号一致,PM 不增加;不一致则加上 |LLR|。这个近似在工程上够用,精确的 PM 更新要考虑 LLR 的分布,但差别不大。

3.4 CA-SCL 的 CRC 校验接入

CA-SCL 就是在 SCL 输出的 L 条路径里,选第一条 CRC 通过的。CRC 用 24 位,多项式用 5G 标准规定的那个。实现上就是在信息位后面附加 CRC 校验位,译码时把 CRC 位也当作信息位一起译,最后校验。

def crc24_check(bits): """CRC-24 校验,多项式 0x864CFB""" poly = 0x864CFB crc = 0 for b in bits: crc ^= (b << 23) for _ in range(1): if crc & 0x800000: crc = ((crc << 1) ^ poly) & 0xFFFFFF else: crc = (crc << 1) & 0xFFFFFF return crc == 0

CRC 校验本身很简单,关键是位置要对:CRC 位要放在信息位之后、冻结位之前,且译码时这些位置不能冻结。很多仿真包在这里翻车——CRC 位被误设为冻结位,导致校验永远不过。

4. 仿真链路里最容易翻车的五个地方

4.1 LLR 符号约定反了,BER 永远 0.5

现象:译码输出和原始比特完全不相关,BER 稳定在 0.5 左右。原因:解调时 LLR 的符号定义和译码器内部不一致。有的代码定义 LLR = log(P(0)/P(1)),有的定义相反。解决:在链路里加一个已知全零码字的测试,看译码输出是否全零。如果不是,把 LLR 取反再试。

4.2 冻结位集合和码长不匹配

现象:换码长后性能突然恶化,或者直接报数组越界。原因:可靠性序列没有按码长正确截取。5G 的 PW 序列是嵌套的,N=512 时应该取 N=1024 序列中对应位置的子集,而不是简单取前 512 个。解决:用标准给出的嵌套规则生成,或者直接用现成的序列表。

4.3 SCL 的 PM 排序不稳定导致路径丢失

现象:SCL 性能时好时坏,L 增大后反而变差。原因:PM 排序时用了不稳定的排序算法,或者 PM 更新公式有误导致数值溢出。解决:用稳定的排序,PM 用浮点数时注意归一化,每步可以减去最小 PM 防止溢出。

4.4 CRC 校验位被错误冻结

现象:CA-SCL 的 BLER 和纯 SCL 一样,CRC 从来没起过作用。原因:冻结位掩码把 CRC 位置也标成了冻结。解决:构造冻结位掩码时,先确定信息位+CRC 位的总长度,再在剩余位置里选冻结位。

4.5 噪声方差估计错误导致 LLR 尺度失配

现象:低 SNR 下性能还行,高 SNR 下反而恶化。原因:LLR 计算时用的噪声方差是固定值,没有随实际 SNR 调整。解决:每个 SNR 点重新计算 σ²,或者用接收信号的统计量在线估计。

5. 把 BER 曲线跑对之后还能做什么

链路跑通、BER 曲线和理论值对得上之后,这份仿真包的价值才真正开始释放。我一般会做三件事。

第一,做算法对比。把 SC、SCL(L=2/4/8/16/32)、CA-SCL 的曲线画在同一张图上,横轴 Eb/N0,纵轴 BLER。你会直观看到 L 从 8 增到 32 的增益已经很小,但复杂度翻了两番——这个 trade-off 在写论文或做方案时是最有说服力的数据。

第二,做码长和码率的扫描。5G Polar 码支持从几十到 1024 的码长,不同码长下最优的 L 和 CRC 长度可能不同。我习惯固定码率、扫码长,找到性能拐点。这个拐点往往就是实际系统里该选的配置。

第三,把译码器做成可综合的定点版本。浮点仿真只是验证算法,真正落地要考虑定点化。重点观察 LLR 的位宽和 PM 的位宽——LLR 一般 6-8 bit 够用,PM 需要更宽防止溢出。定点化之后 BER 损失控制在 0.2 dB 以内就算合格。

最后说一个我自己的习惯:每次改完译码器,先跑一个 N=128、SNR=2dB 的小规模测试,确认译码输出和编码输入一致,再跑完整曲线。这个“后悔药”帮我省过很多次重新跑一晚上仿真的时间。极化码译码的坑大多不在算法本身,而在链路各模块之间的接口约定上,小规模端到端测试是最便宜的排查手段。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进