Alamouti 2x2 MIMO仿真指南:STBC空时分组码原理与MATLAB实现 简介一套围绕Alamouti空时编码的MATLAB实现代码包面向无线通信方向的研究者、工程师与高年级学生可解决2x2与2x1 MIMO系统中空间分集编码的学习、复现与仿真验证问题。Alamouti编码是1998年提出的两时隙空间分集方案两个发射天线在两个连续时隙内发送成对符号接收端通过特定编码矩阵进行线性译码从而无需增加带宽和发射功率即可获得全空间分集接收天线数量的不同使得2x1与2x2系统在分集增益和复杂度上各有取舍。对于2x1配置代码中分别引入了预编码和天线选择策略以增强链路可靠性对于2x2配置则利用两个接收天线进一步提升频谱效率和抗多径能力。压缩包内共6个m脚本大小约6KB覆盖基础Alamouti发射分集、2x2联合解码、2x1预编码与天线选择等模块结构清晰适合直接运行、修改和对照理论排错。目前已有313人学习该资源尤其适用于课程设计、科研预研及算法对比使用过程中还应注意信道状态信息的获取与反馈准确的信道状态信息是正确执行Alamouti译码的前提借助仿真可直观对比不同收发配置下的分集增益和误码性能为后续学习其他空时编码及大规模MIMO技术打下基础。1. Alamouti 2x2 MIMO 仿真为什么经典空时分组码至今仍是分集方案的必修课如果你刚拿到一份名为 MIMO Alamouti.rar 的仿真资源或者准备自己复现 2x2 的 Alamouti 编码第一件事不是急着跑代码而是想清楚一个问题这个东西到底在解决什么。无线信道里最头疼的是多径衰落信号可能在某根天线上深衰落十几 dB直接导致误码率飙升。Alamouti 编码是最简单的空时分组码STBC发射端用 2 根天线、2 个时隙发送 2 个符号接收端用线性合并就能获得满分集增益不需要迭代检测也不需要反馈信道信息。它至今仍是验证分集方案的首选仿真对象因为结构简单、结果可预测、理论曲线现成。这篇文章会从编码矩阵讲到 MATLAB 复现再给出参数设置和最容易踩的坑让 2x2 MIMO 仿真对你来说不再是黑匣子而是能亲手调通、画出一条可信误码率曲线的实战技能。2. Alamouti 编码原理与系统模型2x1 和 2x2 共用的三句核心公式2.1 编码矩阵与正交条件Alamouti 的全部秘密Alamouti 编码的核心约束是发射天线数固定为 2。一个码块由 2 个时隙和 2 根发射天线组成承载 2 个数据符号。假设当前码块要发送 x1 和 x2映射关系如下表时隙天线 1天线 2时隙 1x1x2时隙 2-conj(x2)conj(x1)表格里每一列代表一根天线上的发射序列每一行代表一个时隙。天线 1 在两个时隙上发 x1 和 -conj(x2)天线 2 发 x2 和 conj(x1)。这个设计的巧妙之处在于两列的序列天然正交把第一列的共轭与第二列对应相乘再求和得到 x1 与 conj(x2) 的乘积加上 -conj(x2) 与 x1 的乘积两项恰好抵消。这个正交性保证了接收端通过简单的共轭加权合并就能把 x1 和 x2 分离成两个互不干扰的单符号判决问题不需要做任何穷举搜索。用公式看更直接。把两个时隙两个天线的发射写成矩阵 X行是时隙、列是天线那么 X 的第一列与第二列满足正交条件。接收端只需要对接收信号做共轭、加权、相加三步就能把两路符号彻底分开。这也是 Alamouti 编码和后来更复杂的 STBC 最大的区别译码复杂度是 O(1) 而不是 O(M)M 是星座大小。换句话说即使星座从 QPSK 换成 256QAM译码端的复杂度也不变这对工程仿真是很大的优势。2.2 2x1 与 2x2 的接收信号形态差异2x1 系统只有一根接收天线但接收端仍然要在 2 个时隙上分别采样。设发射天线 1 到接收端的信道为 h1发射天线 2 到接收端的信道为 h2两个时隙的接收信号可以写成时隙 1r1 h1x1 h2x2 n1时隙 2r2 -h1conj(x2) h2conj(x1) n2注意 r2 里来自 x2 的部分是 -h1conj(x2)而不是 h1conj(x2)这个负号由发射端编码矩阵第二时隙天线 1 的 -conj(x2) 决定。它是后面译码时分离两路符号的关键也是写代码时最容易翻车的地方。很多人把负号丢掉或者把 conj 放错位置结果误码率曲线怎么调都对不上理论值。2x2 系统有 2 根接收天线信道从 2 个复数变成 2x2 的复数矩阵 H。每根接收天线独立接收 2 个时隙的信号所以接收数据是一个 2x2 的矩阵行是时隙、列是接收天线。发射端仍然是 2x2 的矩阵时隙乘发射天线中间用 2x2 的信道矩阵相乘。这也是新手写 2x2 仿真时最容易绕晕的地方发射端矩阵、信道矩阵、接收端矩阵三个二维结构叠在一起维度写错一个后面全部对不上。2.3 接收端译码为什么这样合并就能拿到满分集先看 2x1。接收端用最大比合并的思路构造两个判决量。对 x1 的估计y1 conj(h1)r1 h2conj(r2)把 r1、r2 代进去展开交叉项完全消掉只剩 (|h1|^2 |h2|^2)*x1 加上噪声项。对 x2 的估计用y2 conj(h2)r1 - h1conj(r2)展开后同样只剩 (|h1|^2 |h2|^2)*x2。两个估计量都带上了两个信道模平方之和这个系数就是分集增益的来源。因为 h1、h2 同时深度衰落的概率远小于单个信道深度衰落的概率所以误码率曲线的斜率变陡这就是二阶分集。2x2 时每一根接收天线都独立做上述合并得到各自天线的 y1、y2然后再把两根天线的 y1 相加、y2 相加等价于把四组信道系数的模平方全部加进来。最终判决量的增益是 |h11|^2 |h21|^2 |h12|^2 |h22|^2也就是四阶分集。后面第 5 章会讲到很多人仿真结果里 2x2 的曲线斜率只有 2 而不是 4问题多半出在合并时少加了一根天线的支路或者把 conj 的取法写反了。还有一个常见的疑问为什么接收端用 conj(h) 加权而不是直接除以 h因为直接均衡会放大噪声而 MRC 的加权方式让信号在各支路同相相加、噪声按功率叠加输出信噪比最大。Alamouti 的正交性保证了两个符号在合并后互不干扰所以这个线性加权就是最大似然译码。写代码时可以验证把 y1 除以 (|h1|^2|h2|^2) 之后做星座判决得到的误码率应该和理论上的 2 支路 MRC 曲线完全重合。3. 用 MATLAB 复现 Alamouti 2x2发射端编码、瑞利信道与接收端译码的完整脚本3.1 发射端从 QPSK 比特流到 Alamouti 编码矩阵我习惯把仿真拆成三段发射端负责比特到天线的映射信道负责加衰落和噪声接收端负责合并、判决和统计。这样每一段都能单独验证出问题时不用把整个脚本从头翻到尾。发射端的第一个关键是 QPSK 调制要归一化第二个关键是编码矩阵要按 2 个符号一组把 x1、x2、-conj(x2)、conj(x1) 四个序列分别准备好。% 参数区 N 1e6; % 每个信噪比点发送的 QPSK 符号数 M 4; k log2(M); % QPSK每符号 2 比特 EbN0_dB 0:2:20; % 比特信噪比扫描范围 Nt 2; % 发射天线数Alamouti 固定为 2 Nr 2; % 接收天线数改成 1 就是 2x1 配置 block_len 200; % 每个准静态信道块内的符号数必须为偶数 num_blocks N / block_len; % 信道块总数 % 随机比特流与 QPSK 调制Gray 映射 bits randi([0 1], 1, N*k); symbols (1 - 2*bits(1:2:end)) 1j*(1 - 2*bits(2:2:end)); symbols symbols / sqrt(2); % 星座点归一化平均功率为 1 bits_mat reshape(bits, 2, N); % 第 i 列是第 i 个符号的两个比特 % Alamouti 编码每 2 个符号为 1 组 S reshape(symbols, 2, N/2); % S(1,:)x1S(2,:)x2 ant1_t1 S(1,:) / sqrt(2); % 时隙1 天线1 发 x1 ant2_t1 S(2,:) / sqrt(2); % 时隙1 天线2 发 x2 ant1_t2 -conj(S(2,:)) / sqrt(2); % 时隙2 天线1 发 -conj(x2) ant2_t2 conj(S(1,:)) / sqrt(2); % 时隙2 天线2 发 conj(x1)这里有两个归一化容易搞混。第一个是 symbols / sqrt(2)因为原始的 (±1 ± j) 星座点平均功率是 2除以 sqrt(2) 后平均功率变成 1这样后面 Es 的定义才干净。第二个是每根发射天线再除以 sqrt(2)这是 MIMO 总发射功率约束2 根天线共享总功率每根天线分一半。如果漏掉这一步仿真结果会比理论曲线好 3 dB因为实际发射总功率变成了 2而噪声没变。这个坑在 5.2 节还会专门说。bits_mat 是为后面统计误比特准备的。QPSK 的 Gray 映射规则是第一个比特决定实部符号第二个比特决定虚部符号所以第 i 个符号对应 bits_mat 的第 i 列。后面译码出 x1、x2 后要和原始比特逐位比对没有这个索引矩阵统计阶段会很痛苦。3.2 信道模拟准静态瑞利衰落与噪声功率换算信道仿真选准静态块衰落模型一个信道块内 H 保持不变块与块之间独立重新生成。这个模型对应慢衰落场景也是 Alamouti 论文里的经典假设。注意一个前提Alamouti 编码跨 2 个时隙所以至少在一个码块2 个时隙内信道必须恒定否则正交性会被破坏。block_len 取 200 意味着 200 个符号共用一个 H这是为了减少循环次数不影响分集阶数的统计结果。% 信噪比换算与主循环 BER zeros(size(EbN0_dB)); for sidx 1:length(EbN0_dB) EsN0_dB EbN0_dB(sidx) 10*log10(k); % 符号信噪比 比特信噪比 3 dB N0 10^(-EsN0_dB/10); % 单边噪声功率谱密度 noise_std sqrt(N0/2); % 复噪声实部/虚部各自的标准差 bit_err 0; for blk 1:num_blocks sym_idx (blk-1)*block_len (1:block_len); % 准静态瑞利信道块内恒定块间独立CN(0,1) H (randn(Nr, Nt) 1j*randn(Nr, Nt)) / sqrt(2); % 本块内的 Alamouti 发射矩阵列数 block_len/2 Sb reshape(symbols(sym_idx), 2, block_len/2); Tx1 [Sb(1,:); Sb(2,:)] / sqrt(2); % 时隙1天线1 x1天线2 x2 Tx2 [-conj(Sb(2,:)); conj(Sb(1,:))] / sqrt(2); % 时隙2 % 接收Nr 行 x block_len/2 列 R1 H * Tx1 noise_std * (randn(Nr, block_len/2) 1j*randn(Nr, block_len/2)); R2 H * Tx2 noise_std * (randn(Nr, block_len/2) 1j*randn(Nr, block_len/2)); % 译码与误码统计见 3.3 节 end end噪声功率换算是个高频出错点。仿真里输入变量是 Eb/N0但信道里加噪声需要用符号信噪比 Es/N0。QPSK 每符号 2 比特所以 EsN0_dB EbN0_dB 3 dB。16QAM 时这个偏移是 6 dB很多人换调制没改这里曲线整体平移好几个 dB。N0 是单边功率谱密度复噪声的实部和虚部各占一半所以每维标准差是 sqrt(N0/2)。信道矩阵 H 除以 sqrt(2) 是为了让每个元素方差为 1这样 MRC 合并后的增益期望值才好和理论曲线对齐。3.3 接收端译码与误码统计MRC 合并的完整代码译码器要做的就是 2.3 节公式的向量化实现。对每个接收天线先算本天线的 y1、y2再把所有接收天线的 y1、y2 分别相加。这里用 sum(..., 1) 对列求和实际上就是把 2 根接收天线的支路合并了。代码里最容易看错的是第二个等号前面的符号Y1 是两项相加Y2 是两项相减。% ---- 本块 MRC 译码 ---- Y1 sum(conj(H(:,1)) .* R1, 1) sum(H(:,2) .* conj(R2), 1); Y2 sum(conj(H(:,2)) .* R1, 1) - sum(H(:,1) .* conj(R2), 1); g sum(abs(H(:)).^2); % 四链路合并后的分集增益 x1_hat Y1 / g * sqrt(2); % 恢复原始星座幅度 x2_hat Y2 / g * sqrt(2); % QPSK 硬判决实部/虚部大于 0 判 0否则判 1 est1 [real(x1_hat) 0; imag(x1_hat) 0]; % x1 的两个比特 est2 [real(x2_hat) 0; imag(x2_hat) 0]; % x2 的两个比特 % 与参考比特比对 ref reshape(bits_mat(:, sym_idx), 2, block_len); bit_err bit_err sum(est1 ~ ref(:, 1:2:end)) ... sum(est2 ~ ref(:, 2:2:end)); end BER(sidx) bit_err / (N * k); end % 画图 figure; semilogy(EbN0_dB, BER, o-); grid on; xlabel(Eb/N0 (dB)); ylabel(BER);对照 2.3 节的公式看conj(H(:,1)) .* R1 实现的是 conj(h1)r1H(:,2) .conj(R2) 实现的是 h2*conj(r2)。这里 H(:,1) 是所有接收天线来自发射天线 1 的信道系数不是某一个系数。很多 2x2 仿真只取了 H(1,1) 和 H(1,2)相当于丢弃了第二根接收天线的信息结果误码率曲线永远差一截。g 是四组信道模平方之和对 2x1 配置它自然退化成两组之和同一个脚本改 Nr 就能跑两种配置。x1_hat 乘 sqrt(2) 是为了恢复原始星座幅度。因为发射端每根天线都除了 sqrt(2)如果不乘回来估计值会整体缩小虽然硬判决阈值 0 不受影响但如果你想在判决前计算软信息或者星座误差图幅度恢复就是必须的。误码统计时est1 的两个行向量分别对应 x1 的实部比特和虚部比特ref(:, 1:2:end) 取出原始符号序列中第 1、3、5... 个符号的比特正好对应每个 Alamouti 组的 x1ref(:, 2:2:end) 对应 x2。索引错位是最隐蔽的 bug会让 BER 停在 0.5 附近且不随 SNR 变化排查时优先怀疑这里。4. 仿真参数怎么调调制阶数、天线配置与帧数的三个关键选择4.1 调制阶数影响误码率曲线形态QPSK 与 16QAM 的取舍把第 3 章的 M 从 4 改成 16k 从 2 改成 4表面看起来只改了两个数字实际上整个发射端和判决端都要跟着改。16QAM 的星座点是 16 个复数不能再用实部虚部正负号直接判 0 和 1必须做最小欧氏距离判决。更关键的是功率归一化16QAM 星座点的平均功率不是 1如果你直接映射到标准的 ±1、±3 星座网格平均功率是 10发射端必须除以 sqrt(10) 做归一化否则符号信噪比的定义全乱了。调制阶数对仿真结果的直接影响是同一 Eb/N0 下 BER 变差。QPSK 在 BER1e-4 时大约需要 12 dB 左右2x2 配置16QAM 同样 BER 大约需要 20 dB 左右多出来的 8 dB 是高频谱效率的代价。如果你做的是算法验证建议先跑 QPSK因为判决简单、曲线和理论容易对照等确认编码和译码逻辑无误后再换 16QAM。换的时候注意 EsN0_dB EbN0_dB 10*log10(k) 里的 k 变成 4符号信噪比偏移变成 6 dB这是最容易被忽略的一处。4.2 2x1 与 2x2 的增益对比同一份代码里看到分集阶数的变化用第 3 章的脚本跑 2x1 只需要把 Nr 改成 1。这个对比实验几乎是必做的因为它能直观验证分集阶数的概念画在同一张图上2x1 的 BER 曲线在 BER1e-3 附近比 2x2 大约高 4 到 6 dB而且两条曲线的斜率明显不同。配置分集阶数阵列增益高 SNR 下 BER 曲线斜率1x1 QPSK1无-12x1 Alamouti2无-22x2 Alamouti4约 3 dB-4表格里的斜率指 log-log 坐标下 BER 随 SNR 的下降指数。1x1 的曲线每增加 10 dB 误码率下降一个量级2x1 下降两个量级2x2 下降四个量级。仿真结果如果斜率不对基本可以断定编码或译码有逻辑错误。阵列增益来自接收端多天线的信号合并2x2 比 2x1 多了两根接收支路的能量收集所以整体曲线还会向左平移约 3 dB。我在实际仿真中会用一组固定的随机种子同时跑这两种配置确保两者用完全相同的信道实现和噪声实现。这样对比是干净的曲线之间的差异纯粹来自天线数和分集阶数而不是随机抖动。第 4.3 节会讲抖动怎么控制。4.3 仿真帧数与随机种子误码率曲线抖动的根源与解决办法误码率仿真本质是蒙特卡洛统计BER 曲线上的每个点都是一个随机估计值。想让一个点落在 1e-4 附近至少需要统计到 100 个错误比特否则点与点之间抖动非常大曲线看起来像锯齿。按错误事件数反推符号数BER 目标 1e-4需要约 1e6 个比特目标 1e-5需要 1e7 个比特。N1e6 只够稳定跑到 1e-5 的水平再往下就需要加大 N否则会出现 0 错误事件semilogy 把 0 点画成缺口。随机种子是曲线可复现的关键。MATLAB 里在脚本开头写 rng(2024)每次运行结果完全一致这在调参和对比实验时非常重要。我曾经踩过这个坑同一份代码两次运行画出的曲线不一样排查了半天以为信道模型写错了最后发现只是忘了固定种子。做学术插图或实验对比时固定种子是基本素养否则审稿人复现不了你的图。块长度 block_len 不会改变平均性能只要块间信道独立200 个符号一块和 2 个符号一块的统计结果在期望上一致。它只影响程序运行速度。但要注意如果你想模拟快衰落场景每 2 个符号更新一次 H 是上限因为 Alamouti 编码本身就假设一个码块内信道恒定。这个边界在 6.3 节还会提。5. Alamouti 仿真最常翻车的 4 个坑现象、原因与排查方法5.1 复共轭方向写反正交性被破坏现象2x1 的 BER 曲线斜率接近 -1 而不是 -2或者低 SNR 区域曲线比 1x1 还差高 SNR 区域无法收敛到理论值。原因发射端编码矩阵里 -conj(x2) 写成了 conj(-x2) 或者干脆丢了负号接收端 Y1、Y2 的共轭组合方向反了导致两路符号的交叉项消不掉x1 的估计里残留 x2 的干扰反之亦然。这是 Alamouti 仿真里最经典的翻车现场。解决把 2.1 节的表格贴到代码边上逐项核对。更有效的自查方法是设 H 为单位阵、关掉噪声输入已知的 x1、x2单步跑通编码和译码打印 Y1、Y2 看是不是等于 (|h1|^2|h2|^2)*x1 和 (|h2|^2|h1|^2)*x2。如果交叉项没消掉你会看到 Y1 里混着 x2 的分量这时候 90% 的概率是某个 conj 的位置放错了。5.2 功率归一化漏掉SNR 定义对不上现象BER 曲线形状完全正常斜率也对但整条曲线比理论值偏左或者偏右偏移量恰好是 3 dB 或 6 dB 这种规整数字。原因三个归一化至少有一个漏了QPSK 星座平均功率没归一化到 1发射端每根天线没除以 sqrt(Nt)符号信噪比和比特信噪比换算时漏掉了 10*log10(k)。我做 16QAM 时漏过第二个曲线整体好了 3 dB一开始还以为算法有增益后来发现是发射功率超了。解决在代码里显式定义 Es 1然后检查三处symbols 的平均功率是否为 1发射端每个天线的序列是否除了 sqrt(2)N0 10^(-EsN0_dB/10)。可以把这三处各加一行注释注释里写明当前功率关系。再做一个极端测试把噪声设为 0如果 BER 不是 0说明功率或者判决逻辑有问题。5.3 信道矩阵维度写错2x2 系统却按 2x1 的方式衰落了现象Nr2 时跑出来的曲线和 Nr1 几乎重合或者只好了不到 1 dB分集阶数看起来还是 2。原因信道矩阵写成了 1x2单接收天线或 2x1把发射天线和接收天线搞反了或者接收端合并时只用了 H(1,:) 而丢掉了 H(2,:)。维度错误不会报错因为 MATLAB 的矩阵乘法对 1x2 和 2x2 都能算结果只是少了一个天线维度的信息。解决在信道生成和译码两处各放一个断点检查 size(H) 是否等于 [Nr Nt]size(R1) 是否等于 [Nr block_len/2]。另一个快速检查是看 g sum(abs(H(:)).^2)2x2 配置下 g 应该是 4 个随机数的平方和期望值约 4如果 g 的期望只有 2说明 H 只包含了两条链路后面合并支路数一定不对。5.4 高 SNR 下 BER 掉不下去错误事件数不足与索引错位现象曲线在 20 dB 以上突然变成一条水平线不再下降或者出现莫名的 0.5、0.25 这类固定错误率平台。原因最常见的是错误事件数不足N1e6 时BER 真实值低于 1e-6 后整个仿真可能只出现几个错误甚至零个错误semilogy 画出来像平台。另一种是比特索引错位判决结果的顺序和参考比特顺序对不上错误率是一个与 SNR 无关的常数怎么提高 SNR 都降不下去。解决先算一下当前 SNR 点的错误事件数如果小于 100增加 N 或跳过该点。索引错位用无噪声测试排查把噪声标准差设成 0跑一个块bit_err 必须是 0只要有一个错就说明 est 和 ref 的顺序没对齐逐行检查 ref 的列索引和 est 的符号对应关系。6. 验证仿真结果可靠的 3 个方法分集斜率、退化测试与理论曲线对照6.1 对数坐标下的分集斜率检查把 BER 曲线转成 log-log 坐标取高 SNR 段的最后几个点做线性拟合斜率应该接近分集阶数。2x1 接近 -22x2 接近 -4。低于这个值编码或合并逻辑大概率有问题。这个方法比肉眼对比理论曲线更灵敏能立刻暴露维度错误和合并丢支路的问题。6.2 退化到 1x1 与理论 BER 对照把 Nt 改成 1跳过 Alamouti 编码直接发单天线接收端也去掉合并逻辑变成纯 SISO 链路。QPSK 在瑞利信道下的理论 BER 是 0.5*(1 - sqrt(gamma/(1 gamma)))gamma 是 Eb/N0 的线性值。仿真曲线如果能和这个公式重合说明调制、噪声和统计端的逻辑是对的Alamouti 的问题可以单独隔离出来。这一步相当于给整条链路做一次校准。6.3 快衰落与准静态的边界意识Alamouti 正交性依赖相邻两个时隙信道恒定。如果做快衰落仿真H 的更新周期不要小于 2 个符号否则正交性被破坏分集增益会退化。我一般会在脚本注释里写明当前是准静态块衰落H 每 block_len 个符号更新一次这个 block_len 任意改不影响空间分集结论但改成 1 就意味着每个符号独立衰落那时候 Alamouti 已经不是标准 Alamouti 了。这个边界想清楚后续做 LTE 或 OFDM 帧结构仿真时就不会拿错模型。这套脚本和验证方法是我做分集方案对比时的固定起点先跑 1x1 校准再跑 2x1 看二阶分集最后上 2x2 看四阶分集三条曲线画在一起方案好坏一目了然。希望帮到你。本文还有配套的精品资源点击获取