简介:基于Matlab的STBC-MIMO-OFDM系统仿真源码,面向无线通信方向的学生、工程师及科研人员,可用于理解2×2 MIMO配置下Alamouti空时分组编码与OFDM子载波调制的完整链路。资源包共4个m文件,整体仅3KB,按功能划分为STBC编码、2发2收STBC解码、OFDM调制与解调等模块,代码精简,便于学习与二次修改。已有455人学习下载。通过运行仿真,可直观观察多径瑞利衰落信道下的误码率变化,帮助掌握空时编码增益与OFDM抗频率选择性衰落的原理,也可作为课程设计、毕业设计或算法对比实验的基础框架,适合Matlab基础较好、希望快速上手MIMO-OFDM仿真的读者。
1. STBC MIMO-OFDM的MATLAB实现:先搞懂这三层再动手
在MATLAB上跑STBC MIMO-OFDM仿真,最常遇到的结果不是“报错”,而是BER曲线掉不下去,甚至在高信噪比时出现平台期。问题通常不在数值计算,而在三个环节没对齐:STBC的空时分组关系、OFDM子载波上的频域映射、以及MIMO信道响应的维度。这个标题看着像某个m文件命名,实际是在问一条完整链路能否在MATLAB里被复现。你需要同时处理空时编码、多天线信道和OFDM调制,三者缺一个,误码率曲线都会失真。
这个仿真值得花时间做,是因为它对物理层算法验证和课程设计都有直接价值。你可以用50行左右的矩阵运算复现Alamouti编码在频率选择性信道下的性能,而不必依赖完整的通信工具箱。我说清楚的是:参数从哪里来、信号怎么排列、解码怎么验证。
2. 从信道模型到STBC编码:MIMO-OFDM的理论边界与MATLAB参数映射
2.1 MIMO-OFDM信号在MATLAB里的三维数组表示
MIMO-OFDM在频域上可以看作一个三维资源块:发射天线、子载波、OFDM符号时间。我在MATLAB里惯用的表示是txFreq = zeros(Nt, Nfft, Nsymbols),其中Nt是发射天线数,Nfft是IFFT点数,Nsymbols是总的OFDM符号数。这个顺序比(子载波, 符号, 天线)更贴近实际矩阵操作,因为频域信道矩阵H在某个子载波上取出的是Nr x Nt的二维矩阵,直接乘以squeeze(txFreq(:, k, n))就能得到接收向量。
初始化代码里需要把这些参数一次定义清楚:
Nfft = 64; % FFT 点数,2 的整数幂,工程上常见 128/256/2048 Ncp = 16; % 循环前缀长度,必须大于信道最大时延 Nt = 2; % 发射天线数,Alamouti 编码固定为 2 Nr = 2; % 接收天线数,1 或 2 都可以 M = 4; % 调制阶数,4 对应 QPSK,16 对应 16QAM Nsymbols = 1000; % OFDM 符号总数,必须为偶数,因为 STBC 按 2 个时隙配对 SNR_dB = 0:2:20; % 信噪比扫描范围,单位 dB参数Nfft决定频域分辨率,Ncp的取舍要看多径信道的时延扩展。我一般先把Ncp设成Nfft/4,然后看帕西瓦尔。比值不够时,误码率曲线会在高SNR段变平,因为码间干扰已经变成主要噪声源。表 2-1 给出常用参考值。
| 参数 | 仿真常用值 | LTE/5G 参考 | 说明 |
|---|---|---|---|
| Nfft | 64/128 | 1024/2048 | 子载波数越多,频域粒度越细 |
| Ncp | 16/32 | 144/512 | 大于最大时延扩展即可,太长浪费吞吐 |
| Nt | 2 | 2/4/8 | Alamouti 只支持 2 天线,更多天线需正交 STBC |
| M | 4/16 | 64/256 | 高阶调制对信道估计误差更敏感 |
2.2 Alamouti编码如何映射到OFDM子载波
Alamouti 空时分组码用两个发射天线、两个时隙传两个数据符号。对每个子载波k,两个连续OFDM符号时间上的频域符号构成一个 2x2 矩阵。不能把整个OFDM符号当作单个信号做STBC,那样会破坏子载波之间的正交性。
正确的频域映射是,对每一个子载波k独立执行:
% 假设 s1 和 s2 是子载波 k 上两个时隙的输入数据 % 构造 2x2 矩阵:行是发射天线,列是时隙 X = [s1 -conj(s2); s2 conj(s1)];对应到MATLAB循环中,就是在一对时隙t和t+1内,把s1放到天线1时隙t,s2放到天线2时隙t,再按共轭和负号填充时隙t+1。这样做的好处是接收端可以用线性合并恢复s1和s2,且分集增益等于发射天线数。表 2-2 比较了 Alfatti 码率与分集的关系:
| 编码方案 | 天线数 | 码率 | 分集阶数 | MATLAB 适用场景 |
|---|---|---|---|---|
| Alamouti | 2 | 1 | 2 | 仿真入门、基线系统 |
| 正交空时分组码 | 4 | 3/4 | 4 | 高阶分集比较 |
| 空间复用 | 2 | 2 | 1 | 频谱效率优先时用 |
2.3 多径信道在频域的表示与SNR设定
MIMO-OFDM 仿真的信道通常不用时域卷积表示,而是把多径时域信道抽头补零到Nfft长度后做 FFT,得到频域响应。每个收发天线对有一个Nfft长度的频域向量,因此整个信道可以存成Nr x Nt x Nfft的三维数组。
L = 4; % 多径数量 tap_delays = [0 1 2 3]; % 时延,以采样点为单位 tap_power = [0 -1 -3 -5]; % 每径相对功率,dB h_t = sqrt(10.^(tap_power/10)) .* ... (randn(1, L) + 1j*randn(1, L)) / sqrt(2); H_freq = fft(h_t, Nfft);这里把每径功率转化为线性幅度,再乘高斯随机变量得到复信道系数。注意randn得到的实部虚部各占一半功率,除以sqrt(2)后平均功率为1。通过fft(h_t, Nfft)把4径时域响应插值到64个子载波上,直接得到频域平坦衰减的近似。若tap_delays中最大时延超过Ncp,ISI 会显著抬低系统性能。
提示:SNR 的定义要统一。我一般把信号平均功率归一化为1,噪声功率按 10^(-SNR/10) 加,这样每个子载波上的接收SNR就是设定值。
3. 用MATLAB搭建STBC MIMO-OFDM发射机:从数据符号到循环前缀
3.1 数据生成与QPSK调制:bit级到符号级
发射机的输入是一串随机比特,经过调制映射成复符号。QPSK 每次取2个比特映射一个符号,MATLAB 的qammod可以直接生成 QPSK 等效的 4-QAM 符号,但要保证数据组织方式符合后面 STBC 编码的要求。
numBitsPerSym = log2(M); dataBits = randi([0 1], Nfft * Nsymbols * numBitsPerSym, 1); % bit2int 把每 numBitsPerSym 个比特转成一个 [0, M-1] 整数 intSymbols = bit2int(reshape(dataBits, [], numBitsPerSym), numBitsPerSym); modSymbols = qammod(intSymbols, M, 'gray'); % 重排成子载波 x OFDM符号 D = reshape(modSymbols, Nfft, Nsymbols);这里特意用bit2int而不是手动索引,是为了让比特到符号的映射更直观。gray参数让相邻星座点只有1个比特不同,在高SNR条件下误码率更接近理论值。reshape按列填充,所以第n列是第n个OFDM符号在所有子载波上的数据。
3.2 频域STBC编码:两种写法都要会
最直观的编码方式是对每一对OFDM符号做循环。我一般先写出这种版本,因为它容易对照Alamouti矩阵检查错误:
txFreq = zeros(Nt, Nfft, Nsymbols); for t = 1:2:Nsymbols s1 = D(:, t); s2 = D(:, t+1); % 天线1 txFreq(1, :, t) = s1; txFreq(1, :, t+1) = -conj(s2); % 天线2 txFreq(2, :, t) = s2; txFreq(2, :, t+1) = conj(s1); end循环里的赋值对应每个子载波独立做Alamouti编码。关键点是conj是对复符号逐点取共轭,不是对矩阵共轭转置。t = 1:2:Nsymbols确保每两个OFDM符号一组,Nsymbols是偶数就是前提条件。
如果仿真规模大,可以用矩阵操作替代循环。把D拆成奇数列和偶数列,再拼到底层向量上:
D1 = D(:, 1:2:end); % s1 D2 = D(:, 2:2:end); % s2 txFreq(:, :, 1:2:end) = zeros(Nt, Nfft, size(D1, 2)); txFreq(1, :, 1:2:end) = D1; txFreq(2, :, 1:2:end) = D2; txFreq(1, :, 2:2:end) = -conj(D2); txFreq(2, :, 2:2:end) = conj(D1);两种写法结果完全一致。注意第二种写法中的zeros是占位符,实际赋值时不用预先分配也可以,但预先分配能提高循环速度。我推荐新手先用循环版,跑通后再改成向量版。
3.3 IFFT与循环前缀:发射时域信号怎么组织
频域符号txFreq需要变成时域发送信号。每个天线上的每个OFDM符号做一次ifft,再在开头复制符号末尾的Ncp个采样点。MATLAB 的ifft默认按 0 到 Nfft-1 的子载波顺序处理,不需要额外fftshift,除非你把直流和负频放到了数组中间。
NtotalSymbol = Nfft + Ncp; txSignal = zeros(Nt, NtotalSymbol * Nsymbols); for tx = 1:Nt for n = 1:Nsymbols dimElements = ifft(squeeze(txFreq(tx, :, n)), Nfft); cp = dimElements(end-Ncp+1:end); txSignal(tx, (n-1)*NtotalSymbol+1 : n*NtotalSymbol) = [cp; dimElements]; end end这段代码里squeeze把Nfft x 1的二维序列缩减成向量,避免维度错误。Ncp如果设成 0,就退化成无循环前缀的OFDM,可以看到性能明显下降。实际系统中Ncp占系统开销的比例也要记录,方便算频谱效率。
4. 接收端解码与信道估计:把STBC MIMO-OFDM链路闭合
4.1 通过频域信道矩阵构造接收信号
接收信号可以直接在频域构造,跳过时域卷积。只要循环前缀长度足够,OFDM就把线性卷积变成了循环卷积,所以频域接收方程为:
Y = zeros(Nr, Nfft, Nsymbols); % 每个子载波上的信道矩阵 Hk 是 Nr x Nt for k = 1:Nfft Hk = H_freq_all(:, :, k); % 预先算好的频域信道 for n = 1:Nsymbols xk = squeeze(txFreq(:, k, n)); noise = sqrt(1/SNR_linear/2) * (randn(Nr,1) + 1j*randn(Nr,1)); Y(:, k, n) = Hk * xk + noise; end end其中SNR_linear = 10^(snr_dB/10)。噪声分成实部和虚部,各占一半功率,所以系数是sqrt(1/(SNR_linear*2))。如果信号平均功率是1,接收噪声功率就是 1/SNR_linear。这种频域建模比时域滤波快得多,而且不涉及循环前缀的移除,适合验证空时码部分。
4.2 LS信道估计与线性插值
实际接收机不知道信道,需要从导频估计。常见做法是在部分子载波上插入已知符号,接收端用最小二乘法(LS)求信道系数,再插值到所有子载波。
NpilotSpacing = 4; pilotIdx = 1:NpilotSpacing:Nfft; X_pilot = qammod(randi([0 M-1], Nt, length(pilotIdx)), M, 'gray'); H_est = zeros(Nr, Nt, Nfft); for rx = 1:Nr for tx = 1:Nt % 取第一个OFDM符号中导频位置上的接收值 Y_pilot = squeeze(Y(rx, pilotIdx, 1)); Hest_pilot = Y_pilot ./ X_pilot(tx, :); H_est(rx, tx, :) = interp1(pilotIdx, Hest_pilot, 1:Nfft, 'linear', 'extrap'); end end这里用第一个OFDM符号做信道估计,并假设信道在一个符号块内不变化。除以X_pilot就是 LS 的核心思想:没有噪声时得到的估计就是真实信道系数。需要注意导频的位置和数值是收发两端约定的;interp1的extrap保证端点处回落到边界值。
提示:如果导频间隔太大,插值会漏掉频域选择性衰落,一般要求导频间距远小于信道相干带宽。
4.3 Alamouti合并解码的实现与等效信道增益
解码时对每个子载波k和每个接收天线rx,取两个相邻时隙的接收值y1和y2,用估计出的信道h1 = H_est(rx,1,k),h2 = H_est(rx,2,k)做合并。
s1_hat = zeros(Nfft, 1); s2_hat = zeros(Nfft, 1); for k = 1:Nfft mmseDenom = 0; acc1 = 0; acc2 = 0; for rx = 1:Nr h1 = H_est(rx, 1, k); h2 = H_est(rx, 2, k); y1 = Y(rx, k, t); % 时隙 t y2 = Y(rx, k, t+1); % 时隙 t+1 acc1 = acc1 + conj(h1) * y1 + h2 * conj(y2); acc2 = acc2 + conj(h2) * y1 - h1 * conj(y2); mmseDenom = mmseDenom + abs(h1)^2 + abs(h2)^2; end s1_hat(k) = acc1 / mmseDenom; s2_hat(k) = acc2 / mmseDenom; end % 对两个时隙的符号分别硬判决 rxSymbols = zeros(Nfft, Nsymbols); rxSymbols(:, t) = qamdemod(s1_hat, M, 'gray'); rxSymbols(:, t+1) = qamdemod(s2_hat, M, 'gray');合并公式里conj(h1) * y1 + h2 * conj(y2)和conj(h2) * y1 - h1 * conj(y2)是Alamouti的线性解码,等效信道增益是|h1|^2 + |h2|^2。多根接收天线时,所有天线上的合并值累加,分母同样累加,这就是最大比合并。把归一化后的s1_hat直接扔给qamdemod,可得到判决符号。
5. 让STBC MIMO-OFDM仿真更快的3个技巧
5.1 用图像化的方式检查时空调制映射
写一个只发射单载波数据的测试:将D(k, 1)置1,其他置0。运行编码后打印txFreq,你会看到天线1时隙1是1,天线2时隙1是0;如果输入变为纯虚数,再检查负共轭是否正确落在时隙2。这样一次检查就能发现绝大多数相位映射错误,比直接看BER快得多。
testD = zeros(Nfft, Nsymbols); testD(1, 1) = 1 + 1j; % 执行编码后,检查 txFreq(:, 1, 1) 和 txFreq(:, 1, 2) 的值5.2 用“信道估计理想化”开关隔离误差源
我把代码里设一个idealChannelFlag,为真时直接用真信道解码,为假时才用LS估计。这样如果实际BER比理想BER差很多,就能确定是信道估计的插值密度不够,还是解码部分仍有错。这个开关也能用来测试不同导频间距下的性能差异,是找到仿真隐藏bug的有效手段。
5.3 监控矩阵的维度一致性
STBC MIMO-OFDM的报错大多出现在reshape和squeeze的维度不匹配。我是这样处理的:每段代码后打印关键矩阵的size,并且断言Nsymbols是偶数、Nfft是2的幂。把断言写进仿真脚本,能提前拦截非常愚蠢的维度错误。
assert(mod(Nsymbols,2)==0, 'Nsymbols 必须为偶数,STBC需要两时隙配对'); assert(mod(Nfft,2)==0, 'Nfft 建议为2的整数幂'); fprintf('txFreq size: %d %d %d\n', size(txFreq));最后一个值得记住的参数关系是:当误码率曲线在高SNR段出现错误平层时,先检查Ncp再检查Nt,别急着改解码算法。大多数STBC MIMO-OFDM仿真问题,出在时延扩展比循环前缀长,或者STBC的时隙配对与实际信道时间变化速度不匹配。
本文还有配套的精品资源,点击获取