简介:这份MATLAB源码资源面向无线通信方向的学生与研究人员,聚焦MIMO-OFDM系统中空间分集与空间频率分组编码(SFBC)的仿真实现,可用于课程设计、毕业课题或算法验证。压缩包共13个文件,以12个.m脚本为主,另含1个txt说明,整体约10KB,体积轻便但覆盖链路完整:既有OFDM调制与QPSK映射、解映射模块,也包含Rayleigh与Jakes多径信道生成函数,以及ZF、OSIC等MIMO检测与SML软判决译码实现,并区分有无误包丢弃两种主程序流程。已有135人学习下载,说明该方向具备一定关注度。读者可借此搭建从调制、信道建模到检测译码的完整仿真链路,对比不同检测算法在衰落信道下的性能差异,理解SFBC分集增益与接收端解码复杂度的权衡,为后续系统设计与优化提供可复用的代码基础。
1. OFDM-SFBC 在 MIMO 里到底解决什么问题:从一次信道塌陷说起
做无线通信基带的人,大概都遇到过这种场景:终端在移动,信道从视距突然切到强散射,单天线 OFDM 的误码率曲线像被人从中间掰断一样,陡坡瞬间变缓。你加大发射功率,曲线纹丝不动;你换调制阶数,只是把拐点挪了个位置。问题不在功率,也不在调制,而在频率选择性衰落把某些子载波直接打进了深谷。OFDM 本身把宽带拆成窄带子载波,已经缓解了一部分,但子载波级的深衰落依然存在。SFBC(Space-Frequency Block Coding,空频块编码)就是冲着这个来的——它在频域上做 Alamouti 式的正交编码,把同一份信息映射到相邻子载波和不同天线上,让深衰落只吃掉一半信息,另一半还能救回来。而 MIMO-OFDM 是把多天线和 OFDM 结合后的系统框架,OFDM-SFBC 则是这个框架里最经典、最容易落地的一种发射分集方案。它适合谁?适合正在做基带链路仿真、想验证分集增益、或者要在 SDR 平台上跑通 MIMO 发射链路的工程师。这一章先把「为什么需要它」讲透,后面再拆怎么搭、参数怎么设、哪里会翻车。
2. OFDM-SFBC 的数学骨架与发射链路搭建
2.1 Alamouti 在频域上的搬移:SFBC 编码矩阵怎么来的
Alamouti 码原本是时域方案:两个符号周期内,天线 1 发 $[s_1, s_2]$,天线 2 发 $[-s_2^, s_1^]$。接收端用两根天线的信道估计做合并,就能得到二阶分集。SFBC 做的事,是把「两个符号周期」换成「两个相邻子载波」。假设第 $k$ 个子载波和第 $k+1$ 个子载波上要传的符号是 $s_1$ 和 $s_2$,那么天线 1 在子载波 $k$ 和 $k+1$ 上分别发 $s_1$ 和 $s_2$,天线 2 在同样两个子载波上发 $-s_2^$ 和 $s_1^$。接收端在频域上对这两个子载波做合并,就能恢复出 $s_1$ 和 $s_2$。
这里有一个关键前提:相邻子载波的信道要足够接近,才能近似认为 $H_1(k) \approx H_1(k+1)$,$H_2(k) \approx H_2(k+1)$。如果信道在频域上变化太快,这个近似就崩了,分集增益会明显缩水。所以 SFBC 通常要求子载波间隔和信道相干带宽匹配,不能盲目把子载波间距拉得太大。
用矩阵写出来更清楚。设发射符号向量为 $\mathbf{s} = [s_1, s_2]^T$,SFBC 编码矩阵为:
$$ \mathbf{X} = \begin{bmatrix} s_1 & s_2 \ -s_2^* & s_1^* \end{bmatrix} $$
第一行对应天线 1,第二行对应天线 2;第一列对应子载波 $k$,第二列对应子载波 $k+1$。接收端在子载波 $k$ 和 $k+1$ 上收到的信号分别是:
$$ y_k = H_1(k)s_1 + H_2(k)(-s_2^*) + n_k $$
$$ y_{k+1} = H_1(k+1)s_2 + H_2(k+1)s_1^* + n_{k+1} $$
在 $H_i(k) \approx H_i(k+1) = H_i$ 的假设下,合并后就能得到 $s_1$ 和 $s_2$ 的估计。这套推导不复杂,但它是后面所有参数设置的依据。
2.2 用 Python 搭一条最小可跑的 SFBC-OFDM 发射链路
下面这段代码不依赖任何通信库,只用 NumPy,目的是把 SFBC 编码、子载波映射、IFFT 和 CP 加接串起来。你可以直接复制运行,观察发射信号的频谱和 PAPR。
import numpy as np def sfbc_encode(symbols): """ 输入: symbols 长度为 2N 的复数数组,每两个一组 [s1, s2] 输出: 两根天线的频域数据,形状 (2, 2N) """ n_pairs = len(symbols) // 2 tx1 = np.zeros(2 * n_pairs, dtype=complex) tx2 = np.zeros(2 * n_pairs, dtype=complex) for i in range(n_pairs): s1 = symbols[2 * i] s2 = symbols[2 * i + 1] tx1[2 * i] = s1 tx1[2 * i + 1] = s2 tx2[2 * i] = -np.conj(s2) tx2[2 * i + 1] = np.conj(s1) return np.vstack([tx1, tx2]) def ofdm_modulate(freq_data, fft_size, cp_len): """ freq_data: 形状 (n_tx, n_subcarriers) 返回: 形状 (n_tx, fft_size + cp_len) 的时域信号 """ n_tx, n_sc = freq_data.shape assert n_sc <= fft_size, "子载波数不能超过 FFT 点数" time_data = np.zeros((n_tx, fft_size), dtype=complex) # 把子载波放到 FFT 输入的两侧,中间补零 half = n_sc // 2 time_data[:, :half] = freq_data[:, :half] time_data[:, -half:] = freq_data[:, -half:] time_data = np.fft.ifft(time_data, axis=1) * np.sqrt(fft_size) cp = time_data[:, -cp_len:] return np.hstack([cp, time_data]) # 参数设置 fft_size = 64 cp_len = 16 n_subcarriers = 48 qpsk_symbols = (2 * np.random.randint(0, 2, n_subcarriers) - 1) + \ 1j * (2 * np.random.randint(0, 2, n_subcarriers) - 1) qpsk_symbols = qpsk_symbols / np.sqrt(2) tx_freq = sfbc_encode(qpsk_symbols) tx_time = ofdm_modulate(tx_freq, fft_size, cp_len) print("发射时域信号形状:", tx_time.shape)这段代码里,sfbc_encode完成的是 2.1 节里的矩阵映射,ofdm_modulate负责把频域符号搬到 IFFT 输入的两侧,再在时域前面拼上 CP。参数上,fft_size决定子载波总数,n_subcarriers是实际使用的子载波数,通常留出保护带;cp_len要大于信道最大时延扩展对应的采样点数,否则 ISI 会直接毁掉频域正交性。QPSK 符号做了归一化,保证每根天线的平均功率一致。跑完这段,你可以用np.fft.fft把接收端信号变回频域,验证子载波上的符号是否和发射一致。
2.3 接收端合并:把分集增益真正拿到手
发射端搭好只是第一步,接收端合并才是决定 SFBC 能不能拿到二阶分集的关键。假设接收端只有一根天线,收到两个相邻子载波上的信号 $y_k$ 和 $y_{k+1}$,信道估计得到 $H_1$ 和 $H_2$(两根发射天线到接收天线的频域响应)。合并公式是:
$$ \hat{s}1 = H_1^* y_k + H_2 y{k+1}^* $$
$$ \hat{s}2 = H_2^* y_k - H_1 y{k+1}^* $$
这两个式子做完之后,再除以 $|H_1|^2 + |H_2|^2$ 做归一化,就得到符号估计。下面这段代码把合并过程写出来,并和直接迫零均衡做对比。
def sfbc_decode(y, H1, H2): """ y: 形状 (2N,) 接收频域信号,每两个一组 H1, H2: 对应子载波上的信道估计,形状 (2N,) 返回: 合并后的符号估计,形状 (2N,) """ n_pairs = len(y) // 2 s_hat = np.zeros(2 * n_pairs, dtype=complex) for i in range(n_pairs): yk = y[2 * i] yk1 = y[2 * i + 1] h1 = H1[2 * i] h2 = H2[2 * i] denom = np.abs(h1)**2 + np.abs(h2)**2 + 1e-12 s_hat[2 * i] = (np.conj(h1) * yk + h2 * np.conj(yk1)) / denom s_hat[2 * i + 1] = (np.conj(h2) * yk - h1 * np.conj(yk1)) / denom return s_hat这里denom加了一个极小值防止除零。合并后的符号再做 QPSK 判决,就能算误码率。你可以把H1和H2设成独立瑞利衰落,跑几千次蒙特卡洛,对比单天线 OFDM 的误码率曲线,应该能看到明显的斜率差异——这就是分集阶数从 1 变成 2 的直接体现。如果曲线斜率没变,先检查信道估计是不是在相邻子载波上做了平均,或者 CP 长度是不是不够导致 ISI 污染了频域符号。
3. 参数怎么设:FFT 点数、CP 长度与子载波映射的取舍
3.1 FFT 点数与子载波数的匹配关系
FFT 点数决定了 OFDM 符号的频域分辨率,子载波数决定了实际占用的带宽。常见做法是子载波数取 FFT 点数的 3/4 左右,留出两侧保护带。比如 FFT 64 点,用 48 个子载波;FFT 1024 点,用 768 或 600 个子载波。保护带的作用是防止频谱泄漏到相邻频段,同时给滤波器留过渡带。如果你把子载波数设得和 FFT 点数一样,频谱会直接糊到带外,实际系统里过不了射频前端。
SFBC 对子载波数还有一个额外要求:必须是偶数。因为每两个相邻子载波组成一个编码块,奇数个子载波会导致最后一个符号落单。常见做法是把 DC 子载波置零,然后两侧对称分配,这样天然就是偶数。如果你用的子载波数算下来是奇数,要么去掉一个边缘子载波,要么把 DC 也纳入编码——但 DC 通常有本振泄漏,不建议用。
3.2 CP 长度:不是越长越好
CP 长度的选取原则是大于信道最大时延扩展。假设采样率 20 MHz,最大时延扩展 1 微秒,对应 20 个采样点,那 CP 至少取 20 点,工程上留余量取 24 或 32 点。CP 太短,ISI 会直接污染下一个 OFDM 符号,频域正交性被破坏,SFBC 合并时信道估计的近似假设也不成立。CP 太长,频谱效率下降,而且发射端 PAPR 会略微变差。
有一个容易被忽略的点:SFBC 的相邻子载波近似要求信道在频域上平滑。如果 CP 长度刚好卡在临界值,信道估计在边缘子载波上误差会变大,合并时反而引入额外噪声。我一般会在仿真里扫一遍 CP 长度,看误码率曲线的拐点,选拐点之后第一个明显平坦的值,而不是理论最小值。
3.3 子载波映射方式对分集增益的影响
SFBC 要求两个编码符号落在相邻子载波上,但「相邻」有两种理解:物理相邻和逻辑相邻。物理相邻就是子载波索引连续,逻辑相邻是经过交织或置换后的相邻。在频率选择性信道下,物理相邻的两个子载波信道相关性高,近似假设成立,但分集效果可能不够「散」;逻辑相邻可以把两个符号拉到更远的频段,获得更大的频率分集,但信道近似误差会变大。
常见做法是折中:在相干带宽内做物理相邻,同时通过子载波交织让不同编码块之间拉开距离。如果你在仿真里发现分集增益比理论值低 1 dB 以上,先检查一下子载波映射是不是把编码块都挤在了信道深谷附近。把映射方式改成块交织,往往能救回来。
4. 避坑与排查:SFBC-OFDM 仿真里最容易翻车的 5 个地方
4.1 误码率曲线斜率不对,分集增益像假的
现象:跑出来的 BER 曲线和单天线 OFDM 几乎平行,斜率没有明显变化,理论上应该更陡。
原因:最常见的是接收端合并时用了错误的信道估计。比如把两个子载波的信道分别估计后直接代入,但没有做相邻子载波平均,导致 $H_1(k)$ 和 $H_1(k+1)$ 差异过大,合并公式里的近似假设失效。另一个可能是发射端 SFBC 编码时共轭位置写反了,比如把 $-s_2^$ 写成了 $s_2^$,这样合并出来的是干扰而不是信号。
解决:先单独验证发射端编码矩阵,用已知符号和已知信道做一次无噪声合并,看能不能恢复出原始符号。如果这一步过了,再检查接收端信道估计是否在相邻子载波上做了平滑。仿真里可以把信道设成平坦衰落,如果平坦信道下曲线正常,频率选择性信道下不正常,那问题就在子载波近似上。
4.2 加了 CP 之后误码率反而变差
现象:CP 长度从 0 增加到理论值时,误码率先降后升,或者根本没降。
原因:CP 本身不引入噪声,但如果 CP 长度超过了信道最大时延扩展,多出来的部分只是浪费功率。更隐蔽的问题是:CP 加在时域,而 SFBC 编码在频域,如果 IFFT 之前子载波映射把编码块拆到了保护带两侧,IFFT 之后这两个子载波在时域上的相关性会被破坏,CP 去除后频域符号反而对不齐。
解决:检查子载波映射,确保每个 SFBC 编码块的两个子载波都在有效频带内,且没有被保护带隔开。另外确认 CP 去除的位置和 FFT 窗口对齐,偏移一个采样点就会引入相位旋转,在频域上表现为信道估计偏差。
4.3 信道估计误差在深衰落子载波上被放大
现象:整体误码率还行,但某些子载波上的误码率异常高,像是有规律地出现坏点。
原因:SFBC 合并公式里除以了 $|H_1|^2 + |H_2|^2$,当两个信道都处于深衰落时,分母极小,噪声被放大。如果信道估计在这些子载波上还有误差,放大效应更明显。
解决:在合并时加一个正则化项,比如分母加 $10^{-6}$ 量级的小常数,或者设置一个信道增益门限,低于门限的子载波直接置零不参与合并。更工程化的做法是在接收端做 MMSE 合并而不是迫零合并,代价是需要知道噪声功率。
4.4 发射端两根天线的功率不均衡
现象:接收端合并后星座图明显偏斜,或者误码率比理论值差 2 dB 以上。
原因:SFBC 编码矩阵里,天线 1 发 $[s_1, s_2]$,天线 2 发 $[-s_2^, s_1^]$,两者功率相同。但如果代码里对天线 2 的符号做了额外的归一化,或者 IFFT 之前补零方式不一致,两根天线的平均功率就会不同。接收端合并时假设两根天线功率相等,不均衡会直接导致合并权重失配。
解决:在发射端分别计算两根天线的时域平均功率,确保差异小于 0.1 dB。如果用的是硬件平台,还要检查两根射频链路的增益是否校准过。
4.5 用 MIMO 信道容量图像验证时对不上
现象:想用信道容量曲线验证 SFBC 的增益,但画出来的容量和理论值对不上,甚至比单天线还低。
原因:SFBC 是发射分集方案,不是空间复用方案。它的目标是降低误码率,不是提高信道容量。在低信噪比下,SFBC 的容量可能略低于单天线,因为发射分集牺牲了一部分复用增益。如果你拿容量公式去套 SFBC,得到的结论会误导。
解决:验证 SFBC 应该看误码率曲线或者分集阶数,而不是信道容量。如果一定要看容量,应该对比的是「相同误码率下的信噪比增益」,而不是绝对容量值。这一点在写仿真报告时特别容易踩坑,血泪经验是先把评价指标定对,再跑数据。
5. 进阶技巧:用分集阶数验证 SFBC 是否真的生效
5.1 从误码率曲线斜率反推分集阶数
分集阶数 $d$ 可以从误码率曲线的斜率看出来。在高信噪比区域,误码率近似正比于 $\text{SNR}^{-d}$。把 BER 曲线画在对数坐标下,斜率就是 $-d$。SFBC 在两根发射天线一根接收天线下,理论分集阶数是 2;如果接收端有两根天线,分集阶数可以到 4。你可以用下面的代码从仿真数据里拟合斜率。
import numpy as np def estimate_diversity_order(snr_db, ber): """ snr_db: 信噪比数组,单位 dB ber: 对应的误码率数组 返回: 拟合出的分集阶数 """ snr_linear = 10 ** (np.array(snr_db) / 10) log_ber = np.log10(np.array(ber)) log_snr = np.log10(snr_linear) # 取高信噪比段做线性拟合 idx = snr_db > np.median(snr_db) coeff = np.polyfit(log_snr[idx], log_ber[idx], 1) return -coeff[0] # 假设你有一组仿真数据 snr = np.arange(0, 21, 2) ber_sfbc = np.array([0.12, 0.08, 0.045, 0.022, 0.009, 0.0035, 0.0012, 0.0004, 0.00012, 0.00003, 0.000008]) print("估计分集阶数:", estimate_diversity_order(snr, ber_sfbc))这段代码取高信噪比段做线性拟合,返回的斜率绝对值就是分集阶数。如果拟合出来接近 1,说明 SFBC 没生效,或者信道估计把分集增益吃掉了;接近 2 才算正常。注意不要用低信噪比段拟合,那里曲线还没进入渐近区,斜率会偏小。
5.2 用信道相关矩阵检查子载波近似是否成立
SFBC 合并的前提是相邻子载波信道近似相等。你可以直接算相邻子载波的信道相关系数,如果低于 0.9,合并公式的误差就会开始明显。下面这段代码计算频域信道自相关。
def subcarrier_correlation(H, max_lag=4): """ H: 频域信道响应,形状 (n_subcarriers,) 返回: 不同 lag 下的相关系数 """ H = H - np.mean(H) corr = np.correlate(H, H, mode='full') corr = corr / corr[len(H) - 1] center = len(H) - 1 return corr[center:center + max_lag] # 生成一个频率选择性信道 n_sc = 64 h_time = np.zeros(n_sc, dtype=complex) h_time[:4] = np.random.randn(4) + 1j * np.random.randn(4) H_freq = np.fft.fft(h_time) print("相邻子载波相关系数:", subcarrier_correlation(H_freq))如果 lag=1 的相关系数低于 0.9,说明信道在相邻子载波上变化太快,SFBC 的近似假设不成立。这时候要么减小子载波间隔,要么改用 SFBC 的变体,比如在更多子载波上做正交编码,牺牲一部分频谱效率换分集可靠性。
5.3 一个我常用的验证习惯
每次搭完 SFBC-OFDM 链路,我不会直接跑完整仿真,而是先做三步:第一步,平坦信道下验证编码和合并矩阵,确保无噪声能恢复;第二步,频率选择性信道下看相邻子载波相关系数,确认近似成立;第三步,跑误码率曲线并拟合分集阶数,确认斜率接近理论值。这三步过了,再去调 CP 长度、子载波映射和信道估计参数。这个习惯帮我省了很多「曲线不对但不知道从哪查」的时间。希望帮到你。
本文还有配套的精品资源,点击获取