极化码MATLAB仿真入门:编码、可靠度与SC译码实现
2026/9/16 14:25:51 网站建设 项目流程

简介:这是一份面向通信工程、电子信息类学生及研究人员的MATLAB极化码系统仿真代码包,聚焦5G极化码从构造、编码、信道模拟到SC/SCL/BP译码与误码率分析的全流程实现。压缩包共44个文件,其中39个m脚本覆盖序列生成、信息位选择、速率匹配、CRC校验、子块交织、SCL译码、UCI编解码等核心模块;另有2个txt序列参数、2个mat数据文件及1个说明文档,便于直接运行与对照学习。资源包仅32KB,体量轻但功能完整。目前已有526人学习下载。通过该资源可掌握5G NR极化码的完整MATLAB实现思路,获得可直接调用或二次开发的仿真脚本,适合以此为基础展开性能对比、算法优化或毕业设计实验。

1. 极化码系统仿真前,先把编码、可靠度、SC 译码这三件事对齐

极化码是少数能在理论上严格证明逼近香农极限的编码方案,但“理论简单”和“仿真能跑通”经常是两回事。很多人看论文知道要用生成矩阵和 SC 译码,真到 MATLAB 里写系统仿真时,却容易被比特顺序、LLR 符号、可靠度排序和蒙特卡洛帧数这些细节拖住。这篇博文适合准备做编码研究或通信链路验证的工程师,目标很明确:用 MATLAB 从零搭出一条可复现的极化码系统仿真链路,先按 N=256、码率 1/2 跑通,再平滑扩展到更长码长、CRC-SCL 和码率匹配。这里不依赖额外通信工具箱,核心代码用基础 MATLAB 函数就能运行,方便你拆开看每一步的输入输出。

2. 生成矩阵与可靠度计算:MATLAB 里搭极化码编码器

2.1 为什么编码不用查表,而要先写一个 Kron 递归

极化码的编码器本质是一个二进制线性变换:码字x = u * G_N,其中G_NN x N的生成矩阵。Arikan 的原始构造从核矩阵F = [1 0; 1 1]出发,通过克罗内克幂得到G_N = F^{⊗n},这里N = 2^n。也就是说,编码部分并不需要提前查 3GPP 的表格,写一个递归的 Kronecker 积循环就够了。

使用 MATLAB 的kron函数时,递归方向要固定。常见做法是G = kron(F, G),也就是每次把新的核矩阵放在左边。如果换成kron(G, F),得到的生成矩阵在比特排列上是等价的,但信息位集合的索引会变化。所以我的建议是:选定一种方向后在所有脚本里保持一致,不要在编码和译码之间混用。

2.2 用 kron 函数生成 N 阶生成矩阵

下面这个函数生成N = 2^n阶生成矩阵:

function G = polar_gen_matrix(n) % n: 极化层数,N = 2^n % 返回 N x N 的二进制生成矩阵 F = [1 0; 1 1]; G = 1; for k = 1:n G = kron(F, G); % 按 F 在左、旧矩阵在右的方式递归 end end

调用方式:

N = 256; G = polar_gen_matrix(log2(N));

这段代码的核心是kron(F, G)。第一次循环得到F,第二次得到F ⊗ F,第三次得到F ⊗ F ⊗ F。由于克罗内克积满足结合律,这个循环其实就是计算F^{⊗n}。对于N=256G256 x 256的 double 矩阵,占内存约 0.5 MB,直接做mod(u * G, 2)没问题;但如果把N拉到 4096,就要考虑用逻辑矩阵或分块运算,否则中间乘法会很慢。

2.3 BEC 巴氏参数:用递归挑选信息位

生成矩阵只解决了“怎么编码”,接下来要解决“哪些位置放信息比特”。极化码把N个原始信道分成可靠和不可靠两组,信息位应放在可靠度最高的K个位置上。可靠度可以用 BEC 假设下的巴氏参数Z来估计,也可以在高斯信噪比下用高斯近似。这里先用 BEC 递推,因为逻辑最直观、可复现性最强。

% 以 BEC 擦除概率 eps=0.5 为例,计算 N=256 各子信道可靠度 N = 256; K = 128; eps = 0.5; Z = eps; for n = 1:log2(N) Znew = zeros(2*numel(Z), 1); Znew(1:2:end) = 2*Z - Z.^2; % 分裂出的“坏”信道 Znew(2:2:end) = Z.^2; % 分裂出的“好”信道 Z = Znew; end [~, idx] = sort(Z); info_bits = sort(idx(1:K)); % 选最可靠的 K 个位置

递归公式很简洁:上支路Z1 = 2Z - Z²,下支路Z2 = Z²。BEC 下Z越小信道越可靠,所以排序后取前K个即可。这里info_bits是 1-based 索引,和 MATLAB 的数组下标一致,后面编码和译码都用它定位信息位。

需要说明的是,这个可靠度集合是在eps=0.5下算出来的,相当于一种“中等擦除”假设。实际 AWGN 信道下,不同Eb/N0的最优冻结位集合并不完全相同。初级仿真可以固定一套,做精细化仿真时建议用高斯近似在每个工作信噪比下重新生成info_bits

2.4 编码函数、参数表与自洽性检查

有了生成矩阵和信息位集合,编码函数可以写成:

function x = polar_encode(u_all, G) % u_all: 长度 N 的 0/1 向量,冻结位已经填 0 % G: N x N 生成矩阵 x = mod(u_all * G, 2); end

注意u_all是完整的N比特向量,而不是只把K个信息位塞进去。信息位只占据info_bits位置,其余位置为 0。这样设计的好处是编码和后面 SC 译码的索引完全一致。

下表是初始仿真推荐的一组参数:

参数说明
N256码长,必须是 2 的幂
K128信息比特数,码率 R=0.5
eps0.5BEC 可靠度估计用擦除概率
Gpolar_gen_matrix(8)256x256 生成矩阵
frozen_bits非 info_bits 位置冻结位固定填 0

写完之后建议先做一次自洽检查:随机生成K个信息比特,填充成u_all,编码后马上做一次无噪声 SC 译码,看是否能恢复出u_all(info_bits)。如果这里都不对,后面加噪声没有任何意义。

2.5 三个容易踩的坑:比特序、冻结位集合和固定可靠度

第一个坑是比特反序。部分教材里的G_N = B_N F^{⊗n}带了比特反序矩阵B_N,而我们这里直接用kron(F, G)构造,没有额外反序。这不是错误,是等价表示下选择了不同的信息位集合。只要编码、可靠度递归、译码器三者使用同一套索引,曲线就是对的。第二个坑是冻结位集合弄反。信息位选Z最小的K个,冻结位是剩下的;如果反了,系统会直接崩掉。第三个坑是用一套固定可靠度跑所有信噪比。对入门仿真是可以的,但如果你发现低 SNR 区域误码率平台下不来,先检查是不是冻结位集合对当前 SNR 太不匹配。

3. AWGN 信道下用 SC 译码跑通最小极化码系统仿真

3.1 系统仿真链路与 LLR 的符号约定

实际的极化码系统仿真可以拆成发射端、信道和接收端三部分。发射端先做可靠度排序,把信息比特放到info_bits位置,然后乘生成矩阵得到码字x;调制采用 BPSK,映射关系固定为0 -> +1, 1 -> -1;信道模型用 AWGN;接收端把接收符号转成对数似然比 LLR,交给 SC 译码器恢复所有比特。

LLR 的符号约定是整个链路的命门。接收信号y = s + n,其中s是 BPSK 符号,n ~ N(0, σ²)。对等概率二进制输入,LLR 定义为log(P(b=0|y)/P(b=1|y)),在 AWGN 下可以直接写为LLR = 2y/σ²。LLR 为正代表译码器倾向 0,为负倾向 1。这里的σ²不是随意设的,它和码率REb/N0的关系是:

σ² = 1 / (2 * R * 10^(EbN0_dB/10))

推导思路不复杂:BPSK 每个码字符号能量归一化为 1,每个信息比特等价到1/R个符号,所以Eb/N0 = 1/(2Rσ²)。很多仿真结果对不上,就是因为直接用σ² = 1/(2*SNR)而漏掉了码率因子。

3.2 min-sum SC 译码的递归实现

SC 译码器不需要遍历整棵信道极化树,它用两个核心运算递归完成:

  • f(a, b) = sign(a)·sign(b)·min(|a|, |b|),对应左子节点
  • g(a, b, u_left) = b + (-1)^u_left · a,对应右子节点

这里用的是 min-sum 近似,比精确 LLR 计算少很多对数和指数运算,性能损失在 0.1 dB 量级,适合先把链路跑通。下面是一个可直接保存为sc_decode.m的递归实现:

function u_hat = sc_decode(alpha, info_bits) % alpha: 接收端对数似然比,长度 N % info_bits: 逻辑向量,1 表示信息位,0 表示冻结位 N = numel(alpha); u_hat = zeros(1, N); decode_node(1, N, alpha); function beta = decode_node(start_pos, node_len, node_llr) if node_len == 1 if info_bits(start_pos) beta = node_llr < 0; % LLR 为负判为 1 else beta = false; % 冻结位固定为 0 end u_hat(start_pos) = beta; return; end half = node_len / 2; llr1 = node_llr(1:half); llr2 = node_llr(half+1:end); % f 运算:给左子节点 alpha_left = sign(llr1) .* sign(llr2) .* min(abs(llr1), abs(llr2)); beta_left = decode_node(start_pos, half, alpha_left); % g 运算:给右子节点 s = 1 - 2 * double(beta_left); alpha_right = llr2 + s .* llr1; beta_right = decode_node(start_pos + half, half, alpha_right); beta = [beta_left, beta_right]; end end

这段代码的逻辑是:先把当前节点的N个 LLR 分成左右两半,用f算出左子节点的输入,递归译码得到左子节点所有比特估计beta_left;再把这些估计代入g运算,得到右子节点的输入,继续递归。叶子节点是最终硬判决位置,信息位按 LLR 符号做判决,冻结位直接返回 0。

注意嵌套函数decode_node可以直接读写外层u_hatinfo_bits,但 MATLAB 的嵌套函数在递归调用时会有额外开销。N=256时单帧递归调用深度为 8,速度还能接受;如果跑到N=2048以上,建议改成迭代式 SC 或直接用coder生成 C 代码。

3.3 可复现的蒙特卡洛仿真脚本

下面的脚本把编码、AWGN 信道、SC 译码和误码统计串在一起。每个信噪比点先用少量帧验证稳定性,遇到错误帧就累积,统计BLERBER

N = 256; K = 128; R = K / N; G = polar_gen_matrix(log2(N)); % 用 BEC 递归计算信息位 eps = 0.5; Z = eps; for n = 1:log2(N) Znew = zeros(2*numel(Z), 1); Znew(1:2:end) = 2*Z - Z.^2; Znew(2:2:end) = Z.^2; Z = Znew; end [~, idx] = sort(Z); info_bits = sort(idx(1:K)); frozen_idx = setdiff(1:N, info_bits); info_bits_flag = false(1, N); info_bits_flag(info_bits) = true; EbN0_dB = 0:0.5:4; min_frame_errors = 100; max_frames = 2e5; for snr_idx = 1:length(EbN0_dB) EbN0 = 10^(EbN0_dB(snr_idx)/10); sigma2 = 1 / (2 * R * EbN0); sigma = sqrt(sigma2); frame_err = 0; bit_err = 0; frames = 0; while frame_err < min_frame_errors && frames < max_frames u = randi([0 1], 1, K); u_all = zeros(1, N); u_all(info_bits) = u; x = polar_encode(u_all, G); s = 1 - 2 * x; % BPSK: 0 -> +1, 1 -> -1 noise = sigma * randn(1, N); y = s + noise; llr = 2 * y / sigma2; u_hat_all = sc_decode(llr, info_bits_flag); u_hat = u_hat_all(info_bits); if ~isequal(u_hat, u) frame_err = frame_err + 1; bit_err = bit_err + sum(u_hat ~= u); end frames = frames + 1; end BLER(snr_idx) = frame_err / frames; BER(snr_idx) = bit_err / (frames * K); end semilogy(EbN0_dB, BLER, 'o-', EbN0_dB, BER, 's-'); xlabel('Eb/N0 (dB)'); ylabel('BLER / BER'); grid on;

脚本里最值得注意的变量是info_bits_flag。它在进入sc_decode前被转成逻辑向量,1 表示这一位是信息位;冻结位在u_all中本来就是 0,SC 译码也会把冻结位强制判为 0。这样编码和译码共享同一套索引,不会出现英文文献里常见的A^c集合混淆。

3.4 仿真配置表:先按这些值跑,再改参数

配置项建议值说明
N256编码矩阵和 SC 树的规模刚好合适
K128R=0.5,信息位占一半
调制BPSK方便 LLR 公式验证
译码算法min-sum SC先验证链路,再换 SCL
每 SNR 错误帧数100减少蒙特卡洛抖动
max_frames2e5防止低 SNR 点死循环
SNR 步长0.5 dB曲线足够平滑,又不至于太慢

如果电脑性能有限,可以先把min_frame_errors改成 30 跑通全流程,确认没有索引错误后再加帧数。直接跑 100 个错误帧在低 SNR 时很快,但在高 SNR 区域可能非常慢,用max_frames做保护是必要的。

3.5 如何确认译码和编码顺序没有错位

最简单的方法是把脚本里的noise设为全 0,也就是做一个无噪声极限测试。这时 LLR 的绝对值很大,符号完全由发送比特决定,SC 译码应该能逐比特恢复u_all。如果无噪声测试都出现错误,问题几乎一定出在生成矩阵递归方向、BEC 可靠度索引或 SC 译码的左右子节点顺序上。建议先调试这一关,再加噪声。

4. 码率匹配、CRC-SCL 与仿真参数的调优方向

4.1 从 SC 到 SCL:路径度量 PM 与列表剪枝

SC 译码器在译码一个比特时只保留一条路径,一旦判错,后续比特可能跟着错。CRC 辅以 SCL 译码的思路是:在海选阶段保留L条候选路径,最后用 CRC 校验挑出真正合法的信息比特序列。这里的L也叫列表大小,常用值有 2、4、8、16、32。每条路径需要一个路径度量 PM 来度量它和接收信号之间的距离。

路径度量更新的核心思想很朴素:如果当前候选比特和该位置的硬判决不一致,就给这条路径加上一个惩罚;一致则不增加惩罚。用近似表达式写就是:

% 在当前路径 pm 上,根据当前比特的 LLR 分裂出两条新路径 for b = 0:1 if b == (llr < 0) % 与硬判决一致 pm_new(b + 1) = pm; else pm_new(b + 1) = pm + abs(llr); end end % 每处理完一个信息位,按 pm_new 升序保留前 L 条

这个近似在实际工程中足够稳定。更精确的形式是pm_new = pm + log(1 + exp(-(1-2b)·llr)),在|llr|较大时退化成上面的近似。要注意的是:SCL 不只是简单地在每个叶子节点保留L个硬判决,它需要维护每条路径的中间节点 LLR 和部分和。在上面的代码片段里,llr只是当前叶子的软信息,完整的 SCL 实现还会在f/g运算时按路径分别存储。

4.2 CRC 附加的两种写法

CRC 的主要作用是从 SCL 保留的L条路径中选出正确路径。如果所有保留路径都没通过 CRC,则退而选择 PM 最小的一条。为了不让仿真依赖通信工具箱,可以先写一个最简 CRC-8 计算函数:

function crc_bits = crc8_bits(bits) % bits: 0/1 行向量,返回 8 位 CRC 余数 % 生成多项式 0x07,初始值为 0,MSB-first reg = uint8(0); poly = uint8(7); for i = 1:numel(bits) fb = xor(logical(bits(i)), bitand(reg, uint8(128)) ~= 0); reg = bitand(bitshift(reg, 1), uint8(255)); if fb reg = bitxor(reg, poly); end end crc_bits = zeros(1, 8); for i = 1:8 crc_bits(i) = double(bitand(bitshift(reg, -(8-i)), 1)); end end

调用方式是在信息比特u_msg后面直接拼接 CRC:

u_crc = [u_msg, crc8_bits(u_msg)]; K_total = length(u_crc); % 注意此时 K 要包含 CRC 位

这段 CRC 只用于 SCL 选路演示,不是 3GPP 标准里的 24 位 CRC。做标准对比时,请替换成目标系统规定的 CRC 多项式。如果你装了 Communications Toolbox,也可以直接用comm.CRCGenerator,但手写版本能让你更清楚 CRC 是在哪个环节影响码率。

4.3 速率匹配:打孔和缩短的取舍

极化码天然要求码长是 2 的幂,但实际系统需要的码长往往不是。速率匹配常见做法有打孔和缩短。打孔是删掉某些码字比特不发送,缩短是固定某些码字比特为 0 再删掉。两者都会影响可靠度排序,缩短通常保留更好的距离特性。

N = 256; % 编码器码长 target_len = 192; % 实际发送的码长 shorten_mask = true(1, N); shorten_mask(end - (N - target_len) + 1 : end) = false; x_tx = x(shorten_mask); % 通过缩短得到目标码长

这里的shorten_mask把后半段码字屏蔽掉,属于一种比较粗糙但能跑通的缩短方式。工程上更常用的是按可靠度顺序构造的 QUP 算法,但实现复杂度高很多。如果只是验证系统仿真流程,先用尾部缩短或均匀打孔,把误码率对比出来再细化。

4.4 仿真参数表:L、错误帧数、CRC 长度和 SNR 步长

参数入门值调优方向
L8先 4 后 16,对比 BLER 下降幅度
CRC 长度8选 16 或 24 可减少误检概率
每 SNR 错误帧数100曲线平滑后再提到 300
SNR 步长0.5 dB接近瀑布区可改 0.1 dB
可靠度更新固定 eps=0.5随 SNR 做高斯近似
并行单核多 SNR 点用 parfor

列表大小L从 4 增到 8 通常能带来明显增益,但从 16 增到 32 时收益递减,同时仿真时间几乎线性上涨。CRC 长度也不能盲目加长,因为它会挤占有效信息位,码率会下降。做曲线对比时,需要固定有效信息位数不变,而不是固定总编码长度不变。

4.5 调优的先后顺序

我一般会按照这样的顺序调:先确认无噪声极限下的 SC 译码完全正确;再在低 SNR 区域用少帧数跑通 SC 基线;接着加 CRC-SCL,观察相同 SNR 下 BLER 是否下降;最后才做速率匹配和可靠度随 SNR 更新。

如果在 SCL 跑完后 BLER 没有明显改善,先检查 CRC 是否真的参与了编码。常见的错误是:编码时把 CRC 位放在冻结位里,译码时又把 CRC 位当信息位,这样 SCL 最后无法选出正确路径。另一个常见问题是每条路径的 PM 没有随f/g运算同步更新,导致剪枝剪掉了正确路径。

5. 验证与收尾:无噪极限检查、单帧时间预算和 BER 曲线坐标

5.1 用无噪 LLR 做 one-shot 自检

每次改码长或改变可靠度计算方式后,都应该先跑一个极端的自检:让接收端直接使用发射端已知的码字构造 LLR,验证译码器能在无噪声条件下完全恢复信息位。

% 假设 u_all 是填充好的编码器输入,x 是编码输出 llr_clean = 20 * (1 - 2*x); % 正数对应 0,负数对应 1 u_clean = sc_decode(llr_clean, info_bits_flag); assert(isequal(u_clean(info_bits), u), '无噪声自检失败');

这里使用20代替无穷大 LLR,既避免数值溢出,又能保证硬判决符号绝对可靠。如果这个断言失败,检查方向集中在生成矩阵方向、信息位索引和 SC 译码的左右子节点顺序上。

5.2 单帧耗时与蒙特卡洛时间预算

极化码仿真的最大风险不是代码错误,而是跑了一晚上才发现帧数不够。建议在正式扫描前用 10 帧估算单帧耗时:

tic; for i = 1:10 noise = sigma * randn(1, N); y = s + noise; llr = 2 * y / sigma2; u_hat_all = sc_decode(llr, info_bits_flag); end t_frame = toc / 10; fprintf('单帧耗时 %.3f s\n', t_frame);

有了单帧耗时,就能快速估算整个 SNR 扫描需要的总时间。比如 6 个 SNR 点、每点 200 个错误帧、高 SNR 区可能需要 3000 帧,那么总帧数可能到两万帧。如果单帧 20 ms,总时间就是 400 秒,可以接受;如果单帧 1 秒,就要立刻降低帧数或改用并行。

5.3 画曲线时的横纵坐标

画 BER/BLER 曲线时,横坐标用EbN0_dB,纵坐标用semilogy,别把码符号 SNR 当成信息比特 SNR。两者相差10*log10(R),也就是 3 dB 左右。如果你发现自己的极化码曲线和文献差一个固定偏移,优先检查这个公式。帧数足够后,把每个 SNR 点的错误帧数提到 300,曲线会比 100 时平滑很多,但耗时约三倍;先用少量帧跑通全流程,确认索引和 SNR 映射都对,再放帧数,这是极化码系统仿真最稳妥的推进方式。

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

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

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

立即咨询