简介:面向通信系统仿真研究者的 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的回溯逻辑,才意识到这说的是 TCM(Trellis 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/2,SNR = 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/N0:16QAM 的换算不能想当然
误码率曲线的横坐标到底是 SNR、Es/N0 还是 Eb/N0,直接决定你画出来的曲线能不能和文献对比。16QAM 每符号 4 bit,所以 Es = 4·Eb,换成 dB 是 10·log10(4) ≈ 6.02 dB。
| 参数 | 定义 | 与 SNR(Es/N0)的关系 |
|---|---|---|
| 每符号信噪比 Es/N0 | 信号平均能量 / 噪声单边功率谱密度 | 等于 SNR(信号能量归一化时) |
| 每比特信噪比 Eb/N0 | 每比特能量 / N0 | Es/N0 − 6.02 dB(16QAM) |
| 噪声总功率 | 实部 + 虚部 | 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 bit,100 帧只有约 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 扫描”这个流程,再开始跑误码率,磨合新代码的时间从一整天压缩到两小时以内。希望这个流程对你同样管用。
本文还有配套的精品资源,点击获取