第四章 发射机链路(二):加扰与调制映射、层映射与预编码、资源单元映射
各位同学,欢迎来到第四章。上一章我们聊了信道编码和速率匹配,把数据比特流整理得井井有条。今天咱们继续往前走,看看这些比特在发射机里还要经历哪些“折腾”。
我个人习惯把这一章看作是“比特到信号的变形记”。说白了,就是从一串0和1,变成能在空中飞行的复杂信号。这里头有三个关键步骤:加扰与调制、层映射与预编码、资源单元映射。每一步都有它的门道,咱们一个一个来拆解。
4.1 加扰与调制映射
先说说加扰。你可能会问,数据不是已经编码好了吗?为什么还要加扰?
嗯,这里要注意。加扰的目的不是为了纠错,而是为了“随机化”。你想啊,如果发射的全是连续的0或者连续的1,那信号功率谱上就会出现尖峰,干扰其他用户不说,还容易让接收机同步出问题。加扰就是用一串伪随机序列跟数据比特做异或,把数据打散。
核心要点:加扰不改变数据长度,只改变比特的统计特性。扰码序列由Gold序列生成,跟小区ID和用户标识有关。
我在项目中遇到过一个问题:某次测试发现邻小区干扰特别大,查了半天,原来是加扰初始化参数配错了。两个小区用了相同的扰码序列,数据直接“撞车”。所以啊,加扰的种子配置一定要仔细核对。
加扰完了,就是调制映射。这一步把比特变成复数符号。LTE支持三种调制方式:QPSK、16QAM、64QAM。说白了,就是每个符号携带的比特数不同。
| 调制方式 | 每符号比特数 | 星座点数 | 典型应用场景 |
|---|---|---|---|
| QPSK | 2 | 4 | 控制信道、边缘用户 |
| 16QAM | 4 | 16 | 中等信道质量 |
| 64QAM | 6 | 64 | 近点用户、高吞吐量 |
调制映射的规则是固定的。QPSK把2个比特映射成一个复数,16QAM是4个比特,64QAM是6个比特。星座图上的点怎么排?协议里有明确的规定,格雷编码,相邻点只差1个比特。这样做的好处是,万一解调错了,大概率只错1个比特。
我的小技巧:写仿真代码时,我习惯先把星座点做成查找表。比如QPSK,直接定义四个点:[1+1j, 1-1j, -1+1j, -1-1j]再归一化。这样比实时计算快得多,而且不容易出错。
来看看代码怎么写。下面是一个简单的QPSK调制函数:
def qpsk_mod(bits):
# bits: 长度为2的整数倍
# 输出: 复数符号
n = len(bits) // 2
symbols = np.zeros(n, dtype=complex)
for i in range(n):
b0 = bits[2*i]
b1 = bits[2*i+1]
# 映射规则: 00->1+1j, 01->1-1j, 10->-1+1j, 11->-1-1j
real = 1 if b0 == 0 else -1
imag = 1 if b1 == 0 else -1
symbols[i] = (real + 1j*imag) / np.sqrt(2)
return symbols
16QAM和64QAM的原理类似,只是映射表更大。我建议你动手写一个通用的调制函数,把调制阶数作为参数传进去,这样代码复用性更好。
4.2 层映射与预编码
调制完的符号,接下来要进入MIMO处理环节。这里有两个概念:层映射和预编码。
层映射,说白了就是把符号流分配到不同的“层”上。层是什么?你可以理解为并行的数据流。LTE支持1到4层,对应单天线、2x2 MIMO、4x4 MIMO等配置。
举个例子,如果是2层传输,就把符号流交替分配给层1和层2。第1个符号去层1,第2个符号去层2,第3个符号再去层1,以此类推。
注意:层数不能超过天线端口数,也不能超过信道秩。我曾经在仿真里把层数设成4,天线端口只有2,结果预编码矩阵维度对不上,报错报了半天。
预编码呢,就是把层上的符号映射到天线端口上。这一步用一个预编码矩阵W来实现。W的维度是天线端口数 × 层数。说白了,就是给每层的数据乘上一个权重,然后叠加到各个天线上。
预编码的好处是什么?你想想看,如果直接发,信号可能在某些方向上相互抵消。通过预编码,我们可以让信号在接收端的方向上增强,在其他方向上减弱。这就是波束赋形的雏形。
协议里定义了几种预编码方式:
- 码本预编码:用固定的码本,接收端反馈PMI(预编码矩阵指示)
- 非码本预编码:基于信道估计,动态计算预编码矩阵
- 发射分集:SFBC(空频分组码),用于提高可靠性
我个人习惯在仿真里先实现码本预编码,因为简单、可复现。等码本跑通了,再上非码本。这样调试起来方便。
下面是一个2天线端口、1层的预编码示例:
def precoding(symbols, W):
# symbols: 长度为N的复数向量 (1层)
# W: 2x1 预编码矩阵
# 输出: 2xN 矩阵,每行对应一个天线端口
tx_signals = np.outer(W, symbols)
return tx_signals
# 举例:使用码本索引0
W = np.array([[1], [1]]) / np.sqrt(2)
tx = precoding(symbols, W)
4.3 资源单元映射
最后一步,把预编码后的信号放到资源格子里。LTE的资源格子叫资源单元(RE),一个RE对应一个OFDM符号上的一个子载波。
资源映射的规则比较复杂,因为要放的东西很多:
- PDSCH数据符号
- 参考信号(CRS、DMRS等)
- 控制信道(PDCCH、PCFICH等)
- 同步信号(PSS、SSS)
- 广播信道(PBCH)
这些信号在时频资源上有固定的位置。比如CRS,在时域上每个时隙的第0和第4个OFDM符号,频域上每6个子载波出现一次。具体位置跟小区ID有关。
避坑指南:我曾经在写资源映射时,忘了预留CRS的位置,结果数据符号把参考信号覆盖了。接收端解调时,信道估计全乱套。所以,一定要先画一个资源格子图,把每个信号的位置标清楚,再动手写代码。
资源映射的流程大致是这样的:
- 初始化一个全零的资源格子,维度是 [天线端口数, 子载波数, OFDM符号数]
- 按照协议规定,填入参考信号、同步信号等固定信号
- 找到PDSCH可用的RE位置(排除已占用的)
- 把数据符号按顺序填入这些RE
这里有个细节:数据符号的填充顺序是先频域后时域。也就是说,先填完一个OFDM符号上所有可用的子载波,再填下一个符号。这个顺序不能搞反,否则接收端解映射会乱。
下面是一个简化的资源映射代码框架:
def resource_mapping(tx_signals, n_subcarriers, n_symbols, n_ports):
# tx_signals: 每个天线端口的数据符号列表
# 初始化资源格子
grid = np.zeros((n_ports, n_subcarriers, n_symbols), dtype=complex)
# 填入CRS (简化版,仅示意)
for port in range(n_ports):
for sym_idx in [0, 4]: # 每个时隙的第0和第4个符号
for k in range(0, n_subcarriers, 6):
grid[port, k, sym_idx] = crs_value(port, k, sym_idx)
# 填入PDSCH数据
for port in range(n_ports):
data_idx = 0
for sym_idx in range(n_symbols):
for k in range(n_subcarriers):
if grid[port, k, sym_idx] == 0: # 未被占用
grid[port, k, sym_idx] = tx_signals[port][data_idx]
data_idx += 1
return grid
资源映射完成后,信号就可以送入OFDM调制器了。这部分我们下一章再细聊。
我的建议:写资源映射代码时,一定要做单元测试。比如先映射一个已知的参考信号,然后检查位置对不对。我习惯把资源格子可视化出来,用热力图看看有没有重叠或空洞。这一步能省去后面很多调试时间。
好了,这一章的内容就到这里。加扰、调制、层映射、预编码、资源映射,每一步都是发射机链路中不可或缺的环节。你想想看,从一串比特到填满资源格子的复数信号,中间经历了多少变换?这就是通信系统的魅力所在。
如果你在写代码时遇到问题,不妨回头看看这一章的内容。特别是资源映射的顺序和预编码矩阵的维度,这两个地方最容易出错。多动手,多调试,慢慢就熟练了。
公众号:蓝海资料掘金营,微信deep3321