多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

TCM与16QAM的MATLAB实现:Viterbi硬判决译码链路全解析

TCM与16QAM的MATLAB实现:Viterbi硬判决译码链路全解析 简介面向通信系统仿真研究者的 MATLAB 源码包围绕 TCM网格编码调制与 16QAM 的联合编译码展开采用 Viterbi 硬判决译码恢复数据适合本科毕设、研究生课题或通信原理进阶学习。资源共 12 个 M 文件压缩包仅 7KB包含二进制/十进制转换、状态转移、欧氏距离计算、星座图映射、硬判决 Viterbi 译码等典型子函数模块划分清晰便于逐段阅读和调试。已有 193 人学习下载可作为理解 TCM 编码增益、16QAM 高阶映射与最大似然序列估计的入门范例。仿真链路覆盖信源编码、TCM 编码、16QAM 调制、信道加噪、Viterbi 译码与误码率统计调整信噪比即可观察不同条件下的系统性能为后续优化编码速率或星座设计提供可复用的 MATLAB 实现。对正在学习卷积码、网格图和 QAM 星座的读者这套代码也能帮助快速建立从比特映射到硬判决译码的完整链路认知。1. 为什么 TCM 要配 16QAM 和 Viterbi这套源码解决的是链路误码率问题第一次打开这个压缩包我以为又是一个“卷积码 16QAM 调制”的简单拼接直到看到constellation.m里的子集划分表和viterbi.m的回溯逻辑才意识到这说的是 TCMTrellis Coded Modulation网格编码调制。TCM 最反直觉的地方在于它不像传统方案那样把纠错编码和调制当作两件事而是把编码器和星座映射揉成一个状态机同一时刻既改变发送符号也改变网格状态。配合 16QAM 的 4×4 星座点用 Ungerboeck 集分割选出 8 个相互距离更大的点再让 Viterbi 按欧氏距离做最大似然路径搜索最终在相同带宽和信噪比下比未编码 QPSK/8PSK 多拿 2~3 dB 的增益。这套代码适合正在做数字通信课程设计、需要复现 TCM 误码率曲线、或者想把硬判决译码换成软判决做性能对照的人。核心链路是比特生成 → 卷积编码 → 16QAM 子集映射 → AWGN 信道 → 硬判决 → Viterbi 译码 → 误码比对。本文按我拆这个包的实际顺序从文件功能映射、编码侧逻辑、译码主线、仿真参数到踩坑一步一步还原整个系统。2. 摸清压缩包里的 .m 文件先分清编码侧、译码侧和工具函数拿到一个 MATLAB 源码包第一步不是急着点运行而是把每个文件按“它到底服务于哪一段链路”分类。这个包一共 14 个.m文件功能边界其实很清楚——编码器、状态表、星座映射、度量计算、Viterbi 主循环、进制转换工具外加一个硬判决入口脚本。2.1 文件清单与阅读顺序我建议按下面的顺序读而不是按文件名排序读文件功能分类我的判断chc_16QAM_hard.m主控脚本整条链路的入口定义参数、调编码、加噪、调译码、统计误码cnv_encd.m卷积编码器输入信息比特输出编码比特是 TCM 的状态机源头nxt_stat.m状态转移表根据当前状态和输入比特返回下一状态TCM 网格的骨架constellation.m星座与集分割16QAM 星座点坐标以及按集分割规则选出的子集o_dist.m星座点距离计算按命名习惯推断是计算子集内/子集间欧氏距离供映射和度量用metric.m分支度量计算接收符号与每个候选发送符号之间的距离量度viterbi.m译码主函数累加路径度量、找幸存路径、回溯输出int2bin.m/bin2int.m进制转换整数 ↔ 二进制向量bin2deci.m/deci2bin.m进制转换二进制 ↔ 十进制int2bin2.m/bin2int2.m进制转换可能是不同位宽或矩阵形式下的转换变体constellation.m内嵌的坐标星座定义后面单独讲归一化问题这个分类做完链路就清晰了cnv_encd负责编码constellation负责把编码比特映射成符号信道加噪后metric给每个状态分支打分viterbi沿着网格找最优路径chc_16QAM_hard把这一切串起来统计误码率。2.2 编码侧的核心cnv_encd 与 nxt_stat 的配合方式TCM 的编码器和普通卷积码的编码器在结构上没有本质区别区别在于编码比特的“去向”。普通卷积码的编码比特直接送调制器TCM 的编码比特则决定当前时刻选择哪个星座子集。cnv_encd.m做的事在绝大多数教材实现里长这样function [output] cnv_encd(input, g, state) % input: 信息比特向量每个元素为 0 或 1 % g: 生成多项式矩阵行数 输出比特数列数 约束长度 % state: 初始寄存器状态行向量长度 约束长度 - 1 % output: 编码输出长度为 length(input) * size(g, 1) output []; for k 1:length(input) % 把当前输入比特和寄存器状态拼起来形成 n 位窗口 window [input(k), state]; for j 1:size(g, 1) % 生成多项式向量与窗口逐位相与再模 2 求和 mask mod(sum(window .* g(j, :)), 2); output [output, mask]; end % 寄存器左移一位输入比特进入最低位 state [input(k), state(1:end-1)]; end逻辑说明这里把输入比特和寄存器状态组成一个宽度等于约束长度的窗口每个输出比特是该窗口与一个生成多项式的模 2 内积。g(j,:)里为 1 的位置表示对应的寄存器抽头参与异或运算为 0 的位置表示不参与。这是最基础的“线性移位寄存器”实现方式和教材里画的状态图完全对应。参数说明g的取值直接决定网格状态数和自由距离。常见 TCM 设计用约束长度 3~5、状态数 4~16 的卷积码生成多项式常用八进制表示比如八进制 13 对应[1 0 1 1]按高位到低位。在这个包里nxt_stat.m的存在说明状态转移表是预计算好的而不是每次现算——这一点很关键因为 Viterbi 的主循环会反复查状态表预计算能省掉大量重复的移位置位操作。2.3 编码率与未编码比特TCM 的特殊之处我在读这段代码时最容易绕晕的地方是TCM 不是所有信息比特都进卷积编码器。16QAM 每符号传 4 bit如果全部进编码器编码率会很低带宽效率反而下降。Ungerboeck 的设计思路是把 4 个比特拆成两组一部分进卷积码一部分直接映射。常见方案里1 bit 通过 rate-1/2 卷积码变成 2 bit用来在 16QAM 的 4×4 星座图中选定某个“子集对”剩下 2 bit 不编码在子集内部选择具体星座点。这样每符号仍然传 4 bit带宽效率不降但星座点之间的最小欧氏距离被拉开了。映射关系是这个包最值得对照constellation.m和cnv_encd.m一起看的地方比特组长度去向作用编码比特1 bit → 2 bit进cnv_encd输出经nxt_stat查状态选定当前使用的星座子集未编码比特2 bit直接参与星座映射在子集内选定具体星座点所以读chc_16QAM_hard.m时不要以为conv_encd的输出就直接是星座点索引它输出的只是编码部分后面还要和未编码比特拼起来才能查星座表。这个细节正是后面误码率“错得莫名其妙”的常见原因。3. Viterbi 硬判决译码主线metric 计算与路径回溯的 MATLAB 实现TCM 的接收端和普通卷积码的 Viterbi 译码最大的区别是度量定义。普通卷积码常用汉明距离因为它工作在二进制解调之后TCM 里接收端拿到的是带噪的复符号最自然的度量是接收符号与每个候选发送符号之间的欧氏距离。这个包是硬判决方案意味着接收端先做了一次星座点判决再进入 Viterbi。3.1 硬判决和软判决的差别在哪先看清楚这个包为什么要叫chc_16QAM_hard。硬判决在接收端先把带噪的接收符号映射到离它最近的星座点上得到一个“干净”的星座索引然后把这个索引交给 Viterbi。软判决则保留接收符号的原始连续值直接用欧氏距离计算分支度量。对比项硬判决软判决输入给 Viterbi 的数据离散星座点索引连续接收符号度量类型星座点之间的欧氏距离接收符号与候选星座点的欧氏距离性能损失相对软判决损失约 2 dB最优实现复杂度低先做最近邻查找高每个分支都要算距离硬判决的唯一优点是计算量小性能上从信息论角度看是次优的。但作为教学和课程设计的源码包硬判决实现起来直观每一步都能断点调试理解了硬判决再往软判决迁移时只需要把“先判决再查表”改成“直接用连续值算度量”主循环结构几乎不用动。3.2 metric.m分支度量怎么打分在我的实现习惯里metric 函数输入是当前接收到的星座索引硬判决后和所有可能发送的星座索引表输出是一个行向量长度等于星座点总数第 i 个元素表示“如果发送端发的是第 i 个星座点与当前接收点的距离”。function m metric(rx_idx, constellation_points, subset_table) % rx_idx: 接收符号经过硬判决后的星座索引标量 % constellation_points: N 个星座点坐标每行一个点复数形式 % subset_table: 子集划分表行 状态列 输入分支值是子集编号 % m: 分支度量向量长度 星座点数值越小表示越可能 N size(constellation_points, 1); % 先把接收索引对应的复数坐标取出来 rx_point constellation_points(rx_idx, :); m zeros(1, N); for i 1:N % 平方欧氏距离不取根号省计算量且单调性不变 diff rx_point - constellation_points(i, :); m(i) sum(abs(diff) .^ 2); end逻辑说明这里刻意没有对距离开根号因为 Viterbi 只需要比较度量大小平方距离保持同样的单调性省掉 16 次 sqrt 操作在高 SNR 长帧仿真里差别很明显。subset_table的作用是把“哪个状态、哪条输入分支对应哪个星座子集”这个映射关系固定下来主循环里查表比每次现算快得多。参数说明rx_idx的取值和星座点编号方式必须和constellation.m完全一致。这个包里bin2int2.m、int2bin2.m的存在暗示星座点编号可能不是简单地从 0 到 15 顺序排而是按子集划分后的编号规则排的——如果你发现 hard decision 出来的索引总是不对优先检查这里。3.3 viterbi.m 主循环累加、比较、回溯三步走Viterbi 译码的核心是动态规划每个时刻、每个状态维护一条幸存路径的累积度量然后下一时刻让所有分支度量加到对应的前一状态上每个状态只保留度量最小的那条路径。function dec_bits viterbi(recv_seq, trellis, metric_func_handle) % recv_seq: 接收符号硬判决索引序列长度 帧长 % trellis: 结构体包含 next_state、output_symbol 等预计算表 % metric_func_handle: 指向 metric 计算函数的句柄 % dec_bits: 译码输出的二进制比特序列 num_states trellis.num_states; frame_len length(recv_seq); path_metric inf(1, num_states); % 初始只有零状态是 0其余为无穷大 path_metric(1) 0; survivor zeros(num_states, frame_len); for t 1:frame_len new_metric inf(1, num_states); for state 1:num_states % 遍历到达当前状态的两条或更多分支 for input_bit 0:1 prev_state trellis.prev_state(state, input_bit 1); branch_symbol trellis.output_symbol(prev_state, input_bit 1); branch_cost metric_func_handle(recv_seq(t), branch_symbol); cand_metric path_metric(prev_state) branch_cost; if cand_metric new_metric(state) new_metric(state) cand_metric; survivor(state, t) prev_state; % 记录幸存路径的前一状态 end end end path_metric new_metric; end % 回溯从最后一刻度量最小的状态开始往前倒推 best_state find(path_metric min(path_metric), 1); dec_bits zeros(1, frame_len); for t frame_len:-1:1 dec_bits(t) trellis.input_of_transition(survivor(best_state, t), best_state); best_state survivor(best_state, t); end逻辑说明inf初始化是 Viterbi 的经典写法因为译码开始时只有零状态是合法的其他状态的累积度量设为无穷大后任何从非法起点延伸出来的路径都不可能被选中。内层循环对每个状态比较所有输入分支的候选度量只保留最小的一条这就是“加比选”ACS单元。回溯时从最后一刻的最小度量状态出发沿着 survivor 记录反向找完整条路径反推出每个时刻的输入比特。注意我这里输出的是输入比特而不是编码比特——如果你的误码统计把这两者搞混误码率曲线会直接在空中飘。参数说明frame_len必须和发端cnv_encd的输入长度一致。trellis.prev_state、trellis.output_symbol这两个表可以由nxt_stat的计算结果构建也可以用 MATLAB 通信工具箱的poly2trellis直接生成。这个源码包没有依赖通信工具箱所以nxt_stat.m手动建立了这些表。3.4 回溯深度不够会怎样实际跑仿真时Viterbi 有两种输出模式一种是每个时刻都输出但要等足够长的判决深度另一种是收完一整帧再回溯。这个包的viterbi.m用的是后者——整帧回溯好处是逻辑简单坏处是帧长内尾部若干比特的误码会和头部的路径搜索耦合在一起。经验值回溯深度取约束长度的 5~6 倍基本够用。8 状态对应约束长度 4回溯深度取 20~30 足够。如果帧长本身就小于这个值那整帧回溯反而比连续输出更稳因为最后全局最小度量的路径几乎等价于最大似然序列——这是整帧回溯相比滑动窗口的一个小优势。4. 把误码率曲线跑出来主控脚本、SNR 换算与参数设置代码读通了下一步就是把chc_16QAM_hard.m里的链路跑起来。这个脚本从文件名看是 16QAM 硬判决的完整仿真入口实际执行时横跨五件事生成随机比特、TCM 编码、星座映射、加 AWGN、硬判决后 Viterbi 译码并统计误码。4.1 主循环结构SNR 扫描加帧平均真正能出误码率曲线的脚本外层循环一定在扫 SNR内层循环一定是多帧取平均。单帧仿真出来的误码率毛刺很大尤其在低 SNR 区域一个点差两倍都正常。snr_dB 0:2:14; % 扫 16QAM 的合理信噪比区间 num_frames 100; % 每个 SNR 点仿真多少帧 frame_len 1000; % 每帧比特数 total_errors zeros(size(snr_dB)); total_bits num_frames * frame_len; for s 1:length(snr_dB) snr_lin 10^(snr_dB(s)/10); % 线性信噪比 for f 1:num_frames % 1. 生成随机信息比特 info_bits randi([0 1], 1, frame_len); % 2. TCM 编码得到编码后的比特序列 encoded cnv_encd(info_bits, g, init_state); % 3. 星座映射编码比特 未编码比特 - 复数符号 tx_symbols map_tcm(encoded, constellation_points); % 4. 加 AWGN注意归一化和噪声方差换算 noise_var 1 / (2 * snr_lin); % 复噪声每个维度的方差 noise sqrt(noise_var) * (randn(size(tx_symbols)) 1j*randn(size(tx_symbols))); rx_symbols tx_symbols noise; % 5. 硬判决把接收符号映射到最近的星座点 rx_indices nearest_neighbor(rx_symbols, constellation_points); % 6. Viterbi 译码 dec_bits viterbi(rx_indices, trellis, metric); % 7. 误码统计只对信息比特做比对 err sum(abs(dec_bits - info_bits)); total_errors(s) total_errors(s) err; end ber(s) total_errors(s) / total_bits; end semilogy(snr_dB, ber, b-o); grid on; xlabel(SNR (dB)); ylabel(BER);逻辑说明噪声方差写成1/(2*snr_lin)的前提是信号平均能量归一化为 1。复 AWGN 每维方差是 N0/2SNR Es/N0 1/N0所以 N0 1/snr_lin每维方差再除 2得到1/(2*snr_lin)。很多人在这一步直接把awgn(tx_symbols, snr_dB, measured)塞进去虽然也能跑出曲线但不同帧的信号功率随机波动会导致 SNR 误差曲线会比理论值偏移 0.5 dB 左右。参数说明snr_dB 0:2:14是经验区间。16QAM 理论上在 10 dB 左右误码率能到 10^-3 量级TCM 的编码增益会把它再压低一段14 dB 以上硬判决方案要跑很久才能统计到足够的误码数这时候不如直接提高 SNR 或者增大帧长。num_frames 100在低 SNR 区域够用高 SNR 区域建议换成“累计误码数达到 100 个就停”的自适应模式否则白白跑几百万比特。4.2 Eb/N0 和 Es/N016QAM 的换算不能想当然误码率曲线的横坐标到底是 SNR、Es/N0 还是 Eb/N0直接决定你画出来的曲线能不能和文献对比。16QAM 每符号 4 bit所以 Es 4·Eb换成 dB 是 10·log10(4) ≈ 6.02 dB。参数定义与 SNREs/N0的关系每符号信噪比 Es/N0信号平均能量 / 噪声单边功率谱密度等于 SNR信号能量归一化时每比特信噪比 Eb/N0每比特能量 / N0Es/N0 − 6.02 dB16QAM噪声总功率实部 虚部N0每维 N0/2画图时我习惯横坐标用 Eb/N0因为不同调制方式之间比较带宽效率时只有 Eb/N0 才有意义。TCM 的编码增益对比也是基于 Eb/N0。如果你只看“跑出来的曲线形状”用哪个横坐标都行但如果你想和论文里的 TCM 增益数字做对比必须用 Eb/N0。这个包里的主控脚本如果直接在 SNR 下统计误码建议你改成 Eb/N0 再画一次曲线会整体左移约 6 dB。4.3 先画星座图子集划分对不对一眼就看出来在跑完整仿真之前我强烈建议先执行一次星座映射直接把发送符号画在复平面上检查集分割是否生效。这个步骤能提前发现 70% 的映射错误不用等到误码率曲线飞了再回头查。% 随便生成一段编码比特映射成符号后画图 test_bits randi([0 1], 1, 2000); test_enc cnv_encd(test_bits, g, zeros(1, mem)); test_sym map_tcm(test_enc, constellation_points); figure; plot(real(test_sym), imag(test_sym), b.); axis equal; grid on; title(TCM 映射后的星座点分布); % 再画出全部 16QAM 星座坐标作为底图 hold on; plot(real(constellation_points), imag(constellation_points), ro, MarkerSize, 6);逻辑说明TCM 集分割后发送符号不会均匀落在全部 16 个点上而是固定落在某个“子集”的点上。如果你看到蓝点均匀铺满了全部 16 个点说明映射逻辑还是普通 16QAM没有子集选择这一步骤如果你看到蓝点集中在 8 个点上且这些 8 个点之间的间距明显大于标准 16QAM 的最小间距说明集分割生效了。参数说明map_tcm是我为举例写的接口名对应这个包里的映射逻辑实际调用时以constellation.m里提供的函数或脚本为准。mem是卷积编码器的寄存器长度8 状态对应 mem 3初始寄存器状态一般是全零。4.4 帧长和蒙特卡洛次数别让仿真时间失控这个源码包默认的多帧循环结构在高 SNR 点会有明显的效率问题。SNR 14 dB 时硬判决 Viterbi 的误码率大概在 10^-4 量级每帧 1000 bit100 帧只有约 10 个误码统计误差超过 30%。要得到稳定曲线我一般用两种策略中的一种要么每帧 10000 bit 以上要么在循环里检测累计误码数到达 100 或 200 个就跳出这一档 SNR。total_errors_snr 0; total_bits_snr 0; while total_errors_snr 200 total_bits_snr 2e6 [err, bits] simulate_one_frame(snr_dB(s)); total_errors_snr total_errors_snr err; total_bits_snr total_bits_snr bits; end ber(s) total_errors_snr / total_bits_snr;这个改动每个 SNR 点的运行时间差距可以到几十倍。低 SNR 区域误码来得快几帧就够高 SNR 区域要跑到 200 个误码才停虽然耗时长但曲线平滑度有保障。这种“自适应停止”是所有通信仿真脚本里最值得养成的习惯之一我几乎在所有链路仿真里都用同样的模板。5. 避坑记录TCM/16QAM 仿真里翻得最多的五个问题这套代码我在拆解和复现过程中遇到过五个有代表性的问题前四个在代码逻辑层面第五个在仿真方法论层面。每个都按“现象 → 原因 → 解决”记录方便你对照排查。5.1 高 SNR 时误码率“掉不下去”出现地板效应现象BER 曲线在低 SNR 段正常下降但到 12 dB 以上突然变平怎么加 SNR 都不再降低就像地面上有个天花板。原因最常见的是星座点坐标没有归一化。constellation.m如果直接用了标准的 16QAM 坐标如 ±1、±3 的组合平均信号能量是 10而加噪时假设 Es 1信噪比被系统性放大了 10 倍10 dB。SNR 换算失真后实际每比特信噪比比标注低很多高 SNR 段自然表现为误码率“贴地飞行”。解决检查constellation.m里星座点坐标是否除以 sqrt(10)。标准 16QAM 坐标 (±1 ±1)、(±3 ±1)、(±1 ±3)、(±3 ±3) 的平均能量是 10除以 sqrt(10) 后平均能量归一到 1。归一化之后再加噪BER 曲线的绝对位置才会和理论值对齐。这个坑几乎每个 16QAM 项目都有我第一次看到曲线整体偏右 10 dB 时还以为是 Viterbi 写错了。5.2 帧尾连续出错前面全对现象Viterbi 译码结果中帧中间大部分比特正确但尾部 20~30 个比特频繁错误帧越短越明显。原因整帧回溯的 Viterbi 依赖全部接收符号来确定最终幸存路径帧尾的状态会向多个路径收敛如果回溯深度小于网格的“记忆长度”尾部状态还没有收敛到唯一正确路径输出自然错误。8 状态网格的约束长度是 4但实际记忆长度可能到 5~6 个符号周期。解决要么在发端尾比特加 2 个 0 bit 让网格强制回到零状态要么把回溯深度加大到约束长度的 6 倍以上。注意增加回溯深度后译码输出的序列长度会缩短需要在统计误码时做对齐。从那以后我拿到任何viterbi.m都先看一眼它的回溯逻辑是循环式的还是整帧式的再决定帧长怎么设。5.3 误码率曲线比理论值差很多但星座图和编码器都检查过了现象每个模块单看都是对的编码器状态转移正确、星座映射画出来也正常整条链路的 BER 就是比预期差 3~4 dB。原因编码比特和未编码比特的拼接顺序错了。TCM 的符号映射需要先把编码比特映射到子集再把未编码比特映射到子集内的点。如果主控脚本直接把cnv_encd的输出和未编码比特按错误顺序拼起来接收端 Viterbi 出来再按同样的错误顺序拆分每个模块看都是自洽的但整个系统的集合划分关系已经完全乱了自由距离优势被破坏性能退化成普通 16QAM 甚至更差。解决在chc_16QAM_hard.m的发射端和接收端各加一个字段打印把“编码比特 未编码比特 → 星座点索引”的映射过程逐帧打印出来人工对着子集划分表核对一遍。我见过最隐蔽的情况是编码比特在前未编码在后是对的但bin2int2.m的位序是反的——打印出来对比时看不出转成十进制后索引错位 1~2 个区间。5.4 MATLAB 报“矩阵维度不一致”位置在 metric.m 或 viterbi.m现象运行到metric.m里constellation_points(i,:)这一行时报数组越界或维度不匹配或者viterbi.m里trellis.prev_state索引越界。原因状态数设置不一致。cnv_encd.m的生成多项式决定网格是 8 状态还是 16 状态但nxt_stat.m预计算的状态表可能按 4 状态生成或者反过来。两边的状态数只要不一致prev_state表的大小就和主循环的遍历范围对不上。解决在脚本开头统一维护一个num_states变量cnv_encd、nxt_stat、viterbi全部从同一个变量取状态数不要在每个函数里各自定义。并在主控脚本里加一句断言assert(size(trellis.prev_state, 1) num_states)。这个坑本质上不是算法问题而是 MATLAB 脚本式编程里“全局参数散落各处”的典型症状。5.5 直接把awgn函数塞进去曲线偏移半个 dB 还找不到原因现象用awgn(tx, snr_dB, measured)加噪后BER 曲线相比理论值整体向右偏移 0.3~0.6 dB而且偏移量随帧长变化。原因awgn的measured模式会先测量输入信号的实际功率再计算噪声功率但如果 TCM 的星座映射导致不同帧的信号实际功率有波动比如帧内子集切换比例不一样测量功率和标称平均功率就不一致每帧的有效 SNR 在抖动。解决不用measured改用显式的噪声方差生成。先对constellation.m的星座点坐标求一次平均能量并归一化然后按第 4.1 节的公式直接生成复高斯噪声。显式控制噪声方差是通信仿真里最值得坚持的习惯虽然代码多两行但整个链路的 SNR 语义变得完全确定和理论对比时不会出现“差半 dB 却不知道差在哪”的尴尬处境。6. 拿到这套源码先做无噪声自检三步验证与 traceback 深度扫描这套代码包能不能信不是看误码率曲线跑得多漂亮而是看你在加噪声之前能不能把整条链路的每一个环节验证到比特级一致。我拆完这个包后养成了一个习惯任何译码相关的源码包第一件事就是关掉噪声跑一遍“无损复现”这一关过了后面的所有问题都只可能是信道或参数设置的问题。第一步把 SNR 设成极大值比如 60 dB甚至直接把噪声置零。目的是让接收端收到的符号和发端完全一致硬判决必然全部正确Viterbi 的输入就是星座点的真实索引。这时候如果译码输出和原始信息比特不完全相等说明编码、映射、译码三个环节至少有一个存在逻辑错位——先把这个问题解决再谈信道适应性。第二步在置零信道下逐帧核对cnv_encd输出、map_tcm输出、Viterbi 的路径度量和幸存状态表对照nxt_stat.m的状态转移表手工推演前几个符号的状态序列。这两步做完链路内部的一致性才算被真正确认过。第三步做 traceback 深度扫描。整帧回溯的 Viterbi 在不同深度下表现差异很大直接扫描是最快找到安全区间的方法% 置零信道下扫描回溯深度找出译码完全正确的判据 for len 5:5:60 dec viterbi(tx_indices, trellis, metric, traceback_len, len); if isequal(dec, info_bits) fprintf(traceback len %d: OK\n, len); else fprintf(traceback len %d: FAIL\n, len); end end我跑下来两种不同的 TCM 参数安全回溯深度分别是 12 和 20保守选择使用 30用于后续所有带噪声仿真。这个扫描一次只要几秒钟却能省掉后面几十轮“曲线不收敛但不知道哪里不对”的排查时间。从那以后我每次收到类似的 MATLAB 源码包都会强制走一遍“置零信道 → 逐模块核对 → traceback 扫描”这个流程再开始跑误码率磨合新代码的时间从一整天压缩到两小时以内。希望这个流程对你同样管用。本文还有配套的精品资源点击获取
返回列表