☰
802.11n LDPC 校验矩阵、编码与最小和译码实现完全指南
2026/10/10 10:57:47 网站建设 项目流程

简介:面向802.11n无线局域网标准中的LDPC(低密度奇偶校验码)技术,这套MATLAB/C仿真源码包专为无线通信方向学生、算法研究者及需要快速搭建LDPC编解码仿真环境的工程师设计。包内共25个文件,以m脚本、c源码、mat数据文件及README说明为主,涵盖校验矩阵生成、LDPC编码、置信传播译码、收发链路仿真与误码率测试等核心环节;同时提供不同码长(648、1296、1944)与多种码率(1/2、2/3、3/4、5/6)的protoH参数文件,便于对照实验,压缩后仅22KB,结构紧凑清晰。目前已有338人浏览学习。这套源码包可直接用于理解802.11n中LDPC在GF(2)上的编译码实现、MIMO场景下的并行解码机制,并通过修改码长码率观察不同配置下的纠错性能;既适合作为LDPC课程设计的参考工程,也可作为优化无线通信系统性能的实验基础,省去从零编写核心算法的功夫。

1. 802.11n LDPC:一块码率自适应芯片,为什么要把校验矩阵焊死在协议里

802.11n LDPC 是 Wi-Fi 4 时代从“卷积码 + 可选 RS 码”跨到“逼近香农限”的关键一跳。它把低密度奇偶校验码从实验室搬进了路由器,靠一组 648、1296、1944 的准循环码字支撑 20/40MHz 下的高吞吐。很多从业者拿到带“802.11n LDPC”字样的源码包后,第一反应是找 .m 文件跑仿真,结果不是译码器不收敛,就是误码率曲线在 1e-3 附近掉不下去。问题往往不在译码算法本身,而在校验矩阵没对齐、LLR 符号约定写反,或者打孔位和缩短位没按协议处理。这篇笔记就从校验矩阵的结构讲起,把编码、译码、参数设置和最容易翻车的几个环节一次说透。

2. 802.11n LDPC 码结构:三种码长、四种码率与子块 Z 的对应关系

2.1 先抄参数表:码长、码率、子块大小与信息位长度

802.11n 的 LDPC 码是准循环 LDPC(QC-LDPC),不是随便给一个稀疏校验矩阵就能跑。协议固定了 12 种(码率,码长)组合,码率取 1/2、2/3、3/4、5/6,码长取 648、1296、1944。整个码字的列被分成 24 个列块,每个列块的大小记为 Z,所以三种码长对应的 Z 分别是 27、54、81。校验位对应的行块数由码率决定:基矩阵行数 mb = 24×(1-R)。明白这个关系,你打开源码包时就不会被一堆 .txt 矩阵文件吓到——文件名里通常会带 R 和 Z,比如“12_24_81”就是在说 1/2 码率、1944 码长。

码长 N子块大小 ZR=1/2R=2/3R=3/4R=5/6
64827324432486540
1296546488649721080
194481972129614581620

表格里每个格子是该码字的信息位长度 k,校验位长度就是 N-k。选码型时有一个容易忽略的约束:如果 PSDU 长度折算出来的信息比特不是表格里的标准 k,协议规定用“缩短 + 打孔”来适配,而不是自己去改 H。这点后面专门说。仿真阶段最省事的做法是先按标称 k 跑误码率,确认链路没问题,再引入缩短和打孔逻辑。

2.2 QC-LDPC 的基矩阵与循环移位:从 24 列小表展开成完整 H

QC-LDPC 的核心是“基矩阵 + 循环移位”。基矩阵是一个 mb×24 的整数矩阵,每个元素只告诉你怎么展开:-1 表示全零子块,非负整数 s 表示单位阵按某个固定方向循环移位 s 列。真正参与译码的校验矩阵 H 尺寸是 (mb×Z) 行、24×Z 列,对 1944 码长、1/2 码率来说就是 972×1944。用 Python 展开基矩阵的代码很短,但方向约定必须一致。

import numpy as np def qc_expand(Bm, Z): """ 将基矩阵 Bm 展开为校验矩阵 H。 Bm 的形状是 (mb, 24),元素 -1 表示全零块,s >= 0 表示循环移位量。 这里按"单位阵每一行向右循环移位 s 列"展开。 """ mb, nb = Bm.shape H = np.zeros((mb * Z, nb * Z), dtype=np.int8) for i in range(mb): for j in range(nb): s = Bm[i, j] if s >= 0: for r in range(Z): H[i * Z + r, j * Z + (r + s) % Z] = 1 return H

这段代码的逻辑很直白:单位阵的第 r 行原本在列 r 有个 1,向右移 s 列之后落到了 (r+s) mod Z,于是 H 里对应位置填 1。为什么非要强调方向?因为不同版本的源码包可能把 s 解释成“向左移”,或者把“循环移位”定义在列上而不是行上。若基矩阵表是从某个参考实现里抠出来的,而展开方向与译码器不一致,编译码两端对不上,结果是 BER 一直在 0.5 附近。我一般会在拿到别人的基矩阵表之后,先取第一个非 -1 元素单独展开成小矩阵目测一眼,确认方向再继续。

2.3 多码长与移位表:别指望一份基矩阵打天下

802.11n 的 LDPC 基矩阵并不是“一张表适配所有 Z”。协议里对同一码率,不同码长可能使用不同的循环移位值,这是因为大 Z 和小 Z 的循环移位模式需要分别设计,才能保证短环少、最小距离不塌。你从压缩包里解出来的基矩阵文件,如果只有 3 个,很可能分别对应 Z=27、54、81;如果只有 1 个,那多半是某个特定码长的,硬套到别的 Z 上会出问题。判断方法是看矩阵里是否有大于等于 Z 的元素——如果有,说明这张表不是给当前 Z 用的,展开前要单独处理或直接换文件。

3. 用校验矩阵 H 编码:从乘 G 矩阵到 RU 递推,仿真与硬件各走各的路

3.1 编码方案选型:为什么仿真阶段可以先乘稠密生成矩阵

LDPC 是线性分组码,理论上编码就是 c = G×s,G 是 H 的零空间基。802.11n 的 H 本身是稀疏的,但 G 通常不是,乘 G 矩阵的复杂度是 O(N²),硬件上没人这么干,协议里的编码算法利用校验位部分的双对角结构做递推,复杂度只有 O(N)。可是在 MATLAB 或 Python 仿真里,1944 码长乘 G 矩阵只需要几百微秒,一次蒙特卡洛仿真要编几百万个码字,这个开销仍然可以接受,所以我的做法是:协议帧格式调试阶段先用 G 矩阵编码,把“编码过程本身”当作已知正确,专心查调制和信道;等链路通了再换成结构化编码。

3.2 直接从 H 求系统生成矩阵:可直接运行的最小实现

下面这段代码接受一个展开好的 H,先检查校验位对应的右侧 mb 列是否满秩,然后做 GF(2) 行化简,把右侧化为单位阵,左侧就得到系统形式的 P,最终拼出生成矩阵 G。编码结果满足 H×c ≡ 0。

def systematic_g_from_H(H): """ 输入 H:mb × n 的二元校验矩阵,列顺序必须为 [系统位列 | 校验位列]。 返回 G:n × k 的系统生成矩阵,k = n - mb。 """ A = H.copy() % 2 mb, n = A.shape k = n - mb # 把右测 mb 列化简为单位阵 for i in range(mb): col = k + i pivot = np.nonzero(A[i:, col])[0] if len(pivot) == 0: raise ValueError("校验位部分奇异,无法构造系统 G") pivot = pivot[0] + i A[[i, pivot]] = A[[pivot, i]] for r in range(mb): if r != i and A[r, col] == 1: A[r] = (A[r] + A[i]) % 2 # 此时 A = [P | I_mb],系统码校验位 p = P @ s P = A[:, :k] % 2 G = np.vstack([np.eye(k, dtype=np.int8), P]) return G def encode_ldpc(H, s): G = systematic_g_from_H(H) n, k = G.shape s = np.asarray(s).reshape(k, 1) c = (G @ s) % 2 assert np.allclose((H @ c) % 2, 0), "编码结果不满足 H c = 0" return c.flatten()

这段代码里有三个地方要留意。第一,列顺序必须是 [系统位 | 校验位];802.11n 的基矩阵展开后天然是这个顺序,但如果你从别处拿到一个打乱过的 H,要先做列重排。第二,行化简只看校验位列找主元,如果校验位部分本身奇异,说明 H 不是系统码形式或者基矩阵 Z 不匹配,直接抛异常比带病跑仿真好。第三,生成矩阵 G 是稠密的,n=1944、k=972 时大约 1.9MB 内存,完全够用,不要拿 10 万码长的矩阵来套这个函数。

3.3 打孔与缩短:从标准信息位到任意包长的一步

实际 WiFi 帧长不可能永远等于标准表中的 k,协议通过缩短和打孔把 LDPC 码字“裁剪”成需要的长度。常见做法是:信息比特不足 k 时,在系统位前面补 0,编码后这些补的 0 不发送,对应缩短位;如果缩短后码字仍然太长,再从校验位里按协议规则打掉若干比特。接收端必须知道哪些位置被缩短、哪些被打孔,把打孔位置的 LLR 置 0,把缩短位置当作已知 0 参与译码。我见过最隐蔽的 bug 是发射端缩短和打孔的顺序与接收端相反,导致译码器拿错软信息,误码率平台在 1e-2 下不来。建议仿真时用一个 dict 把缩短位索引和打孔位索引随码字一起传给接收端,先别省这个传参。

4. 译码实现:从和积 BP 到最小和 NMS,收敛速度与实现代价怎么换算

4.1 变量节点与校验节点的消息传递:先写对 LLR 的符号约定

LDPC 译码的本质是因子图上的置信传播。变量节点保存信道软信息 L(BPSK 下 L = 2y/σ²),校验节点负责把“其它变量节点的消息”融合成约束。和积算法用 tanh 规则精确计算校验消息,硬件实现几乎都换成最小和近似,因为 tanh 的查表和乘法太贵。最小和近似的校验节点更新规则是:消息符号等于所有输入符号的乘积,幅度等于所有输入幅度里最小的那一个。归一化最小和(NMS)在此基础上乘一个 0.75 左右的系数,用来补偿近似误差。

LLR 符号约定是第一个坑。我习惯用 0→+1、1→-1 映射,也就是 LLR 为负表示判决为 1。有些源码包用相反的约定,译码器内部符号统一还好,但如果你把外部模块算好的 LLR 直接灌进来,符号反了的表现很典型:编码正确、信道无误,译码输出却全反,BER 在 0.5 附近一动不动。

4.2 可直接运行的最小和译码函数:NMS 与参数说明

下面这段实现是给理解消息更新顺序用的,没有做向量化,适合 N 在几百到几千的码字。工程上要提速的话,把 H 的稀疏结构预先存成邻接表,再用 numpy 对校验节点批量更新。

def min_sum_decode(H, llr, max_iter=20, alpha=0.75): """ H:mb × n 校验矩阵 llr:长度为 n 的信道对数似然比 max_iter:最大迭代次数 alpha:归一化最小和系数,通常取 0.75~0.8 返回:硬判决比特(0/1) """ mb, n = H.shape vn_nb = [np.nonzero(H[:, j])[0] for j in range(n)] # 每个变量节点连接的校验节点 cn_nb = [np.nonzero(H[i, :])[0] for i in range(mb)] # 每个校验节点连接的变量节点 v2c = np.zeros((mb, n)) # 变量节点发给校验节点的消息 c2v = np.zeros((mb, n)) # 校验节点发给变量节点的消息 for _ in range(max_iter): # 变量节点更新:信道 LLR + 除目标校验节点外的所有 c2v for j in range(n): for i in vn_nb[j]: msg = llr[j] + c2v[np.delete(vn_nb[j], np.where(vn_nb[j] == i)[0]), j].sum() v2c[i, j] = msg # 校验节点更新:归一化最小和 for i in range(mb): for j in cn_nb[i]: others = np.delete(cn_nb[i], np.where(cn_nb[i] == j)[0]) signs = np.sign(v2c[others, i]) mags = np.abs(v2c[others, i]) prod_sign = np.prod(signs) min_mag = np.min(mags) if len(mags) else 0 c2v[i, j] = alpha * prod_sign * min_mag # 总后验 LLR 与硬判决 decisions = np.zeros(n, dtype=np.int8) for j in range(n): total = llr[j] + c2v[vn_nb[j], j].sum() decisions[j] = 1 if total < 0 else 0 return decisions

参数上最需要关心的是 max_iter 和 alpha。max_iter 在浮点仿真里取 20 左右就够,继续加大收益很小,但会让仿真时间翻倍。alpha 取 0.75 是经验值,取值太小在高 SNR 区会出现平台期,取值太大则退化成标准最小和,性能损失约 0.2dB。如果你的误码率曲线比理论曲线差出 0.5dB 以上,先怀疑 alpha 而不是信道模型。

4.3 迭代终止条件与量化位宽:浮点仿真和定点实现的差距从哪来

浮点仿真里判断译码成功有两种做法:一是达到最大迭代次数就停,二是每次迭代后做一次硬判决并用 H 校验,满足 H×c=0 提前终止。仿真阶段用第二种能省不少时间,也不会影响误码率曲线。定点实现则是另一套逻辑:LLR 和消息都要量化,常见做法是 Q5.2 或者 Q4.3,符号位加整数位加小数位总共 6~8 bit。位宽每减少 1 bit,性能可能掉 0.1~0.3dB,但 FPGA 资源能省下不少,这个权衡要靠扫位宽决定,别听别人说 6bit 够就照抄。

5. 802.11n LDPC 落地避坑:从 H 矩阵校验到性能曲线的 5 条排查记录

5.1 现象一:译码输出全错,BER 稳定在 0.5

原因是最常见的“H 矩阵和码字没对齐”。可能是基矩阵展开方向反了,也可能是信息位长度 k 没对上,系统码列位置错了一位。解决方法是先做一个确定性测试:构造一个全 0 信息位序列,用编码函数生成码字,然后直接过译码器,不断信道,LLR 设成很大,看输出是否全 0。这一步不过,后面所有统计都没意义。

5.2 现象二:迭代次数增加但误码率出现平台期

这通常不是迭代次数不够,而是 LLR 初始化符号反了或者幅度算错。BPSK 下,如果 y 的符号和映射反了,译码器会把高置信度的错误比特当成可靠信息,迭代越多错误越固化。排查时把 SNR 设到 -5dB 以下,这时候信道信息很弱,符号错误的影响不大,误码率应接近 0.5;再把 SNR 提到 10dB,正确符号的 LLR 幅度足够大,误码率应迅速掉到 1e-4 以下。如果趋势不对,先把 LLR 计算独立出来单测。

5.3 现象三:C 代码仿真速度比 MATLAB 快不起来

很多人以为换成 C 就一定快,结果发现瓶颈在译码器里对 H 的二维数组扫描,而不是算法本身。原因是没有把 H 预处理成邻接表,每条消息更新都在重复遍历全零元素。解决方法是编译码前把 H 转成 CSR 格式或者三张索引表,分别存列索引、行起始、非零值位置;迭代循环只访问非零位置,1944 码长下速度能提升一个数量级。

5.4 现象四:FPGA 上性能比仿真差 1dB 以上

先查打孔位和缩短位有没有按协议对齐。仿真里你可能随手删了最后几个校验比特,但协议规定的打孔位置往往在固定列块,删错位置等于给校验方程挖洞。另一个原因是定点溢出:校验节点消息在迭代中可能超过量化范围,NMS 结果被截断后性能会掉。建议把定点译码器的每级消息最大值打出来,和浮点对比,确认饱和点。

5.5 现象五:同一个 H 和相同 SNR,两个版本源码性能曲线不一致

这类玄学问题九成出在归一化因子 alpha 被改过,或者校验节点更新的“除自身外”逻辑写错。标准最小和更新时,目标变量节点自己的消息必须排除在外;有些实现图省事,把自身消息也放进去参与 min 运算,性能会莫名其妙掉 0.3~0.5dB。逐行对比更新公式,再在 H 退化为单校验节点时做单元测试,最容易暴露这类问题。

6. 进阶验证技巧:一套自建 802.11n LDPC 链路,怎么判断实现是懂行的

验证 LDPC 实现是否真正按 802.11n 协议工作,不能只看 BER 曲线好看。我习惯按三步走。第一步,先做无信道测试:编码 → 过 AWGN 之前的 LLR 直接置大数 → 译码,输出必须和输入信息位完全一致,这一步排除编译码逻辑错误。第二步,把 H 矩阵的每一行单独拎出来,随机生成满足该行校验关系的码字,验证译码器在单校验节点下能正确更新,这能抓住消息索引错位的隐蔽 bug。第三步才上完整 AWGN 链路,至少跑 100 帧、统计到 1e-4 以下,看瀑布区斜率是否正常。

链路中值得额外检查的是缩短和打孔位置的记录。我会把缩短位索引和打孔位索引作为元数据随帧传递,接收端构造 LLR 时对打孔位填 0,对缩短位填一个很大的正 LLR 表示已知 0。这个细节没做对,短帧的误码率会比长帧差很多,但长帧曲线还正常,很容易被当成信道波动忽略掉。最后再核对一次 H×c=0 的恒等式:

assert np.allclose((H @ c) % 2, 0), "H @ c != 0"

这个断言我每次换 H 矩阵或者换基矩阵展开方向都会跑一遍。跑完这些检查,你的实现至少是自洽的,接下来再去调 alpha 和迭代次数才有意义。这些年调 LDPC,我最大的教训就是不要先动译码器参数,先把矩阵方向和 LLR 约定查清楚——前者是协议问题,后者是习惯问题,定下来之后,误码率曲线往往自己就对了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询