简介:这份 MATLAB 代码包面向通信与信道编码学习者,聚焦 LDPC 编解码仿真与性能分析,尤其适合正在学习 LDPC 原理、开展课程设计或需要参考实现的本科生、研究生及工程师。资源共 7 个 m 文件,压缩包仅 8KB,体积小巧、结构清晰,便于快速阅读与二次修改。代码内包含校验矩阵生成、和积(SPA)、最小和(MS)、归一化最小和(NMS)、偏移最小和(OMS)等多种译码器,以及误码率测试脚本,覆盖常见迭代译码算法;通过误码率测试可观察不同信噪比下的性能差异,也能直接对比各译码策略在性能与复杂度上的折中。已有 280 人学习,能帮助读者快速搭建从矩阵构造到仿真验证的完整 LDPC 链路,深入理解不同迭代译码算法的收敛特性与实现细节,作为算法教学演示、课程设计参考或工程预研脚本均很合适。
1. LDPC译码为什么绕不开NMS
做接收机链路的人都有这个经验:LDPC译码器的吞吐率直接决定整条链路能不能跑满,而SPA在AWGN信道下虽然能逼近香农限,但tanh域的双向变换对硬件极其不友好,FPGA上每周期只能处理很少几个校验节点。把校验节点更新换成min运算之后,性能损耗大概在0.3 dB量级,但逻辑资源直接省一半以上,这让NMS成了吞吐敏感场景下的默认选择。ldpc-decode这个MATLAB包把SPA、MS、NMS、OMS四种译码算法和基矩阵生成、BER仿真链路放在一起,适合需要快速对比算法差异、或者打算把译码器落成硬件实现的工程师。你不需要自己造校验矩阵,也不需要从零搭蒙特卡洛仿真,直接跑ldpcBER就能拿到四条对比曲线,再按项目指标挑算法或做定点化改造。
2. 从基矩阵到译码器:LDPC核心模块的MATLAB拆解
2.1 makeLdpc.m:基矩阵扩展出校验矩阵
LDPC的校验矩阵 (H) 决定了码字的性能上限。这个包里用base41作为基矩阵,41对应基矩阵的列数,扩展因子 (Z) 决定最终码长,这是典型QC-LDPC结构的做法。QC-LDPC的优势在于校验矩阵可以由循环移位子矩阵拼装,生成逻辑简单,译码时也容易做并行调度。makeLdpc.m做的工作就是把基矩阵中每个元素扩展成 (Z \times Z) 的循环置换矩阵或全零矩阵。
% makeLdpc.m 的典型扩展逻辑(QC-LDPC) function H = makeLdpc(base, Z) [mb, nb] = size(base); % 基矩阵行数、列数 H = zeros(mb*Z, nb*Z); % 预分配扩展后的稀疏矩阵 for i = 1:mb for j = 1:nb shift = base(i, j); % 移位值,-1 表示全零块 if shift >= 0 P = circshift(eye(Z), -shift); H((i-1)*Z+1:i*Z, (j-1)*Z+1:j*Z) = P; end end end end这段代码里,shift来自基矩阵,控制单位阵循环移位多少位。circshift(eye(Z), -shift)生成一个每行只有1个1的置换矩阵,负号是为了和译码时变量节点、校验节点的索引习惯对齐。全零块用-1标记,这在QC-LDPC里是标准做法,目的是避免围长太短导致错误平层。扩展完成后,(H) 的行数等于校验位长度,列数等于码长,行重和列重都较小,保持了稀疏性。
实际调试时,我一般会打印sum(H, 2)检查行重是否均匀,如果某些行重差异过大,说明基矩阵设计有缺陷,后续译码时会出现错误平层提前出现的问题。
2.2 makeParityChk.m:校验关系的验证工具
生成校验矩阵之后,第一步要确认它能用。makeParityChk.m做的就是这件事:对任意合法码字 (c),验证 (H \cdot c^T \bmod 2 = 0) 是否成立。
% makeParityChk.m:校验 LDPC 码字合法性 function s = makeParityChk(H, c) s = mod(H * c(:), 2); % 校验子向量 if all(s == 0) fprintf('校验通过:H*c = 0\n'); else fprintf('校验失败:存在 %d 个不满足的校验方程\n', sum(s)); end end这个函数的用途不只是仿真前的自检。在调试译码器时,如果译码结果始终不对,先用它判断问题是出在编码端还是译码端:把发送的码字和译码输出分别做一次校验,哪边不为零就是哪边的bug。另外,误码率仿真中判错帧的标准也是它——不是比较比特是否一致,而是先看校验子是否为零,因为LDPC译码器的输出有时会落在别的合法码字上,这时候比特全错但校验通过,属于不可检测错误,统计误帧时应该单列一类。
2.3 信道LLR初始化和译码主循环
译码开始前要把信道观测值转成对数似然比。BPSK调制下,(0 \to +1)、(1 \to -1),AWGN噪声方差为 (\sigma^2),LLR的计算式是:
[ L(c) = \ln \frac{P(c=0|y)}{P(c=1|y)} = \frac{2y}{\sigma^2} ]
% 信道LLR初始化:BPSK + AWGN function llr = initLLR(y, sigma2) llr = 2 * y / sigma2; % L(c) = 2y / σ² end这个式子看着简单,但有一个经常踩的坑:如果发送端做了能量归一化,即把 (E_b) 归一为1,那么噪声方差要根据实际信噪比换算,直接套一个固定值会让高信噪比区域的误码率曲线出现错误平层。正确做法是在每个SNR点计算 (\sigma^2 = 1 / (2 \cdot R \cdot 10^{SNR/10})),其中 (R) 是码率。
译码主循环的骨架是标准的迭代信念传播:
% 译码主循环:迭代直到校验通过或达到最大次数 for iter = 1:maxIter % 校验节点更新(具体算法见第3章) % 变量节点更新 % 计算后验LLR c_hat = double(llr_post < 0); % 硬判决 if all(mod(H * c_hat(:), 2) == 0) % 提前终止条件 break; end end每轮迭代里,先更新校验节点消息,再更新变量节点消息,然后算后验LLR做硬判决。提前终止的意义在低信噪比区间特别明显:大部分帧在第2、3轮迭代就能通过校验,只有少数坏帧吃满最大迭代次数,平均迭代次数远小于上限,这对吞吐率仿真很重要。
3. SPA、MS、NMS、OMS四种译码算法的实现差异
3.1 SPA校验节点更新:tanh域的完整消息传递
和积算法是LDPC译码的理论基准,校验节点的更新公式是:
[ L(r_{ji}) = 2 \cdot \tanh^{-1} \left( \prod_{i' \in N(j) \setminus i} \tanh \left( \frac{L(q_{i'j})}{2} \right) \right) ]
这里的 (N(j) \setminus i) 表示与校验节点 (j) 相连、除去变量节点 (i) 之外的所有邻居。MATLAB实现时,为了防止小数值下溢,一般先取对数再计算:
% decode_SPA.m:校验节点更新(对数域) function r_ji = updateCheckSPA(q_ij) % q_ij: 变量节点到校验节点的消息矩阵 alpha = sign(q_ij); beta = abs(q_ij); % 用 log(tanh(|x|/2)) 形式计算,避免下溢 phi = -log(tanh(beta / 2)); for j = 1:size(q_ij, 1) for i = 1:size(q_ij, 2) if q_ij(j, i) ~= 0 prod_phi = sum(phi(j, :)) - phi(j, i); prod_alpha = prod(alpha(j, :)) / alpha(j, i); r_ji(j, i) = prod_alpha * 2 * atanh(exp(-prod_phi)); end end end end这个双层循环只是演示消息更新逻辑,实际项目中没人用纯循环实现SPA,因为太慢了。MATLAB里用for遍历非零元素是常态,但至少要预先把 (H) 转成稀疏矩阵并只遍历非零位置,否则规模稍大就卡死。phi = -log(tanh(beta/2))这一步是把乘法变成加法,利用 (\tanh) 的性质把概率域的乘积转换到对数域求和,这是数值稳定的关键。
变量节点更新相对简单,就是把信道LLR和所有校验节点传回的消息加起来,再减去回传方向的那条:
[ L(q_{ij}) = L(c_i) + \sum_{j' \in M(i) \setminus j} L(r_{j'i}) ]
3.2 MS算法:用min运算替代tanh的双向变换
最小和算法的核心观察是:(\tanh) 域的消息乘积主要受绝对值最小的那一项支配。因此校验节点更新可以近似为:
[ L(\hat{r}{ji}) = \left( \prod{i' \in N(j) \setminus i} \text{sign}(L(q_{i'j})) \right) \cdot \min_{i' \in N(j) \setminus i} |L(q_{i'j})| ]
这个近似把原本需要做指数、对数、双曲函数的运算降到了只做比较和符号运算。实现上比SPA简单一个数量级:
% decode_MS.m:最小和校验节点更新 function r_ji = updateCheckMS(q_ij) % 符号项:所有邻居符号的乘积 alpha = sign(q_ij); % 幅度项:找最小绝对值 [min_val, min_idx] = min(abs(q_ij), [], 2); for j = 1:size(q_ij, 1) for i = 1:size(q_ij, 2) if q_ij(j, i) ~= 0 prod_alpha = prod(alpha(j, :)) / alpha(j, i); if i == min_idx(j) % 最小项排除后,取第二小值 min_other = min(abs(q_ij(j, [1:i-1, i+1:end]))); r_ji(j, i) = prod_alpha * min_other; else r_ji(j, i) = prod_alpha * min_val(j); end end end end end注意当排除的是最小项本身时,需要退而取第二小的绝对值。很多简化实现忽略了这个细节,直接把最小值乘上去,会导致对应边的消息被高估,解码性能下降约0.1到0.2 dB。我在实际仿真中发现,这个“第二小值”的处理对性能影响比归一化因子还大。
3.3 NMS和OMS:从近似到参数化修正
MS算法的0.3到0.5 dB性能损失,来源是min近似系统性地高估了校验节点消息的幅度。NMS和OMS分别用乘法和减法做修正,思路都很直接:
[ \text{NMS:} \quad L(\hat{r}{ji}) = \alpha \cdot \min{i'} |L(q_{i'j})| \cdot \prod_{i'} \text{sign}(L(q_{i'j})) ]
[ \text{OMS:} \quad L(\hat{r}{ji}) = \max\left( \min{i'} |L(q_{i'j})| - \beta, 0 \right) \cdot \prod_{i'} \text{sign}(L(q_{i'j})) ]
实现代码:
% decode_NMS.m:归一化最小和 function r_ji = updateCheckNMS(q_ij, alpha) % alpha 是归一化因子,典型值 0.75 ~ 0.85 r_ji = alpha * updateCheckMS(q_ij); % 在MS基础上乘系数 end% decode_OMS.m:偏移最小和 function r_ji = updateCheckOMS(q_ij, beta) % beta 是偏移量,典型值 0.5 ~ 1.0 r_ms = updateCheckMS(q_ij); r_ji = max(r_ms - beta, 0); % 截断到非负 endNMS里 (\alpha) 的作用是压缩min近似产生的高估消息,让译码器收敛更稳。(\alpha) 选太大等于没修正,几乎退回MS的性能;(\alpha) 太小会让消息衰减过度,迭代次数明显增加。OMS的 (\beta) 类似。两者推荐的初始值分别是 (\alpha=0.8)、(\beta=0.5),后续按具体码型的行重做细调,第5章具体说调法。
四种算法的运算量对比如下:
| 算法 | 校验节点运算 | 相对复杂度 | 性能损耗(相对SPA) |
|---|---|---|---|
| SPA | tanh / artanh | 1.0 | 基准 |
| MS | min + 符号 | 0.15~0.2 | 0.3~0.5 dB |
| NMS | min × α | 0.15~0.2 | 0.1~0.2 dB |
| OMS | min − β | 0.15~0.2 | 0.15~0.3 dB |
从表格看,NMS在复杂度和性能之间平衡得最好。OMS的优势在于定点实现时减法比乘法更省资源,而且 (\beta) 的取值对性能不如 (\alpha) 敏感,这在实际调参时是个隐藏好处。
4. 误码率仿真链路:从脚本到曲线
4.1 ldpcBER.m主脚本结构
误码率仿真的主脚本是把前面所有模块串起来的纽带。标准框架是:外层循环SNR,内层循环随机发送帧,每个SNR点统计到足够多的错误比特或错误帧才停。下面是典型结构:
% ldpcBER.m 主仿真框架 clear; clc; addpath('ldpc-decode'); Z = 81; % 扩展因子 H = makeLdpc(base41, Z); % 生成校验矩阵 [N, K] = size(H); % N码长,K信息位长度 snr_list = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5]; maxIter = 20; % 最大迭代次数 nFrames = 200; % 每SNR点帧数 for snr_idx = 1:length(snr_list) snr = snr_list(snr_idx); sigma2 = 1 / (2 * (K/N) * 10^(snr/10)); % 噪声方差 ber = 0; fer = 0; for frame = 1:nFrames % 1. 随机构造合法码字(全零码字+编码或直接搜索零空间) % 2. BPSK调制 % 3. 加高斯白噪声 % 4. 译码:decode_NMS / decode_MS / decode_OMS / decode_SPA % 5. 错帧和错比特统计 end end这里的 (K/N) 是码率,(N) 是码长。全零码字是LDPC仿真中一个经常被问到的问题:LDPC码是线性码,全零码字是合法码字,BPSK调制后对应全+1序列,由于AWGN信道是对称的,误码率表现和随机码字完全一致。所以很多仿真直接发全零码字,省去编码环节,只测译码器。严谨的做法是用全零码字做性能下界,再补几个随机码字点验证没有码字依赖。
4.2 关键参数配置与运行环境
实际跑仿真之前,先确认这几个参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 扩展因子 Z | 27~162 | 由目标码长除以基矩阵列数决定,Z越大码越长 |
| 最大迭代次数 | 10~30 | 超过后强制输出当前硬判决 |
| SNIR扫描范围 | 瀑布区±1 dB | 先用粗粒度找到瀑布区再加密 |
| 每点帧数 | 100~500 | 误码率目标低于1e-4时需要更多帧 |
| 归一化因子 α | 0.75(初值) | 按算法和H行重微调 |
运行方式是在MATLAB命令行里切到项目目录,直接执行脚本:
cd('ldpc-decode') ldpcBER如果发现低信噪比区间跑得极慢,优先检查是不是没有做提前终止——最大迭代次数设成20,但正常在第2、3轮就通过了,只有坏帧才吃满次数。可以在主循环里加计数器统计平均迭代次数,一般是5次以内。
4.3 四种算法的一次性对比实验
要做公平对比,需要在完全相同的信道条件和迭代次数下切换译码函数。在仿真脚本里加一层算法选择:
% 在帧循环内切换译码算法 switch algo case 'SPA' c_hat = decode_SPA(y, H, maxIter); case 'MS' c_hat = decode_MS(y, H, maxIter); case 'NMS' c_hat = decode_NMS(y, H, maxIter, alpha); case 'OMS' c_hat = decode_OMS(y, H, maxIter, beta); end一次跑出四条误码率曲线后,通常会看到这样的规律:SPA在瀑布区领先NMS约0.1 dB,MS掉得最多,NMS和OMS在低信噪比区间几乎重叠,但NMS在误码率低于1e-4后下降斜率更陡——因为OMS的 (\beta) 修正对小幅值消息的截断更激进,在高信噪比区域反而成了限制因素。如果NMS的坡度不够陡,问题多半出在 (\alpha) 偏大,让校验节点消息高估导致的错误平层提前。
5. 归一化因子的调优策略与硬件部署要点
NMS的 (\alpha) 不是拍脑袋定的,值得花时间用蒙特卡洛仿真去微调。做法是在目标SNR点(通常是误码率1e-3那个点)固定信道条件,把 (\alpha) 从0.6以0.02步进扫到0.95,然后对比误帧率。特别注意不要只看平均大概率,要看最差帧的表现——有些 (\alpha) 值平均误码率好看但长尾效应严重,在低密度奇偶校验码里这会表现为小概率的提前错误收敛。
工程部署时,(\alpha) 的乘法在硬件上不是直接用浮点乘,而是定点化:把 (\alpha=0.78125) 近似为 (25/32 = 0.78125),乘法变成一次左移5位和一次加法,代价可以忽略。如果选 (\alpha=0.75),甚至可以写成 (3/4),用移位和减法实现,连乘法器都省了。定点化后一定要在MATLAB里把浮点译码结果和定点结果逐轮对比LLR消息的差值,差太多的位置往往是饱和截断造成的,需要在变量节点更新阶段做钳位。最后可以用分层译码替换泛洪调度,把收敛迭代次数从20压到8到10轮。
从调试角度看,这五个文件的顺序也很重要:先跑通makeParityChk做校验验证,把H矩阵确认无误,再逐个验证译码函数。每次只改动一个模块,性能曲线出现异常时才能快速定位是消息更新还是参数设置的问题。
本文还有配套的精品资源,点击获取