简介:面向无线通信初学者,这份MATLAB代码实现了空时分组码(STBC)的编码与译码全流程,并配套误码率(BER)曲线绘制功能。通过实际运行即可直观对比不同信噪比下的误码性能,适合用于课程设计、毕业设计或自学入门,帮助理解STBC如何利用空间分集提升传输可靠性。
资源包共包含3个m文件,整体大小仅1KB,结构非常精简。主要文件涵盖STBC编解码主程序、测试脚本以及绘图函数,用户无需复杂配置,直接运行测试脚本即可得到仿真结果与曲线,有效降低上手门槛。由于代码体积小、逻辑清晰,也便于逐行阅读和二次修改。
目前该资源已有2318人学习使用,反映出其在初学者群体中的实用价值。下载后可直接获得可运行的仿真脚本与误码率绘图代码,省去从零搭建的时间,可作为理解空时编码理论的辅助工具。
1. STBC 空时分组码:能直接跑出 BER 曲线的编码译码实现
STBC(空时分组码)的编码译码实现,是无线通信仿真里绕不开的一个坎。Alamouti 提出的 2×1 正交分组码,用两根发射天线和一根接收天线,在没有信道反馈的条件下拿到与最大比合并相同的 2 重分集增益,LTE 的发射分集模式至今还在用这套思想。这份资源把调制映射、STBC 编码、瑞利信道、最大似然译码、BER 误码统计全链路串了起来,跑完能直接得到 1×1 与 2×1 的 BER 对比曲线,分集增益一目了然。适合正在做课程设计、毕业论文仿真部分的同学,也适合想在 MATLAB 里快速验证发送分集思路的工程师。
2. Alamouti 编码:2×1 发送分集的数学原理与矩阵实现
2.1 正交编码矩阵与分集增益的来源
Alamouti 方案的核心是一个 2×2 的复正交编码矩阵。两个符号 s1、s2 在两根发射天线上跨两个时隙发送,映射关系可以用一张表说清楚:
| 时隙 1 | 时隙 2 |
|---|---|
| 天线 1 | s1 |
| 天线 2 | s2 |
矩阵的每一列是一个时隙,每一行是一根发射天线。conj 表示复共轭。这个矩阵乘上它自己的共轭转置,结果是对角阵,这就是「正交」的含义。正交性保证了两个符号在接收端的干扰项可以被线性合并干净地消掉,不需要像空间复用那样做联合最大似然搜索。
分集增益的来源其实很直观:s1 的能量被同时分配到了天线 1 的时隙 1 和天线 2 的时隙 2,也就是说同一个符号经历了两个独立的衰落路径。只要两根天线到接收天线之间的信道系数 h1、h2 不高度相关,两条路径同时深衰落的概率就比单天线低得多。量化来看,2 重分集在瑞利衰落下的误码率随 SNR 的下降斜率是单天线的两倍,在 BER 曲线上体现为更陡的瀑布区。
选 Alamouti 而不是更复杂的 STBC 方案,理由有三个。第一,译码复杂度极低,合并器是线性的,不需要迭代。第二,满分集增益——2 根发射天线能够提供的最大分集阶数就是 2,Alamouti 正好达到这个上限。第三,它不需要接收端反馈任何信道状态信息,发射端也不知道信道,这对下行链路是至关重要的特性。后面仿真里你会看到,2×1 与 1×1 的 BER 曲线差距在高 SNR 区域接近 10dB,这就是分集增益的直接体现。
2.2 编码器的 MATLAB 实现与维度关系
编码器的输入是一对复数符号,输出是一个 2×2 的复数矩阵。最直接的实现方式如下:
function tx = stbc_enc(s) % s: 1x2 复数符号对 [s1, s2] % tx: 2x2 编码矩阵 % tx(1,:) -> 天线1在两个时隙的发送信号 % tx(2,:) -> 天线2在两个时隙的发送信号 tx = [ s(1), -conj(s(2)); s(2), conj(s(1)) ]; end这段代码里最容易被忽略的是 conj。如果输入符号是 BPSK 实数,共轭等于原值,问题不大;换成 QPSK 或 16QAM 后符号是复数,漏掉 conj 会让译码端的干扰项无法抵消。tx 的行对应天线、列对应时隙,这个维度关系决定了后面信道矩阵的乘法方向。
实际工程里符号流是一长串,需要按 2 个一组切分。我一般先把调制后的符号序列 reshape 成 N×2 的矩阵,再逐行调用 stbc_enc:
% 假设 symbols 是 1xN 的复数符号向量,N 为偶数 pairs = reshape(symbols, [], 2); % 每行是一对符号 tx_seq = zeros(2, 2, size(pairs, 1)); for k = 1:size(pairs, 1) tx_seq(:, :, k) = stbc_enc(pairs(k, :)); end这里的三维数组 tx_seq 第一维是天线,第二维是时隙,第三维是符号对序号。后面的信道仿真会沿第三维循环,把每个编码块独立送入信道。用循环而不是向量化写法,性能差不了多少,但中间变量维度清楚,出问题容易定位。
2.3 调制映射对编码的影响:QPSK 与 16QAM
STBC 编码本身不关心符号怎么调制出来的,它只对符号做共轭和取负。调制阶数影响的不是编码器代码,而是两件事:Eb/N0 与 Es/N0 的换算,以及高 SNR 下对信道估计误差的敏感度。
QPSK 每个符号 2 比特,16QAM 每个符号 4 比特。在平均功率归一化为 1 的前提下,16QAM 的星座点间距更小,同样的噪声功率下判决裕量更小。做 STBC 验证时我建议先用 QPSK 把链路完整跑通,确认编码、信道、译码各环节无误,再切 16QAM 看性能退化。调制映射的功率归一化是仿真里常见的误差源:QPSK 星座点如果用 [1+1j, -1+1j, -1-1j, 1-1j] 而忘了除以 sqrt(2),发射功率就是 2,BER 曲线的横坐标会整体偏移 3dB。
调制星座点功率归一化的标准做法是构造星座后除以其 RMS 值。对 QPSK 是除 sqrt(2),对标准 16QAM 是除 sqrt(10)。这个归一化要和信道系数的归一化配合起来看,发射功率为 1、信道平均功率为 1,才能保证仿真里 SNR 的定义是自洽的。
3. 最大似然译码:从接收方程到合并器的 MATLAB 代码
3.1 接收信号的数学模型
两个时隙的接收信号,在平坦瑞利衰落信道下可以写成:
时隙 1: r1 = h1 * s1 + h2 * s2 + n1 时隙 2: r2 = -h1 * conj(s2) + h2 * conj(s1) + n2h1、h2 分别是两根发射天线到接收天线的信道系数,n1、n2 是复高斯白噪声。这里默认信道在连续两个时隙内保持不变,也就是准静态假设。如果信道变化太快,Alamouti 的正交性被破坏,BER 会出现地板效应,这一点后面避坑章节专门说。
把 r2 取共轭,让两个方程里的符号都变成 s1、s2 而不是它们的共轭:
r2* = -conj(h1) * s2 + conj(h2) * s1 + conj(n2)现在 r1 和 r2* 联立,s1 出现在两个方程里,s2 也出现在两个方程里。线性合并的思路是给每个方程配一个权重,让目标符号的系数叠加增强,同时让干扰符号的系数正负相消。最大似然译码在加性高斯白噪声下退化为最小距离判决,但由于 Alamouti 的正交性,判决前可以先做线性合并,把二维联合搜索变成两个独立的一维判决,复杂度大幅降低。
3.2 合并器推导与完整译码实现
估计 s1 时,给 r1 乘 conj(h1),给 r2* 乘 h2。看 s2 的系数:前一项是 conj(h1)h2,后一项是 h2(-conj(h1)),恰好抵消。估计 s2 时,给 r1 乘 conj(h2),给 r2* 乘 -h1,s1 的系数同样抵消。这就是合并器的全部秘密。
function s_hat = stbc_dec(r, h) % r: 1x2 接收向量 [r1, r2] % h: 1x2 信道向量 [h1, h2] % 返回合并后的符号估计 r2c = conj(r(2)); s1 = conj(h(1)) * r(1) + h(2) * r2c; s2 = conj(h(2)) * r(1) - h(1) * r2c; s_hat = [s1, s2] / (abs(h(1))^2 + abs(h(2))^2); end这里有三个容易写错的地方,逐一说明。第一,s1 的第二个权重是 h(2) 而不是 conj(h(2)),这是因为 r2* 里面 s1 的系数已经是 conj(h2) 了,再乘 h2 才能得到 |h2|^2。第二,s2 的第二个权重是 -h(1),这个负号不能丢,丢了 s1 的干扰项就消不掉。第三,最后的除法是归一化,做完之后 s_hat 等效于 s + 等效噪声,后续判决可以直接查星座表。如果只做硬判决,这个归一化因子可以省略,但输出幅度会随信道增益变化,不利于统一阈值判决。
从最大似然的角度看,这个合并器是严格最优的。因为 Alamouti 矩阵正交,两个符号的联合最大似然估计可以分解成两个独立的标量最大似然估计,而上面这个线性合并恰好等价于各自的最大似然判决统计量。这也是为什么我在前面强调正交性——没有正交性,线性合并一定不是最优的。
3.3 解映射与硬判决实现
合并器输出的是带噪声的复数估计,下一步要根据调制星座做最小距离判决。判决规则很简单:计算 s_hat 到每个星座点的欧氏距离,取最近的那个。
function bits_hat = qpsk_demod(s_hat) % s_hat: 合并后的符号估计(复数) % 返回 QPSK 解调比特 const = [ 1+1j, -1+1j, -1-1j, 1-1j ] / sqrt(2); bits_map = [0 0; 1 0; 1 1; 0 1]; % Gray 映射 bits_hat = zeros(1, 2*length(s_hat)); for k = 1:length(s_hat) [~, idx] = min(abs(s_hat(k) - const)); bits_hat(2*k-1 : 2*k) = bits_map(idx, :); end endGray 映射的目的是让相邻星座点只有 1 比特差异,这样单个符号判决错误时,平均只错 1 个比特而不是 2 个。如果不做 Gray 映射,BER 仿真结果会比理论值明显差一截,这不是 STBC 的问题,是比特映射的问题。
s_hat 是复数,abs(s_hat - const) 计算的是复平面上的欧氏距离。MATLAB 的 min 函数返回最小值下标,idx 再映射到比特。这一段逻辑简单,但它是整个链路里唯一决定「误码」的地方,值得单独测试:输入一个干净的星座点,确认输出比特和调制端一致。
3.4 信道估计误差的影响
译码器假设接收端完美已知 h1、h2,但真实系统里信道是估出来的,估计值和真实值之间有偏差。这个偏差对 Alamouti 的影响是:正交性仍然成立,但合并器的信噪比下降。具体来说,估计误差相当于给合并输出增加了一个额外的干扰项,这个干扰项的功率与信道估计误差功率成正比。在高 SNR 区域,当热噪声已经很低时,信道估计误差成为主导,BER 曲线出现地板效应。
仿真时如果要模拟非理想信道估计,常见做法是给真实信道系数加一个高斯扰动:h_est = h + sigma_e * (randn + 1j*randn)/sqrt(2),然后译码器用 h_est 做合并。sigma_e 取 0.01 到 0.1 之间就能看到明显的地板效应。这个扰动模型虽然粗糙,但能帮你理解信道估计误差对 STBC 性能边界的限制。
4. BER 性能仿真:Eb/N0 设置与蒙特卡洛参数怎么定
4.1 完整仿真链路搭建
把前面所有模块串起来,就是完整的 BER 仿真主循环。我习惯把调制、STBC 编码、信道加噪、译码、解调全部放在一个脚本里,每个 SNR 点独立跑蒙特卡洛。
%% STBC 2x1 BER 主仿真脚本 clear; clc; M = 4; % QPSK numBits = 1e6; % 总仿真比特数 EbN0_dB = 0:2:20; % Eb/N0 扫描范围 ber_2x1 = zeros(size(EbN0_dB)); ber_1x1 = zeros(size(EbN0_dB)); for idx = 1:length(EbN0_dB) EbN0 = 10^(EbN0_dB(idx)/10); N0 = 1 / EbN0; % 归一化噪声功率谱密度 err2 = 0; err1 = 0; bits = randi([0 1], 1, numBits); sym = qpsk_mod(bits); % 符号功率归一化为 1 pairs = reshape(sym, [], 2); % 每行一个符号对 for k = 1:size(pairs, 1) % --- 2x1 STBC --- tx = stbc_enc(pairs(k, :)); h = (randn(1,2) + 1j*randn(1,2)) / sqrt(2); noise = sqrt(N0/2) * (randn(1,2) + 1j*randn(1,2)); r = h * tx + noise; % 1x2 接收向量 s_hat = stbc_dec(r, h); b_hat = qpsk_demod(s_hat); err2 = err2 + sum(b_hat ~= bits(2*k-1 : 2*k)); % --- 1x1 单天线参考 --- h1 = (randn + 1j*randn) / sqrt(2); noise1 = sqrt(N0/2) * (randn + 1j*randn); r1 = h1 * pairs(k,1) + noise1; b_hat1 = qpsk_demod(r1 / h1); err1 = err1 + sum(b_hat1 ~= bits(2*k-1 : 2*k)); end ber_2x1(idx) = err2 / numBits; ber_1x1(idx) = err1 / numBits; end这段代码里最关键的一行是r = h * tx + noise。h 是 1×2 行向量,tx 是 2×2 矩阵,矩阵乘法 h*tx 完成两件事:把天线 1 的信号乘以 h1、天线 2 的信号乘以 h2,然后分别在两个时隙求和,结果正好是 1×2 的接收向量。如果写成tx * h'维度就错了,结果是一个 2×2 矩阵而不是 1×2 向量。
1×1 参考链路里r1 / h1就是迫零均衡,把信道效应除掉再判决。这个参考曲线的意义是展示没有分集时的基线性能,和 2×1 曲线对比才能量化 STBC 的分集增益。
4.2 信噪比换算与噪声功率设置
仿真里最容易翻车的地方是噪声功率。我这里用的归一化约定是:信号平均功率为 1,QPSK 每符号 2 比特,所以 Es/N0 = Eb/N0 + 3dB。复高斯噪声实部和虚部各占 N0/2 的功率,因此:
noise = sqrt(N0/2) * (randn(1,2) + 1j*randn(1,2));sqrt(N0/2) 是每个维度的标准差,不是 N0。如果把 N0 当标准差用,实际噪声功率变成 2*N0,等效于把 SNR 提高了 3dB,BER 曲线会「意外地好」。这种自我欺骗在仿真里很常见,我见过不止一次有人拿错误的噪声模型跑出比理论值还好的 BER,还以为是分集增益的功劳。
验证噪声功率设置是否正确的办法:固定信道 h=[1, 0],只发一个符号 s=1,接收 r = s + noise,然后统计 noise 的实测功率是否等于 N0。MATLAB 里mean(abs(noise).^2)应该约等于 N0。这个检查一分钟就能完成,建议每次搭新仿真环境都跑一遍。
4.3 仿真参数表与 BER 曲线解读
| 参数 | 取值 | 说明 |
|---|---|---|
| 调制方式 | QPSK | 每符号 2 比特,功率归一化 |
| 信道模型 | 平坦瑞利衰落 | 每个编码块重新生成 h |
| 信道归一化 | E[ | h |
| 噪声模型 | 复高斯白噪声 | 实部虚部各 N0/2 |
| Eb/N0 范围 | 0~20 dB | 步进 2dB,覆盖水衰区和瀑布区 |
| 每点仿真比特 | 1e6 | 保证 BER 统计到 1e-4 量级 |
仿真跑完后,1×1 和 2×1 两条曲线对比,在 BER = 1e-3 处两者大约相差 9~10dB。这个差值就是 2 重分集增益的量化表现。如果差值明显小于这个数,优先检查:信道是否归一化、符号功率是否归一化、噪声功率是否多乘了 sqrt(2)。这三个问题造成的误差往往互相掩盖,需要逐个排除。
高 SNR 区域 BER 下降斜率也是重要的判断依据。单天线无分集时 BER 下降斜率是每 10dB 大约降一个数量级,2 重分集时每 10dB 降两个数量级,也就是斜率翻倍。如果仿真曲线斜率没有翻倍,大概率是编码或译码里某个共轭符号写错了,分集增益没有真正实现。
5. STBC 编码译码避坑:五个最容易翻车的环节
5.1 共轭符号写错导致 BER 卡在 0.5
现象:BER 曲线在 0.5 附近不动,或者随 SNR 升高反而恶化。
原因:编码器里-conj(s(2))写成了-s(2),或者译码合并时漏乘了 conj。QPSK 符号是复数,共轭改变虚部符号,漏掉之后合并器里干扰项消不掉,正交性直接失效。这个问题在 BPSK 下测不出来,因为实数的共轭等于自身。
解决:先跑无噪声闭环验证,固定一组 h、s,手算 r1、r2,再走译码对比。这是我定位共轭问题最快的方法,比对着 BER 曲线猜快得多。仿真环境里加一个断言:无噪声时所有符号判决必须完全正确。
5.2 噪声功率把 N0 当成了标准差
现象:仿真 BER 比理论值好 1~3dB,高 SNR 区域尤其明显。
原因:复高斯噪声的实部和虚部各服从 N(0, N0/2),总功率是 N0。代码里如果写成sqrt(N0) * (randn + 1j*randn),实际噪声功率是 2*N0,相当于 SNR 被人为抬高 3dB。
解决:统一用sqrt(N0/2) * (randn + 1j*randn)。加一个自检脚本,统计纯噪声序列的mean(abs(noise).^2),确认等于 N0。我每次新建仿真工程都保留这个自检,防止改参数时改坏。
5.3 信道归一化不一致导致分集增益虚高
现象:2×1 与 1×1 的 BER 差距超过 12dB,甚至在高 SNR 区域出现异常陡峭的下降。
原因:信号功率归一化为 1,但信道系数 h 没有归一化。如果h = randn + 1j*randn忘了除 sqrt(2),E[|h|^2] = 2,等效于放大了信号功率 3dB。合并器输出 SNR 虚高,看起来分集增益「超额」。
解决:瑞利信道固定用(randn + 1j*randn) / sqrt(2),保证每个信道系数平均功率为 1。同时检查调制星座点是否做了功率归一化,两个归一化必须同时满足,SNR 定义才自洽。
5.4 准静态信道假设被打破
现象:BER 低 SNR 区域正常,中高 SNR 出现地板效应,曲线下不去。
原因:Alamouti 要求信道在连续两个时隙内近似不变。如果仿真里给 h 加了时变特性(比如多普勒频移),或者每个符号都重新生成 h 但实际相干时间小于两个符号周期,正交性被破坏。时变信道下 s1、s2 的干扰项无法完全抵消,形成残余干扰。
解决:仿真阶段坚持每个编码块内 h 不变。要模拟真实时变信道,需要引入带时间相关性的信道模型(如 Jakes 模型),并且接受 STBC 性能会有上限。做课程设计时先固定 h,不要一上来就加多普勒,否则分不清性能变差是编码问题还是信道模型问题。
5.5 高 SNR 误码统计量不足
现象:BER 曲线在 1e-5 以下剧烈抖动,相邻 SNR 点跳变超过一个数量级。
原因:蒙特卡洛仿真在低误码率时需要的比特数急剧增加。每点只跑 1e5 比特,BER 到 1e-4 时误码个数只有十几个,统计方差极大。
解决:按目标误码率设定比特数,评估 1e-5 至少需要 1e7 比特。更好的做法是按误码个数动态停止:累计误码达到 100 个就结束当前 SNR 点。这样低 SNR 点跑得快,高 SNR 点自动增加样本量,总体时间能省三分之一。
6. 从 2×1 到 2×2:STBC 扩展实现与端到端验证
6.1 接收端增加天线的合并拓展
2×2 场景下,两根接收天线各自独立经历衰落,每根天线都收到两个时隙的信号。译码时对每根接收天线做一次独立的 2×1 Alamouti 合并,再把两个估计结果相加。等效分集阶数从 2 变成 4,BER 瀑布区斜率进一步加大。
function s_hat = stbc_dec_2x2(r1, r2, h) % r1, r2: 两根接收天线的 1x2 接收向量 % h: 2x2 信道矩阵, h(i,j) 是第 j 根发射天线到第 i 根接收天线的系数 s1_hat = stbc_dec(r1, h(1,:)); % 接收天线 1 的合并 s2_hat = stbc_dec(r2, h(2,:)); % 接收天线 2 的合并 s_hat = (s1_hat + s2_hat) / 2; % 平均合并,保持归一化一致 end这个实现本质是两根接收天线的最大比合并。注意 stbc_dec 内部已经做了/(abs(h1)^2+abs(h2)^2)的归一化,且分母在两根天线上是不同的。把两个输出直接相加再除 2,等效于每根天线合并后取平均,噪声功率减半、信号功率不变,所以分集增益来自两路独立衰落的合并。归一化系数这块很容易算乱,我的建议是:先不除 2,对比 2×2 与 2×1 的 BER 相对位置,再微调归一化。
6.2 无噪声端到端验证脚本
扩展实现最怕的是归一化和天线映射错位。我的习惯是先跑一次严格的无噪声闭环验证:随机生成大量符号对,走完整链路,判决结果必须与输入逐比特一致。
%% 2x2 无噪声闭环验证 M = 4; err_count = 0; for k = 1:1000 bits = randi([0 1], 1, 4); sym = qpsk_mod(bits); tx = stbc_enc(sym); h = (randn(2,2) + 1j*randn(2,2)) / sqrt(2); r1 = h(1,:) * tx; % 无噪声接收 r2 = h(2,:) * tx; s_hat = stbc_dec_2x2(r1, r2, h); bits_hat = qpsk_demod(s_hat); err_count = err_count + sum(bits_hat ~= bits); end fprintf('无噪声误码总数: %d\n', err_count);这里用 1000 个符号对做验证,任何归一化误差或符号方向错误都会产生大量误码。这个脚本的价值在于把「编码-信道-译码-解调」整个链路锁定在一个确定性状态,之后任何修改只要回归跑一遍这个验证,就能立刻发现是否破坏了原有逻辑。
6.3 扩展验证:BER 斜率翻倍检验
2×2 的 BER 曲线相比 2×1 应该在瀑布区有更陡的斜率。具体到数值,在 BER = 1e-3 处 2×2 通常比 2×1 再优 5~7dB。如果只优了 2~3dB,问题大概率出在两根接收天线的信道相关性上——仿真里如果 h(1,:) 和 h(2,:) 用了相同的随机种子,两路衰落完全相关,分集增益就退化了,等效于只有一路独立信号。
我用这个判断来验证 2×2 扩展是否正确:分别跑 2×1 和 2×2 的曲线,对比同一 BER 处的 SNR 差值。理论差值在 5~7dB 范围内说明实现正确,明显偏小就检查信道生成有没有混用随机流。从此我每次改接收天线数,都会先跑无噪声闭环,再对比相邻配置的 BER 差值,这两步能挡住大多数低级的实现错误。希望帮到你。
本文还有配套的精品资源,点击获取