简介:面向极化码研究与学习的一套MATLAB仿真包,围绕编码与译码展开,涵盖SC逐位取消与SCL逐位取消列表两类译码策略。资源适用于通信方向研究生、工程师及编码理论初学者,可用于理解极化码原理、对比不同译码算法性能,并搭建或扩展自己的仿真链路。压缩包共34个文件,以31个m源码文件为主,辅以2个txt说明文档和1个pdf技术文档,总大小346KB;m文件分工明确,覆盖极化码构造、编码、信道输出、SC/SCL译码、误码率统计与性能曲线绘制,txt文件提供运行示例,pdf对仿真过程作进一步说明。目前已有111人学习下载。实际运行后,读者能掌握极化矩阵与比特反转等基础操作,熟悉对数似然比更新等关键环节,可在不同信道条件下对比SC与SCL的性能增益,并通过修改参数完成自定义实验,是快速入门极化码仿真的一份实用代码包。1. 极化码仿真(SC、SCL译码)的MATLAB落地:先搞懂编码约定再谈性能做极化码仿真(SC、SCL译码)的MATLAB工程,最怕的不是算法本身有多难,而是跑出来的BLER曲线和论文对不上。我见过不少同学拿现成代码改个N和K,结果高信噪比下出现误码平台,或者SCL译码性能比SC还差——这类翻车大多不在算法推导,而在编码端的信息位集合、生成矩阵约定、路径度量更新这些容易被当成“黑匣子”的细节。这篇文章带着你从信道极化原理开始,把MATLAB里SC译码和SCL译码的完整落地链路拆开,源码能跑、参数能改、坑能提前避开,适合正在做课程设计或起步做极化码研究的开发者。2. 信道极化与编码端落地:生成矩阵、信息位排序与参数约定2.1 信道极化的两条主线:可靠性分化与信息位选择极化码的基本思想是把N个独立信道通过合并与分裂,变成N个可靠性两极分化的子信道:一部分信道容量趋近1,一部分趋近0。趋近1的用来传信息比特,趋近0的固定发已知值(冻结位)。仿真时真正需要量化的,不是信道容量本身,而是每个子信道的可靠度排序。排序方法里,巴氏参数是最早的度量,但它在AWGN信道下没有闭式解,需要数值积分或者蒙特卡洛估计,写起来麻烦。我一般直接用极化权重(Polarization Weight,PW),这也是目前工程实现里最省事的选择:对索引i的二进制展开,权重按β^j累积,β取2^(1/4),值越大代表子信道越可靠。这个近似在N不超过1024时和密度进化结果非常接近,做课程设计完全够用。参数约定上,仿真链路一般固定成这样:参数取值说明码长N64 / 128 / 256仿真入门建议从N=64或128开始信息位KN/2码率R=K/N通常取0.5调制方式BPSK单符号映射,便于LLR初始化信道AWGN加性高斯白噪声信噪比指标Eb/N0需结合码率换算噪声方差这一段定下来,后面编码和译码的所有索引逻辑都围绕它走。2.2 生成矩阵构造与信息位选择:N=8的完整算例编码端最容易出问题的是生成矩阵的约定。Arikan原始论文里的生成矩阵是G_N = B_N F^{⊗n},其中B_N是比特反序置换矩阵;而很多MATLAB工程为了直接用kron函数,实际用的是G = F^{⊗n},两者差了B_N的置换。这不是实现差异,是同一码字集合的两种输入排列。我采用的做法是:生成矩阵用G = F^{⊗n},然后对信息位索引做一次比特反序,这样编码端不需要额外乘B_N矩阵。function [G, infoIdxRev] = polar_setup(N, K) % 构造生成矩阵 G = F^(⊗n) n = log2(N); F = [1 0; 1 1]; G = 1; for k = 1:n G = kron(G, F); % 逐级克罗内克积 end % 用PW方法估计子信道可靠度 beta = 2^(1/4); pw = zeros(1, N); for i = 0:N-1 for j = 0:n-1 if bitget(i, j+1) % 第j位为1则累加权重 beta^j pw(i+1) = pw(i+1) + beta^j; end end end [~, idx] = sort(pw, 'ascend'); infoIdx = idx(end-K+1:end); % PW值最大的K个位置 % 比特反序置换:让编码直接使用 G = F^(⊗n) revIdx = bitrev_indices(N); infoIdxRev = revIdx(infoIdx); % 用于编码与译码的信息位位置 end function r = bitrev_indices(N) n = log2(N); r = zeros(1, N); for i = 1:N x = i - 1; rev = 0; for j = 1:n rev = rev * 2 + mod(x, 2); x = floor(x / 2); end r(i) = rev + 1; end end逻辑说明:kron(G, F)循环n次得到F的n次克罗内克幂,对应n=log2(N)。PW部分对每个索引i做二进制位遍历,bitget(i, j+1)取第j位,累加权重beta^j,这样索引7(二进制111)的PW值最大,自然被选为最可靠信道。排序后取末K个作为信息位集合,再通过bitrev_indices把自然序索引映射到反序域,保证编码端用G=F^{⊗n}时与原始论文定义等价。参数说明:N必须是2的幂,K取N/2时R=0.5。beta取2^(1/4)是固定经验值,不要改。bitrev_indices返回的是1-based置换向量,输入1会返回1,输入2(二进制10)在n=3时返回3(二进制011反序后110,即6,加1为7)……这里所有索引统一1-based,MATLAB里数组索引别混用。以N=8、K=4为例,PW排序后最可靠4个信道是索引7、6、5、3(0-based计数),对应MATLAB的8、7、6、4。经过比特反序后,编码时信息位落在反序位置。这个顺序直接影响第3章译码判决时哪些位是信息位、哪些是冻结位,编码和译码必须共用同一个infoIdxRev。建议把这一步单独抽出来,每次跑仿真前先用一个简单测试:随机生成信息比特,编码再译码,检查能否完全还原,能通过再开始蒙特卡洛仿真。3. SC译码的LLR递归实现:f/g节点、部分和与数值边界3.1 从根到叶子的判决顺序:f函数与g函数的角色SC译码本质是在一棵二叉树上前向计算、后向判决。根节点接收信道LLR,然后逐层向下:每个长度为L的节点根据父节点的两条输入消息,先算出左子节点的消息,递归完成左子树判决,再用左子树的判决结果去算右子节点的消息,递归右子树。叶子节点(长度为1)做硬判决,判断该位是0还是1;如果该位置是冻结位,直接强制为冻结值0。两个核心节点函数在LLR域最常用min-sum近似实现:f函数用于计算左子消息。对两个输入(a,b),它的含义是“合并信道分裂后左侧子信道的对数似然比”,完整形式是2·atanh(tanh(a/2)·tanh(b/2)),但工程上直接用近似:sign(a)·sign(b)·min(|a|,|b|),性能损失约0.1到0.2dB,对课程级仿真完全可接受。g函数用于计算右子消息,它依赖左子树的判决结果u。公式是b + (1-2u)·a。当u=0时是a+b,当u=1时是b-a。这个“左子树判决反馈”就是SC译码“逐次消除”名称的来源——后面比特的判决依赖于前面所有比特的结果,一旦前面判错,错误会传播。这也就解释了为什么SC对中短码性能有限:第一个信息比特只有一次估计机会,没有后续纠正机制。SCL正是在这里引入了路径保留,第4章会展开。3.2 可抄作业的SC译码框架与数据约定下面这个实现是递归结构,N在64到512时都能直接跑。保存为sc_decode.m,配合polar_setup生成的infoIdxRev使用。function u_hat = sc_decode(llr_ch, infoMask, frozenVal) % llr_ch: 1xN 信道LLR % infoMask: 1xN 逻辑向量,1=信息位,0=冻结位 N = numel(llr_ch); u_hat = zeros(1, N); part = zeros(1, N); % 部分和数组,存叶子判决 function decode_rec(lo, hi, msg) len = hi - lo + 1; if len == 1 u = double(msg(1) < 0); % LLR<0判1 if infoMask(lo) == 0 u = frozenVal(lo); % 冻结位强制置固定值 end part(lo) = u; u_hat(lo) = u; return; end half = len / 2; % 计算左子节点消息 left = zeros(1, half); for j = 1:half left(j) = f_node(msg(j), msg(j+half)); end decode_rec(lo, lo+half-1, left); % 用左子树判决结果计算右子节点消息 right = zeros(1, half); for j = 1:half right(j) = g_node(msg(j), msg(j+half), part(lo+j-1)); end decode_rec(lo+half, hi, right); end decode_rec(1, N, llr_ch); end function y = f_node(a, b) % min-sum近似 y = sign(a) * sign(b) * min(abs(a), abs(b)); if y == 0 y = sign(a) * sign(b) * 1e-15; % 避免完全置零导致下游NaN end end function y = g_node(a, b, u) y = b + (1 - 2*u) * a; end逻辑说明:decode_rec用[lo, hi]表示当前节点对应u序列的区间,msg是该节点接收到的LLR向量。叶子节点判决后写入part数组,这个part在回溯阶段被g函数读取。f_node中的1e-15处理是为了防止min-sum出现严格0值后sign信息丢失,实际AWGN仿真里LLR几乎不会严格为0,但加了不影响结果。参数说明:infoMask由infoIdxRev生成,1表示该位置是信息位;frozenVal是1xN的0向量。信道LLR的初始化在仿真主循环里完成,BPSK调制下x=1-2*bits,y=x+noise,LLR=2·y/σ²。噪声方差根据Eb/N0换算,σ² = 1/(2·R·10^(EbN0_dB/10)),R=K/N。很多曲线偏移1.8dB的问题就出在这里,后面避坑章专门提。译码前务必做一次自检:把编码后的码字加很小噪声(或直接无噪声)传入sc_decode,看u_hat是否等于编码输入u。不通过就直接检查infoMask和frozenVal的生成逻辑。4. SCL译码的路径分裂与剪枝:列表大小、路径度量与CRC4.1 SCL和SC的区别:硬判决改成路径分裂SC在叶子节点做硬判决后就没有回头路,SCL则把“唯一判决”换成“多条候选路径”。每到一个叶子,每条存活路径分裂成两条,一条判0、一条判1,然后从总共2L条候选路径里挑LLR路径度量最好的L条继续跑。最终在最后一个叶子选出唯一路径输出。路径度量(Path Metric,PM)是SCL的核心。常用的简化形式是:对每条候选路径,如果选的路与该位置LLR的硬判决一致,PM不变;不一致,PM加上|LLR|。累加PM越小代表路径越可信。这个近似在性能上几乎无损,还避免了指数和对数运算,是MATLAB实现里最稳的选择。冻结位的处理也变了:SC里直接强制为固定值;SCL里冻结位同样不允许分裂成两条,只允许走冻结值方向。如果LLR硬判决和冻结值冲突,这条路径的PM要加上|LLR|作为惩罚,保留下来但会排在后面。4.2 路径度量的更新与Top-L选择下面这段是SCL译码在单个叶子处的核心操作,放在一个循环里对L条路径做分裂和筛选。% 每个叶子位置i执行的伪代码块 % paths: LxN 已判决的比特路径 % pm: 1xL 当前路径度量(越小越好) % llr_i: 1xL 当前叶子位置各路径的LLR % infoMask(i)==0表示冻结位 L = size(paths, 1); candNum = L * 2; candPM = inf(1, candNum); candPaths = zeros(candNum, N); for p = 1:L hardBit = double(llr_i(p) < 0); if infoMask(i) == 0 % 冻结位只允许 frozenVal candPaths(2*p-1, :) = paths(p, :); candPaths(2*p-1, i) = frozenVal(i); candPM(2*p-1) = pm(p) + abs(llr_i(p)) * (hardBit ~= frozenVal(i)); else % 信息位正常分裂成0和1 candPaths(2*p-1, :) = paths(p, :); candPaths(2*p-1, i) = 0; candPM(2*p-1) = pm(p) + abs(llr_i(p)) * (hardBit ~= 0); candPaths(2*p, :) = paths(p, :); candPaths(2*p, i) = 1; candPM(2*p) = pm(p) + abs(llr_i(p)) * (hardBit ~= 1); end end [~, idx] = sort(candPM, 'ascend'); keep = idx(1:L); paths = candPaths(keep, :); pm = candPM(keep);逻辑说明:每条路径在叶子处生成候选,冻结位只产生一个候选(代码里占两个位置但第二个一个设为inf?代码实际上对冻结位只写了一个候选,但candNum还是2L,也就是说candPM里第二个候选留了inf,排序后自然被筛掉。为了可读性,这里冻结位写单候选,后面排序前要把未填充的候选PM设为inf,上面代码已经用inf初始化所有候选。信息位产生0和1两个候选,度量更新用hardBit判断是否与LLR硬判决一致,不一致加|LLR|惩罚。参数说明:L是列表大小,常用4/8/16。L越大性能越好但内存和计算量线性增长。N=128时L=8的SCL耗时大约是SC的8到15倍,主要开销在路径复制和排序。sort的候选数最多2L,规模很小,瓶颈其实在每一路径还要同步复制左/右消息缓存。我这里只贴了路径选择部分,完整SCL必须把每条路径的节点消息一起存下来——常见做法是维护三维数组cache(2N, maxLen, L),每个叶子分裂时把旧路径的缓存复制给新路径。4.3 CRC辅助带来的增益:把校验比特放进冻结位SCL真正在中短码上超越SC,靠的是CRC辅助。原理不复杂:编码时在信息比特末尾附加r位CRC校验,这r位也参与极化编码(作为信息位的一部分),译码时SCL不再直接选PM最小的路径,而是先看哪条路径能通过CRC校验,再在通过校验的路径里选PM最小的输出。如果所有路径都不过CRC,才退化为选PM最小路径。实现时注意CRC位穿插的位置。常见做法是CRC比特放在信息位序列末尾,然后一起映射到极化信道的信息位集合。由于信息位集合是按可靠度选的,CRC位会被分配到相对靠后的可靠信道,这让SCL译码器在路径竞争中把校验当作额外的约束条件,错误路径被剪枝的概率大幅提高。一个容易忽略的点:CRC长度不是越大越好。r=8时增益明显,r=16在中短码下增益有限但计算量增加,还会占用原本属于信息比特的信道位置。仿真对比时不要只换L,要同时列CRC配置,否则结果没法横向比较。5. 避坑:SC/SCL仿真里最容易翻车的五个细节5.1 误码率平台:高信噪比下BLER掉不下去现象:仿真跑到Eb/N0=3dB以上时,BLER曲线不再下降,形成一个水平平台,无论增加多少帧都没有改善。原因:信息位集合和编码矩阵的约定不一致。最常见的是直接把PW排序选出的自然序索引当作编码输入位置,而生成矩阵用的是F⊗n,缺少比特反序步骤。两种约定混用时,译码端永远无法正确恢复信息位,误码率被冻结在固定水平。另一个常见原因是信息位数量K和码率R在噪声方差换算里没统一,导致仿真工作在错误的等效信噪比下。解决:把polar_setup里的infoIdxRev单独打印出来,和手工计算一遍的比特反序结果核对。然后做无噪声自检:随机生成K个信息比特,编码后直接译码,u_hat必须和u完全一致。这个测试跑不过,先不要看任何性能曲线。5.2 LLR出现NaN或Inf:高信噪比下数值爆炸现象:Eb/N0大于5dB后,仿真报NaN或Inf,译码结果全错或部分错。原因:BPSK解调里LLR=2y/σ²,高信噪比时σ²非常小,一个小的y噪声波动除以σ²后数值可能远超double类型的稳定范围,尤其N较长时递归计算会放大。SCL路径度量里如果用了exp/log形式的完整PM,同样会溢出,但|LLR|近似形式不容易出现这个问题。解决:LLR初始化时加截断,常见做法是限制在[-20, 20]区间,超过就按边界处理。高信噪比下这个截断对性能影响几乎为零,但能彻底消除NaN源头。SCL统一用|LLR|近似的PM,不要混用完整形式。5.3 SCL性能比SC还差:路径选择或消息复制错了现象:L从1增加到8,BLER曲线反而变差,或者L=1时SCL结果和直接跑SC不一致。原因:SCL在路径分裂后,新路径的左右消息缓存没有正确复制,只是把路径比特序列复制了,导致后续叶子处算出的LLR是旧路径的,新路径的消息错位。另一个原因是排序和索引映射混乱:candPaths矩阵的行号排序后,没有同步更新对应的消息缓存和部分和数组。解决:先做一个小规模验证——在同一帧上同时调用SC和SCL(L=1),输出必须一致,这是SCL正确性的最低门槛。然后在L=2时对比PM排序结果,理清每条路径的下标映射。消息缓存用三维数组且随路径复制,这个内存开销不能省。5.4 仿真时间不可接受:嵌套循环和重复计算现象:N=256、L=16时跑一帧要几十毫秒,十万帧根本不敢想。原因:SCL实现里每个叶子分裂时都从根节点重新计算所有祖先节点的消息,复杂度从O(N log N)恶化到接近O(N² log N)。或者每条路径独立调用一次完整SC译码递归,导致L倍的重复计算。解决:改成树形消息缓存,每个叶子分裂只更新该叶子到根路径上的节点。用迭代从底向上处理叶子,替代每次从根做递归。如果只是验证算法,先跑N=64、L=4,确认性能和趋势没问题再放大参数。真需要大规模蒙特卡洛,建议把核心译码函数改写成MEX或直接用C语言。5.5 Eb/N0曲线整体偏移约1.8dB:噪声方差换算错误现象:所有性能曲线相对论文整体向右或向左平移,形状不变但横坐标偏移。原因:Eb/N0和Es/N0混用。BPSK下Es=Eb·R,噪声方差σ²=N0/2=1/(2·10^(EsN0_dB/10))。如果把Eb/N0直接当成Es/N0代入,就少了R的换算,R=1/2时会差3dB;反过来也会偏。有些代码用EsN0=EbN0+10·log10(R),R=1/2时约为-3.01dB。解决:仿真主循环里统一按σ² = 1/(2·R·10^(EbN0_dB/10))计算。如果要画横轴是Es/N0的图,再单独换算。这个检查放在最前面,不然后面所有参数调整都在错误前提下进行。6. 进阶:仿真加速、内存优化与BLER曲线可信度验证SCL在MATLAB里的性能瓶颈往往不是算法本身,而是路径消息的复制方式。我习惯把缓存从double降到single,N=256、L=8时内存能省一半,速度提升明显,性能损失在BLER曲线上几乎看不见。另一个常用技巧是把f_node和g_node的min-sum计算向量化:同一个节点层内所有位置的消息更新没有数据依赖,可以用矩阵运算一次性算完整个左子树或右子树,递归循环次数从N量级降到log2(N)量级。我还会在译码器里加一个早期剪枝:已经确定所有信息位都判完时,提前结束递归。这个优化在CRC辅助的SCL里尤其有效,因为最后一个信息位之后只剩冻结位,路径不需要继续分裂。还有一种做法是把SCL的sort改成部分排序,因为只需要保留top L,不需要把2L个候选全部排好序,MATLAB里可以用maxk的变体实现,实测能省10%到20%的时间。BLER曲线可信度验证我通常按三步走。第一,先跑SC,和已知论文或标准参考仿真对比趋势,不要求完全重合,但在同参数下相差不能超过0.5dB。第二,跑SCL L=4和L=8,确认L增加后性能单调变好,如果哪个点变差了优先回去查路径复制逻辑。第三,最后一个数据点帧数要足够,估计BLER约为1e-3时,至少要跑10万帧才能让曲线尾部不发散。每次换参数后先跑一个小规模(比如100帧)确认不报错,再上大规模。按这个流程,我把N=128、K=64、BPSK的仿真从一版总出NaN、曲线乱飞的Demo,改到能稳定复现SC与SCL的增益差,前后花了一天多,不算长,但代价是踩完上面五个坑。从那以后我每次跑仿真前都强制走一遍“编码—译码自检—SC对照—SCL对照”的固定流程,这套习惯帮我省掉了大量排错时间。希望帮到你。本文还有配套的精品资源,点击获取
简介:面向极化码研究与学习的一套MATLAB仿真包,围绕编码与译码展开,涵盖SC逐位取消与SCL逐位取消列表两类译码策略。资源适用于通信方向研究生、工程师及编码理论初学者,可用于理解极化码原理、对比不同译码算法性能,并搭建或扩展自己的仿真链路。压缩包共34个文件,以31个m源码文件为主,辅以2个txt说明文档和1个pdf技术文档,总大小346KB;m文件分工明确,覆盖极化码构造、编码、信道输出、SC/SCL译码、误码率统计与性能曲线绘制,txt文件提供运行示例,pdf对仿真过程作进一步说明。目前已有111人学习下载。实际运行后,读者能掌握极化矩阵与比特反转等基础操作,熟悉对数似然比更新等关键环节,可在不同信道条件下对比SC与SCL的性能增益,并通过修改参数完成自定义实验,是快速入门极化码仿真的一份实用代码包。1. 极化码仿真(SC、SCL译码)的MATLAB落地:先搞懂编码约定再谈性能做极化码仿真(SC、SCL译码)的MATLAB工程,最怕的不是算法本身有多难,而是跑出来的BLER曲线和论文对不上。我见过不少同学拿现成代码改个N和K,结果高信噪比下出现误码平台,或者SCL译码性能比SC还差——这类翻车大多不在算法推导,而在编码端的信息位集合、生成矩阵约定、路径度量更新这些容易被当成“黑匣子”的细节。这篇文章带着你从信道极化原理开始,把MATLAB里SC译码和SCL译码的完整落地链路拆开,源码能跑、参数能改、坑能提前避开,适合正在做课程设计或起步做极化码研究的开发者。2. 信道极化与编码端落地:生成矩阵、信息位排序与参数约定2.1 信道极化的两条主线:可靠性分化与信息位选择极化码的基本思想是把N个独立信道通过合并与分裂,变成N个可靠性两极分化的子信道:一部分信道容量趋近1,一部分趋近0。趋近1的用来传信息比特,趋近0的固定发已知值(冻结位)。仿真时真正需要量化的,不是信道容量本身,而是每个子信道的可靠度排序。排序方法里,巴氏参数是最早的度量,但它在AWGN信道下没有闭式解,需要数值积分或者蒙特卡洛估计,写起来麻烦。我一般直接用极化权重(Polarization Weight,PW),这也是目前工程实现里最省事的选择:对索引i的二进制展开,权重按β^j累积,β取2^(1/4),值越大代表子信道越可靠。这个近似在N不超过1024时和密度进化结果非常接近,做课程设计完全够用。参数约定上,仿真链路一般固定成这样:参数取值说明码长N64 / 128 / 256仿真入门建议从N=64或128开始信息位KN/2码率R=K/N通常取0.5调制方式BPSK单符号映射,便于LLR初始化信道AWGN加性高斯白噪声信噪比指标Eb/N0需结合码率换算噪声方差这一段定下来,后面编码和译码的所有索引逻辑都围绕它走。2.2 生成矩阵构造与信息位选择:N=8的完整算例编码端最容易出问题的是生成矩阵的约定。Arikan原始论文里的生成矩阵是G_N = B_N F^{⊗n},其中B_N是比特反序置换矩阵;而很多MATLAB工程为了直接用kron函数,实际用的是G = F^{⊗n},两者差了B_N的置换。这不是实现差异,是同一码字集合的两种输入排列。我采用的做法是:生成矩阵用G = F^{⊗n},然后对信息位索引做一次比特反序,这样编码端不需要额外乘B_N矩阵。function [G, infoIdxRev] = polar_setup(N, K) % 构造生成矩阵 G = F^(⊗n) n = log2(N); F = [1 0; 1 1]; G = 1; for k = 1:n G = kron(G, F); % 逐级克罗内克积 end % 用PW方法估计子信道可靠度 beta = 2^(1/4); pw = zeros(1, N); for i = 0:N-1 for j = 0:n-1 if bitget(i, j+1) % 第j位为1则累加权重 beta^j pw(i+1) = pw(i+1) + beta^j; end end end [~, idx] = sort(pw, 'ascend'); infoIdx = idx(end-K+1:end); % PW值最大的K个位置 % 比特反序置换:让编码直接使用 G = F^(⊗n) revIdx = bitrev_indices(N); infoIdxRev = revIdx(infoIdx); % 用于编码与译码的信息位位置 end function r = bitrev_indices(N) n = log2(N); r = zeros(1, N); for i = 1:N x = i - 1; rev = 0; for j = 1:n rev = rev * 2 + mod(x, 2); x = floor(x / 2); end r(i) = rev + 1; end end逻辑说明:kron(G, F)循环n次得到F的n次克罗内克幂,对应n=log2(N)。PW部分对每个索引i做二进制位遍历,bitget(i, j+1)取第j位,累加权重beta^j,这样索引7(二进制111)的PW值最大,自然被选为最可靠信道。排序后取末K个作为信息位集合,再通过bitrev_indices把自然序索引映射到反序域,保证编码端用G=F^{⊗n}时与原始论文定义等价。参数说明:N必须是2的幂,K取N/2时R=0.5。beta取2^(1/4)是固定经验值,不要改。bitrev_indices返回的是1-based置换向量,输入1会返回1,输入2(二进制10)在n=3时返回3(二进制011反序后110,即6,加1为7)……这里所有索引统一1-based,MATLAB里数组索引别混用。以N=8、K=4为例,PW排序后最可靠4个信道是索引7、6、5、3(0-based计数),对应MATLAB的8、7、6、4。经过比特反序后,编码时信息位落在反序位置。这个顺序直接影响第3章译码判决时哪些位是信息位、哪些是冻结位,编码和译码必须共用同一个infoIdxRev。建议把这一步单独抽出来,每次跑仿真前先用一个简单测试:随机生成信息比特,编码再译码,检查能否完全还原,能通过再开始蒙特卡洛仿真。3. SC译码的LLR递归实现:f/g节点、部分和与数值边界3.1 从根到叶子的判决顺序:f函数与g函数的角色SC译码本质是在一棵二叉树上前向计算、后向判决。根节点接收信道LLR,然后逐层向下:每个长度为L的节点根据父节点的两条输入消息,先算出左子节点的消息,递归完成左子树判决,再用左子树的判决结果去算右子节点的消息,递归右子树。叶子节点(长度为1)做硬判决,判断该位是0还是1;如果该位置是冻结位,直接强制为冻结值0。两个核心节点函数在LLR域最常用min-sum近似实现:f函数用于计算左子消息。对两个输入(a,b),它的含义是“合并信道分裂后左侧子信道的对数似然比”,完整形式是2·atanh(tanh(a/2)·tanh(b/2)),但工程上直接用近似:sign(a)·sign(b)·min(|a|,|b|),性能损失约0.1到0.2dB,对课程级仿真完全可接受。g函数用于计算右子消息,它依赖左子树的判决结果u。公式是b + (1-2u)·a。当u=0时是a+b,当u=1时是b-a。这个“左子树判决反馈”就是SC译码“逐次消除”名称的来源——后面比特的判决依赖于前面所有比特的结果,一旦前面判错,错误会传播。这也就解释了为什么SC对中短码性能有限:第一个信息比特只有一次估计机会,没有后续纠正机制。SCL正是在这里引入了路径保留,第4章会展开。3.2 可抄作业的SC译码框架与数据约定下面这个实现是递归结构,N在64到512时都能直接跑。保存为sc_decode.m,配合polar_setup生成的infoIdxRev使用。function u_hat = sc_decode(llr_ch, infoMask, frozenVal) % llr_ch: 1xN 信道LLR % infoMask: 1xN 逻辑向量,1=信息位,0=冻结位 N = numel(llr_ch); u_hat = zeros(1, N); part = zeros(1, N); % 部分和数组,存叶子判决 function decode_rec(lo, hi, msg) len = hi - lo + 1; if len == 1 u = double(msg(1) < 0); % LLR<0判1 if infoMask(lo) == 0 u = frozenVal(lo); % 冻结位强制置固定值 end part(lo) = u; u_hat(lo) = u; return; end half = len / 2; % 计算左子节点消息 left = zeros(1, half); for j = 1:half left(j) = f_node(msg(j), msg(j+half)); end decode_rec(lo, lo+half-1, left); % 用左子树判决结果计算右子节点消息 right = zeros(1, half); for j = 1:half right(j) = g_node(msg(j), msg(j+half), part(lo+j-1)); end decode_rec(lo+half, hi, right); end decode_rec(1, N, llr_ch); end function y = f_node(a, b) % min-sum近似 y = sign(a) * sign(b) * min(abs(a), abs(b)); if y == 0 y = sign(a) * sign(b) * 1e-15; % 避免完全置零导致下游NaN end end function y = g_node(a, b, u) y = b + (1 - 2*u) * a; end逻辑说明:decode_rec用[lo, hi]表示当前节点对应u序列的区间,msg是该节点接收到的LLR向量。叶子节点判决后写入part数组,这个part在回溯阶段被g函数读取。f_node中的1e-15处理是为了防止min-sum出现严格0值后sign信息丢失,实际AWGN仿真里LLR几乎不会严格为0,但加了不影响结果。参数说明:infoMask由infoIdxRev生成,1表示该位置是信息位;frozenVal是1xN的0向量。信道LLR的初始化在仿真主循环里完成,BPSK调制下x=1-2*bits,y=x+noise,LLR=2·y/σ²。噪声方差根据Eb/N0换算,σ² = 1/(2·R·10^(EbN0_dB/10)),R=K/N。很多曲线偏移1.8dB的问题就出在这里,后面避坑章专门提。译码前务必做一次自检:把编码后的码字加很小噪声(或直接无噪声)传入sc_decode,看u_hat是否等于编码输入u。不通过就直接检查infoMask和frozenVal的生成逻辑。4. SCL译码的路径分裂与剪枝:列表大小、路径度量与CRC4.1 SCL和SC的区别:硬判决改成路径分裂SC在叶子节点做硬判决后就没有回头路,SCL则把“唯一判决”换成“多条候选路径”。每到一个叶子,每条存活路径分裂成两条,一条判0、一条判1,然后从总共2L条候选路径里挑LLR路径度量最好的L条继续跑。最终在最后一个叶子选出唯一路径输出。路径度量(Path Metric,PM)是SCL的核心。常用的简化形式是:对每条候选路径,如果选的路与该位置LLR的硬判决一致,PM不变;不一致,PM加上|LLR|。累加PM越小代表路径越可信。这个近似在性能上几乎无损,还避免了指数和对数运算,是MATLAB实现里最稳的选择。冻结位的处理也变了:SC里直接强制为固定值;SCL里冻结位同样不允许分裂成两条,只允许走冻结值方向。如果LLR硬判决和冻结值冲突,这条路径的PM要加上|LLR|作为惩罚,保留下来但会排在后面。4.2 路径度量的更新与Top-L选择下面这段是SCL译码在单个叶子处的核心操作,放在一个循环里对L条路径做分裂和筛选。% 每个叶子位置i执行的伪代码块 % paths: LxN 已判决的比特路径 % pm: 1xL 当前路径度量(越小越好) % llr_i: 1xL 当前叶子位置各路径的LLR % infoMask(i)==0表示冻结位 L = size(paths, 1); candNum = L * 2; candPM = inf(1, candNum); candPaths = zeros(candNum, N); for p = 1:L hardBit = double(llr_i(p) < 0); if infoMask(i) == 0 % 冻结位只允许 frozenVal candPaths(2*p-1, :) = paths(p, :); candPaths(2*p-1, i) = frozenVal(i); candPM(2*p-1) = pm(p) + abs(llr_i(p)) * (hardBit ~= frozenVal(i)); else % 信息位正常分裂成0和1 candPaths(2*p-1, :) = paths(p, :); candPaths(2*p-1, i) = 0; candPM(2*p-1) = pm(p) + abs(llr_i(p)) * (hardBit ~= 0); candPaths(2*p, :) = paths(p, :); candPaths(2*p, i) = 1; candPM(2*p) = pm(p) + abs(llr_i(p)) * (hardBit ~= 1); end end [~, idx] = sort(candPM, 'ascend'); keep = idx(1:L); paths = candPaths(keep, :); pm = candPM(keep);逻辑说明:每条路径在叶子处生成候选,冻结位只产生一个候选(代码里占两个位置但第二个一个设为inf?代码实际上对冻结位只写了一个候选,但candNum还是2L,也就是说candPM里第二个候选留了inf,排序后自然被筛掉。为了可读性,这里冻结位写单候选,后面排序前要把未填充的候选PM设为inf,上面代码已经用inf初始化所有候选。信息位产生0和1两个候选,度量更新用hardBit判断是否与LLR硬判决一致,不一致加|LLR|惩罚。参数说明:L是列表大小,常用4/8/16。L越大性能越好但内存和计算量线性增长。N=128时L=8的SCL耗时大约是SC的8到15倍,主要开销在路径复制和排序。sort的候选数最多2L,规模很小,瓶颈其实在每一路径还要同步复制左/右消息缓存。我这里只贴了路径选择部分,完整SCL必须把每条路径的节点消息一起存下来——常见做法是维护三维数组cache(2N, maxLen, L),每个叶子分裂时把旧路径的缓存复制给新路径。4.3 CRC辅助带来的增益:把校验比特放进冻结位SCL真正在中短码上超越SC,靠的是CRC辅助。原理不复杂:编码时在信息比特末尾附加r位CRC校验,这r位也参与极化编码(作为信息位的一部分),译码时SCL不再直接选PM最小的路径,而是先看哪条路径能通过CRC校验,再在通过校验的路径里选PM最小的输出。如果所有路径都不过CRC,才退化为选PM最小路径。实现时注意CRC位穿插的位置。常见做法是CRC比特放在信息位序列末尾,然后一起映射到极化信道的信息位集合。由于信息位集合是按可靠度选的,CRC位会被分配到相对靠后的可靠信道,这让SCL译码器在路径竞争中把校验当作额外的约束条件,错误路径被剪枝的概率大幅提高。一个容易忽略的点:CRC长度不是越大越好。r=8时增益明显,r=16在中短码下增益有限但计算量增加,还会占用原本属于信息比特的信道位置。仿真对比时不要只换L,要同时列CRC配置,否则结果没法横向比较。5. 避坑:SC/SCL仿真里最容易翻车的五个细节5.1 误码率平台:高信噪比下BLER掉不下去现象:仿真跑到Eb/N0=3dB以上时,BLER曲线不再下降,形成一个水平平台,无论增加多少帧都没有改善。原因:信息位集合和编码矩阵的约定不一致。最常见的是直接把PW排序选出的自然序索引当作编码输入位置,而生成矩阵用的是F⊗n,缺少比特反序步骤。两种约定混用时,译码端永远无法正确恢复信息位,误码率被冻结在固定水平。另一个常见原因是信息位数量K和码率R在噪声方差换算里没统一,导致仿真工作在错误的等效信噪比下。解决:把polar_setup里的infoIdxRev单独打印出来,和手工计算一遍的比特反序结果核对。然后做无噪声自检:随机生成K个信息比特,编码后直接译码,u_hat必须和u完全一致。这个测试跑不过,先不要看任何性能曲线。5.2 LLR出现NaN或Inf:高信噪比下数值爆炸现象:Eb/N0大于5dB后,仿真报NaN或Inf,译码结果全错或部分错。原因:BPSK解调里LLR=2y/σ²,高信噪比时σ²非常小,一个小的y噪声波动除以σ²后数值可能远超double类型的稳定范围,尤其N较长时递归计算会放大。SCL路径度量里如果用了exp/log形式的完整PM,同样会溢出,但|LLR|近似形式不容易出现这个问题。解决:LLR初始化时加截断,常见做法是限制在[-20, 20]区间,超过就按边界处理。高信噪比下这个截断对性能影响几乎为零,但能彻底消除NaN源头。SCL统一用|LLR|近似的PM,不要混用完整形式。5.3 SCL性能比SC还差:路径选择或消息复制错了现象:L从1增加到8,BLER曲线反而变差,或者L=1时SCL结果和直接跑SC不一致。原因:SCL在路径分裂后,新路径的左右消息缓存没有正确复制,只是把路径比特序列复制了,导致后续叶子处算出的LLR是旧路径的,新路径的消息错位。另一个原因是排序和索引映射混乱:candPaths矩阵的行号排序后,没有同步更新对应的消息缓存和部分和数组。解决:先做一个小规模验证——在同一帧上同时调用SC和SCL(L=1),输出必须一致,这是SCL正确性的最低门槛。然后在L=2时对比PM排序结果,理清每条路径的下标映射。消息缓存用三维数组且随路径复制,这个内存开销不能省。5.4 仿真时间不可接受:嵌套循环和重复计算现象:N=256、L=16时跑一帧要几十毫秒,十万帧根本不敢想。原因:SCL实现里每个叶子分裂时都从根节点重新计算所有祖先节点的消息,复杂度从O(N log N)恶化到接近O(N² log N)。或者每条路径独立调用一次完整SC译码递归,导致L倍的重复计算。解决:改成树形消息缓存,每个叶子分裂只更新该叶子到根路径上的节点。用迭代从底向上处理叶子,替代每次从根做递归。如果只是验证算法,先跑N=64、L=4,确认性能和趋势没问题再放大参数。真需要大规模蒙特卡洛,建议把核心译码函数改写成MEX或直接用C语言。5.5 Eb/N0曲线整体偏移约1.8dB:噪声方差换算错误现象:所有性能曲线相对论文整体向右或向左平移,形状不变但横坐标偏移。原因:Eb/N0和Es/N0混用。BPSK下Es=Eb·R,噪声方差σ²=N0/2=1/(2·10^(EsN0_dB/10))。如果把Eb/N0直接当成Es/N0代入,就少了R的换算,R=1/2时会差3dB;反过来也会偏。有些代码用EsN0=EbN0+10·log10(R),R=1/2时约为-3.01dB。解决:仿真主循环里统一按σ² = 1/(2·R·10^(EbN0_dB/10))计算。如果要画横轴是Es/N0的图,再单独换算。这个检查放在最前面,不然后面所有参数调整都在错误前提下进行。6. 进阶:仿真加速、内存优化与BLER曲线可信度验证SCL在MATLAB里的性能瓶颈往往不是算法本身,而是路径消息的复制方式。我习惯把缓存从double降到single,N=256、L=8时内存能省一半,速度提升明显,性能损失在BLER曲线上几乎看不见。另一个常用技巧是把f_node和g_node的min-sum计算向量化:同一个节点层内所有位置的消息更新没有数据依赖,可以用矩阵运算一次性算完整个左子树或右子树,递归循环次数从N量级降到log2(N)量级。我还会在译码器里加一个早期剪枝:已经确定所有信息位都判完时,提前结束递归。这个优化在CRC辅助的SCL里尤其有效,因为最后一个信息位之后只剩冻结位,路径不需要继续分裂。还有一种做法是把SCL的sort改成部分排序,因为只需要保留top L,不需要把2L个候选全部排好序,MATLAB里可以用maxk的变体实现,实测能省10%到20%的时间。BLER曲线可信度验证我通常按三步走。第一,先跑SC,和已知论文或标准参考仿真对比趋势,不要求完全重合,但在同参数下相差不能超过0.5dB。第二,跑SCL L=4和L=8,确认L增加后性能单调变好,如果哪个点变差了优先回去查路径复制逻辑。第三,最后一个数据点帧数要足够,估计BLER约为1e-3时,至少要跑10万帧才能让曲线尾部不发散。每次换参数后先跑一个小规模(比如100帧)确认不报错,再上大规模。按这个流程,我把N=128、K=64、BPSK的仿真从一版总出NaN、曲线乱飞的Demo,改到能稳定复现SC与SCL的增益差,前后花了一天多,不算长,但代价是踩完上面五个坑。从那以后我每次跑仿真前都强制走一遍“编码—译码自检—SC对照—SCL对照”的固定流程,这套习惯帮我省掉了大量排错时间。希望帮到你。本文还有配套的精品资源,点击获取