(2,1,7)卷积码:从生成多项式到维特比译码与Simulink仿真
2026/9/17 13:32:41 网站建设 项目流程

简介:面向通信工程、电子信息类专业学生与初学信道编码的工程师,这份文档围绕MATLAB/Simulink环境下卷积编码器的设计与仿真展开,可用于课程设计、毕业设计选题参考以及自学纠错编码原理。全文从分组码与卷积码的区别切入,梳理卷积编码原理、编码算法与结构描述方法,说明约束长度、码率、生成多项式对纠错性能的影响,并给出(2,1,2)、(2,1,3)等典型编码器的推导过程。第二章以MATLAB程序与Simulink模块图实现建模,观察编码后码流,结合最小距离与误码率曲线分析性能,并延伸到Viterbi译码的配合使用思路。压缩包为doc格式,仅1个文件,约365KB,体积轻巧,可直接作为理论推导与仿真实验的报告底稿。目前已有592人学习下载,适合希望把编码原理快速落到仿真验证的读者。

1. 同样码率 1/2,(2,1,7) 卷积码为什么能比分组码省下几个 dB

做链路预算的时候经常撞到这种局面:调制方式已经定死 BPSK,发射功率不能再加,误码率指标还差一截,唯一能动的是信道编码。分组码的思路是把 k 个信息位攒够一组再算校验位,组与组之间互不相关,译码必须等整组收齐,码长 n 一大,缓冲和延时跟着涨;卷积码反过来,k 和 n 取得很小,常见的就是 (2,1,K),每来一个信息比特立刻吐出两个码元,输出不仅和当前比特有关,还和前面 K-1 个比特有关。

这个「有关」就是约束长度,也是纠错能力的来源。约束长度每增加 2,自由距离 d_free 大概往上抬 2~3,误码率曲线在同一个 Eb/N0 下会明显往下压。代价是编码器移位寄存器级数变多,维特比译码的状态数按 2^(K-1) 指数增长,K=7 时是 64 个状态,K=9 就跳到 256 个,译码器的分支度量运算量和存储量跟着翻倍。

下面按「生成多项式 → 状态转移表 → MATLAB 编码 → Simulink 链路 → 参数取舍」推一遍,每一段都尽量落到能跑起来的代码和能改的参数上。适合做通信原理课程设计、写 FPGA 编码器逻辑、或者在链路仿真里需要一张可对比误码率曲线的人。

2. 生成多项式、状态转移表与网格图:把编码器写成一个有限状态机

2.1 冲击响应视角:约束长度约束了什么

先拿最简单的 (2,1,3) 编码器做解剖。它有两级移位寄存器 D1、D2,一个输入比特 m,两个模 2 加法器输出 c1、c2。生成多项式写作 g1 = 1 + D + D²,g2 = 1 + D²,意思是 c1 由「当前输入、上一拍输入、上上拍输入」三者异或得到,c2 由「当前输入、上上拍输入」异或得到。

判断约束长度的一个笨办法但很好用:往编码器里塞一个孤立的 1,其余全塞 0,看这个 1 能在输出端「影响」几个时刻。M 个输出时刻后就彻底消失,说明寄存器最多记 K-1 = 2 个历史比特,约束长度 K = 3,编码后相互关联的码元总数是 n·K = 6。

% 冲击响应实验:(2,1,3) 编码器,输入单个 1 后跟一串 0 g1 = [1 1 1]; g2 = [1 0 1]; % 对应八进制 7 和 5,第 1 位是当前输入 reg = [0 0]; % 两级寄存器,初始全零 out = []; for m = [1 0 0 0 0] buf = [m reg]; % 拼成 1x3 的窗口 out = [out mod(sum(buf.*g1),2) mod(sum(buf.*g2),2)]; %#ok<AGROW> reg = [m reg(1:end-1)]; % 右移,丢掉最老的比特 end disp(out) % 1 1 0 1 1 1 0 0 0 0

这段代码里的buf就是抽头窗口,g1/g2中为 1 的位置表示该位参与异或。输出前三个时刻分别是 11、01、11,三个时刻之后就全是 0——冲击响应的长度正好等于 K。把任意输入序列看成若干个移位后的冲击响应叠加,异或起来就是编码输出,这就是「卷积」这个名字的来历。

2.2 八进制生成多项式的读法和抽头对应关系

工程上不会把抽头写成 1+D+D²,而是八进制。171 这个数转成二进制是 1111001,从右往左数第 1 位对应当前输入比特,依次往左对应 D¹、D²……D⁶,K=7 时一共 7 位,正好覆盖 6 级寄存器加当前输入。133 转成二进制是 1101011,抽头接在 1、2、4、6、7 位上。

八进制二进制(K 位)K含义
71113当前输入、D¹、D² 全参与
51013当前输入和 D² 参与
17111110017六条支路异或,典型前向抽头
13311010117反馈感更强的抽头组合
5611011100019K=9 常用好码之一

把八进制串转成抽头向量没有必要手算,一行就够:

function taps = oct2taps(octstr, K) % 八进制生成多项式字符串 -> 1xK 抽头向量 % 输出第 1 位对应当前输入,第 K 位对应最早的寄存器位 taps = dec2bin(base2dec(octstr, 8), K) - '0'; end % 用法 oct2taps('171', 7) % 1 1 1 1 0 0 1 oct2taps('133', 7) % 1 1 0 1 0 1 1

base2dec(...,8)负责八进制转十进制,dec2bin(...,K)补足 K 位,减字符'0'把 ASCII 字符变成 0/1 数值。这个函数在做码搜索和参数批量扫描时比查表靠谱得多。

2.3 状态转移表:手推 (2,1,3) 的四个状态

把两级寄存器 D1D2 的取值当成状态,就有 a=00、b=01、c=10、d=11 四种。每来一个输入比特,寄存器右移一格,D1 更新为当前输入,D2 更新为原来的 D1,同时拍出两个码元。定义 c1 = m ⊕ D1 ⊕ D2,c2 = m ⊕ D2,可以把四个状态的两条出边全部列出来:

当前状态输入 m下一状态输出 c1c2
a (00)0a00
a (00)1c11
b (01)0a11
b (01)1c00
c (10)0b10
c (10)1d01
d (11)0b01
d (11)1d10

这张表就是编码器的完整行为描述。拿输入序列 101101 从状态 a 出发走一遍:1 → c,输出 11;0 → b,输出 10;1 → c,输出 00;1 → d,输出 01;0 → b,输出 01;1 → c,输出 00,码流是 11 10 00 01 01 00。不同教材对寄存器编号方向的定义不一致,同一串输入可能整体位移一位,核对时以工具箱输出为准,别硬扛。

注意:手推状态表时最容易错的是状态编号顺序。D1 是最近一次输入还是最老一次输入,直接决定表格长什么样,改动之前先确认自己的约定。

2.4 从网格图到自由距离

把状态表沿时间轴铺开就成了网格图,每个时刻一列状态点,连线代表转移,线上的标注是输出码元。维特比译码要干的事,就是在这张网格里找一条与接收序列汉明距离(硬判决)或欧氏距离(软判决)累计最小的路径。

衡量一个卷积码好坏的核心指标是自由距离 d_free,即从全零路径出发再回到全零路径的所有非零路径中最小的重量。它决定了渐近误码率随 Eb/N0 下降的斜率,也决定能纠几个错:硬判决下大致能纠 (d_free-1)/2 个比特错误。常见好码的自由距离大致是

码型生成多项式(八进制)d_free状态数
(2,1,3)7, 554
(2,1,5)23, 35716
(2,1,7)171, 1331064
(2,1,9)561, 75312256

K 从 7 涨到 9,d_free 只从 10 涨到 12,但状态数翻了四倍。这就是为什么实际系统里 K=7 用得最多——增益和复杂度在这个点上比较划算。

3. MATLAB 编码实现:poly2trellis、convenc 与手写移位寄存器

3.1 用 poly2trellis 生成网格描述

MATLAB 里描述卷积码的标准结构叫 trellis,字段包括 numInputSymbols、numOutputSymbols、numStates、nextStates 和 outputs。手填这五个字段既烦又容易错,poly2trellis可以把生成多项式直接翻译过去:

K = 7; % 约束长度 trellis = poly2trellis(K, [171 133]); % 生成多项式,八进制写字符串或十进制都行 disp(trellis.numStates) % 64

poly2trellis的第一个参数是约束长度,第二个参数是生成多项式数组,元素个数等于 n(这里是 2)。八进制必须写成'171'这种字符串形式,写成数字 171 会被当成十进制算错。如果生成多项式对应的寄存器级数不足 K-1,函数会直接报错,这算是免费的自检。

3.2 convenc 一行完成编码,尾比特不能忘

rng(0); msg = randi([0 1], 1, 200); % 200 个随机信息比特 msg_pad = [msg zeros(1, K-1)]; % 末尾补 K-1 个零,把状态逼回全零 code = convenc(msg_pad, trellis); % 输出 2*(200+6) = 412 个码元

convenc只做编码,不认识「帧结束」这件事。如果不补尾比特,译码端最后一小段的状态是未知的,维特比译码在这一段的路径选择会失去约束,每帧末尾几个比特错误率明显偏高,整条 BER 曲线会卡在一个错误地板上不往下走。补零之后状态归零,译码端可以从已知的终止状态回溯,这段损失就消失了。

代价是额外传了 K-1 个冗余比特,码率从 1/2 略微下降到 200/412 ≈ 0.485。信息帧很短的时候这个开销不能忽略,所以短包系统里常见另一种做法:不补零,译码端用 Truncated 模式按固定回溯长度强行判决。

3.3 手写移位寄存器版本:和硬件逻辑一一对应

工具箱函数跑得再顺,写 FPGA 或者写 C 的时候还是得自己实现一遍。下面这个函数按硬件的数据流写,reg就是那排移位寄存器:

function code = conv_encode_2_1_K(msg, g1, g2, K) % (2,1,K) 卷积编码,逐比特推进的移位寄存器模型 % msg : 1xN 信息序列(含尾比特) % g1,g2: 长度 K 的抽头向量,第 1 位对应当前输入 % code : 1x2N 编码输出,c1 c2 交替排列 reg = zeros(1, K-1); % K-1 级寄存器,初始全零 code = zeros(1, 2*numel(msg)); idx = 1; for i = 1:numel(msg) buf = [msg(i) reg]; % 当前输入 + 历史状态 c1 = mod(sum(buf .* g1), 2); % 模 2 加等价于异或 c2 = mod(sum(buf .* g2), 2); code(idx:idx+1) = [c1 c2]; idx = idx + 2; reg = [msg(i) reg(1:end-1)]; % 右移一位,丢弃最老比特 end end

三个地方值得盯住:buf的长度必须是 K,多了少了sum(buf.*g1)都会算错;mod(...,2)是异或的数学等价形式,换成xor链也行但写法更啰嗦;reg的更新必须在计算输出之后,顺序颠倒就等于提前把当前比特移进了寄存器,输出整体错一格。

和工具箱对拍一下:

K = 3; msg = [1 0 1 1 0 1 0 0 0]; % 末尾自带 2 个尾比特 mine = conv_encode_2_1_K(msg, [1 1 1], [1 0 1], K); trellis = poly2trellis(3, [7 5]); ref = convenc(msg, trellis); isequal(mine, ref) % 返回 1 才算通过

对拍不通过时先看长度,conv_encode_2_1_K输出 2N 个码元,convenc在输入已含尾比特的情况下输出也是 2N 个。长度一致再逐位比,前面几位就错通常是抽头顺序反了,只有末尾几位错基本是尾比特数量不对。

3.4 矩阵法和移位寄存器法的差别

通信原理教材里还有一套基于生成矩阵 G 的写法,MATLAB 里对应的是把生成矩阵和输入序列做模 2 乘:

function output = cnv_encd(g, k0, input) % g : 生成矩阵,行数 = 输出端口数 n0,列数 = l*k0(l 为约束长度) % k0 : 每拍输入比特数 % input: 输入信息序列 if rem(length(input), k0) > 0 % 长度不是 k0 整数倍就补零 input = [input, zeros(1, k0 - rem(length(input), k0))]; end n = length(input) / k0; l = size(g, 2) / k0; % 约束长度 n0 = size(g, 1); % 输出端口数 u = [zeros(1,(l-1)*k0), input, zeros(1,(l-1)*k0)]; % 首尾补零,状态从零出发回到零 ul = u(l*k0:-1:1); for i = 1:n+l-2 ul = [ul, u((i+l)*k0:-1:(i*k0+1))]; % 反向滑窗,取 l 组 end uu = reshape(ul, l*k0, n+l-1); % 重排成矩阵,每列一拍 output = reshape(rem(g*uu, 2), 1, n0*(l+n-1)); % 模 2 乘 + 展平 end

这段代码的巧妙之处在于把「移位」这个时序动作压成了矩阵乘法:uu的每一列是同一时刻所有寄存器里的值,g*uu一次性算出所有时刻的输出。rem(g*uu,2)是逐元素取模 2,对应硬件里的异或门阵列。

矩阵法在批量处理长序列时比 for 循环快不少,但它不适合直接映射到硬件,而且中间变量ul的构造依赖l*k0这个长度,k0 变大之后索引很容易写错。仿真验证用矩阵法,硬件实现参考移位寄存器版本,是比较省事的组合。

4. Simulink 链路搭建:编码—AWGN—维特比译码与误码率扫描

4.1 模块清单与连线顺序

Simulink 里跑通一条卷积码链路不需要多少模块,关键是把维特比译码器的参数和编码器对齐:

Bernoulli Binary Generator -> Convolutional Encoder -> BPSK Modulator Baseband -> AWGN Channel -> BPSK Demodulator Baseband -> Viterbi Decoder -> Error Rate Calculation -> Display

误码率统计需要一个参考支路:从 Bernoulli Binary Generator 直接拉一根线到 Error Rate Calculation 的 Tx 端口,Rx 端口接维特比译码的输出。如果 AWGN 已经用 Eb/No 模式设置,BPSK 调制解调这两个模块可以省掉,让 AWGN 直接工作在比特域,仿真速度会快很多。

4.2 关键参数表

模块参数名建议取值说明
Bernoulli Binary GeneratorSample time1/1000决定每帧比特数
Convolutional EncoderTrellis structurepoly2trellis(7,[171 133])与译码器必须完全一致
Operation modeTerminated结尾补 K-1 个零,状态归零
AWGN ChannelEb/No (dB)0 到 8扫曲线时用变量EbNo驱动
Number of bits per symbol1BPSK 对应 1
Input signal power1与 BPSK 输出功率匹配
Viterbi DecoderTrellis structure同编码器改一个必须同步改另一个
Decision typeHard decision想试软判决选 Unquantized
Traceback depth35取 5·K,K=7 时为 35
Error Rate CalculationReceive delay0(Terminated 模式)补零方案下译码输出与输入对齐

Traceback depth是维特比译码里最需要调的一个参数。它决定译码器回溯多少步才输出判决比特,取值太小误码率降不下去,取值太大只是白白增加延迟,超过 5K 之后曲线基本不再变化。

4.3 用脚本批量扫 Eb/N0

手动改一个参数点一次运行,扫六条曲线要点几十次。用set_paramsim循环更省事:

model = 'convcode_link'; load_system(model); EbNo = 0:1:8; ber = zeros(size(EbNo)); for k = 1:numel(EbNo) set_param([model '/AWGN Channel'], 'EbNo', num2str(EbNo(k))); set_param([model '/AWGN Channel'], 'EsNodB', num2str(EbNo(k))); simOut = sim(model, 'StopTime', '0.1'); % 每点跑固定时长 ber(k) = simOut.get('ber_out'); % To Workspace 变量名 end semilogy(EbNo, ber, '-o'); grid on; xlabel('E_b/N_0 (dB)'); ylabel('BER');

set_param的第一个参数是模块的完整路径,模型层级深的时候路径写错会直接报找不到模块。sim返回的对象取变量的方式取决于你在模型里用的是 To Workspace 还是logsout,前者用get('变量名'),后者要遍历simOut.logsout。噪声是大数定律游戏,Eb/N0 高的时候单次短仿真可能一个错误都统计不到,曲线尾部会出现断点,把StopTime调大或者固定错误数停止才是正确做法。

4.4 三个常见报错的处理思路

误码率恒为 0.5:几乎一定是发送和接收的比特没对齐。Terminated 模式补了尾比特,但误码统计没有设置对应的延迟,或者译码器输出的是硬判决 0/1 而统计模块期望的是 ±1。

误码率随 Eb/N0 完全不变:编码器和译码器用了两套不同的 trellis。常见于改了编码器的生成多项式却忘了同步译码器,或者一边写'171'字符串、另一边写 171 数字。

每帧固定错最后几个比特:译码器选了 Truncated 模式但输入没有补零,或者回溯深度小于 K,末尾状态还没收敛就被强行判决。把 Operation mode 改成 Terminated,或者把回溯深度提到 5K 以上。

提示:模型跑通之后,如果打算把编码器搬到 FPGA,可以用 Embedded Coder 对编码器子系统做 C 代码生成,把手写移位寄存器版本和自动生成的版本逐位对拍,比对着波形手算快得多。

5. 参数取舍:回溯长度、打孔码率和生成多项式搜索

维特比译码的回溯长度 τ 和误码率的关系不是线性的。τ 从 5 涨到 20,BER 明显下降;τ 超过 5N(K=7 时是 35)之后曲线基本贴合,再加大只增加译码延迟。下面这张表是 K=7、Eb/N0=4 dB 附近常见的趋势,实际数值会随仿真长度浮动:

回溯长度 τ相对译码延迟误码率趋势
10明显偏高,有错误地板
25接近收敛
35 (5N)较高基本收敛
50无明显改善

工程上取 τ = 5N 是默认选择,只有对延迟极敏感的场景才会往下压,压到 3N 以下要重新评估误码率指标。

码率调整靠打孔。母码 (2,1,7) 的码率是 1/2,带宽吃紧时按固定图案删掉一部分校验位就能提到 2/3 甚至 3/4:

trellis = poly2trellis(7, [171 133]); puncpat = [1 1 0 1 0 1]; % 6 位窗口保留 4 位,删掉第 3、5 位 msg = randi([0 1], 1, 300); code = convenc([msg zeros(1,6)], trellis, puncpat); % 长度约为输入的 2/3 decoded = vitdec(code, trellis, 35, 'term', 'hard', puncpat);

puncpat里 1 表示保留、0 表示删除,译码端的图案必须和编码端逐位一致,差一位整条链路的输出就全乱。打孔换来带宽,代价是 d_free 下降、抗噪能力变差,1/2 打到 3/4 通常要损失 1~2 dB 的编码增益。

生成多项式可以直接暴力搜。约束长度 K 固定后,候选就是 2^(K-1) 到 2^K-1 之间的奇数(首位必须为 1,否则当前输入不参与编码),穷举一遍用距离谱打分:

K = 7; best = struct('dfree', 0, 'g', []); for g1 = 2^(K-1)+1 : 2 : 2^K-1 for g2 = 2^(K-1)+1 : 2 : 2^K-1 trellis = poly2trellis(K, [g1 g2]); ds = distspec(trellis, 4); % 距离谱,返回最小距离等信息 if ds.dfree > best.dfree best.dfree = ds.dfree; best.g = [g1 g2]; end end end fprintf('最优: %o %o, d_free = %d\n', best.g(1), best.g(2), best.dfree);

步长取 2 是为了只遍历奇数,distspec的第二个参数是搜索深度,给小了可能搜不到真正的最小重量路径。这套穷举不需要 Optimization Toolbox,标准通信工具箱就能跑,K=7 时双层循环大概几千次,几秒钟出结果,搜出来的典型解就是 171 和 133 这一对。

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

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

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

立即咨询