☰
卷积码与Viterbi译码MATLAB仿真及误码率排查
2026/11/3 10:53:20 网站建设 项目流程

简介:这份文档围绕卷积码编译码算法研究与实现展开,面向学习数字通信、信道编码及维特比译码的本科生与工程入门者,可用于课程设计、毕业设计选题参考和MATLAB仿真复现。包内仅含1个doc文件,压缩包约991KB,主体为排版完整的论文类文档,涵盖纠错码基础、卷积码编码与译码原理、Viterbi算法流程及仿真分析章节。资源已有349人学习,价值在于把码率、约束长度、回溯深度以及硬判决/软判决对误码率的影响放在同一仿真框架下比较,并给出参数选择与性能优化结论,读者可据此理解编码器记忆、路径度量、回溯搜索等关键概念,也可借鉴其MATLAB实验设计、结果对比和论文组织结构,适合需要快速建立知识框架或查漏补缺的中初级读者。

1. 一条下不去的误码率曲线:卷积码与 Viterbi 译码到底解决什么问题

前阵子帮人看一份链路级仿真脚本,加了卷积码之后误码率曲线在 1e-3 附近就躺平了,信噪比从 4 dB 加到 10 dB,曲线纹丝不动。查了两天,不是信道模型的问题,也不是调制映射的问题,是 Viterbi 译码的回溯深度只给了 8,而约束长度是 7。回溯深度比约束长度还短,等于把刚攒起来的路径信息又扔了。

这个坑很典型。卷积码编译码的原理本身不难,难的是参数之间那点耦合关系——码率、约束长度、回溯深度、判决方式,任何一个选错,仿真出来的结果都会骗你,而且骗得很有说服力,曲线看着平滑、单调、像那么回事。

这份资源是一套完整的卷积码编译码算法研究与 MATLAB 仿真实现,从编码器的移位寄存器一路做到 Viterbi 译码的回溯判决,再用误码率曲线把不同码率、不同约束长度、不同回溯深度、不同判决方式的影响量化出来。论文正文把原理、算法、仿真结论都写全了,代码侧能把整条链路跑通。适合正在做通信方向课程设计或毕业设计的人,也适合手上有个链路想加纠错、又不想从头翻教科书的开发者。

2. 工程结构拆解:一条链路级仿真由哪几段拼起来

2.1 目录结构:五个部分各管什么

这套东西的组织方式很朴素,但边界划得清楚。它不是一个大脚本从头跑到尾,而是把编码、信道、译码、统计拆成独立函数,主脚本只负责串流程和画图。这么拆的好处是改一个参数不用动其他文件,坏处是第一次看得先搞清楚调用关系。

conv_code_sim/ ├── main_sim.m % 主仿真脚本:参数配置 + 流程串联 + 画 BER 曲线 ├── encoder/ │ ├── conv_encode.m % 卷积码编码器,(n,k,N) 通用实现 │ └── build_trellis.m % 由生成多项式生成网格图(状态转移表) ├── channel/ │ ├── bpsk_mod.m % BPSK 映射:0 -> -1, 1 -> +1 │ └── awgn_channel.m % 加高斯白噪声,按 Eb/N0 换算噪声方差 ├── decoder/ │ ├── viterbi_hard.m % 硬判决 Viterbi(汉明距离作分支度量) │ └── viterbi_soft.m % 软判决 Viterbi(欧氏距离平方作分支度量) ├── utils/ │ ├── ber_count.m % 误码率统计,返回误比特数与总比特数 │ └── plot_ber.m % 半对数坐标画曲线,支持多条对比 └── result/ % 仿真结果图与数据落盘

encoder/和decoder/对称放置是有意的:编码和解码共用同一张网格图,build_trellis.m生成的结构体同时被两边引用。这样改码型的时候只需要改一次生成多项式,编码器和译码器自动同步,不会出现「编码按 (2,1,7)、译码按 (2,1,3)」这种对不上的情况。

utils/ber_count.m单独拎出来也值得说一句。很多人的仿真脚本把误码统计写在主循环里,一行sum(dec ~= info)/length(info)了事,看着省事,实际有个隐患:短帧仿真时误码数太少,统计出来的误码率抖动极大,曲线画出来像心电图。单独成函数之后可以顺手加一个「累计误比特数达到阈值就提前结束」的保护。

2.2 一条仿真链路的五段式数据流

整条链路是标准的五段式:信源产生随机比特,编码器加冗余,BPSK 映射成 ±1 符号,AWGN 信道叠噪声,Viterbi 译码还原,最后比对原始比特算误码率。主脚本的骨架大概长这样:

% main_sim.m —— 单次仿真流程骨架 clear; clc; close all; % ===== 1. 参数配置 ===== N = 3; % 约束长度 g = [7 5]; % 生成多项式,八进制表示:(1,1,1) 和 (1,0,1) rate = 1/2; % 码率 k/n K = 10000; % 信息比特数 tb = 20; % 回溯深度 ebno = 0:1:8; % Eb/N0 扫描范围 (dB) nIter = 20; % 每个信噪比点的重复次数 trellis = build_trellis(g, N); % 网格图,编解码共用 berHard = zeros(size(ebno)); for i = 1:length(ebno) errCnt = 0; bitCnt = 0; for it = 1:nIter % ===== 2. 信源 ===== info = randi([0 1], 1, K); % ===== 3. 编码(末尾补 m 个零比特,把寄存器冲干净)===== m = N - 1; coded = conv_encode([info, zeros(1, m)], g, N); % ===== 4. 调制 + 信道 ===== tx = bpsk_mod(coded); % 0->-1, 1->+1 snr = ebno(i) + 10*log10(2*rate); % Eb/N0 -> 符号 SNR rx = awgn_channel(tx, snr); % ===== 5. 译码 + 统计 ===== rxHard = double(rx < 0); % 硬判决:负值判 1 dec = viterbi_hard(rxHard, trellis, tb); dec = dec(1:K); % 截掉补的收尾比特 errCnt = errCnt + sum(dec ~= info); bitCnt = bitCnt + K; end berHard(i) = errCnt / bitCnt; end plot_ber(ebno, {berHard}, {'硬判决 Viterbi'});

这里有几个必须交代清楚的点,否则跑出来的结果会莫名其妙。

第一,收尾比特。卷积码的编码器是有记忆的,最后一组信息的输出还没完全放出来,状态也不在零态。标准做法是在信息序列尾部补m = N-1个零比特,让寄存器彻底清零,译码后再把这部分截掉。补零会让码率略微下降,实际码率变成K/(K+m)*rate,K 足够大时可以忽略,但仿真里 K 只有几千的时候,这个偏差会影响和理论曲线的对齐。

第二,Eb/N0到符号域信噪比的换算。awgn函数吃的是符号信噪比,而论文里横轴通常写 Eb/N0,两者的换算是SNR = Eb/N0 + 10*log10(k*R),BPSK 条件下k=1,R是码率。这个+3dB左右的常数如果漏了,整条曲线会整体右移,看起来「性能变差了」,其实是横轴没对齐。

第三,每个信噪比点重复 20 次取平均。单次 10000 比特在高信噪比下可能一个错都没有,误码率为 0 在半对数坐标上画不出来。重复多次累计误码数,才能在 1e-5 这个量级上还有统计意义。

2.3 参数在代码里的落点:一张表说清

四个核心参数分别影响什么、改哪个变量、预期结果怎么变,这张表我一般贴在脚本开头,省得每次都要回忆:

参数代码变量物理含义增大后的影响
码率 R = k/nrate每 k 比特信息产生 n 比特码字码率越高冗余越少,纠错能力下降,同信噪比下误码率高
约束长度 NN编码器记忆深度,状态数 2^(N-1)纠错能力增强,状态数指数增长,译码复杂度和延迟同步上升
回溯深度tbViterbi 回溯保留的路径长度太短则性能急剧恶化,一般取 5N 以上趋于饱和
判决方式viterbi_hard/viterbi_soft输入是 0/1 还是量化软值软判决通常带来 2 dB 左右的增益,但需要信道侧提供可靠度信息

表格里最容易被忽略的是回溯深度。码率和约束长度大家都熟,教科书公式明明白白,唯独回溯深度是工程参数,书上往往一句「一般取约束长度的 5 倍」带过。但对短帧、低时延场景,取不到 5 倍是常态,这时候性能损失有多大,得自己扫出来。

提示:build_trellis的输出结构体同时被编解码使用,改码型时只改g和N,其他地方不要动。

3. 编码器实现:生成多项式怎么变成网格图

3.1 生成多项式与移位寄存器:三个比特怎么变成两个输出

卷积码编码器的物理形态就是一个移位寄存器加几个模二加法器。以 (2,1,3) 码为例,约束长度 N=3,记忆级数 m=2,寄存器里存着当前输入和前两个历史输入,共 3 个比特。两个子生成元决定这两个输出分别从哪几个寄存器抽头:

  • g1 = (1,1,1),八进制写作 7,意思是三个寄存器全都参与第一个输出的模二加;
  • g2 = (1,0,1),八进制写作 5,意思是第 1 个和第 3 个参与第二个输出。

所以每来一个信息比特,会同时产生 2 个输出比特,码率 1/2。那个「八进制」的写法是通信领域的习惯,因为生成多项式本质是二进制系数向量,写成八进制正好三个比特一组,读起来紧凑。

注意:八进制转二进制时位序问题是最容易翻车的地方。dec2bin(7, 3)得到的是'111',看似没问题,但dec2bin(5, 3)得'101',而有人习惯性写成[1 0 1]的反序[1 0 1]——恰好这个对称看不出问题。换成 6(110)就会暴露:到底是[1 1 0]还是[0 1 1],取决于你把当前输入放在最高位还是最低位。

3.2 状态转移与网格图:4 个状态、8 条分支

N=3 的编码器有 m=2 个寄存器,2^2 = 4 个状态。每个状态在来一个输入比特后有两条出边,所以网格图每个时刻有 4 个状态、8 条分支。把这 8 条分支的输出和去向事先算好存成表,译码时就不用每次现算,这是 Viterbi 能跑快的前提。

function trellis = build_trellis(g, N) % 由生成多项式构建网格图 % g: n 行 N 列的生成多项式矩阵,每行一个子生成元 % N: 约束长度 m = N - 1; % 寄存器级数 n = size(g, 1); % 每个时刻输出比特数 S = 2^m; % 状态总数 trellis.nextState = zeros(S, 2); % nextState(当前状态+1, 输入+1) trellis.output = zeros(S, 2, n); % output(当前状态+1, 输入+1, 输出序号) trellis.numStates = S; trellis.n = n; trellis.N = N; for s = 0:S-1 % 状态编号转寄存器内容,'left-msb' 保证最新比特在最高位 reg = de2bi(s, m, 'left-msb'); if m == 0, reg = []; end for u = 0:1 buf = [u, reg]; % 当前输入 + 历史输入 for j = 1:n % 模 2 内积:抽头系数与寄存器内容逐位相乘求和 trellis.output(s+1, u+1, j) = mod(sum(buf .* g(j, :)), 2); end % 状态转移:新输入进最高位,旧内容右移丢弃最低位 trellis.nextState(s+1, u+1) = bi2de([u, reg(1:m-1)], 'left-msb'); end end end

参数说明:g的每一行是一个子生成元,长度必须等于 N,否则内积对不上;de2bi的'left-msb'选项不能省,默认是右高位,会导致状态和寄存器内容反序,进而让译码器在错误的网格上搜索。trellis.output用三维数组存,第三维是输出序号,这样squeeze(output(s,u,:))就能拿到当前分支的完整 n 比特输出,比用 cell 数组省事。

这个函数是编解码共用的,写完先自测一遍:从状态 0 输入比特 1,输出应该是[1 1],下一状态应该是 2。手算验证一两个分支,比后面调试半天强。

3.3 编码函数的逐段实现

有了网格图,编码其实可以不查表、直接按抽头算,这样代码更直观,也方便和教科书公式对照:

function coded = conv_encode(info, g, N) % 通用 (n,k,N) 卷积码编码,k=1 版本 % info: 1 x L 信息比特行向量 % g: n x N 生成多项式矩阵 % N: 约束长度 m = N - 1; n = size(g, 1); L = length(info); reg = zeros(1, m); % 移位寄存器,初始全零 coded = zeros(1, n*L); % 预分配,避免循环内扩容 for t = 1:L buf = [info(t), reg]; % 当前输入置最高位,历史输入在后 for j = 1:n % 第 j 个子生成元对应的输出,放在该时刻的第 j 个位置 coded((t-1)*n + j) = mod(sum(buf .* g(j, :)), 2); end % 寄存器右移一位,新输入进入最高位 reg = [info(t), reg(1:m-1)]; end end

分段说明:reg是长度为 m 的行向量,初始化为全零代表编码器从零态开始;循环里先拼出buf,长度正好是 N,与生成多项式长度一致,sum(buf .* g(j,:))就是模二加的结果;输出按(t-1)*n + j的位置写入,保证同一时刻的 n 个比特连续存放;最后一行更新寄存器,reg(1:m-1)取前 m-1 个元素,配合新输入完成右移。

有一个容易忽略的边界:m=0 的情况(N=1,无记忆),reg(1:m-1)会变成reg(1:0),MATLAB 里返回空,[info(t), []]得到单元素向量,恰好也对。所以这段代码不用特判 N=1。

3.4 用 poly2trellis 交叉验证,别信自己的第一版

自己写的编码器一定要和 MATLAB 通信工具箱的标准实现对齐一次。poly2trellis、convenc、vitdec这一套是经过大量工程验证的,拿来当基准最省心:

% 交叉验证:自己写的编码器 vs 工具箱 convenc N = 7; gOctal = [171 133]; % (2,1,7) 标准码,八进制 trellisRef = poly2trellis(N, gOctal); info = randi([0 1], 1, 200); m = N - 1; % 工具箱版本 codedRef = convenc([info, zeros(1,m)], trellisRef); % 自己写的版本,先把八进制转成二进制系数矩阵 gBin = zeros(2, N); for j = 1:2 gBin(j, :) = de2bi(oct2dec(gOctal(j)), N, 'left-msb'); end codedMine = conv_encode([info, zeros(1,m)], gBin, N); assert(isequal(codedRef, codedMine), '编码结果与工具箱不一致,先查位序');

这段代码的价值不在跑通,在于「不一致时能立刻定位」。如果 assert 挂了,八成是两个原因:八进制转二进制时位序反了,或者生成多项式矩阵的行对应关系搞混了(第一行应该对应第一个输出比特)。把oct2dec和de2bi拆开写就是为了让位序在代码里显式暴露出来,而不是藏在一个base2dec里。

对齐之后,网格图也可以顺手验证:用同一个trellisRef和自建的trellis分别对同一段随机序列做编码,输出相同则说明build_trellis的状态编号、转移关系、输出表全部一致。

4. Viterbi 译码核心:加比选、路径度量与回溯

4.1 分支度量:硬判决数不同位,软判决算欧氏距离

Viterbi 的本质是在网格图上找一条最大似然路径。最大似然等价于最小欧氏距离,而距离是可以逐段累加的,这就是它能用动态规划求解的原因。每个时刻、每条分支算出来的「这一段有多像接收信号」,叫分支度量。

硬判决下接收端已经把模拟量压成了 0/1,能用的信息只有「不同位的个数」,也就是汉明距离:

% 硬判决分支度量:接收比特与分支期望输出逐位比较 bm = sum(expected ~= received); % expected、received 均为 0/1 行向量

软判决下接收端保留模拟值,分支度量换成欧氏距离平方:

% 软判决分支度量:期望输出映射为 ±1 后与软值求平方误差 expectedSigned = 1 - 2*expected; % 0 -> +1, 1 -> -1 bm = sum((expectedSigned - received).^2);

两种度量差的不只是一个公式。硬判决把接收信号的幅度信息扔了,只留符号;软判决保留了「这个比特有多像 0」的置信度,在低信噪比下能多榨出 2 dB 左右的增益。代价是信道侧要能把软值送上来,中间不能有硬判决环节,实际系统里需要解调器配合。

注意:1 - 2*expected这个映射(0 映 +1、1 映 -1)和前面 BPSK 调制里的0 -> -1, 1 -> +1是反的。这不是笔误,两种约定都有人用,但必须全链路统一。混用的话译码器会系统性地把所有比特判反,误码率恒等于 0.5 且完全不随信噪比变化——这个现象后面还会细说。

4.2 加比选:每个时刻对每个状态只留一条幸存路径

ACS 是 Viterbi 的心脏。每个时刻,对每个状态,把所有能到达它的分支都算一遍「前驱路径度量 + 分支度量」,取最小的那条留下,其余的丢弃。因为后续的路径只依赖当前状态和当前度量值,被丢弃的路径不可能再翻身,这就是最优性成立的前提。

function dec = viterbi_hard(rx, trellis, tbDepth) % 硬判决 Viterbi 译码 % rx: 1 x (n*L) 硬判决比特 % trellis: build_trellis 生成的结构体 % tbDepth: 回溯深度 n = trellis.n; S = trellis.numStates; L = floor(length(rx) / n); INF = 1e9; pm = INF * ones(S, 1); % 路径度量,初始只有零态可达 pm(1) = 0; surv = zeros(S, L); % surv(状态+1, t) = t-1 时刻的前驱状态 decIn = zeros(S, L); % decIn(状态+1, t) = 到达该状态所走的输入比特 for t = 1:L r = rx((t-1)*n + (1:n)); newPm = INF * ones(S, 1); for ps = 0:S-1 if pm(ps+1) >= INF, continue; end % 不可达状态直接跳过 for u = 0:1 ns = trellis.nextState(ps+1, u+1); out = squeeze(trellis.output(ps+1, u+1, :))'; bm = sum(out ~= r); % 汉明距离 cand = pm(ps+1) + bm; if cand < newPm(ns+1) % 加比选:比 newPm(ns+1) = cand; % 更新度量 surv(ns+1, t) = ps; % 记录幸存前驱 decIn(ns+1, t) = u; % 记录幸存输入 end end end pm = newPm; end % 回溯:从度量最小的终态出发,沿幸存路径倒推 [~, st] = min(pm); st = st - 1; dec = zeros(1, L); for t = L:-1:1 dec(t) = decIn(st+1, t); st = surv(st+1, t); end end

逐段看:pm初始化为[0, INF, INF, INF],表示编码器从零态出发,其他状态在 t=0 时刻不可达;外层循环遍历时间,内层遍历前驱状态和输入,cand < newPm(ns+1)就是「比」这一步,只有更优的才覆盖;surv和decIn两个矩阵把决策历史全部存下来,这是回溯的前提。

参数说明:tbDepth在这个完整实现里没有直接用上,因为这里存了全程的决策历史。真正需要tbDepth的是滑窗版本——为了省内存,只保留最近tbDepth个时刻的决策,每译完一段就回溯一段输出。工程实现几乎都用滑窗,但教学和仿真用全量回溯更直观,也方便对比「不同回溯深度」的影响:把surv和decIn截断到tbDepth列即可。

4.3 回溯:为什么回溯深度不能省

回溯深度是个纯工程参数,数学上没有它。理论上最优路径要一直追到 t=0 才能确定第一个比特,但实际中路径在若干步之后就会「合流」——两个状态的前驱在高信噪比下大概率收敛到同一条路径。所以回溯tbDepth步得到的判决,和全量回溯的结果几乎一样,而内存和延迟从 O(L) 降到 O(tbDepth)。

问题在于tbDepth取多少才算够。经验值是5N到7N,但这个「经验」有前提:信噪比不能太低、帧长不能太短、码率不能太高。低信噪比时路径合流慢,5N 可能不够;短帧时连 5N 都凑不齐,只能退化成全量回溯。

我一般会做一次扫描:固定其他参数,让tbDepth从 N 开始按步长递增,看误码率什么时候不再明显下降。对 (2,1,7) 码在 4 dB 附近,这个拐点通常在 25 到 35 之间。低于 20 就开始明显恶化,低于 10 基本等于没译码。

4.4 完整译码函数与一次跑通的自测

写完译码器,第一件事不是接信道,而是做一次「无噪声回环」:编码后的码字直接喂给译码器,看能不能原样还原。这一步能排掉绝大部分低级错误。

% 无噪声回环自测:编码 -> 译码,必须完全一致 N = 7; g = [171 133]; trellis = build_trellis(g, N); info = randi([0 1], 1, 500); m = N - 1; coded = conv_encode([info, zeros(1,m)], g, N); dec = viterbi_hard(coded, trellis, 5*N); dec = dec(1:500); fprintf('回环误比特数:%d\n', sum(dec ~= info)); assert(isequal(dec, info), '无噪声回环失败,先查网格图和状态编号');

回环通过只说明编码和译码用的是同一张网格图,还不能说明网格图本身是对的。第二步要和工具箱对齐:用vitdec译同一段码字,比较输出。第三步才是接 AWGN 看误码率曲线是否落在合理区间——(2,1,7) 码在 4 dB 附近,硬判决的误码率应该在 1e-3 量级,软判决应该更低。如果差了数量级,先怀疑Eb/N0换算,再怀疑度量公式。

提示:调试 Viterbi 时把pm每个时刻的最小值和最大值打出来看一眼。正常情况两者之差不会随时间无限增长,如果差值越来越大,说明度量没有归一化,长帧会溢出或者精度丢失。

5. 参数扫描与踩坑排查:四组对比实验和五条血泪记录

5.1 四组对比实验怎么组织脚本

论文里做了四组对比,对应四个参数。脚本层面不用写四份,把主仿真包成函数,参数从外面传进去就行:

function ber = run_one_case(cfg) % 单组参数下的误码率仿真 % cfg 字段:N, g, rate, K, tb, ebno, mode('hard'/'soft'), nIter trellis = build_trellis(cfg.g, cfg.N); ber = zeros(size(cfg.ebno)); for i = 1:length(cfg.ebno) errCnt = 0; bitCnt = 0; for it = 1:cfg.nIter info = randi([0 1], 1, cfg.K); m = cfg.N - 1; coded = conv_encode([info, zeros(1,m)], cfg.g, cfg.N); tx = bpsk_mod(coded); snr = cfg.ebno(i) + 10*log10(2*cfg.rate); rx = awgn_channel(tx, snr); if strcmp(cfg.mode, 'hard') dec = viterbi_hard(double(rx < 0), trellis, cfg.tb); else dec = viterbi_soft(rx, trellis, cfg.tb); end dec = dec(1:cfg.K); errCnt = errCnt + sum(dec ~= info); bitCnt = bitCnt + cfg.K; end ber(i) = errCnt / max(bitCnt, 1); end end

四组实验分别改cfg的一个字段:码率组改rate和对应的g(比如 1/2、2/3、3/4 的凿孔码),约束长度组改N和g(N 从 3 到 9),回溯深度组改tb(从 5 到 60),判决方式组改mode。其他字段保持不变,这样曲线之间的差异才能归因到那一个参数上。

画图时统一横轴是 Eb/N0(dB),纵轴是误码率的对数坐标,曲线数量控制在四条以内,否则图例盖住数据。每组实验单独出一张图,不要挤在一张里。

5.2 五条踩坑记录

坑一:误码率曲线在高信噪比处不再下降,形成「地板」

现象:Eb/N0 从 6 dB 加到 12 dB,误码率卡在 1e-3 附近不动,曲线几乎是水平的。

原因:回溯深度太短。tb给了 8 而 N=7,回溯步数比约束长度还少,路径还没合流就被强行截断判决,每次截断都引入一次随机错误。这个错误率与信噪比无关,所以形成了地板。

解决:tb至少取5N,短帧场景改全量回溯。改完再扫一遍,曲线会在高信噪比处继续往下走。

坑二:软判决反而比硬判决差

现象:同样信噪比下,viterbi_soft的误码率比viterbi_hard还高,理论上不该出现。

原因:BPSK 映射和软判决度量里的符号约定不一致。调制端用的是0 -> -1, 1 -> +1,译码端度量里写的是1 - 2*expected(即0 -> +1),两者反了。译码器在找最不相似的路径,自然越译越错。

解决:全链路统一一个约定,写在脚本开头的注释里。我习惯用0 -> -1, 1 -> +1,那么软判决的期望符号就是2*expected - 1。改完立刻见效。

坑三:编码后长度对不上,尾部丢比特

现象:length(coded)不等于n * (K + m),或者译码输出截掉前 K 位之后还剩一截,误码率虚高。

原因:收尾比特没处理。要么编码前没补零让寄存器清零,要么译码后截断的位置算错了(截了K位但实际信息位不在最前面)。

解决:编码前显式补m个零,译码后显式截前K位,两步都写成独立语句,不要藏在函数调用里。补零造成的码率损失在 K 大于 1000 时可以忽略,但要在论文里说明。

坑四:误码率恒等于 0.5,完全不随信噪比变化

现象:任何 Eb/N0 下误码率都是 0.5 左右,曲线是一条水平线。

原因:译码器根本没有在正确的网格上搜索。常见触发点有三个——生成多项式八进制转二进制时位序反了;build_trellis里de2bi没加'left-msb';编码器和译码器用的不是同一张网格图。

解决:先用无噪声回环自测,再用poly2trellis+convenc+vitdec做三方对齐。位序问题在对齐时会立刻暴露,因为工具箱的行为是确定的。

坑五:低信噪比下译码比不编码还差

现象:Eb/N0 在 0 dB 附近时,编码系统的误码率比未编码的 BPSK 还高,曲线交叉。

原因:这不是 bug,是正常现象。卷积码的纠错能力依赖于「错误是稀疏的」,低信噪比下错误密度超过码的纠错半径,译码器反而会把正确的比特翻成错的。香农限以下的区域本来就该这样。

解决:不要试图「修」这个问题。论文里应该把交叉点标出来并解释清楚,这恰恰是编码增益概念的落脚点——编码增益是在给定误码率下比较的,不是所有信噪比下都占优。

另外补一条容易混淆的:awgn函数默认按符号功率加噪,如果信号功率不是 1(比如 16QAM 归一化没做),噪声方差会算错,表现为整条曲线平移。BPSK 下tx是 ±1,功率为 1,不会有这个问题,但换调制方式时必须检查。

6. 二次开发改造:换码型、加凿孔与验证方法

6.1 换一个码型:从 (2,1,3) 到 (2,1,7)

改码型只需要动两个变量:N和g。约束长度从 3 提到 7,状态数从 4 涨到 64,译码复杂度大约翻 16 倍,但误码率在 1e-4 处能改善将近 2 dB。标准七号码的生成多项式是[171 133](八进制),换上去之后一定要重跑无噪声回环和工具箱对齐,因为位序问题的暴露概率随长度增加而上升。

如果想试 (3,1,N) 这种 1/3 码率,把g改成三行,build_trellis和conv_encode都不用改,它们对 n 是通用的。但viterbi_hard里的bm = sum(out ~= r)会自动适配,因为out和r的长度都由trellis.n决定。

6.2 加凿孔:用删余换码率

凿孔(puncturing)是工程上调整码率的标准手段:按固定图案删掉一部分编码输出,接收端在对应位置插入「擦除」标记,译码时这些位置的度量置零即可。1/2 母码删掉 1/4 的输出就是 2/3 码,删掉 1/3 就是 3/4 码。

改造点有三处:编码后按图案删位;译码前按同一图案插回擦除标记;分支度量里遇到擦除位时跳过该位的贡献。第三处最容易写错,正确做法是把该位的度量记为 0(不贡献任何距离),而不是当成 0.5 或者随机值。

6.3 验证方法:别只看曲线好看

改完之后怎么确认没改错,我一般走三步。第一步,无噪声回环必须零误码,这一条挂了说明网格图或者补零逻辑有问题。第二步,和工具箱的vitdec逐比特比对一段带噪数据,输出必须完全一致——不一致说明度量公式或者状态编号有偏差。第三步,和bercoding给出的理论值比对量级,硬判决在 4 dB 附近应该是 1e-3 量级,软判决应该更好一截,差了数量级就回去查Eb/N0换算。

第三步容易被跳过,但它其实最有价值。理论和实现的偏差如果稳定存在,通常不是随机 bug,而是某个系统性约定不一致,比如码率换算漏了、收尾比特没扣掉、噪声方差按符号功率算了但信号实际不是单位功率。这种偏差看曲线看不出来,和理论值一比就现形。

最后一条是我自己的教训:早期做这套仿真,我把「无噪声回环通过」当成了「编码器写对了」的证明,结果网格图里状态编号是反的,回环照样能过(因为编码译码用的是同一张错图),接上信道才发现曲线整个是废的。从那以后我每次改完编解码代码,无噪声回环、工具箱对齐、理论量级比对这三步都要强制走一遍,一步都不省。希望帮到你。

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

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

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

立即咨询