ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MATLAB中Polar码QPSK SCL译码仿真全流程详解与踩坑指南

MATLAB中Polar码QPSK SCL译码仿真全流程详解与踩坑指南 简介本资源是一套面向通信工程专业高年级本科生及研究生的Polar码性能仿真教学材料聚焦5G核心编码技术在实际链路中的误码率验证问题。完整实现QPSK调制、AWGN信道建模与SCL连续消除列表译码全流程支持列表长度L1/2/4对比并集成CRC辅助校验提升译码可靠性便于深入理解Polar码构造、编码增益与译码复杂度权衡。压缩包共16个文件14个MATLAB函数脚本、1张说明图、1段操作录屏AVI总大小2.07MB其中主程序Runme1_Polar_code_SCL.m驱动仿真SCL_CRCdecoder_llr.m实现核心译码逻辑PLOT_BER_SNR.m自动绘制SNR-BER曲线配套AVI录像详细演示MATLAB路径设置与运行流程。已有934人学习下载所有代码均含中文注释关键模块如信道构造、冻结比特选取、QPSK符号映射与LLR计算均已模块化封装可直接复现论文级BER性能曲线是开展极化码课程设计、毕设仿真或科研入门的理想实践素材。 从去年开始陆续帮几个研究生和工程师朋友调过Polar码的仿真发现大家问得最多的往往不是Polar码本身而是“整套流程怎么在MATLAB里搭起来才对”。Polar码、QPSK、SCL译码、AWGN信道这四个词单拎出来都有大量资料但真正把它们串在一起跑出一张干净的误码率曲线中间需要抠的细节远比想象中多。这篇文章把我自己搭建这套仿真时的完整思路、代码结构和踩坑记录整理出来适合正在做Polar码入门仿真、或者想把手头SC译码改成SCL的读者参考。1. 这个仿真练的是什么Polar码入门组合拳的合理性Polar码作为第一种被理论证明可达香农极限的信道编码核心思想是利用信道极化现象把物理信道拆分成一组可靠性两极分化的比特信道。但这套理论在MATLAB里落地时很多人会卡在“怎么把论文里的公式变成能跑的代码”这一步。而QPSK加AWGN信道恰好是信道编码仿真里最标准的验证环境SCL译码又是在SC译码基础上最自然的扩展这套组合练完之后再去碰比特交织编码调制、速率匹配或者硬件实现思路会顺很多。1.1 为什么选QPSK而不是BPSK起步BPSK确实更简单实部一个星座点就搞定了。但Polar码的QPSK调制涉及符号级LLR对数似然比的计算也就是要把一个复数符号携带的2个比特信息分别提取出来。这个过程直接关系到后续SCL译码器输入的LLR质量跳过它直接做BPSK后面换到更高阶调制时会重新踩坑。另外从实际系统角度看5G NR的Polar码上行控制信道用的就是QPSK所以这个仿真和工程实践的贴合度也更高。用QPSK做出来的误码率曲线在低信噪比区域能明显看到编码增益的积累过程比BPSK更有观察价值。1.2 这套组合能帮你建立什么认知跑通这套仿真之后你至少能收获三块东西。第一Polar码编码的过程不是简单地拿生成矩阵乘一下而是要先确定哪些比特信道放信息比特、哪些放冻结比特这个“位置选择”会直接影响性能。第二SCL译码的“列表”到底在维护什么东西——它和SC译码的最大区别不是“多试几条路”而是引入了路径度量值PM来对候选路径进行排序和剪枝。第三AWGN信道仿真里最容易被忽视的信噪比口径换算即Eb/N0、Es/N0、噪声方差这三者之间的关系。这三块内容里第一块决定了你的仿真是不是“真Polar码”第二块决定了误码率性能能不能提上去第三块决定了曲线横坐标对不对、和理论值比不比对得上。任何一个环节出问题最终画出来的曲线都会是乱的。2. 编码端不啰嗦生成矩阵、极化权重与信息位选择Polar码的编码公式是d u · G_N其中u是包含信息位和冻结位的输入向量G_N是极化核矩阵的n次克罗内克积。MATLAB里实现这个不算难但有几个容易被带偏的地方需要特别说清楚。2.1 生成矩阵的递归构造与比特反转最基本的极化核是F [1 0; 1 1]G_N F^{⊗n}。很多资料里会同步提到比特反转排列矩阵B_N实践中有两种做法一种是在生成矩阵里显式乘上B_N另一种是在构造信息位索引时提前把比特反转考虑进去。我的建议是仿真代码里直接省略B_N保持编码和译码使用相同的索引顺序即可因为信道极化的“哪个位置更可靠”完全取决于索引映射关系。只要收发两端一致省略B_N不会影响BER结果反而代码更简洁。不过如果你准备和MATLAB 5G工具箱的结果对照就要留意工具箱内部是带比特反转处理的两者对比时需要先对齐索引逻辑。MATLAB里生成矩阵的推荐写法如下function G polar_generator(N) % N 2^n, 递归克罗内克积构造生成矩阵 n log2(N); F [1 0; 1 1]; G 1; % 1x1初始矩阵 for i 1:n G kron(G, F); end end注意kron是克罗内克积不是普通矩阵乘法。这个递归方式和matlab里kron函数的循环次数直接对应n 10时N 1024矩阵规模是1024x1024MATLAB内存完全能承受。2.2 信息位选择极化权重法的MATLAB实现确定哪些比特信道放信息比特最常用的方法是计算每个分裂信道的可靠性。5G标准里采用的是极化权重PW方法对每个索引i计算一个权重值然后按权重排序取前K个最大的作为信息位。极化权重的核心公式是W_i sum_{j0}^{n-1} B_j * 2^(j/4)其中i写成n位二进制形式b_{n-1}...b_0B_j是第j个二进制位。这个公式看起来抽象但MATLAB实现非常直接function info_idx polar_info_index(N, K) n log2(N); weights zeros(1, N); for i 0:N-1 idx i; w 0; for j 0:n-1 bit mod(idx, 2); if bit 1 w w 2^(j/4); % 注意这里j从0开始 end idx floor(idx/2); end weights(i1) w; end [~, sort_idx] sort(weights, descend); info_idx sort(sort_idx(1:K)); % 按升序排列方便编码时使用 info_idx info_idx - 1; % MATLAB索引从1开始需要转换到0基索引 end如果只想快速验证而不是完全复刻5G标准也可以用高斯近似GA来估计各比特信道的可靠性得到一个LLR均值的递推然后把均值小的信道作为信息位。高斯近似和极化权重在中等码长下的选位结果差异极小但理解上比PW公式更贴近“信道极化”的本质。我自己的经验是第一次做仿真时先用固定N和K把PW方法跑通后面想对比不同码率的影响时再封装成一个函数反复调用。2.3 编码过程的两种写法拿到生成矩阵和信息位索引后编码有两种实现方式我都用过。方式一是矩阵乘法直接构造完整的输入向量u把冻结位置0、信息位填入数据然后执行d mod(u * G, 2)。这种写法直观适合N在512以下时使用。方式二是利用迭代结构不需要显式生成G矩阵核心是每级执行一次“xor-sum”操作类似FFT的蝶形结构。这种方法减少内存占用但代码可读性差一些而且SC译码器本身也是蝶形结构容易和编码逻辑混淆。初学者我建议用方式一先确保编码结果正确再考虑优化。验证编码正确性的方法很简单对全零信息位编码输出应该全零对只有一个1的输入编码输出应该对应生成矩阵的某一行。这两条检查在新写代码时能快速定位大部分问题。3. QPSKAWGN的仿真地基LLR初值是怎么算出来的这一部分是整套仿真里最容易出错、也最影响最终曲线“颜值”的环节。很多人的Polar码译码代码明明没问题但误码率曲线在高信噪比下出现明显的错误平层原因就是QPSK解调的LLR初值算错了。3.1 符号映射、能量归一化与噪声方差QPSK调制通常采用Gray映射即将2个比特映射到4个星座点。最常见的映射方式是比特00 - (1j)/sqrt(2)比特01 - (1-j)/sqrt(2)比特10 - (-1j)/sqrt(2)比特11 - (-1-j)/sqrt(2)在MATLAB里生成QPSK符号时最容易忘的是能量归一化。如果不除以sqrt(2)每个符号的能量E_s 2而不是1噪声方差就要相应调整。我的建议是星座点都用(±1 ± j)/sqrt(2)让每个符号平均能量E_s 1。这样后面计算噪声方差时不容易出错。AWGN信道下接收信号可以写成y x n其中n是均值为0、方差为σ²的复高斯噪声。σ²的取值取决于目标Eb/N0每比特能量与噪声功率谱密度之比EbN0_dB 0:0.5:4; EbN0_lin 10.^(EbN0_dB/10); R K / N; % 编码码率 EsN0_lin EbN0_lin * R; % 符号能量与噪声比 N0 1 ./ EsN0_lin; % 单边噪声功率谱密度 sigma2 N0 / 2; % 每个维度实部/虚部的噪声方差 sigma sqrt(sigma2);这里有个关键点QPSK每个符号携带2个比特所以E_s 2·E_b。再乘上码率R是因为每个信息比特实际上发射了1/R个符号。这个换算链断在哪一环你的仿真就和理论值差在哪一截。3.2 QPSK软解调的LLR计算接收到y之后需要计算每个比特的对数似然比LLR。QPSK符号的实部和虚部分别对应一个比特所以计算可以拆成独立的两路。对于实部Re(y)对应的比特b0LLR公式是LLR(b0) (2/sigma^2) * Re(y) * cos(pi/4)因为QPSK等效于两个正交BPSK所以符号归一化后LLR可以简化为llr0 2 * real(y) / sigma2 / sqrt(2); llr1 2 * imag(y) / sigma2 / sqrt(2);为什么前面有系数sqrt(2)因为归一化星座点是(±1 ± j)/sqrt(2)实部的幅度是±1/sqrt(2)代入BPSK的LLR公式时信号幅度会影响分子分母的比例。很多教材里会把cos(pi/4)约掉直接写2*real(y)/sigma2但那是在星座点幅度为±1时才成立。如果你用(±1 ± j)/sqrt(2)归一化就必须带上sqrt(2)这一项。这一步做错了LLR整体偏大或偏小SCL译码器的路径度量计算就会失真误码率曲线会明显偏离理论值。3.3 一个趁手的LLR函数封装建议把调制、加噪、解调封装成独立的函数方便后面做不同码率、不同N值的对比实验。我常用的函数签名是function llr qpsk_awgn_llr(data_bits, N, K, EbN0_dB) % 输入信息比特向量、码长、信息位长度、信噪比 % 输出译码前LLR % 内部处理编码、QPSK调制、AWGN加噪、软解调 end这种封装的便利之处在于你可以在不改变主脚本的情况下换成BPSK或16QAM来做对比实验。对于Polar码的属性来说LLR输入质量直接决定SCL译码的上限所以这个函数值得单独多花点心思。4. SCL译码的MATLAB落地从SC到带列表SCL译码器的实现是整个仿真项目里代码量最大、逻辑最绕的部分。它本质上是在SC译码的递归计算之上额外维护了L条候选译码路径。最开始我先说SC译码的蝶形计算框架然后再说明SCL做了什么扩展。4.1 SC译码的蝶形计算与LLR更新SC译码的核心是计算分裂信道的LLR。设接收LLR序列为α按长度为N的数组组织。SC译码过程可以通过一棵深度为n的二叉树来理解树的叶子节点对应每个比特索引内部节点对应长度为2^k的子块。标准SC译码在每个节点的操作包括从父节点接收LLR向量上层传递根据奇偶索引关系计算上半部分的LLR即f操作f(a, b) sign(a)*sign(b)*min(|a|, |b|)在MATLAB里可以写成f sign(a).*sign(b).*min(abs(a), abs(b))下层传递在获得部分比特估计后计算g操作g(a, b, u_hat) (-1).^(u_hat) .* a b叶子节点根据LLR硬判决得到比特估计在MATLAB里最简单的实现是递归函数。但递归深度在N1024时已经接近MATLAB默认栈深度上限所以更稳妥的方式是用迭代结构。业界常用的做法是预先分配所有节点的LLR张量再用一个深度优先的循环遍历节点。初次实现时递归函数更容易读建议先把递归版本调通再考虑改成迭代版本。4.2 从SC到SCL路径度量PM的计算逻辑SCL和SC的最大区别在于SCL在每一个信息位处会把当前已有的每条路径分裂成0和1两个分支然后计算每个新分支的路径度量PM最后按PM从小到大排序保留前L条。PM的计算公式是PM_new PM_old penaltypenalty的定义是如果硬判决结果和LLR符号一致即LLR 0时判0LLR 0时判1则penalty 0如果不一致则penalty |LLR|这里有个很隐蔽的细节SCL在叶子节点做软判决时实际上会同时试探两条分支。对于信息位两条分支都允许保留只是在排序后截断对于冻结位则不允许自由选择必须取固定值此时如果固定值和LLR硬判决符号不一致也要加上penalty。在MATLAB里路径度量的存储建议直接用L×1的向量每轮更新时复制扩展成2L个候选值再排序截断。4.3 SCL译码器的MATLAB代码框架下面给出一个经过简化但可运行的SCL译码核心框架重点在于展示路径分裂和排序的逻辑function u_hat_all scl_decode(llr_in, N, K, info_idx, L) n log2(N); % 路径相关存储 pm zeros(L, 1); % 路径度量值 u_hat zeros(L, N); % 每条路径的译码结果 active zeros(L, 1); % 活跃路径标识可选 % 初始化只有一条路径 cur_L 1; pm(1) 0; % 注简化起见用循环遍历每个比特类似逐比特SCL for i 1:N if ismember(i, info_idx) % 信息位 % 每条路径分裂成两个候选 cand_pm zeros(cur_L*2, 1); cand_u zeros(cur_L*2, N); for p 1:cur_L for b 0:1 cand_pm((p-1)*2b1) pm(p) penalty(b, llr_of_bit(i, p)); cand_u((p-1)*2b1, :) u_hat(p, :); cand_u((p-1)*2b1, i) b; end end % 排序保留L条 [pm_sorted, idx] sort(cand_pm); cur_L min(L, cur_L*2); pm(1:cur_L) pm_sorted(1:cur_L); u_hat(1:cur_L, :) cand_u(idx(1:cur_L), :); else % 冻结位 % 固定置为0更新PM for p 1:cur_L u_hat(p, i) 0; pm(p) pm(p) penalty(0, llr_of_bit(i, p)); end end end [~, best] min(pm); u_hat_all u_hat(best, :); end这段代码胜在逻辑直观但性能不是最优的。实际仿真时llr_of_bit(i, p)不能每次都重新算一次完整SC的LLR否则复杂度爆炸。正确做法是先把整棵蝶形树的LLR全部算好并缓存然后再进入路径分裂循环。这里我强烈建议把“LLR树计算”和“路径维护”两层分开。前者用非递归蝶形网络计算出一棵LLR树后者只负责在缓存好的LLR树基础上做路径分裂。这样代码更清晰也更好调试。4.4 一个省内存的实现技巧SCL译码最耗资源的地方在于每条路径都要维护一份部分和partial sum数组。在有L条路径时如果每条路径都保存完整的N长度部分和L32、N1024时就需要33000个浮点数MATLAB虽然有优化但循环中大量复制还是会拖慢速度。一种常见的省内存做法是不为每条路径保存完整的N比特估计而是保存“路径历史”即每条路径在哪个比特索引处分叉、选了哪个分支。在最终选中最优路径后再回溯重建完整译码序列。这样内存占用从O(L·N)降到O(L·N/2)但代价是回溯逻辑更复杂。建议初学者先不做这个优化直接保存每条路径的完整估计换来的是更短的上手时间。等码长加大到2048以上时再考虑回溯优化。5. 主循环、性能曲线与调试笔记这一章讲仿真主体怎么组织以及那些让我折腾到半夜的坑。整体流程不复杂但细节决定曲线质量。5.1 蒙特卡洛主循环的组织方式仿真主脚本的逻辑一般是for idx 1:length(EbN0_dB) err_bits 0; err_blocks 0; total_bits 0; total_blocks 0; while err_blocks target_errors total_blocks max_blocks info_bits randi([0 1], K, 1); % 编码、调制、信道加噪、软解调 % SCL译码 % 统计错误 end ber(idx) err_bits / total_bits; bler(idx) err_blocks / total_blocks; end统计误码率时需要注意统计口径BER统计的是信息位比特错误数除以信息位总比特数BLER统计的是错误块数除以总块数。很多人只画BER但BLER在工程中同样重要尤其在讨论HARQ和重传机制时BLER更有参考价值。蒙特卡洛的核心控制参数是max_blocks和target_errors。高信噪比下误码率很低时需要跑足够多的块才能统计到足够多的错误。我的经验是在目标BER为1e-5量级时至少要统计到100个错误块置信区间才比较可信。当然仿真时间也会变长这需要权衡。5.2 调试时我踩过的五个坑第一个坑LLR初值符号搞反。QPSK解调出来的LLR正负号定义一定要和译码器内部的硬判决规则一致。如果LLR为正表示比特0而你译码器判断LLR0时输出1那所有性能都会崩溃。排查方法很简单在无噪声条件下跑一次仿真译码结果应该和发送端一致如果对不上先检查符号。第二个坑Eb/N0和Es/N0混用。忘了乘码率R或者忘了QPSK每个符号携带2比特导致噪声方差算小了一倍误码率曲线会向左偏移约3dB。这一点在论文里经常看到曲线“比理论值还好”多半就是这种口径错误。第三个坑信息位索引的升序和降序搞错。如果你编码时用升序索引放信息位但译码时按照降序索引做性能会急剧恶化。建议把所有索引统一传递并写单元测试验证编码译码闭环。第四个坑随机数种子没固定。蒙特卡洛仿真时如果不固定种子每次运行的曲线都会有小幅波动。在调代码时要固定种子确保前后版本结果可比。例如rng(2024)放在主脚本开头。第五个坑PM的数值累加溢出。SCL译码过程中路径度量值是LLR累加得到的LLR在高信噪比下会变得很大如果LLR计算中有除法1/sigma^2当sigma很小时数值会很大。建议把LLR做一次全局归一化比如除以所有LLR绝对值的最大值的某个比例防止溢出。5.3 程序结构建议我在实际工作里一般把仿真拆成4个文件polar_params.m参数配置包括N、K、L、EbN0范围等polar_generator.m生成矩阵和索引计算qpsk_awgn_llr.m调制解调与LLR计算scl_decode.mSCL译码器主脚本只负责循环、统计和绘图。这种结构的优点是当你想对比不同L值或不同码率时只需要改参数文件或者用循环调用不同参数不用动核心算法代码。6. 曲线长这样才算对性能对照与后续扩展方向仿真跑完之后最后一步是确认曲线“长对了”。如果曲线形状不对后面的分析都是白做。6.1 性能结果的初步判读和未编码QPSK的理论误码率相比Polar码的误码率曲线应该在低信噪比区域略差然后随着SNR增加曲线斜率明显变陡出现“瀑布区”。以N256、K128、R0.5、L8为例在AWGN信道下大约在Eb/N0为2.5-3.5dB时进入瀑布区。如果曲线在低信噪比就急剧下降多半是仿真错误。SCL的列表大小L对性能的影响也有规律L从1增加到4时性能提升明显从4到16继续增加但幅度变小到32以上时已经非常接近ML界。如果L1和L32的曲线完全重合说明你的SCL实现可能根本没有生效——很可能是路径分裂或排序逻辑有误导致每轮都只保留了一条路径。6.2 理论界的参考价值对于中等码长N在128到512之间SCL译码的性能上界可以用最大似然译码界来近似不过实际仿真时很少有人真的去算ML界。一个更实用的做法是对比相同码率下Polar码SCL译码和LDPC码BP译码的误码率曲线。在我们的仿真条件下码率为0.5时两套码在瀑布区的性能接近但Polar码在短码时错误平层更低——这是Polar码的重要优势之一。6.3 后续可以做的扩展这套仿真框架搭好后扩展方向非常多。第一个方向是给SCL加上CRC辅助即CRC-SCL这是5G NR里实际采用的技术能够在路径选择时用CRC校验来打破PM相近的僵局显著提升短码性能。第二个方向是把QPSK换成16QAM或更高阶调制这时需要处理符号内多比特的LLR计算软解调复杂度会提升。第三个方向是换成衰落信道或者带信道估计误差的场景这会引入信道状态信息的使用和插值算法。我个人觉得最值得做的是CRC-SCL因为它的改进效果立竿见影而且能加深你对“路径度量”和“外部约束”如何结合的理解。从SCL到CRC-SCL的代码改动量并不大主要是增加一个CRC编码器和一个校验步骤。如果你已经把这套仿真跑顺了不妨试一试。说到底Polar码仿真这件事最大的门槛不是理论有多深而是把理论一步步落到可验证的代码里。我写这篇文章时特意把那些“看起来理所当然、其实容易致命”的细节都翻出来讲就是因为这些细节最能区分一个仿真到底能不能用、能不能发论文、能不能支持后续研究。希望这份记录能帮你少走一些弯路。本文还有配套的精品资源点击获取
返回列表