ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MATLAB卷积编码Viterbi译码仿真:软硬判决BER对比与避坑指南

MATLAB卷积编码Viterbi译码仿真:软硬判决BER对比与避坑指南 简介面向通信系统学习者的Matlab仿真资源聚焦卷积编码与Viterbi译码算法性能对比通过软判决与硬判决两种译码方式的仿真结果直观展示误码率差异适用于本科课程设计、研究生算法验证或工程人员快速上手。压缩包共16个文件以10个m源程序为核心涵盖PN码生成、数据分组、调制、编码、软/硬判决接收等完整仿真链路另有4个mat数据文件用于中间结果保存、1个操作录像和1张图示整体大小约109.92MB。目前已有1529人学习浏览足见其实用价值。随包附带的mp4操作录像明确演示了Matlab路径设置与运行注意事项可有效规避环境配置问题核心脚本如VitEnc、SoftVitDec、ReceiverHD、ReceiverSD等模块划分清晰便于按模块研读和二次开发。整体框架适合对比不同判决策略下的卷积码性能也能作为卷积码与Viterbi译码相关课程设计的仿真实例帮助读者深入理解Viterbi译码实现细节。1. 卷积编码和Viterbi译码软判决“应该”更好为什么你的仿真却在翻车同样是卷积编码和Viterbi译码性能仿真A同学把硬判决曲线跑通之后把vitdec的参数从hard换成softBER 曲线反而更差甚至在高信噪比处出现平台。这不是 Viterbi 算法出了问题而是软判决的量化映射、度量计算和 Eb/N0 换算三处各错了一点。这个标题要解决的正是这样一件事在 MATLAB 里把卷积编码、Viterbi 译码的完整链路搭起来分别用硬判决和软判决译码跑出误码率曲线再做一次公平对比。适合正在做信道编码课程设计、或者需要评估编码增益的通信工程师。你只需要会 BPSK 和 AWGN 的仿真套路剩下的代码可以直接抄坑位我提前踩给你看。2. 硬判决链路先跑通poly2trellis 建码、vitdec 译码与最小 MATLAB 仿真2.1 从生成多项式到网格图先把卷积码的“骨架”搭对卷积码本质上是一个有限状态机。以经典的 (2,1,7) 码为例每个时刻输入 1 个信息比特输出 2 个编码比特码率 R1/2约束长度 K7意味着当前输出由当前输入和前 6 个历史比特共同决定寄存器状态有 64 个。MATLAB 里建码只用一行但这一行的参数决定了后面所有仿真结果trellis poly2trellis(7, [171 133]);7是约束长度[171 133]是两个生成多项式的八进制表示对应两个输出支路的抽头位置。poly2trellis会返回一个结构体里面包含numInputSymbols、numOutputSymbols、nextStates、outputs四个字段vitdec和convenc都依赖这个结构体。这是后面所有仿真代码的公共底座。Viterbi 译码做的事情是在这个网格图上找一条与接收序列“距离最近”的路径。硬判决的场景里接收端先把每个符号切成 0/1再计算汉明距离译码器每个状态保留一条幸存路径最后回溯输出。MATLAB 的vitdec把整条路径搜索封装成了黑匣子但你必须知道它有三个行为会影响结果工作模式cont还是term、回溯深度tb、以及输入数据的格式。这三个参数没设对仿真曲线会整体偏移甚至完全错误。2.2 硬判决最小仿真从随机比特到 BER 曲线先给一个能直接跑的硬判决仿真完整脚本。这个脚本我习惯把它作为基线版本后面软判决、对比实验全都基于它改。% hard_decision_ber.m % (2,1,7) 卷积码BPSK AWGN硬判决 Viterbi 译码 clear; clc; trellis poly2trellis(7, [171 133]); % 约束长度7生成多项式 171/133 (八进制) tb 35; % 回溯深度经验值取 5*(约束长度) nFrame 200; % 每个信噪比点的帧数 frameLen 1000; % 每帧信息比特数 ebN0dB 0:1:6; % 横轴比特信噪比 ber zeros(size(ebN0dB)); for idx 1:length(ebN0dB) errBits 0; totalBits 0; esN0dB ebN0dB(idx) 10*log10(2); % 码率1/2 Es 2*Eb N0 10^(-esN0dB/10); % 符号能量归一化为1时的单边噪声功率谱密度 noiseSigma sqrt(N0/2); % 实AWGN每维标准差 for f 1:nFrame data randi([0 1], frameLen, 1); coded convenc(data, trellis); % 编码输出长度 2*frameLen tx 1 - 2*coded; % BPSK映射: 0 - 1, 1 - -1 rx tx noiseSigma*randn(size(tx)); hardBits double(rx 0); % 硬判决: 接收小于0判为1 decoded vitdec(hardBits, trellis, tb, cont, hard); % 丢弃前tb个不可靠输出再与发送数据对齐统计 cmp decoded(tb1:end); ref data(1:length(cmp)); errBits errBits sum(cmp ~ ref); totalBits totalBits length(ref); end ber(idx) errBits / totalBits; end semilogy(ebN0dB, ber, o-); grid on; xlabel(Eb/N0 (dB)); ylabel(BER);这段代码的逻辑分四步编码、调制加噪、硬判决、译码统计。注意data是列向量randi默认生成的也是列向量这保证了convenc的输入输出维度一致。convenc输出长度是输入的两倍因为码率 1/2如果你后面要测 2/3 或 3/4 码率这里的长度关系和噪声功率都要重新算。参数里有三个需要你重点调的地方。第一个是esN0dB它由ebN0dB 10*log10(2)得到含义是符号信噪比。BPSK 每符号携带 1 个比特但卷积编码后每 2 个符号才对应 1 个信息比特所以Es 2*Eb差了约 3.01 dB。第二个是N0 10^(-esN0dB/10)这是在符号能量归一化为 1 的前提下反解出的噪声功率谱密度randn乘的系数是sqrt(N0/2)而不是sqrt(N0)因为复基带等效模型里实部虚部各分一半噪声功率我们仿真的是实信号。第三个是tb 35cont连续模式下vitdec输出有固定延迟前tb个比特不参与统计。tb太小会导致末尾几条路径没有充分收敛BER 出现不该有的地板tb太大会浪费计算量35 对约束长度 7 的码已经足够。提示如果你把cont换成term编码端必须使用convenc(..., trellis, term)或者手动补尾比特否则译码器默认的终态假设和发送端不一致误码率会异常偏高。这里统一用cont最省心。3. 软判决仿真把汉明距离换成欧氏距离量化怎么设才不翻车3.1 软判决的度量本质为什么 AWGN 下的最优度量是欧氏距离硬判决在判决那一刻就把“这个符号离判决门限有多远”这个信息丢掉了。比如接收值 -0.1 判成 1接收值 -5.0 也判成 1但后者的置信度显然高得多。软判决就是把这种置信度保留下来译码器计算路径度量时不再数汉明距离而是比较接收值和假设发送符号之间的欧氏距离。在 AWGN 信道下欧氏距离对应的就是最大似然准则这是理论上最优的译码方式。从汉明距离换成欧氏距离之后路径度量的数值范围变大了Viterbi 译码的加法比较选择结构不变变的只是分支度量的计算方式。MATLAB 的vitdec用soft模式帮你省掉了这一步但它有一个硬性要求输入必须是量化后的整数范围是0到2^nq - 1nq是量化比特数。也就是说你必须先把接收软值映射成整数这个映射直接决定了软判决增益能不能兑现。硬判决可以看成nq1的特例输入只有 0 和 1。量化比特数和量化范围是软判决仿真的两个旋钮。nq3即 8 电平是工程上性价比最高的选择相比无限精度损失一般不超过 0.2 dBnq4到 5 基本可以忽略量化损失。真正容易翻车的是量化边界 A边界取小了高信噪比下软值全部被限幅软判决退化成一堆相同的最大值边界取大了量化间隔变大小噪声被量化误差淹没。我一般先看最低 Eb/N0 下接收序列的幅度分布再定 A而不是拍脑袋取 ±1。3.2 3bit 量化与软值映射软判决仿真的核心代码软判决和硬判决的差别只有两处接收端多了一步量化映射vitdec的参数从hard改成soft, nq。其余编码、调制、加噪、误码统计完全共用。% soft_decision_ber.m % 与硬判决相比唯一本质区别是软值量化和vitdec的soft模式 clear; clc; trellis poly2trellis(7, [171 133]); tb 35; nFrame 200; frameLen 1000; ebN0dB 0:1:6; nq 3; % 3bit量化 qLevels 2^nq; % 8电平 A 4.0; % 量化边界覆盖低信噪比下的接收动态范围 ber zeros(size(ebN0dB)); for idx 1:length(ebN0dB) errBits 0; totalBits 0; esN0dB ebN0dB(idx) 10*log10(2); N0 10^(-esN0dB/10); noiseSigma sqrt(N0/2); for f 1:nFrame data randi([0 1], frameLen, 1); coded convenc(data, trellis); tx 1 - 2*coded; rx tx noiseSigma*randn(size(tx)); % 软值映射到 [0, qLevels-1] y max(min(rx, A), -A); % 限幅 softIn round((y A) / (2*A) * (qLevels-1)); softIn min(softIn, qLevels-1); % 防止round溢出到8 decoded vitdec(softIn, trellis, tb, cont, soft, nq); cmp decoded(tb1:end); ref data(1:length(cmp)); errBits errBits sum(cmp ~ ref); totalBits totalBits length(ref); end ber(idx) errBits / totalBits; end semilogy(ebN0dB, ber, s-); grid on; xlabel(Eb/N0 (dB)); ylabel(BER);软值映射那两行是这条链路的命门。tx是 ±1所以rx大致分布在±(1 3σ)附近在 Eb/N0 从 0 到 6 dB 的扫描范围里A4能覆盖住绝大多数采样点。(y A) / (2*A)把[-A, A]线性映射到[0,1]再乘qLevels-1得到[0,7]的浮点数round取整后就是vitdec要的软判决输入。这里有一个容易被忽略的细节round在y恰好等于A时会算出8超出允许范围所以必须紧跟一行min(softIn, qLevels-1)。为什么映射方向是“大值对应发送 0”因为 BPSK 映射里0 - 1接收值越正说明越可能是 0vitdec的软判决度量会把大数解释为“更偏向某个星座点”这个对应关系只要和你的调制映射保持一致即可。最后统计误码时同样丢弃前tb个输出和硬判决的处理方式保持一致两条曲线才有可比性。注意vitdec的soft模式要求输入是整数向量且最大值不超过2^nq - 1。如果你在别处看到有人把浮点软值直接塞进去要么是他的 MATLAB 版本做了自动转换要么那条曲线本身就是错的。4. 软硬判决对比Eb/N0 换算、参数扫描和一张图上的两条 BER 曲线4.1 Eb/N0、Es/N0 和噪声方差对比实验的“对齐”问题软硬判决对比最容易犯的错误是两条曲线用的横轴物理意义不一致。硬判决里你可能直接按符号信噪比画图软判决里又换成了比特信噪比两条曲线根本没有对齐得出的“软判决差 3dB”属于假象。正确做法是仿真内部统一用符号能量归一化计算噪声功率横轴统一画 Eb/N0换算公式固定为Es/N0 Eb/N0 * 2码率 1/2 时转成 dB 就是esN0dB ebN0dB 10*log10(2)。另一个隐蔽问题是符号能量归一化。上面代码里tx 1 - 2*coded每个符号能量确实是 1所以N0 1 / (Es/N0)成立。如果你把星座改成tx 2*coded - 1符号能量同样是 1没问题但如果你用tx sqrt(2)*...或者加了个归一化系数 1/√2噪声方差就必须跟着改否则整条 BER 曲线会左右平移。我自己的习惯是把符号能量归一化写在注释里每次改调制方式先更新这一行再动其他参数。4.2 扫参脚本与画图让对比曲线可复现为了方便对比把硬判决和软判决封装成同一个函数只留mode和nq两个开关。这样扫参代码短也避免两份代码在帧数、帧长上不一致。% viterbi_ber.m % 统一接口modehard 或 softnq为软判决量化比特数 function ber viterbi_ber(ebN0dB, mode, nq) trellis poly2trellis(7, [171 133]); tb 35; nFrame 200; frameLen 1000; ber zeros(size(ebN0dB)); for idx 1:length(ebN0dB) esN0dB ebN0dB(idx) 10*log10(2); N0 10^(-esN0dB/10); noiseSigma sqrt(N0/2); errBits 0; totalBits 0; for f 1:nFrame data randi([0 1], frameLen, 1); coded convenc(data, trellis); tx 1 - 2*coded; rx tx noiseSigma*randn(size(tx)); if strcmp(mode, hard) decIn double(rx 0); decoded vitdec(decIn, trellis, tb, cont, hard); else A 4.0; qLevels 2^nq; y max(min(rx, A), -A); decIn min(round((yA)/(2*A)*(qLevels-1)), qLevels-1); decoded vitdec(decIn, trellis, tb, cont, soft, nq); end cmp decoded(tb1:end); ref data(1:length(cmp)); errBits errBits sum(cmp ~ ref); totalBits totalBits length(ref); end ber(idx) errBits / totalBits; end end调用和画图脚本% run_compare.m ebN0dB 0:0.5:6; ber_hard viterbi_ber(ebN0dB, hard, 1); ber_soft viterbi_ber(ebN0dB, soft, 3); semilogy(ebN0dB, ber_hard, o-, ebN0dB, ber_soft, s-); grid on; legend(硬判决, 软判决(3bit), Location, southwest); xlabel(Eb/N0 (dB)); ylabel(BER); title((2,1,7)卷积码 Viterbi 译码软硬判决对比);这个扫参脚本的步长取了 0.5 dB而不是第一章里的 1 dB。原因是软硬判决的差值在 1.52.5 dB 之间步长太粗看不出差异随信噪比的变化趋势。帧数nFrame200、帧长frameLen1000时每点统计约 20 万个信息比特在 BER1e-3 处大约有 200 个错误事件曲线还算平滑如果你要把曲线画到 1e-5需要把帧数提高到 2000 以上否则曲线的尾部全是抖动。画图必须用semilogy线性坐标系下 BER 低端会被压成一条贴地的线什么都看不出来。提示nFrame和frameLen的乘积决定了每个信噪比点的计算量。0.5 dB 步长 × 13 个点 × 200 帧在普通笔记本上大约要跑几分钟如果你用parfor把帧循环并行化记得每个 worker 里的随机数流要独立否则并行跑出来的曲线和串行对不上。5. 仿真避坑软判决翻车、Eb/N0 混用等 5 条血泪经验5.1 现象软判决 BER 曲线在高信噪比下比硬判决还差同一套参数soft模式跑出来的曲线在 Eb/N0 较高时反而比hard差甚至出现平台。原因量化边界 A 取太小高信噪比下大部分软值被限幅到最大电平软判决实际退化成若干个“死值”路径度量里只剩极少的信息或者nq1被误用成软判决那本质上就是硬判决。另一个常见原因是把浮点rx直接喂给vitdec没有量化MATLAB 某些版本会悄悄做四舍五入导致映射关系和你预期不一致。解决打印量化后softIn的直方图确认高信噪比点没有大量值堆积在 0 或 7 两端把 A 适当调大从 4 调到 6 试试同时把nq至少设成 3。如果曲线还是不对回到硬判决基线确认基线的 Eb/N0 换算没问题再切软判决。5.2 现象同样脚本重跑两次BER 曲线对不上尾部尤其明显现象是每次运行结果都有细微差别低信噪比勉强一致高信噪比点抖动剧烈。原因随机数种子没有固定而且高信噪比下错误事件非常稀疏比如 BER1e-4 意味着每 1 万个比特才错 1 个如果帧数不够这次跑出 0 个错误、下次跑出 5 个错误BER 就差出一个数量级。解决脚本开头加rng(42)固定全局随机流每帧的randi和randn会按固定顺序产生随机数。更重要的是按“错误事件数”决定仿真是否停止而不是只按固定帧数。我一般要求每个信噪比点至少累计 50100 个错误比特BER 低于 1e-4 时用这个标准控制帧数曲线尾部会稳定很多。5.3 现象BER 曲线整体比理论值偏移 3dB 左右但形状正常现象是曲线的斜率、趋势都和预期一致但整体右移或者左移了约 3dB。原因Eb/N0 和 Es/N0 混用了。码率 1/2 卷积码的Es2Eb如果在esN0dB里漏加10*log10(2)噪声功率会比真实值大一倍曲线右移 3dB反过来如果编码后每符号仍按 1 比特算横轴标成 Eb/N0 但实际算的是 Es/N0曲线左移 3dB。解决对照 4.1 节的换算公式逐行检查。有一个笨但可靠的自检方法把卷积码换成直通即去掉convencvitdec也换成直接判决跑一条未编码 BPSK 的理论曲线0.5*erfc(sqrt(10.^(ebN0dB/10)))和仿真值画在一起两边差在 0.1dB 以内说明噪声链路是对的。5.4 现象译码输出长度不对误码统计错位BER 异常偏高现象vitdec输出长度和发送data不一致或者统计时没对齐BER 一直在 0.5 附近徘徊。原因cont模式输出有回溯延迟开头一部分对应的是历史状态的估计不能和data前几比特直接比较term模式下编码端没补尾比特终态不匹配也会输出整段乱码。还有一个隐蔽问题data是行向量时convenc输出是行vitdec输出是列长度对上了但方向没对上逐位比较时维度不一致。解决统一用列向量统计时固定丢弃前tb个输出再取和data等长度的片段。可以加一行断言assert(length(cmp)length(ref))一旦对不上立刻报错而不是让错误静默扩散。5.5 现象录像里调过的参数之后照着操作却复现不出来现象看操作录像时信噪比范围、帧数都很清楚但自己照着重敲曲线形状对不上。原因录像是线性播放的中间演示者可能临时改过某个变量后来又改回去这个动作在录像里很难被注意到或者录像只展示了最终脚本没有展示脚本顶部的参数块和随机种子。解决把关键参数集中放在脚本最前面并用注释写清楚每一行的含义和单位录像开始前先执行clear; clc; rng(42)让观看者能明确看到初始化动作每跑一个信噪比点在命令行打印EbN0xx dB, BERxx这样即使画面切走了日志也能对得上脚本状态。6. 把仿真做成可复用的录像录屏习惯、种子管理和结果验证6.1 录像前的四个固定动作操作录像的价值不在演示而在可复现。我拿到“含仿真操作录像”这类需求时一般不会直接开录而是先做四件事清空工作区、固定随机种子、把全部参数打印在命令行、把脚本调成单信噪比点短帧快速跑通。前三个动作保证观看者从录像里看到的环境和他自己的环境一致第四个动作是为了避免录到一半卡在长仿真上。录屏时我会把 MATLAB 编辑器窗口和命令行窗口并排让参数块和运行结果同时出现在画面里。单纯录曲线图是最没用的因为曲线只能证明“跑通了”证明不了“参数是什么”。真正有用的录像是先展示参数块再运行一段短脚本然后把 BER 值打印出来最后画出曲线。我在每个信噪比点的fprintf里会带上当前帧号这样观看者能实时看到进度也知道哪一次运行刷了多少帧。6.2 收尾验证看曲线之前先看错误事件数最后验证仿真结果时我不会先画曲线而是先看每个信噪比点的错误事件数。BER 是 1e-4 但只统计到 10 个错误和统计到 500 个错误可信度完全不同。经验下限是每个点至少 50 个错误比特再少的话曲线尾部出现异常上翘时你分不清是算法问题还是统计抖动。我会在脚本里加一行errEvents的计数跑完画图前先扫一眼这个数组。更严格一点的验证是拿未编码 BPSK 曲线做参照。理论上 (2,1,7) 卷积码加软判决 Viterbi 译码在 BER1e-4 附近比未编码 BPSK 好 45dB硬判决比软判决差 1.52.5dB。如果两条编码曲线都比未编码曲线差几乎可以肯定是调制映射或者噪声方差的问题而不是译码器的问题。这个仿真方案帮我验证过不止一个编码方案也帮某同学把课程设计从“跑不出对比图”救到“和理论对得上”。最深的教训是软判决的增益不是换一个字符串参数就能白拿的量化映射和信噪比换算才是真正的坑位。希望你按这套代码跑通之后能把软硬判决的差值画出一个平滑的、可解释的 2dB 左右的间距。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进