5G LDPC码是3GPP NR标准中为数据信道(如PDSCH/PUSCH)采用的信道编码方案,其核心是一种**准循环低密度奇偶校验码(QC-LDPC)**。它通过两个优化的基图(Base Graph)和灵活的扩展因子,实现了高吞吐、低时延和高可靠性的统一。
### 📡 5G LDPC码的核心设计
#### 1. 基础:稀疏校验矩阵
LDPC码是一种线性分组码,由稀疏的奇偶校验矩阵 `H` 定义。其码字 `c` 满足 `H·cᵀ = 0`。`H` 的每一行对应一个校验方程,每一列对应一个比特,矩阵的稀疏性(非零元素少)保证了译码的高效性。
#### 2. 准循环结构(QC-LDPC)
为了支持5G多样化的码长和码率,5G采用了QC-LDPC。其核心思想是:
* **基矩阵(Base Matrix, Hb)**:一个 `mb × nb` 的小矩阵,元素为 `-1`、`0` 或正整数。
* **扩展因子(Lifting Size, Z)**:一个正整数,用于将基矩阵“放大”为实际的校验矩阵 `H`。
扩展过程如下:将基矩阵中的每个元素替换为一个 `Z × Z` 的子矩阵。
* 若 `hbij = -1`,替换为 `Z × Z` 的全零矩阵。
* 若 `hbij = 0`,替换为 `Z × Z` 的单位矩阵。
* 若 `hbij = k > 0`,替换为单位矩阵循环右移 `k` 位后的置换矩阵。
最终,实际的校验矩阵 `H` 的尺寸为 `(mb·Z) × (nb·Z)`。这种结构使得编码和译码可以高度并行化。
#### 3. 两个基图:BG1 与 BG2
5G定义了两个基矩阵,以适应不同的业务场景:
* **BG1 (Base Graph 1)**:尺寸为 **46 × 68**。适用于**较大的信息块**和**较高的码率**(1/3 至 8/9),最大信息块长度可达 **8448 bits**。
* **BG2 (Base Graph 2)**:尺寸为 **42 × 52**。适用于**较小的信息块**和**较低的码率**,常用于时延敏感业务、控制信令和IoT应用。
### ⚙️ C++ 实现:编码与译码
以下是一个简化的 C++ 实现示例,展示了基于 5G LDPC 的**编码**和**最小和译码**的核心逻辑。注意,完整的 5G 标准实现非常复杂,此处代码用于阐明原理。
#### 1. 编码实现 (QC-LDPC)
编码的目标是根据信息序列 `s` 计算出校验位 `p`,最终码字为 `c = [s, p]`。对于 5G 的 QC-LDPC,编码通常采用**前向代入**的方法。以下代码假设基矩阵 `B` 的校验部分具有双对角结构,以便能够递推计算校验位。
```cpp
#include <iostream>
#include <vector>
#include <cstdint>
// 简化表示:将基矩阵的每个元素(-1, 0, 正数)作为移位值
using BaseMatrix = std::vector<std::vector<int>>;
// 循环移位函数:将 input 循环右移 shift 位
std::vector<uint8_t> circ_shift(const std::vector<uint8_t>& input, int shift) {
int Z = input.size();
std::vector<uint8_t> output(Z);
shift = (shift % Z + Z) % Z; // 确保 shift 在 [0, Z-1] 范围内
for (int i = 0; i < Z; ++i) {
output[(i + shift) % Z] = input[i];
}
return output;
}
// 编码函数(简化版,假设双对角结构)
std::vector<uint8_t> encode_ldpc(const std::vector<uint8_t>& info_bits,
const BaseMatrix& B, int Z) {
int mb = B.size(); // 基矩阵行数
int nb = B[0].size(); // 基矩阵列数
int kb = nb – mb; // 信息位列块数
// 1. 将信息比特分组为 kb 个长度为 Z 的块
std::vector<std::vector<uint8_t>> info_blocks(kb, std::vector<uint8_t>(Z));
for (int i = 0; i < kb; ++i) {
for (int j = 0; j < Z; ++j) {
info_blocks[i][j] = info_bits[i * Z + j];
}
}
// 2. 计算校验位块 p(简化:假设校验部分为双对角,可递推求解)
std::vector<std::vector<uint8_t>> parity_blocks(mb, std::vector<uint8_t>(Z, 0));
// 计算第一个校验块 (使用基矩阵第一行的信息部分)
for (int j = 0; j < kb; ++j) {
if (B[0][j] != -1) {
auto shifted = circ_shift(info_blocks[j], B[0][j]);
for (int k = 0; k < Z; ++k) {
parity_blocks[0][k] ^= shifted[k];
}
}
}
// 递推计算其余校验块 (简化:假设校验部分具有双对角结构)
for (int i = 1; i < mb; ++i) {
// 先计算与信息位的异或
for (int j = 0; j < kb; ++j) {
if (B[i][j] != -1) {
auto shifted = circ_shift(info_blocks[j], B[i][j]);
for (int k = 0; k < Z; ++k) {
parity_blocks[i][k] ^= shifted[k];
}
}
}
// 加上前一个校验块(双对角结构特性)
for (int k = 0; k < Z; ++k) {
parity_blocks[i][k] ^= parity_blocks[i-1][k];
}
}
// 3. 组装码字:信息位 + 校验位
std::vector<uint8_t> codeword;
codeword.reserve(nb * Z);
for (const auto& block : info_blocks) {
codeword.insert(codeword.end(), block.begin(), block.end());
}
for (const auto& block : parity_blocks) {
codeword.insert(codeword.end(), block.begin(), block.end());
}
return codeword;
}
```
#### 2. 译码实现 (偏移最小和算法)
译码采用**偏移最小和算法(Offset Min-Sum)**,这是最小和算法的一种改进,通过减去一个偏移因子来补偿近似带来的性能损失,在复杂度和性能之间取得了良好平衡。
```cpp
#include <cmath>
#include <algorithm>
// 偏移最小和译码 (简化版)
// llr: 输入的对数似然比 (Log-Likelihood Ratio)
// B: 基矩阵
// Z: 扩展因子
// max_iter: 最大迭代次数
// offset: 偏移因子 (典型值 0.5 左右)
std::vector<uint8_t> decode_ldpc_offset_min_sum(
const std::vector<double>& llr,
const BaseMatrix& B,
int Z,
int max_iter = 20,
double offset = 0.5) {
int mb = B.size();
int nb = B[0].size();
int N = nb * Z; // 码字总长度
// 初始化变量节点消息 (先验LLR)
std::vector<double> L(N);
for (int i = 0; i < N; ++i) L[i] = llr[i];
// 存储从校验节点到变量节点的消息 (仅存储非零位置以节省空间)
// 简化起见,使用与基矩阵相同大小的消息数组
std::vector<std::vector<std::vector<double>>> msg_c2v(
mb, std::vector<std::vector<double>>(nb, std::vector<double>(Z, 0.0)));
// 存储从变量节点到校验节点的消息
std::vector<std::vector<std::vector<double>>> msg_v2c(
mb, std::vector<std::vector<double>>(nb, std::vector<double>(Z, 0.0)));
// 初始化 v2c 消息为信道 LLR
for (int i = 0; i < mb; ++i) {
for (int j = 0; j < nb; ++j) {
if (B[i][j] != -1) {
for (int k = 0; k < Z; ++k) {
int col = j * Z + k;
msg_v2c[i][j][k] = llr[col];
}
}
}
}
for (int iter = 0; iter < max_iter; ++iter) {
// — 1. 校验节点更新 (C2V) —
for (int i = 0; i < mb; ++i) { // 遍历每个校验节点(行)
for (int j = 0; j < nb; ++j) { // 遍历该行的每个变量节点(列)
if (B[i][j] == -1) continue;
// 收集该行所有其他非零列的消息
double min1 = 1e9, min2 = 1e9;
int sign = 1;
for (int j2 = 0; j2 < nb; ++j2) {
if (j2 == j || B[i][j2] == -1) continue;
// 注意:这里需要根据移位值获取对应的 v2c 消息
// 简化处理:假设消息已经对齐,实际需要根据循环移位进行索引调整
for (int k = 0; k < Z; ++k) {
double val = msg_v2c[i][j2][k];
double abs_val = std::fabs(val);
if (abs_val < min1) {
min2 = min1;
min1 = abs_val;
} else if (abs_val < min2) {
min2 = abs_val;
}
if (val < 0) sign *= -1;
}
}
// 计算输出消息 (使用最小值和符号,并减去偏移)
for (int k = 0; k < Z; ++k) {
// 此处简化:假设所有 k 的 min 相同,实际每个 k 可能不同
double min_val = min1;
double val = sign * std::max(0.0, min_val – offset);
msg_c2v[i][j][k] = val;
}
}
}
// — 2. 变量节点更新 & 硬判决 —
std::vector<double> total_llr(N);
std::vector<uint8_t> hard_decision(N);
for (int j = 0; j < nb; ++j) {
for (int k = 0; k < Z; ++k) {
int col = j * Z + k;
double sum = llr[col]; // 先验 LLR
for (int i = 0; i < mb; ++i) {
if (B[i][j] != -1) {
sum += msg_c2v[i][j][k];
}
}
total_llr[col] = sum;
hard_decision[col] = (sum < 0) ? 1 : 0; // LLR < 0 判为 1
}
}
// — 3. 校验是否满足 (简化:仅检查信息位部分,或使用完整 H) —
// 实际实现中应计算 H * hard_decision 是否为零向量
// 此处为演示,省略校验步骤,直接进入下一次迭代或返回
// 检查是否所有校验方程满足 (简化版:假设满足)
bool all_checks_satisfied = true;
// (此处应添加实际的校验逻辑)
if (all_checks_satisfied) {
return hard_decision;
}
// — 4. 更新 v2c 消息 (用于下一次迭代) —
for (int i = 0; i < mb; ++i) {
for (int j = 0; j < nb; ++j) {
if (B[i][j] == -1) continue;
for (int k = 0; k < Z; ++k) {
msg_v2c[i][j][k] = total_llr[j * Z + k] – msg_c2v[i][j][k];
}
}
}
}
// 达到最大迭代次数,返回最后的硬判决结果
std::vector<uint8_t> hard_decision(N);
for (int j = 0; j < nb; ++j) {
for (int k = 0; k < Z; ++k) {
int col = j * Z + k;
double sum = llr[col];
for (int i = 0; i < mb; ++i) {
if (B[i][j] != -1) sum += msg_c2v[i][j][k];
}
hard_decision[col] = (sum < 0) ? 1 : 0;
}
}
return hard_decision;
}
```
### 📝 使用示例
以下代码展示了如何定义一个小型基矩阵并调用编码和译码函数。
```cpp
int main() {
// 定义一个简化的基矩阵 (例如,类似于 BG2 的缩小版)
// -1 表示零矩阵,0 表示单位矩阵,正数表示循环移位值
BaseMatrix B = {
{0, 0, -1, -1, 0}, // 校验行 1
{0, -1, 0, 0, -1}, // 校验行 2
{-1, 0, 0, 0, 0} // 校验行 3
};
int Z = 4; // 扩展因子
// 1. 信息比特 (长度 = (nb – mb) * Z)
int nb = B[0].size(); // 5
int mb = B.size(); // 3
int info_len = (nb – mb) * Z; // (5-3)*4 = 8 bits
std::vector<uint8_t> info_bits = {1, 0, 1, 1, 0, 0, 1, 0};
// 2. 编码
auto codeword = encode_ldpc(info_bits, B, Z);
std::cout << "Encoded codeword (size " << codeword.size() << "): ";
for (auto bit : codeword) std::cout << (int)bit << " ";
std::cout << std::endl;
// 3. 模拟信道传输 (假设 BPSK, 添加高斯噪声)
double noise_sigma = 0.5;
std::vector<double> llr(codeword.size());
for (size_t i = 0; i < codeword.size(); ++i) {
// 将比特 0/1 映射为 +1/-1
double tx_sym = (codeword[i] == 0) ? 1.0 : -1.0;
// 简化的 AWGN 噪声
double noise = 0; // 实际中应从高斯分布采样
double rx_sym = tx_sym + noise;
// 计算 LLR (假设噪声方差为 sigma^2)
llr[i] = 2.0 * rx_sym / (noise_sigma * noise_sigma);
}
// 4. 译码
auto decoded = decode_ldpc_offset_min_sum(llr, B, Z, 10, 0.5);
std::cout << "Decoded bits: ";
for (size_t i = 0; i < info_len; ++i) {
std::cout << (int)decoded[i] << " ";
}
std::cout << std::endl;
return 0;
}
```
### 💎 总结
5G LDPC码通过**准循环结构(QC-LDPC)** 实现了在吞吐量、时延和灵活性之间的卓越平衡。其两大基图(BG1和BG2)分别针对高吞吐和低时延场景优化。在实际的 C++ 实现中,编码可利用基矩阵的双对角结构进行高效的**前向代入**,而译码则普遍采用复杂度与性能折衷良好的**偏移最小和算法**。


