简介:面向通信系统学习者的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'换成'soft',BER 曲线反而更差,甚至在高信噪比处出现平台。这不是 Viterbi 算法出了问题,而是软判决的量化映射、度量计算和 Eb/N0 换算三处各错了一点。这个标题要解决的正是这样一件事:在 MATLAB 里把卷积编码、Viterbi 译码的完整链路搭起来,分别用硬判决和软判决译码跑出误码率曲线,再做一次公平对比。适合正在做信道编码课程设计、或者需要评估编码增益的通信工程师。你只需要会 BPSK 和 AWGN 的仿真套路,剩下的代码可以直接抄,坑位我提前踩给你看。
2. 硬判决链路先跑通:poly2trellis 建码、vitdec 译码与最小 MATLAB 仿真
2.1 从生成多项式到网格图:先把卷积码的“骨架”搭对
卷积码本质上是一个有限状态机。以经典的 (2,1,7) 码为例:每个时刻输入 1 个信息比特,输出 2 个编码比特,码率 R=1/2;约束长度 K=7,意味着当前输出由当前输入和前 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(tb+1: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 = 35,'cont'连续模式下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 - 1,nq是量化比特数。也就是说,你必须先把接收软值映射成整数,这个映射直接决定了软判决增益能不能兑现。硬判决可以看成nq=1的特例,输入只有 0 和 1。
量化比特数和量化范围是软判决仿真的两个旋钮。nq=3即 8 电平,是工程上性价比最高的选择,相比无限精度损失一般不超过 0.2 dB;nq=4到 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(tb+1: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 的扫描范围里,A=4能覆盖住绝大多数采样点。(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,接收值越正说明越可能是 0;vitdec的软判决度量会把大数解释为“更偏向某个星座点”,这个对应关系只要和你的调制映射保持一致即可。最后统计误码时同样丢弃前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 % 统一接口:mode='hard' 或 'soft';nq为软判决量化比特数 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((y+A)/(2*A)*(qLevels-1)), qLevels-1); decoded = vitdec(decIn, trellis, tb, 'cont', 'soft', nq); end cmp = decoded(tb+1: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.5~2.5 dB 之间,步长太粗看不出差异随信噪比的变化趋势。帧数nFrame=200、帧长frameLen=1000时,每点统计约 20 万个信息比特,在 BER=1e-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 取太小,高信噪比下大部分软值被限幅到最大电平,软判决实际退化成若干个“死值”,路径度量里只剩极少的信息;或者nq=1被误用成软判决,那本质上就是硬判决。另一个常见原因是把浮点rx直接喂给vitdec,没有量化,MATLAB 某些版本会悄悄做四舍五入,导致映射关系和你预期不一致。
解决:打印量化后softIn的直方图,确认高信噪比点没有大量值堆积在 0 或 7 两端;把 A 适当调大(从 4 调到 6 试试),同时把nq至少设成 3。如果曲线还是不对,回到硬判决基线,确认基线的 Eb/N0 换算没问题,再切软判决。
5.2 现象:同样脚本重跑两次,BER 曲线对不上,尾部尤其明显
现象是每次运行结果都有细微差别,低信噪比勉强一致,高信噪比点抖动剧烈。
原因:随机数种子没有固定,而且高信噪比下错误事件非常稀疏,比如 BER=1e-4 意味着每 1 万个比特才错 1 个,如果帧数不够,这次跑出 0 个错误、下次跑出 5 个错误,BER 就差出一个数量级。
解决:脚本开头加rng(42)固定全局随机流;每帧的randi和randn会按固定顺序产生随机数。更重要的是按“错误事件数”决定仿真是否停止,而不是只按固定帧数。我一般要求每个信噪比点至少累计 50~100 个错误比特,BER 低于 1e-4 时用这个标准控制帧数,曲线尾部会稳定很多。
5.3 现象:BER 曲线整体比理论值偏移 3dB 左右,但形状正常
现象是曲线的斜率、趋势都和预期一致,但整体右移或者左移了约 3dB。
原因:Eb/N0 和 Es/N0 混用了。码率 1/2 卷积码的Es=2Eb,如果在esN0dB里漏加10*log10(2),噪声功率会比真实值大一倍,曲线右移 3dB;反过来如果编码后每符号仍按 1 比特算,横轴标成 Eb/N0 但实际算的是 Es/N0,曲线左移 3dB。
解决:对照 4.1 节的换算公式逐行检查。有一个笨但可靠的自检方法:把卷积码换成直通(即去掉convenc,vitdec也换成直接判决),跑一条未编码 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),让观看者能明确看到初始化动作;每跑一个信噪比点,在命令行打印EbN0=xx dB, BER=xx,这样即使画面切走了,日志也能对得上脚本状态。
6. 把仿真做成可复用的录像:录屏习惯、种子管理和结果验证
6.1 录像前的四个固定动作
操作录像的价值不在演示,而在可复现。我拿到“含仿真操作录像”这类需求时,一般不会直接开录,而是先做四件事:清空工作区、固定随机种子、把全部参数打印在命令行、把脚本调成单信噪比点短帧快速跑通。前三个动作保证观看者从录像里看到的环境和他自己的环境一致,第四个动作是为了避免录到一半卡在长仿真上。
录屏时我会把 MATLAB 编辑器窗口和命令行窗口并排,让参数块和运行结果同时出现在画面里。单纯录曲线图是最没用的,因为曲线只能证明“跑通了”,证明不了“参数是什么”。真正有用的录像是:先展示参数块,再运行一段短脚本,然后把 BER 值打印出来,最后画出曲线。我在每个信噪比点的fprintf里会带上当前帧号,这样观看者能实时看到进度,也知道哪一次运行刷了多少帧。
6.2 收尾验证:看曲线之前先看错误事件数
最后验证仿真结果时,我不会先画曲线,而是先看每个信噪比点的错误事件数。BER 是 1e-4 但只统计到 10 个错误,和统计到 500 个错误,可信度完全不同。经验下限是每个点至少 50 个错误比特;再少的话,曲线尾部出现异常上翘时,你分不清是算法问题还是统计抖动。我会在脚本里加一行errEvents的计数,跑完画图前先扫一眼这个数组。
更严格一点的验证是拿未编码 BPSK 曲线做参照。理论上 (2,1,7) 卷积码加软判决 Viterbi 译码,在 BER=1e-4 附近比未编码 BPSK 好 4~5dB,硬判决比软判决差 1.5~2.5dB。如果两条编码曲线都比未编码曲线差,几乎可以肯定是调制映射或者噪声方差的问题,而不是译码器的问题。
这个仿真方案帮我验证过不止一个编码方案,也帮某同学把课程设计从“跑不出对比图”救到“和理论对得上”。最深的教训是:软判决的增益不是换一个字符串参数就能白拿的,量化映射和信噪比换算才是真正的坑位。希望你按这套代码跑通之后,能把软硬判决的差值画出一个平滑的、可解释的 2dB 左右的间距。希望帮到你。
本文还有配套的精品资源,点击获取