简介:STTC(空时格码)仿真工程聚焦4天线MIMO系统,面向无线通信研究人员与工程师,利用空间和时间双重编码提升系统可靠性与速率,用于评估不同状态数(2态、8态、16态)在1/2天线配置下的误码率与帧错误率表现。压缩包共8个文件,均为MATLAB的m脚本,整体仅4KB,涵盖数据生成、网格编码、调制、分支度量、信道模型及主仿真等环节,可直接运行并复现性能对比实验,也便于按需修改参数。已有177人学习下载,具备一定参考价值。通过调整编码状态与天线组合,可对比2×1、2×2等场景下STTC抗衰落与抗干扰能力的差异,进一步分析编码复杂度增加后误码率与帧错误率的改善程度,为MIMO系统编码策略选择和参数优化提供直接的仿真数据支撑。
1. 从4天线STTC仿真包说起:这个zip里到底有什么
做MIMO仿真的人,十有八九都经历过「论文公式看懂了,代码却不知道从哪行开始」的阶段。这个STTC__.zip 是一份典型的4天线STTC(空时格码)MATLAB仿真工程,里面8个.m文件,从数据生成、格形编码、调制、信道一直到维特比检测,整条链路是通着的。它解决的核心问题不是理论科普,而是让你能直接跑出不同状态数(2态、8态、16态)在2×1和2×2天线配置下的BER/FER对比曲线。适合刚接手空时编码课题的学生、要做MIMO性能评估的工程师,以及所有想快速搭一套可改状态、可换天线数的STTC基线的同志。下载下来解压,打开STTC_SIMULATION.m就能看到主循环。
2. STTC原理与仿真文件架构:先把状态和天线映射搞清楚
2.1 空时格码的核心:分集、编码增益与状态数
STTC全称Space-Time Trellis Coding,它把信道编码和调制揉在一起,用格形(trellis)结构把符号同时在时间维和空间维上展开。发射端每个时隙输入的比特不仅决定当前发送符号,还决定下一时隙的编码状态,接收端则用维特比算法在格形图上找最可能的路径。相比空时分组码(如Alamouti),STTC多了一个编码增益,但代价是译码复杂度随状态数线性到指数增加。
这份仿真包里最常改的三个参数就是状态数和天线数。状态数2、8、16分别对应1位、3位、4位的移位寄存器记忆长度。状态越多,编码器能「记住」的过去符号越多,分集和编码增益理论上越好,但维特比译码的网格节点数也随之翻倍。天线数方面,2×1表示2根发射天线、1根接收天线,2×2则是2发2收,正好是4根天线——这也是标题里「4天线STTC」的由来。注意这里说的配置描述的是链路两端的总天线数,不是单个设备的天线数,别被某些博客里的「单用户双天线」说法带偏。
2.2 文件调用关系:从data_generator到BER统计的完整链路
拿到zip先别急着运行,按文件职责理一遍调用链,后面调试才知道错在哪。这8个文件的分工如下:
| 文件 | 职责 |
|---|---|
| data_generator.m | 生成随机0/1比特流,长度可配置 |
| STTC_stage_modulation.m | 把串行比特流按每时隙比特数切分成阶段 |
| STTC_modulator.m | 调用格形编码,生成每个天线上的发送符号矩阵 |
| trellis_encoder.m | 根据状态转移表输出星座符号索引并更新状态 |
| channel1.m | 生成瑞利衰落信道系数,叠加AWGN,返回接收信号与信道矩阵H |
| branch_metric.m | 计算候选发送符号与接收向量的欧氏距离 |
| STTC_detector.m | 维特比检测,输出估计的比特流 |
| STTC_SIMULATION.m | 主脚本,扫描SNR,统计BER/FER |
调用顺序是:主脚本先让data_generator产出比特,然后经过STTC_stage_modulation切分,再由STTC_modulator内部调trellis_encoder做格形编码并映射到每根天线;发送矩阵进channel1过信道和噪声,接收端用branch_metric算分支度量,最后STTC_detector完成维特比译码,把估计比特拿回去和原始比特对比。这一圈走完,主脚本里累加错误比特数和错误帧数,最后除以总数就是BER和FER。
主脚本的骨架一般长这样,我习惯把参数集中放在文件头部:
% STTC_SIMULATION.m 主脚本骨架 % 天线与编码参数 tx_ant = 2; % 发射天线数(2x2配置时为2) rx_ant = 2; % 接收天线数 num_states = 8; % 格形状态数:可2、8、16 m_bits = 2; % QPSK调制,每时隙2比特 % 仿真参数 snr_list = 0:2:20; % 信噪比扫描,单位dB frames = 100; % 每个SNR下仿真帧数 frame_len = 128; % 每帧比特数 ber = zeros(size(snr_list)); fer = zeros(size(snr_list)); for s = 1:length(snr_list) err_bit = 0; total_bit = 0; err_frame = 0; total_frame = 0; for f = 1:frames bits = data_generator(frame_len); tx_sym = STTC_modulator(bits, tx_ant, num_states); [rx_sym, H] = channel1(tx_sym, snr_list(s), rx_ant); est_bits = STTC_detector(rx_sym, H, tx_ant, rx_ant, num_states); err_bit = err_bit + sum(bits ~= est_bits); total_bit = total_bit + frame_len; if any(bits ~= est_bits) err_frame = err_frame + 1; end total_frame = total_frame + 1; end ber(s) = err_bit / total_bit; fer(s) = err_frame / total_frame; end这里tx_ant和rx_ant要跟STTC_modulator与channel1内部尺寸对上。snr_list从0到20,步长2,覆盖了STTC曲线最有区分度的区间。frames=100是底线,想画平滑BER曲线建议调到500以上,不然高SNR段会像锯齿。frame_len取128,能被m_bits整除,避免最后一截被丢弃导致统计偏差。
这段骨架里没有显式调用STTC_stage_modulation和trellis_encoder,因为STTC_modulator内部会负责调用它们,主脚本只需要看到比特进的比特出的结果。如果你拿到的主脚本不是这个风格,大概率是把调制和编码步骤摊开写在STTC_SIMULATION.m里了,到时候对照文件清单改参数即可。
3. 跑通STTC仿真:数据生成、调制与格形编码
3.1 data_generator.m:怎么生成一组符合MIMO场景的比特流
data_generator.m是整个链路的地基。STTC仿真对输入数据的唯一硬要求是长度要和格形编码的约束长度匹配,否则你会遇到「比特数不够切分」或者「最后一组被截断」这种隐藏问题。最朴素的实现就是等概率随机比特:
function bits = data_generator(frame_len) % data_generator: 生成随机二进制比特流 % 输入frame_len为比特数,输出为0/1行向量 bits = randi([0 1], 1, frame_len); endrandi生成的是整数0和1,在MATLAB里可以直接参与比较和逻辑运算。frame_len建议设置为m_bits的整数倍,并且最好同时是num_states对应帧结构的整数倍。比如QPSK调制下m_bits=2,frame_len=128,切分成64个时隙,刚好。如果你把frame_len改成100,那么最后一个时隙只够半个符号,STTC_stage_modulation里用floor截断的话,会静默丢弃最后几个比特,BER分母和分子对不上,曲线会莫名偏差。
3.2 STTC_stage_modulation.m与STTC_modulator.m:符号映射与空时编码
STTC_stage_modulation.m负责把一维比特流改写成「每时隙一组」的二维矩阵。为什么需要这一步?因为格形编码的输入是分组比特,每个时隙消费m_bits个比特,同时产生一组状态转移。常见的切分方式如下:
function stg_bits = STTC_stage_modulation(bits, m_bits) % stg_bits: 维度 num_stage x m_bits % 将bits按每m_bits个比特切分为时隙组 num_stage = floor(length(bits) / m_bits); stg_bits = reshape(bits(1:num_stage * m_bits), m_bits, num_stage).'; end注意reshape的第二个维度是m_bits,转置后每一行代表一个时隙的输入比特;floor截断意味着末尾不足一组的比特会被丢掉,所以前面强调frame_len必须整除m_bits。切分之后,STTC_modulator.m拿到这些分组比特,调trellis_encoder做编码,再按天线数展开。一个常见的调制器实现是:
function tx_sym = STTC_modulator(bits, tx_ant, num_states) % STTC_modulator: 空时调制器 % 输出tx_sym维度 num_stage x tx_ant,每个元素是QPSK星座点 m_bits = 2; % 默认QPSK stg_bits = STTC_stage_modulation(bits, m_bits); sym_idx = trellis_encoder(stg_bits, num_states); % QPSK星座,能量归一化 const = [1+1j; -1+1j; -1-1j; 1-1j] / sqrt(2); tx_sym = const(sym_idx); tx_sym = reshape(tx_sym, size(sym_idx,1), tx_ant); end这里sym_idx是trellis_encoder返回的索引矩阵,每一行对应一个时隙,每一列对应一根天线上的星座点索引。const四个点模值都是1,总发射功率为1,后面channel1的噪声功率换算就省心。如果你换8PSK,const要改成8个点,m_bits同步改成3,否则索引越界或者星座间距出错,BER直接报废。
3.3 trellis_encoder.m:状态驱动的格形编码实现
trellis_encoder是STTC的黑匣子,也是整个包最容易藏坑的地方。它的职责是根据当前状态和输入比特查表,给出每根天线要发送的星座符号索引,并转移状态。由于不同状态数和调制方式对应不同的生成多项式,业内一般直接把标准查找表写死,而不是每次现算。
function sym_idx = trellis_encoder(stg_bits, num_states) % trellis_encoder: 格形编码器(查表风格) % stg_bits: num_stage x m_bits 分组比特 % num_states: 2, 8, 16 num_stage = size(stg_bits, 1); m_bits = size(stg_bits, 2); sym_idx = zeros(num_stage, 2); % 2发天线 % 实际包内包含查表矩阵: % output_table: 状态数 x 输入数 x 天线数 % next_state_table: 状态数 x 输入数 output_table = get_std_output_table(num_states, m_bits, 2); next_state_table = get_std_next_table(num_states, m_bits); state = 1; % 初始状态编号 for t = 1:num_stage u = stg_bits(t, :); % 当前时隙输入比特 idx = bin2dec(sprintf('%d', u)) + 1; sym_idx(t, :) = squeeze(output_table(state, idx, :)).'; state = next_state_table(state, idx); end end这段代码里get_std_output_table和get_std_next_table是占位函数,真实包中应该是矩阵常量。sprintf('%d', u)把比特行向量拼成字符串,比如[0 1]变成'01',bin2dec再转成十进制并加1作为索引。注意状态编号从1开始,如果你的实现从0开始,整个查表都会偏移。收到状态转移表后,检测器必须用同一张表做逆映射,否则编码和译码对不上,误码率会卡在某个固定值上不去。
这个文件我在实际项目里见过三种翻车模式:一是状态编号从0开始还是从1开始没统一,导致查表偏移一位;二是生成多项式没归一化,星座符号能量不一致;三是只写了2态的表,把num_states改成16后还在用旧表。因此你改状态数时,第一件事确认trellis_encoder里有对应状态数的表,而不是拿2态的生成多项式硬凑。
4. 信道、分支度量与维特比检测:接收端怎么把符号解回来
4.1 channel1.m:平坦衰落与AWGN信道模型的建模方式
STTC的信道通常用平坦瑞利衰落加AWGN,channel1.m要同时完成生成信道系数和叠加噪声。准静态衰落假设一个帧内信道不变,这样每帧只需要生成一次H矩阵,计算快,且便于和理论曲线对照。
function [rx_sym, H] = channel1(tx_sym, snr_db, rx_ant) % channel1: 平坦瑞利衰落信道 + AWGN % tx_sym: num_stage x tx_ant % snr_db: 信噪比(dB) % 返回值rx_sym: num_stage x rx_ant,H: rx_ant x tx_ant [num_stage, tx_ant] = size(tx_sym); % 瑞利信道:实部虚部各为高斯,能量归一化 H = (randn(rx_ant, tx_ant) + 1j*randn(rx_ant, tx_ant)) / sqrt(2); rx_sym = zeros(num_stage, rx_ant); for t = 1:num_stage rx_sym(t, :) = H * tx_sym(t, :).'; end % 噪声功率:假设信号功率为1 snr_lin = 10^(snr_db/10); noise_power = 1 / snr_lin; noise = sqrt(noise_power/2) * (randn(num_stage, rx_ant) + 1j*randn(num_stage, rx_ant)); rx_sym = rx_sym + noise; end这里H用randn(…)/sqrt(2)保证每个元素模方均值是1,即信道不改变平均接收功率。噪声的实部虚部分别用sqrt(noise_power/2)缩放,这样noise的总方差正好是noise_power。如果你要模拟快衰落,把生成H的代码挪到循环内部,每个时隙重新生成;但注意此时detector使用的H必须是时变的,否则性能会虚高。另外,snr_db输入需要在主脚本传的就是dB值,不换算成线性值;很多人在这里把10^(snr_db/10)又除了2,导致所有曲线右移3dB。
4.2 branch_metric.m:分支度量的构造与欧氏距离计算
维特比检测的核心就是分支度量。接收端已知信道H,对每个时隙的接收向量r_t,它要与所有可能的发送符号向量s比较,距离越小越像。这个度量就是欧氏距离的平方:
function metric = branch_metric(rx_vec, H, tx_sym_vec) % rx_vec: 1 x rx_ant 当前接收向量 % H: rx_ant x tx_ant 信道矩阵 % tx_sym_vec: 1 x tx_ant 候选发送符号向量 diff = rx_vec - H * tx_sym_vec.'; metric = sum(abs(diff).^2); end注意H * tx_sym_vec.'得到的是rx_ant x 1的向量,正好和rx_vec维度一致。metric是一个非负实数,表示该候选符号在接收端的「嫌疑程度」。这个文件本身没太多技术含量,但它会被detector在每一时刻、每一状态、每一输入上反复调用,所以性能瓶颈往往在这。如果仿真慢,优先把这段矩阵化成批量计算:一次性算完所有候选符号的度量,而不是在detector里逐调用。
4.3 STTC_detector.m:维特比译码与硬/软判决的选择
STTC_detector是接收端的大脑,它要维护每状态的累计度量,每一时刻对所有分支做「加-比-选」(ACS),最后回溯得到比特序列。典型结构如下:
function est_bits = STTC_detector(rx_sym, H, tx_ant, rx_ant, num_states) % STTC_detector: 维特比检测器 num_stage = size(rx_sym, 1); % 初始化各状态度量,第一状态设为0,其余为inf path_metric = inf(1, num_states); path_metric(1) = 0; % 记录每个时刻、每个状态的存活路径,用于回溯 survivor = zeros(num_states, num_stage); for t = 1:num_stage new_metric = inf(1, num_states); for state = 1:num_states for input = 1:2^m_bits cand = get_candidate_symbol(state, input, num_states); old_metric = path_metric(state); br = branch_metric(rx_sym(t, :), H, cand); next_state = get_next_state(state, input, num_states); if old_metric + br < new_metric(next_state) new_metric(next_state) = old_metric + br; survivor(next_state, t) = state; end end end path_metric = new_metric; end % 回溯 est_bits = traceback(survivor, path_metric, num_stage); end这里get_candidate_symbol和get_next_state需要依赖trellis_encoder里的同一张表,且注意m_bits也要和调制映射一致。硬判决用欧氏距离,软判决则在度量里加上噪声方差的对数似然项,改善有限且增加计算量。STTC的格形结构下,硬判决已经能获得分集增益,软判决主要用于级联Turbo码等场景,这份包里默认硬判决就够用。
回溯需要设定深度:如果帧长较短,直接回溯到帧起点;如果帧很长,一般取5倍记忆长度作为截断深度,否则路径度量可能收敛不到正确状态。这个参数在detector内部或主脚本中配置,我一般会把traceback_length拆出来,方便调优。
5. STTC仿真避坑:从BER曲线不收敛到状态数选择
5.1 现象:BER曲线随SNR增大反而上升
跑出来的结果是SNR越高、BER越多,简直反直觉。最可能的原因是发射符号功率没有归一化:QPSK星座点如果用[1+1j; -1+1j; -1-1j; 1-1j]不除以sqrt(2),星座模值是根号2,实际发射功率是2,但信道和噪声计算仍按功率1来,相当于所有SNR点都抬高了3dB。当然这只会平移不至反转,真正反转多半是噪声方差算错:noise_power = 1/snr_lin没错,但真实噪声是复数,如果写成randn不加1j,实信号的功率只有复信号的一半,曲线会整体偏离。检查办法是单独跑一个无噪声脚本,把snr_db设成100,BER必须为0;再把snr_db设成0,实测接收信号功率和噪声功率比例,对照理论值。
5.2 现象:2×2与2×1配置的BER曲线完全重合
改了天线参数但性能没有提升,第一反应是发送矩阵没有真正分配到两天线。检查STTC_modulator输出的tx_sym第二维是不是等于tx_ant,以及channel1里H的尺寸是不是rx_ant × tx_ant。常见错误是trellis_encoder内部写死了两列,即2发天线;改成2×1时把tx_ant设为1,但编码器仍输出两列,STTC_modulator的reshape又把其中一个符号丢弃或重复,最终两天线符号完全相同,分集自然为零。用size把中间变量打出来,一眼就能看出问题。
5.3 现象:状态数从2升到16后,仿真时间暴涨几十倍
这不是bug,是复杂度真相。状态数为S时,维特比每一时刻要处理S个状态、每状态2^m_bits个输入分支。2态QPSK是2×4=8个分支,16态直接是16×4=64个分支,每时刻计算量翻了8倍,加上状态转移表如果每次循环都重新查,时间暴涨正常。解决思路有三个:一是把状态转移表和输出符号表在进入仿真前预计算成常量矩阵,不要在detector内反复调函数;二是用MATLAB矩阵化批量计算分支度量,避免逐分支调用branch_metric;三是适当降低frames在高状态数下的值,但不要低于50,否则统计不稳定。另外,检查有没有在循环内部动态分配数组,预分配能省掉大量时间。
5.4 现象:FER统计结果不是0就是100%
帧错误率需要按帧统计,如果每SNR只跑10帧,错误帧数为0就得到0%,错误帧数大于0就得到100%,曲线完全是台阶。解决方法是把frames提到100以上,且保持帧长一致;对于低SNR区间(0-6dB),FER接近1很正常,此时想要区分两个算法的差异,需要把SNR范围右移到曲线过渡带,比如8-16dB。另一个细节是any(bits ~= est_bits)判断帧错误时,如果估计比特因为回溯深度不够导致尾部错位,即使前面全对也会被判错帧,结果FER虚高。遇到这种情况,先检查截断深度,再决定是否只统计中间稳定段。
6. 把STTC仿真结果做实:验证BER/FER曲线与理论界的技巧
拿到包先别急着画全曲线,我每次都会先做一次「零噪声自检」:把snr_db设成一个很大的值(比如100),信道H设成单位阵或固定常数矩阵,让channel1只叠加一个极小的噪声。此时BER应该无限接近0。如果检测器有状态初始化、回溯深度、查表偏移等任何问题,这一步会立刻暴露。这个自检成本极低,却能省下后面分析曲线时的大把时间。
自检通过后,再验证2×1配置的2态STTC。2发1收的STTC在瑞利信道下理论分集阶数是2,你可以画一条参考线:把2×1系统的BER曲线斜率,与单发单收BPSK在瑞利信道下的BER曲线对比。如果两者的斜率几乎相同,说明分集阶数没有达到,编码器或detector大概率有问题;如果STTC曲线的斜率明显更陡,说明分集增益兑现了。
还有一个技巧是看FER与BER的关系。在STTC中,FER/BER的比值可以粗略反映错误帧内平均错误比特数。如果这个比值在SNR升高时剧烈变化,说明错误分布不均匀,很可能是帧长切分或回溯导致的边缘效应。我一般会让frame_len至少是m_bits * 64,并且检测器的survivor矩阵预分配成num_states x num_stage,这样回溯时不用动态增长数组。此外,曲线画完记得保留随机数种子:在主脚本开头加一句rng(42),保证每次仿真复现同一组信道和噪声。这一点在做状态数对比时尤其重要,因为不同状态数下如果用了不同随机数,曲线差异会被噪声抖动淹没。
这个包我最初跑的时候,就是忘了在channel1里归一化信道能量,导致2×2和2×1曲线几乎重叠,还怀疑是算法实现问题,折腾了一整天才发现是H的方差多乘了一个sqrt(tx_ant)。从那以后,我每次拿到STTC仿真包,都强制先跑一遍零噪声自检,再检查信道归一化,最后才看BER曲线。这份包含8个.m文件的STTC仿真包,需要的可以直接拿走跑起来,跑之前记得先做自检。希望这份拆解能帮到你,也让你少走我走过的弯路。
本文还有配套的精品资源,点击获取