3、加扰与调制:加扰序列生成、调制映射、层映射与预编码
好,咱们今天聊聊加扰和调制这块。说实话,这是5G物理层里我最喜欢讲的部分之一。为什么?因为它把数字信号从“比特”变成了“符号”,从抽象变成了可传输的波形。你想想看,手机里那一串0和1,最后是怎么变成电磁波飞出去的?就是靠这一套流程。
3.1 加扰序列生成
加扰,说白了就是给数据“洗个牌”。为什么要洗牌?我举个例子。我在项目中遇到过一种情况:终端上报的CQI(信道质量指示)突然掉得厉害,查了半天,发现是上行数据里连续出现了长串的0。这种“直流分量”会让接收机的AGC(自动增益控制)误判,导致解调性能下降。
加扰的目的就是打掉这种长连0或长连1。5G里用的加扰序列是Gold序列,由两个m序列异或生成。嗯,这里要注意,加扰序列是跟小区ID和时隙号绑定的。不同小区、不同时隙,序列都不一样。
关键参数:
- c_init = (n_RNTI << 15) + (n_ID << 10) + (n_slot << 5) + n_sym
- n_RNTI:无线网络临时标识符
- n_ID:加扰标识,通常等于物理层小区ID
- n_slot:时隙号
- n_sym:OFDM符号索引
代码实现其实不复杂。我习惯用查表法来生成Gold序列,这样能省掉实时计算的延迟。
// Gold序列生成示例(C语言风格)
void generate_gold_sequence(uint32_t c_init, int len, int8_t *output) {
uint32_t x1 = 1; // x1初始值固定为1
uint32_t x2 = c_init;
int8_t gold_bit;
for (int n = 0; n < len; n++) {
// x1序列:x1(n+31) = (x1(n+3) + x1(n)) mod 2
// x2序列:x2(n+31) = (x2(n+3) + x2(n+2) + x2(n+1) + x2(n)) mod 2
gold_bit = ((x1 >> 0) & 0x1) ^ ((x2 >> 0) & 0x1);
output[n] = gold_bit ? 1 : -1; // 映射为BPSK符号
// 更新移位寄存器
uint32_t new_x1 = ((x1 >> 3) ^ (x1 >> 0)) & 0x1;
uint32_t new_x2 = ((x2 >> 3) ^ (x2 >> 2) ^ (x2 >> 1) ^ (x2 >> 0)) & 0x1;
x1 = (x1 >> 1) | (new_x1 << 30);
x2 = (x2 >> 1) | (new_x2 << 30);
}
}
避坑指南:我曾经在实现时忘记处理c_init的比特位宽。3GPP规定c_init是31位的,但有些场景下n_RNTI可能占满16位,导致移位后溢出。建议用uint32_t来存,别用int。
3.2 调制映射
加扰完的数据是±1的BPSK符号,接下来要映射成QAM符号。5G NR支持四种调制方式:QPSK、16QAM、64QAM、256QAM。说白了,就是每个符号携带的比特数不一样。
| 调制方式 | 每符号比特数 | 星座点数 | 典型应用场景 |
|---|---|---|---|
| QPSK | 2 | 4 | 控制信道、低信噪比 |
| 16QAM | 4 | 16 | 中等信噪比 |
| 64QAM | 6 | 64 | 高信噪比 |
| 256QAM | 8 | 256 | 极高信噪比(室内、小站) |
调制映射的核心是格雷编码。为什么要用格雷编码?相邻星座点之间只差1个比特。这样即使解调时判错到相邻点,也只错1个比特,而不是多个比特。我在做链路仿真时验证过,格雷编码比自然编码能带来约0.5dB的增益。
以16QAM为例,映射规则是这样的:
// 16QAM映射示例
// 输入:4个比特 b0 b1 b2 b3
// 输出:复数符号 I + jQ
// b0b1决定I轴,b2b3决定Q轴
// 映射表:00->-3, 01->-1, 11->+1, 10->+3
complex_t qam16_map(uint8_t bits[4]) {
int8_t I, Q;
// I轴映射
switch(bits[0] << 1 | bits[1]) {
case 0: I = -3; break; // 00
case 1: I = -1; break; // 01
case 3: I = +1; break; // 11
case 2: I = +3; break; // 10
}
// Q轴映射(同理)
switch(bits[2] << 1 | bits[3]) {
case 0: Q = -3; break;
case 1: Q = -1; break;
case 3: Q = +1; break;
case 2: Q = +3; break;
}
return (complex_t){.real = I, .imag = Q};
}
注意:5G NR的星座图功率需要归一化。QPSK的功率因子是1/√2,16QAM是1/√10,64QAM是1/√42,256QAM是1/√170。忘了归一化,发射功率就不对了,接收端解调会出问题。
3.3 层映射
层映射,就是把调制后的符号分配到不同的传输层上。5G NR最多支持8层传输(MIMO 8×8)。你想想看,一个数据流要分成8份同时发出去,怎么分?
层映射有两种模式:
- 单码字模式:一个传输块映射到1~4层
- 双码字模式:两个传输块分别映射到1~4层,总共最多8层
我个人的习惯是,先判断层数,再决定用哪种映射方式。层数≤4时用单码字,层数>4时用双码字。这样实现起来逻辑清晰。
// 层映射示例:单码字,4层传输
// 输入符号流 d(0), d(1), d(2), ...
// 输出:4层符号流 x(0), x(1), x(2), x(3)
void layer_mapping_4layers(complex_t *input, int len,
complex_t output[4][MAX_SYMBOLS]) {
for (int i = 0; i < len; i++) {
output[0][i] = input[4*i]; // 层0
output[1][i] = input[4*i + 1]; // 层1
output[2][i] = input[4*i + 2]; // 层2
output[3][i] = input[4*i + 3]; // 层3
}
}
3.4 预编码
预编码,是MIMO系统的核心。说白了,就是给每层数据乘上一个复数权重,然后映射到不同的天线端口上。为什么要这么做?为了让信号在空间上“定向发射”,提高接收信噪比。
5G NR支持两种预编码方式:
- 基于码本的预编码:终端上报PMI(预编码矩阵指示),基站查表选矩阵
- 非码本预编码:基站根据信道估计结果,自己算预编码矩阵
我记得在做原型机时,遇到过一个问题:预编码矩阵的量化精度不够,导致波束指向偏差。后来把码本从4比特扩展到6比特,性能才达标。
预编码的数学表达很简单:
// 预编码示例:4层映射到4天线端口
// 预编码矩阵 W (4x4),这里用单位矩阵做演示
// 实际应用中W由PMI或信道估计决定
void precoding_4x4(complex_t layers[4][MAX_SYMBOLS],
complex_t antennas[4][MAX_SYMBOLS],
complex_t W[4][4], int len) {
for (int sym = 0; sym < len; sym++) {
for (int ant = 0; ant < 4; ant++) {
antennas[ant][sym] = 0;
for (int layer = 0; layer < 4; layer++) {
antennas[ant][sym] += W[ant][layer] * layers[layer][sym];
}
}
}
}
经验之谈:预编码矩阵的归一化很容易被忽略。3GPP要求每层的发射功率相等,所以W矩阵的每列范数要相等。我见过有人直接用信道矩阵的共轭转置做预编码,结果功率分配不均,高层数据全被压死了。
好了,加扰、调制、层映射、预编码,这一套流程走下来,数据就从比特变成了可以发射的复数符号。下一章咱们聊OFDM调制和资源映射,那又是另一番天地了。