简介:面向无线通信与信号处理初学者的空时分组码(STBC)编码译码实现示例,围绕STBC编译码流程与误码率性能评估展开,适合用于课程实验、仿真入门或算法验证前的快速理解。压缩包共3个文件,均为.m脚本,整体约1KB,体量轻便,便于在MATLAB环境中直接查看和运行,其中包含编译码实现与误码率曲线绘制相关脚本,可帮助读者把抽象的分组码原理对应到可执行代码与可视化结果。目前已有2322人学习下载,说明其入门参考价值较受关注。通过这份材料,读者可以了解STBC从编码、译码到BER曲线对比的基本实现思路,并据此复现实验、观察不同条件下的性能变化,为后续扩展仿真与排错提供起点。
1. 从一条对不上的 BER 曲线说起:STBC 到底在发射端做了什么
把空时分组码(STBC)的 BER 曲线跑出来,形状对了、位置却整体偏 3 dB,是这套仿真里最常见的翻车点。STBC 解决的是发射端拿不到信道信息时,怎么用多根天线换来分集增益——两个时隙发两个符号,接收端一次共轭合并,原本要做二维联合搜索的检测就退化成两个标量判决。
它适合基站不动、终端乱跑的控制信道和上行链路,也适合当 MIMO 入门的第一份能跑的代码:码矩阵只有两行两列,每一步都能手算验证。
后面把编码矩阵、合并公式、Eb/N0 归一化和五类排查记录拆开,配一份能直接跑的 Python 链路。
2. Alamouti 编码矩阵:两个时隙、两根天线怎么排符号
Alamouti 码之所以被写进教科书,不是因为它的增益最高,而是因为它在“发射端完全不知道信道”这个约束下,用最少的维度换到了满分集。理解它只需要盯住一个 2×2 的复数矩阵,但真正写代码时,第一个会绊住人的问题是索引怎么排:行是天线的还是时间是天线的,负号和共轭加在哪一侧。
2.1 时隙与天线两个维度:先把索引约定固定下来
Alamouti 码块占两个连续的符号周期,跨越两根发射天线。约定矩阵的行是时间、列是天线,一个码块写成:
| 天线 1 | 天线 2 | |
|---|---|---|
| 时隙 t=0 | s1 | s2 |
| 时隙 t=1 | -s2* | s1* |
两个时隙送出两个独立符号,所以码率为 1。这一点常被忽略:很多入门资料先讲重复编码(两个时隙重复同一个符号),码率 1/2,分集阶数是 2 但频谱效率砍半;Alamouti 用同样的两个时隙装了两个符号,分集和速率同时保住,这才是它当年被广泛采用的原因。
功率分配是第二个必须提前定死的事。总发射功率受限的系统中,两根天线各自只承担一半能量,所以发送符号要乘 1/√Nt。这一步在仿真里极容易被漏掉,漏掉的直接后果是曲线看着更“漂亮”,但和理论值对不上,而且换一根天线的配置时结果不可比。
发射端完全不需要信道信息——这正是 STBC 相对波束赋形、预编码这类方案的定位差异。波束赋形要靠下行信道估计反馈,STBC 靠的是码字本身的正交结构,接收端用信道做合并就行。选型时可以这么判断:如果发射端能稳定拿到信道状态信息,空分复用加预编码的吞吐更高;如果拿不到,或者只想在恶劣信道下保一个稳健的低速通道,STBC 是更省心的选择。
2.2 正交性从哪来:Gram 矩阵必须是标量乘单位阵
把上面的矩阵记作 S,两行分别是 $\mathbf{r}_0=(s_1, s_2)$ 和 $\mathbf{r}_1=(-s_2^, s_1^)$。计算行内积:
$\langle \mathbf{r}_0, \mathbf{r}_1 \rangle = s_1 \cdot \overline{(-s_2^)} + s_2 \cdot \overline{(s_1^)} = -s_1 s_2 + s_2 s_1 = 0$
两行正交,且各自范数都是 $|s_1|^2+|s_2|^2$。于是 $S^H S = (|s_1|^2+|s_2|^2) I_2$。这个等式是整个方案的支点:正因为 Gram 矩阵是标量乘单位阵,接收端的两路信号在被合并之后,符号 $s_1$ 和 $s_2$ 之间不会互相串扰,二维联合最大似然搜索才被拆成两个独立的一维搜索。
负号和共轭不是随便加的,它必须与“第二个时隙把两个符号的动位置换一下”配合。写成 $(s_1, s_2)$ 和 $(s_2^, -s_1^)$ 也成立,只是天线列顺序反了,本质等价。但写成 $(s_1, s_2)$ 和 $(s_2, s_1)$ 就完蛋——两行内积变成 $2s_1 s_2$,正交性没了,译码端会看到一对互相干扰的量,BER 曲线在高 SNR 处直接出现平台。
正交结构的存在性有硬上限,这点决定了 STBC 的适用边界。Tarokh 等人证明过,复星座下码率为 1 的正交空时分组码只对两根发射天线存在;三根和四根天线最高只能做到 3/4 码率,需要往码矩阵里填零,五个时隙发三个或四个符号。天线数再往上,正交性只能靠牺牲码率换,所以大规模天线系统里基本见不到 OSTBC,走的是空分复用那条路。做链路选型时记住一句:STBC 的分集增益随天线数涨,但码率和检测复杂度也一起涨,2×2 是甜点。
2.3 写一个可复用的编码函数并做形状自检
下面这段把码块结构固化成函数,返回三维数组(时隙, 块索引, 天线索引)。这种排法在后面加接收天线维度时最省事,广播不会出错。
import numpy as np def alamouti_encode(sym, nt=2, normalize=True): """ sym: (N,) 复数符号序列,N 必须为偶数 返回 tx: (2, N//2, nt) —— [时隙, 块索引, 天线索引] """ sym = np.asarray(sym, dtype=np.complex128) if sym.size % 2 != 0: raise ValueError("符号数必须为偶数,否则凑不满一个 Alamouti 块") if nt != 2: raise ValueError("Alamouti 只对 2 根发射天线成立") s1 = sym[0::2] # 每个块的第一个符号 s2 = sym[1::2] # 每个块的第二个符号 tx = np.empty((2, s1.size, 2), dtype=np.complex128) tx[0, :, 0] = s1 # t=0, 天线1 tx[0, :, 1] = s2 # t=0, 天线2 tx[1, :, 0] = -np.conj(s2) # t=1, 天线1 tx[1, :, 1] = np.conj(s1) # t=1, 天线2 return tx / np.sqrt(2) if normalize else tx逻辑上有三处值得单独说。sym[0::2]和sym[1::2]把序列拆成奇偶两路,这对应“一个码块吃两个符号”的节奏,后面译码端按同样的规则拼回去就能对齐;tx的第一维是时隙而不是天线,因为接收端求和是在时隙上做接收采样、在天线上做信道相乘,这个顺序和物理过程一致;最后除以√2是总功率归一,把两根天线的辐射总能量拉回 1,如果整条链路统一用“每根天线满功率”的口径,把normalize=False打开即可,但全链路必须一致。
写完先别急着跑 BER,做一个形状和正交性自检,几行代码就能挡住一大半低级错误:
def check_orthogonality(sym, tol=1e-10): tx = alamouti_encode(sym, normalize=False) # (2, nblk, 2) S = tx[:, 0, :] # 取第一个块的 2x2 gram = S.conj().T @ S target = (np.abs(sym[0])**2 + np.abs(sym[1])**2) * np.eye(2) assert np.allclose(gram, target, atol=tol), f"正交性破坏:\n{gram}" check_orthogonality(np.array([1+2j, -0.5+0.3j]))S.conj().T @ S就是 Gram 矩阵,如果它不是对角阵,说明共轭或负号放错了位置;如果它是对角阵但两个对角元素不相等,说明两个时隙的符号位置没对称。这一步跑通,发射端的活儿就基本干净了。
3. 接收端合并与 ML 译码:把两路信号压成一个标量
发射端把两个符号摊在了两根天线、两个时隙上,接收端拿到的是一堆混在一起的观测。合并做的事,就是用信道系数当权重,把属于 $s_1$ 的那部分能量从两个时隙里全部抽出来,同时把 $s_2$ 的干扰压掉。这一章的每一步都能手推,推完再写代码就不会对着公式猜。
3.1 平坦瑞利信道与准静态假设
先把信道模型钉死。用平坦衰落、准静态的复数高斯信道:$h_{n} \sim \mathcal{CN}(0,1)$,且在一个码块的两个时隙内保持不变。准静态这个假设不是形式上的偷懒,它直接决定了合并公式能不能成立——推导里默认 $h_1$、$h_2$ 在两个时隙里是同一组值,如果它们每个时隙都重新生成,合并出来的量就会带上一个无法消除的交叉项。
工程上这个假设合不合理,看相干时间。两个符号周期的跨度通常远小于信道相干时间,所以准静态是站得住的;但在高速移动场景下,多普勒扩展会让相干时间掉到几个符号量级,这时候 STBC 会出现误码平台,靠加天线也压不下去。这类平台不是实现 bug,是模型失效,排查时要能区分开。
另外,这里的信道是“发射天线 n 到接收天线 r”的复增益,多个接收天线之间假设独立同分布。真做硬件验证时天线间相关性强,分集增益会打折,仿真里可以加一个相关系数矩阵乘进去看退化幅度。
3.2 共轭合并公式的推导与代码落地
记接收天线上两个时隙的观测为 $r_0, r_1$,噪声为 $n_0, n_1$:
$r_0 = h_1 s_1 + h_2 s_2 + n_0$ $r_1 = -h_1 s_2^* + h_2 s_1^* + n_1$
要凑出只含 $s_1$ 的量,思路是把 $r_0$ 乘 $\overline{h_1}$、把 $r_1$ 取共轭后乘 $h_2$:
$\overline{h_1} r_0 + h_2 \overline{r_1} = (|h_1|^2+|h_2|^2) s_1 + \overline{h_1} n_0 + h_2 \overline{n_1}$
交叉项正好抵消。同理,要凑 $s_2$ 就用 $\overline{h_2} r_0 - h_1 \overline{r_1}$,得到 $(|h_1|^2+|h_2|^2) s_2$ 加噪声。合并增益记为 $g = |h_1|^2+|h_2|^2$,它是一个实数正标量,这一条极其关键——正因为它没有相位,判决时不需要做相位补偿,直接比距离就行。
def alamouti_combine(rx, h): """ rx: (2, nblk, nr) 两个时隙的接收样本 h : (nblk, 2, nr) 信道,nr 为接收天线数 返回 y1, y2: (nblk,) 合并后的两个软输出 g : (nblk,) 合并增益,实数 """ r0, r1 = rx[0], rx[1] # (nblk, nr) h0, h1 = h[:, 0, :], h[:, 1, :] # (nblk, nr) y1 = np.sum(np.conj(h0) * r0 + h1 * np.conj(r1), axis=1) y2 = np.sum(np.conj(h1) * r0 - h0 * np.conj(r1), axis=1) g = np.sum(np.abs(h0) ** 2 + np.abs(h1) ** <p> <a href="https://download.csdn.net/download/yuanclkk/10411105" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>