作者:绳匠_ZZ0
前言:为什么从硬判决开始?
在深入学习LDPC(低密度奇偶校验)码的译码算法时,许多教程一上来就介绍软判决方法如置信传播(BP)、Min-Sum或和积算法(SPA)。这些方法涉及复杂的浮点运算、对数似然比和tanh函数,初学者容易被绕晕。我自己在初学时就深有体会------那些数学公式和概率模型让人望而生畏。后来,我意识到LDPC译码的核心思想其实非常朴素:让校验节点和变量节点互相"反馈"信息,根据反馈强度决定是否修正错误比特。这就是比特翻转算法(Bit Flipping, BF),一种基于硬判决的译码方法。
比特翻转算法只用整数运算和逻辑判断,不涉及任何浮点数或概率计算,特别适合作为LDPC的入门。它不仅帮助理解译码的底层逻辑,还能为后续学习软判决打下基础。本文将用C语言完整实现一个比特翻转译码器,并通过实例一步步解析其工作原理。无论你是通信工程学生还是嵌入式开发者,都能从中获益。
一、LDPC码极简回顾:从矩阵到校验方程
LDPC码是一种线性分组码,由稀疏的校验矩阵 H 定义。所有合法码字 c 必须满足: $$ H \cdot c^T = 0 \quad (\text{模} 2 \text{加法}) $$ 这里的"低密度"指 H 矩阵中"1"的数量很少(通常每行/列只有几个"1"),这使得译码效率高。H 的每一行对应一个校验方程,码字必须通过这些方程的验证。
举个例子,我们用一个简单的(7,4) LDPC码(码长7位,信息位4位,校验位3位),其校验矩阵 H 为3×7: $$ H = \begin{bmatrix} 1 & 1 & 1 & 0 & 0 & 0 & 0 \ 0 & 0 & 1 & 1 & 1 & 0 & 0 \ 0 & 1 & 0 & 0 & 1 & 1 & 1 \end{bmatrix} $$ 对应的校验方程为:
- 行0:c_0 + c_1 + c_2 = 0 \\quad (\\text{mod } 2)
- 行1:c_2 + c_3 + c_4 = 0
- 行2:c_1 + c_4 + c_5 + c_6 = 0
这些方程构成了译码的基础------任何接收到的序列必须满足所有方程,否则说明存在传输错误。
二、比特翻转算法:直觉理解与实例分析
比特翻转算法的核心思想是基于局部反馈的纠错。假设接收到的硬判决序列 r(每个比特为0或1),算法通过迭代翻转最可疑的比特来逼近正确码字。其步骤如下:
- 计算症状(Syndrome):检查每个校验方程是否满足(结果为0表示满足,1表示不满足)。
- 统计不满足数:对每个比特,统计它参与的不满足方程数量。
- 翻转比特:选择不满足数最大的比特进行翻转(0变1或1变0)。
- 迭代:重复上述过程,直到所有方程满足或达到最大迭代次数。
实例演示
假设发送的合法码字为 \[1, 0, 1, 1, 0, 0, 0\],接收序列因噪声变成 \[0, 0, 1, 1, 0, 0, 0\](c_0 从1错为0)。校验过程如下:
- 行0:0 + 0 + 1 = 1 \\neq 0 → 不满足
- 行1:1 + 1 + 0 = 0 → 满足
- 行2:0 + 0 + 0 + 0 = 0 → 满足
此时,只有行0不满足。参与行0的比特是 c_0, c_1, c_2,它们的不满足数均为1。算法选择 c_0(通常优先选索引最小的)翻转,得到 \[1, 0, 1, 1, 0, 0, 0\],再次校验全满足------纠错成功!
为什么翻转最大不满足数的比特? 这基于一个朴素假设:一个比特参与的不满足方程越多,它出错的概率越大。在稀疏矩阵中,这种局部反馈往往能快速收敛。
三、C语言实现:通用比特翻转译码器
下面我们用C语言实现一个通用的比特翻转译码器。它支持任意校验矩阵 H,并包含完整的编码、译码和测试流程。代码注重可读性和实用性,适合嵌入式系统或教学场景。
3.1 数据结构定义
首先定义常量、校验矩阵和函数原型:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 7 // 码长
#define M 3 // 校验方程数
#define K 4 // 信息位长度
// 校验矩阵 H (M行 N列)
int H[M][N] = {
{1, 1, 1, 0, 0, 0, 0}, // 方程0: c0 + c1 + c2 = 0
{0, 0, 1, 1, 1, 0, 0}, // 方程1: c2 + c3 + c4 = 0
{0, 1, 0, 0, 1, 1, 1} // 方程2: c1 + c4 + c5 + c6 = 0
};
3.2 核心函数:计算症状向量
症状向量 s 表示每个校验方程是否满足(s\[i\] = 0 满足,1 不满足)。使用异或运算实现模2加法:
c
// 计算症状向量 s[M]: s[i] = sum_{j} H[i][j] * r[j] (mod2)
void compute_syndrome(int *r, int *syndrome) {
for (int i = 0; i < M; i++) {
int sum = 0;
for (int j = 0; j < N; j++) {
if (H[i][j]) {
sum ^= r[j]; // 模2加法等价于异或
}
}
syndrome[i] = sum;
}
}
3.3 核心函数:比特翻转译码
译码器迭代翻转比特直到成功或超限。关键点包括:
- 动态统计不满足数:为每个比特计数。
- 选择翻转位置:优先处理问题最严重的比特。
- 终止条件:所有方程满足或迭代次数耗尽。
c
// 比特翻转译码
// r: 接收序列 (0/1数组), max_iter: 最大迭代次数, decoded: 输出结果
// 返回值: 1成功, 0失败
int bit_flipping_decode(int *r, int max_iter, int *decoded) {
int working[N]; // 工作副本
memcpy(working, r, N * sizeof(int));
for (int iter = 0; iter < max_iter; iter++) {
int syndrome[M];
compute_syndrome(working, syndrome);
// 检查是否所有校验满足
int all_zero = 1;
for (int i = 0; i < M; i++) {
if (syndrome[i] != 0) {
all_zero = 0;
break;
}
}
if (all_zero) {
memcpy(decoded, working, N * sizeof(int));
return 1; // 成功
}
// 统计每个比特的不满足数
int unsatisfied_count[N] = {0}; // 初始化为0
for (int j = 0; j < N; j++) {
for (int i = 0; i < M; i++) {
if (H[i][j] && syndrome[i]) {
unsatisfied_count[j]++; // 比特j参与不满足方程i
}
}
}
// 找出最大不满足数的比特
int max_cnt = -1;
int flip_pos = -1;
for (int j = 0; j < N; j++) {
if (unsatisfied_count[j] > max_cnt) {
max_cnt = unsatisfied_count[j];
flip_pos = j;
}
}
if (flip_pos == -1) {
break; // 无比特可翻转(理论上不发生)
}
// 翻转比特
working[flip_pos] ^= 1;
// 调试输出(可选)
printf("迭代 %d: 翻转位置 %d, 不满足数=%d\n", iter+1, flip_pos, max_cnt);
}
return 0; // 失败
}
3.4 编码函数:从信息位生成合法码字
为测试译码器,我们需要编码函数。这里采用系统形式:前 K 位为信息位,后 N-K 位为校验位。
c
// 编码: info[K]为信息位, codeword[N]为输出码字
void ldpc_encode(int *info, int *codeword) {
// 复制信息位
for (int i = 0; i < K; i++) {
codeword[i] = info[i];
}
// 求解校验位(基于H的特定结构)
codeword[2] = codeword[0] ^ codeword[1]; // 方程0: c2 = c0 XOR c1
codeword[4] = codeword[2] ^ codeword[3]; // 方程1: c4 = c2 XOR c3
codeword[5] = codeword[1] ^ codeword[4]; // 方程2: c5 = c1 XOR c4
codeword[6] = 0; // 简化处理(实际需根据方程计算)
}
3.5 测试主函数:完整流程演示
模拟信道错误并测试译码效果:
c
int main() {
int info[K] = {1, 0, 0, 1}; // 原始信息
int codeword[N];
ldpc_encode(info, codeword);
printf("原始码字: ");
for (int i = 0; i < N; i++) printf("%d", codeword[i]);
printf("\n");
// 模拟信道:翻转第2位(索引从0开始)
int received[N];
memcpy(received, codeword, N * sizeof(int));
received[2] ^= 1; // 引入错误
printf("接收序列: ");
for (int i = 0; i < N; i++) printf("%d", received[i]);
printf("\n");
// 译码
int decoded[N];
int max_iter = 10;
if (bit_flipping_decode(received, max_iter, decoded)) {
printf("译码成功: ");
for (int i = 0; i < N; i++) printf("%d", decoded[i]);
printf("\n");
} else {
printf("译码失败\n");
}
return 0;
}
运行结果示例
原始码字: 1001100
接收序列: 1011100 // c2从0错为1
迭代 1: 翻转位置 2, 不满足数=1
译码成功: 1001100
四、算法优化与局限性讨论
优化建议
- 并行计算:在统计不满足数时,可用多线程加速。
- 数据结构优化:使用位运算压缩存储 H 矩阵。
- 动态阈值:当多个比特不满足数相同时,添加随机性避免死循环。
局限性
- 多重错误:在多个比特错误时,算法可能不收敛或误纠。例如,如果 c_0 和 c_1 同时出错,不满足数可能分布均匀。
- 稀疏性依赖:在非稀疏矩阵中性能下降。
- 无软信息:硬判决损失了信道可靠性信息,误码率高于软判决方法。
五、总结
比特翻转算法是LDPC译码的理想起点,它用简单的整数运算实现了核心的"节点反馈"思想。通过本文的C语言实现,你可以深入理解校验方程、症状计算和迭代翻转的底层逻辑。尽管它在高噪声环境下不如软判决,但在低复杂度场景(如物联网设备)仍有广泛应用。后续我们将探讨如何从此基础过渡到置信传播等高级算法。