资讯详情

卷积码Viterbi译码MATLAB实现:从编码器到BER曲线避坑指南

📅 2026/10/13 20:29:27 | 华诺云谱 👁 阅读
卷积码Viterbi译码MATLAB实现:从编码器到BER曲线避坑指南
简介这份资源是一套自编的卷积码编解码MATLAB实现面向通信原理、信息论与编码课程的学习者及需要完成相关实验或作业的学生。内容围绕卷积码的编码与Viterbi译码展开涵盖移存器与生成多项式编码、网格图路径搜索、不完全路径长度计算以及硬判决采用Hamming距离、软判决采用Euclidean距离等关键知识点可帮助理解编码效率、约束长度与译码复杂度之间的关系。压缩包共5个文件约39KB包含2个m脚本、1个fig图形文件、1个doc文档和1个mdl模型文件分别对应算法实现、结果可视化、原理说明与Simulink仿真便于对照代码与理论逐步调试。目前已有2072人学习下载适合作为课程作业参考或Viterbi算法入门实践素材。1. 从一次通信课设翻车说起这份卷积编解码 MATLAB 代码到底能干什么带过几届通信原理课设之后我发现一个规律真正卡住人的从来不是 Viterbi 译码的数学推导而是“我明明按公式写了为什么误码率曲线就是不对”。卷积码这个东西课本上讲状态转移、网格图、生成多项式一套下来头头是道可一旦让你自己从零写一份能跑通、能画 BER 曲线、能改约束长度的 MATLAB 代码很多人就懵了。这份自己写的卷积编解码 MATLAB 代码解决的正是这个断层——它不是工具箱里convenc/vitdec的封装调用而是把编码器的移位寄存器、译码器的分支度量计算、加比选ACS和回溯路径全部摊开写给你看。适合正在做通信课设、准备面试被问信道编码、或者想搞明白 Viterbi 到底怎么一步步“活”下来的从业者和学生。下面我按“先跑通、再拆解、后调参”的顺序把这份代码的用法和坑一次讲透。2. 卷积码编码器从生成多项式到移位寄存器的落地实现2.1 为什么不用 MATLAB 自带函数非要自己写MATLAB 通信工具箱确实提供了convenc和vitdec两行代码就能完成卷积编解码。但问题在于工具箱是个黑匣子——你传进去的是生成多项式矩阵出来的是一串比特中间的状态转移、寄存器抽头、尾比特处理全被封装了。课设答辩时老师问一句“你的编码器约束长度是多少寄存器怎么移位的”调用工具箱的人往往答不上来。自己写编码器的价值在于你能精确控制每一个比特进出寄存器的时刻能打印出每个时刻的寄存器状态能验证生成多项式矩阵的每一行到底对应哪几个抽头。这份代码的编码器部分就是按这个思路写的——用一个移位寄存器数组模拟硬件行为逐时钟推进每个时钟周期根据抽头位置做模二加输出编码比特。常见做法是用一个for循环遍历输入比特流内部维护一个长度为约束长度减一的寄存器状态向量。2.2 编码器的核心代码与参数说明先看编码函数的主体结构。假设约束长度为 3生成多项式为 [7, 5]八进制对应二进制 111 和 101即每个输出比特与当前输入及前两个历史比特做模二加function encoded conv_encode(bits, gen_poly, constraint_len) % bits: 输入比特行向量 (0/1) % gen_poly: 生成多项式八进制表示如 [7 5] % constraint_len: 约束长度 K % encoded: 输出编码比特流 n length(gen_poly); % 每输入1比特输出n比特 m constraint_len - 1; % 寄存器级数 reg zeros(1, m); % 初始化移位寄存器 encoded zeros(1, n * length(bits)); % 将八进制生成多项式转为二进制抽头矩阵 taps zeros(n, constraint_len); for i 1:n taps(i, :) de2bi(oct2dec(gen_poly(i)), constraint_len, left-msb); end for k 1:length(bits) % 当前寄存器内容输入比特 历史状态 full_reg [bits(k), reg]; for j 1:n encoded((k-1)*n j) mod(sum(full_reg .* taps(j, :)), 2); end % 移位更新新比特进入最老的移出 reg [bits(k), reg(1:end-1)]; end end逻辑说明taps矩阵把八进制生成多项式展开成二进制抽头模式每一行对应一个输出支路。full_reg把当前输入比特和寄存器历史拼在一起与抽头做点乘后模二加得到该支路的输出。移位操作reg [bits(k), reg(1:end-1)]保证最老的比特被挤出。参数说明constraint_len决定寄存器级数K 越大纠错能力越强但译码复杂度指数增长gen_poly的选择直接影响码的距离谱常用的 (2,1,3) 码生成多项式就是 [7, 5]。注意de2bi的位宽参数必须等于constraint_len否则抽头对不齐编码结果会整体错位——这是新手最容易翻车的地方。2.3 尾比特处理一个容易被忽略的细节卷积码编码有一个绕不开的问题输入比特流结束后寄存器里还残留着历史状态。如果不做处理译码器在末尾几个时刻的分支度量会缺少后续信息导致尾比特译码错误率飙升。标准做法是在信息比特后面补constraint_len - 1个零比特把寄存器“冲干净”这叫尾比特tail bits。这份代码在编码前会自动在输入序列末尾补零译码后再把对应的尾比特丢弃。如果你自己改代码切记补零的数量必须等于寄存器级数少补一个都会让最后几个比特的译码变成玄学。我一般会在编码函数入口处加一行断言检查确保调用者传入的比特流长度和尾比特处理逻辑一致。3. Viterbi 译码器分支度量、ACS 与回溯的完整拆解3.1 网格图构建状态数为什么是 2 的幂Viterbi 译码的核心是网格图trellis。对于约束长度 K、每输入 1 比特输出 n 比特的卷积码状态数等于 2^(K-1)。比如 K3 时有 4 个状态K7 时有 64 个状态。每个时刻每个状态有两条入边对应输入 0 和输入 1译码器要做的就是计算每条边的分支度量然后做加比选。这份代码用一个state_table矩阵预存所有状态转移关系行表示当前状态列表示输入比特矩阵元素是下一状态和对应的输出比特。构建方式很直接——把状态编号转为二进制左移一位后补上输入比特再取低 K-1 位就是下一状态。输出比特则用编码器同样的抽头逻辑算出来。function [next_state, output_bits] build_trellis(constraint_len, gen_poly) num_states 2^(constraint_len - 1); n length(gen_poly); next_state zeros(num_states, 2); output_bits zeros(num_states, 2, n); taps zeros(n, constraint_len); for i 1:n taps(i, :) de2bi(oct2dec(gen_poly(i)), constraint_len, left-msb); end for s 0:num_states-1 state_bin de2bi(s, constraint_len-1, left-msb); for inp 0:1 full_reg [inp, state_bin]; for j 1:n output_bits(s1, inp1, j) mod(sum(full_reg .* taps(j,:)), 2); end next_state(s1, inp1) bi2de([inp, state_bin(1:end-1)], left-msb); end end end这段代码的关键在于next_state的计算新状态等于输入比特拼接当前状态的高 K-2 位。output_bits存的是三维数组第三维是输出支路编号。参数constraint_len一变状态数翻倍整个表的大小跟着变所以译码复杂度对 K 是指数敏感的。3.2 分支度量与 ACS硬判决和软判决的差别分支度量衡量的是“接收到的比特”和“网格图上预期输出”之间的差异。硬判决下接收端先做二值化度量就是汉明距离——预期输出和接收比特不同的位数。软判决则保留解调后的似然比信息用欧氏距离做度量性能通常能提升 2 dB 左右。这份代码默认走硬判决路径分支度量用sum(xor(expected, received))计算简单直接。ACS 单元对每个状态比较两条入边的累积度量保留小的那条同时记录幸存路径。代码里用一个path_metric数组存累积度量一个survivor矩阵存每个时刻每个状态的前驱状态。% ACS 核心循环硬判决 for t 1:num_steps for s 1:num_states % 两条入边的分支度量 bm0 sum(xor(squeeze(output_bits(prev0, 1, :)), rx_bits((t-1)*n1:t*n))); bm1 sum(xor(squeeze(output_bits(prev1, 2, :)), rx_bits((t-1)*n1:t*n))); % 累积度量比较 if path_metric(prev0) bm0 path_metric(prev1) bm1 path_metric(s) path_metric(prev0) bm0; survivor(t, s) prev0; else path_metric(s) path_metric(prev1) bm1; survivor(t, s) prev1; end end end参数说明prev0和prev1是当前状态的两条入边对应的前驱状态需要从网格表中反查。rx_bits是接收比特流按每 n 个一组切片。注意path_metric在每轮开始前要初始化为上一轮的累积值不能清零——清零等于把历史信息全丢了译码结果会完全随机。3.3 回溯译码从终止状态倒推最优路径ACS 循环结束后每个状态都有一个累积度量。理论上应该从度量最小的状态开始回溯但如果加了尾比特终止状态必然是 0 状态直接从状态 0 回溯即可。回溯过程就是沿着survivor矩阵从最后一个时刻往前走每个时刻记录下前驱状态同时根据状态转移反推输入比特。% 回溯 decoded zeros(1, num_steps); current_state 0; % 尾比特保证终止于0状态 for t num_steps:-1:1 prev survivor(t, current_state 1); % 根据 prev - current_state 的转移反推输入比特 decoded(t) find_transition_input(prev, current_state, next_state); current_state prev; end % 去掉尾比特 decoded decoded(1:end - (constraint_len - 1));回溯方向千万别搞反——从后往前是标准做法从前往后需要额外存储前向路径内存开销大且容易出错。find_transition_input是一个查表函数在next_state矩阵里找prev状态下哪个输入对应current_state。如果找不到说明网格表构建有问题通常是生成多项式或约束长度传错了。4. 避坑与排查那些让 BER 曲线跑偏的常见问题4.1 现象误码率曲线在高信噪比下不降反升原因回溯深度不够。Viterbi 译码理论上需要回溯到序列起点才能保证最优但实际实现中通常只回溯5 * constraint_len个时刻就截断。如果回溯深度设得太小幸存路径还没收敛就被强制截断高信噪比下错误反而累积。解决把回溯深度设为5 * constraint_len以上K3 时至少 15K7 时至少 35。代码里用一个traceback_depth参数控制别图省内存把它设成个位数。4.2 现象编码输出全是 0 或者全是 1原因生成多项式转二进制时位宽不对。de2bi默认用最小位宽比如八进制 7 转二进制是 111 需要 3 位但如果约束长度是 4抽头矩阵就会少一列导致寄存器对齐错位。解决de2bi必须显式指定位宽为constraint_len并且用left-msb保证高位在前。这个坑我在三个不同的课设项目里都见过每次都是同一个原因。4.3 现象译码结果比不编码的误码率还高原因ACS 循环里path_metric没有正确初始化或没有跨时刻累积。常见错误是每轮循环都把path_metric清零导致译码器变成“只看当前时刻”的短视决策完全丧失了 Viterbi 算法的优势。解决path_metric只在译码开始时初始化一次通常全零之后每轮 ACS 都在上一轮的基础上累加。可以在循环里加一句打印语句观察度量值是否单调递增如果发现每轮都归零就是初始化位置写错了。4.4 现象不同信噪比下 BER 曲线抖动剧烈原因蒙特卡洛仿真次数不够。每个信噪比点只跑了几百个比特统计涨落太大曲线看起来像心电图。解决每个信噪比点至少跑 10^5 个比特高信噪比下要跑到 10^6 以上才能看到稳定的误码率。代码里用一个num_bits参数控制仿真长度调试阶段可以设小一点快速验证逻辑出图时务必加大。4.5 现象软判决译码性能反而比硬判决差原因软判决的度量计算用错了距离公式。硬判决用汉明距离软判决应该用欧氏距离如果软判决代码里还在用xor算度量相当于把软信息当硬比特用性能自然上不去。解决软判决分支度量改为sum((expected * 2 - 1 - rx_soft).^2)其中rx_soft是解调后的似然比或软符号。注意符号映射关系0 映射为 1 还是 -1 要和调制端保持一致否则度量会反向。5. 进阶调参约束长度、码率与 puncturing 的取舍5.1 约束长度不是越大越好约束长度 K 从 3 增到 7编码增益大约提升 2 dB但译码复杂度从 4 个状态暴涨到 64 个状态ACS 计算量增加 16 倍。在 MATLAB 里跑 K9 的仿真一晚上可能只跑完一个信噪比点。我的经验是课设演示用 K3 或 K4 足够实际工程里 K7 是性能和复杂度的平衡点再往上就要考虑专用硬件了。5.2 码率调整与 puncturing 实现这份代码默认码率是 1/n比如 (2,1,3) 码率就是 1/2。如果想做 2/3 或 3/4 码率需要用 puncturing——按一定模式删除部分编码比特接收端在对应位置插入擦除标记再译码。代码里预留了puncture_pattern参数传入一个 0/1 矩阵1 表示保留0 表示删除。% Puncturing 示例从 1/2 码率得到 2/3 码率 puncture_pattern [1 1; 1 0]; % 每2个输入比特删除1个编码比特 encoded_punctured []; for k 1:size(puncture_pattern, 1) block encoded((k-1)*n1 : k*n); encoded_punctured [encoded_punctured, block(puncture_pattern(k,:) 1)]; end译码时要在删除位置插入NaN或擦除标记分支度量计算时跳过这些位置。注意 puncturing 会降低纠错能力码率越高性能越差这是信息论决定的没有后悔药可吃。5.3 用 BER 曲线验证译码正确性最终验证手段就是画 BER 曲线。横轴 Eb/N0 从 0 dB 到 8 dB纵轴误码率取对数。正确的曲线应该随信噪比单调下降且在 4-5 dB 附近出现明显的“瀑布区”。如果曲线在高信噪比下出现平台error floor通常是回溯深度不够或尾比特处理有误。我一般会把理论值和仿真值画在同一张图上对比偏差超过 1 dB 就要回头查代码。从那以后我每次写完卷积编解码都会先用全零序列跑一遍——全零输入下编码输出应该全是零译码结果也应该全是零这个最简单的测试能过滤掉八成以上的低级错误。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑