☰
LDPC信道编解码:稀疏矩阵与BP译码的MATLAB实现
2026/10/31 5:14:32 网站建设 项目流程

简介:压缩包内提供LDPC码(低密度奇偶校验码)的MATLAB信道编解码实现源码,面向通信工程、电子信息类专业学生及需要掌握纠错编码技术的研发人员。LDPC码因接近香农限、支持并行迭代解码、错误地板低等特点,在5G通信、深空通信和存储系统中广泛采用。包体仅4KB,含1个M文件,核心代码围绕稀疏校验矩阵构造、生成矩阵编码以及基于Tanner图的信念传播(BP)迭代解码展开,可直接运行评估误码率性能。通过研读该程序,读者能将Gallager提出的LDPC原理与MATLAB工程实践对应起来,理解消息传递解码的具体流程,并在此基础上修改矩阵参数、信道条件或迭代次数,扩展仿真实验。目前已有155人学习下载,适合作为课程设计、毕业设计或信道编码自学的入门参考。

1. LDPC信道编解码:一张稀疏矩阵如何逼近香农限

做通信系统仿真的人,迟早会撞上LDPC。不是因为论文里都在写,而是因为当你把Turbo码跑完一轮对比之后,发现LDPC在高斯信道下的纠错能力确实能逼近香农限,而且解码器天生适合并行。这套LDPC.rar里放着一个核心文件LDPC.m,对应信道编解码中最常用的一条主流程:稀疏校验矩阵H的构造、编码端的生成矩阵G、译码端的BP迭代。本文把这套代码拆开讲,从矩阵怎么生成,到编解码函数怎么调,再到哪些地方最容易跑出错误的BER曲线。适合正在做MATLAB通信仿真的研究生,也适合刚接触信道编码、想从代码层面理解LDPC的从业者。

2. 构造与建模:校验矩阵H为什么必须“稀疏”

2.1 低密度到底低到什么程度

LDPC矩阵的所谓低密度,是指校验矩阵H中1的个数远小于矩阵元素总量。以常用的码率0.5、码长1008的规则LDPC码为例,列重通常取3,行重取6,那么H矩阵的尺寸是504×1008,总元素50万左右,其中1的个数大约3024个,密度约0.6%。对比一个随机矩阵,密度是50%,这就是本质区别。

密度低带来两个直接收益。第一,Tanner图上的边少,消息传递算法每次迭代的计算量小;第二,矩阵可以在内存里用稀疏存储,1008码长的矩阵全存成double也才不到1MB,但如果用稠密矩阵去存5000码长的H,内存就开始吃紧了。所以判断一个MATLAB脚本是否专业,先看它有没有用sparse类型存H矩阵,这是一个很朴素的检验标准。

2.2 用MATLAB生成规则LDPC矩阵

代码包里LDPC.m的矩阵构造段一般有两种写法:一种直接从通信工具箱调用ldpcQuasiCyclicMatrix或dvbs2ldpc,另一种是自己写随机构造。自己写的话,常见做法是给定列重dv和行重dc,按列分配每个校验方程参与的位置。下面给出核心逻辑:

% 参数定义 n = 1008; % 码长 k = 504; % 信息位长度 dv = 3; % 列重:每个变量节点连接的校验节点数 dc = 6; % 行重:每个校验节点连接的变量节点数 m = n - k; % 校验位长度,504 H = zeros(m, n); col_pos = zeros(dv, n); % 记录每一列的1所在行位置 % 第一阶段:按列分配行位置,保证列重为dv for col = 1:n % 从m行中随机选dv个不同行 rows = randperm(m, dv); for j = 1:dv H(rows(j), col) = 1; col_pos(j, col) = rows(j); end end

这段代码的问题在于行重不一定均匀,某些行可能分配到的1远多于dc。实际工程需要再加一层“调整循环”,逐行统计行重,把超重的行上的1挪到行重不足的行上,直到每行1的数量稳定在dc附近。否则后面的BP译码在校验节点更新时,不同校验方程的输入边数差异过大,译码收敛速度会明显变慢。

2.3 Tanner图与短环的隐患

有了H矩阵,可以构建对应的Tanner图:左侧n个变量节点,右侧m个校验节点,H中非零元素代表边。Tanner图决定了LDPC码的译码性能,图中最小环长,也就是围长,直接影响了BP迭代的独立性假设。

如果围长为4,意味着两个变量节点之间有一条长度为4的闭合路径,BP译码时这两个节点之间会相互传递重复信息,使得迭代陷入“自我强化”的循环,误码率曲线在高信噪比区间出现不该有的平台。检查围长最直接的办法是比对任意两列的内积:如果H中任意两列的列内积都不超过1,则围长至少为6。

% 检查围长为4的环 girth_ok = true; for c1 = 1:n-1 for c2 = c1+1:n if sum(H(:,c1) & H(:,c2)) > 1 girth_ok = false; break; end end if ~girth_ok, break; end end if ~girth_ok disp('存在4环,需要重新构造矩阵'); else disp('围长至少为6,可以进行编码'); end

注意这段检查的双重循环复杂度是O(n²),码长1008时大概50万次判断,可以接受。但如果码长到64800,这个双重循环就不能再用MATLAB脚本直接跑,应该先分块或者用图论工具生成候选矩阵再本地校验。我自己一般会先跑一次4环检查,通过后再正式用于仿真,绝不跳过这一步。

3. 编码端实现:从H矩阵求出生成矩阵G的完整流程

3.1 为什么要先做高斯消元

LDPC的直接编码并不复杂,难点在于把H矩阵变换成可系统化的形式。如果H已经是系统形式,比如H = [A | I],则生成矩阵G = [I | A'],编码时直接把信息位加上A′×信息位的模2结果,就是完整的码字。但随机构造的H几乎不可能天生就是系统形式,所以通常先对H做高斯消元,把H变成系统形式,再从系统形式推导G。

% 高斯消元:把H转换成系统形式 H_sys = [P | I] H_sys = H; m_sys = m; % 按列主元消元,保证前m列可逆 for col = 1:m % 寻找主元行 pivot = find(H_sys(col:m, col), 1); if isempty(pivot) error('H矩阵前m列不可逆,无法构造系统校验矩阵'); end pivot = pivot + col - 1; % 交换行 H_sys([col, pivot], :) = H_sys([pivot, col], :); % 消去其他行对应列 rows_to_eliminate = find(H_sys(:, col))'; for r = rows_to_eliminate if r ~= col H_sys(r, :) = mod(H_sys(r, :) + H_sys(col, :), 2); end end end

这段代码的核心逻辑是逐列寻找主元,然后把主元所在行交换到当前行位置,再把该列其他行上的1全部消掉。消元结束后,H_sys的前m列构成单位阵,后面n-m列就是P矩阵。如果消元过程中主元找不到,说明前m列线性相关,H对应的码率不成立,要重新生成H,这也是LDPC矩阵构造中常见的翻车点之一。

3.2 生成矩阵G的组装

系统形式校验矩阵得到后,生成矩阵G的尺寸是k×n,G = [I_k | P']。在MATLAB里按位拼接:

% 从系统形式的H_sys提取P矩阵 % H_sys = [P | I],P的尺寸是 m × k P = H_sys(:, 1:k); I_k = eye(k); % 组装生成矩阵 G = [I | P'],P' 尺寸为 k × m G = mod([I_k, P'], 2); % 验证:GH^T 应等于零矩阵 verification = mod(G * H_sys', 2); if any(verification(:)) error('GH^T 不等于0,编码矩阵构造出错'); end

注意这里的P′不是MATLAB的共轭转置符,确实就是普通转置。验证GH^T是否为零矩阵这步不能省,一旦有非零元素,说明前面消元编码过程中出现了符号错误,或者H矩阵本身行重分布有问题,需要回溯前一步检查。

3.3 编码效率与存储现实

拿到G矩阵后,编码本身很简单:coded = mod(info * G, 2)。但有一个现实问题:随机构造的H经过高斯消元后,P矩阵往往会丧失稀疏性。P里1的数量可能达到k×m的一半,编码复杂度从O(n)变成O(n²),码长越长越明显。

如果你只是做1000码长的仿真,这个开销可以接受。但如果码长上万,常见做法是用LU分解预计算,或者直接改用QC-LDPC结构,从构造阶段就保证H本身是准循环形式,编码时用移位寄存器完成,牺牲灵活性换编码速度。我一般在小规模课题仿真中用G矩阵直乘,涉及工程落地的项目则直接上QC-LDPC。

3.4 调制映射不是编码的一部分

很多初学者会把BPSK映射也算进编码流程,这是没必要的。LDPC编码输出的是0/1比特序列,调制是把比特映射成信号电平。用BPSK时,通常把0映射成+1,1映射成-1,或者反过来。这个映射必须在编码和信道之间独立完成,如果映射符号方向反了,BP译码初始LLR的符号会全反,误码率在0dB附近就会表现出完全不可收敛的异常曲线。

4. 译码端实现:对数域BP消息传递算法与迭代停止条件

4.1 对数似然比的基本形式

BP译码有概率域和对数域两种实现方式。概率域直觉上更直观,但数值上容易出现下溢,因为很多接近1的概率相乘后精度丢失。对数域用LLR表示信道的置信度,数值稳定,这也是LDPC.m中译码函数最常见的实现形式。

初始化阶段需要把接收符号映射成LLR。对BPSK + AWGN信道,接收值y对应的LLR初始化公式是2*y/σ²,其中σ²是噪声方差。这个公式的来源是对数似然比的定义化简,符号方向取决于你的星座映射。噪声方差来自信噪比设定,不要用经验的常数去近似,否则不同信噪比下的性能比较就失真了。

function [decoded_bits, iter_used] = decodeLDPC_BP(received_symbols, H, max_iter, sigma2) % 解码前的LLR初始化 LLR_ch = 2 * received_symbols / sigma2; [m, n] = size(H); % 找到每个变量节点相连的校验节点索引 var_to_check = cell(1, n); check_to_var = cell(1, m); for v = 1:n var_to_check{v} = find(H(:, v))'; end for c = 1:m check_to_var{c} = find(H(c, :)); end % 初始化:变量节点保存信道LLR Lv = LLR_ch; % 校验节点消息初始化为0 Lc = cell(m, 1); for c = 1:m Lc{c} = zeros(1, length(check_to_var{c})); end for iter = 1:max_iter % ===== 变量节点更新 ===== for v = 1:n neighbors = var_to_check{v}; for idx = 1:length(neighbors) c = neighbors(idx); % 收集除自身外的全部消息 msg_sum = LLR_ch(v); for j = 1:length(neighbors) if neighbors(j) ~= c msg_sum = msg_sum + Lc{neighbors(j)}(find(check_to_var{neighbors(j)} == v)); end end % 暂存到临时矩阵 var_msg{v}(idx) = msg_sum; end end % ===== 校验节点更新 ===== for c = 1:m neighbors = check_to_var{c}; for idx = 1:length(neighbors) v = neighbors(idx); % tanh 域乘积:收集除自身外的变量消息 prod = 1; for j = 1:length(neighbors) if neighbors(j) ~= v % var_msg 需要正确索引 vidx = find(var_to_check{neighbors(j)} == c); prod = prod * tanh(var_msg{neighbors(j)}(vidx) / 2); end end % 防止数值饱和 if abs(prod) > 1 - 1e-15 prod = sign(prod) * (1 - 1e-15); end Lc{c}(idx) = 2 * atanh(prod); end end % ===== 硬判决与停止准则 ===== L_total = LLR_ch; for v = 1:n for j = 1:length(var_to_check{v}) L_total(v) = L_total(v) + Lc{var_to_check{v}(j)}(find(check_to_var{var_to_check{v}(j)} == v)); end end hard_bits = double(L_total < 0); if mod(H * hard_bits', 2) == 0 decoded_bits = hard_bits; iter_used = iter; return; end end decoded_bits = hard_bits; iter_used = max_iter; end

这段代码是BP译码的完整骨架,注意几个关键点:

tanh域更新是对数域BP实现中数值最稳定的方案,但它的代价是每个校验节点内部要做两次循环,复杂度较高。如果追求速度,可以把tanh和atanh的运算替换为查表法,用定点化处理,这是工程优化层面的内容。

校验节点更新中的prod变量是tanh(LLR/2)的乘积,校验节点消息本质上是这个乘积的atanh变换。当prod的绝对值接近1时,atanh会产生很大的值,代码里特意做了饱和处理,防止数值溢出到Inf。

停止准则采用的是校验方程判定:把硬判决结果乘回H矩阵,如果0向量,说明当前判决满足全部校验方程,译码成功,提前终止迭代。这个准则在低信噪比下很少触发,大部分时候会跑满max_iter,仿真时迭代次数设为20到50比较合理。设太大纯属浪费算力,设太小则性能损失明显。

4.2 硬判决与软判决输出的选择

LDPC译码输出有两种形式:一种直接输出0/1比特,另一种输出LLR软信息。在级联系统中,软信息对后续模块(比如外码译码器)价值更大。LDPC.m中的译码函数通常返回硬判决比特,这是最简单的方案,但如果你在做一个Turbo均衡器或者迭代接收机,应该把L_total也作为函数返回值,方便下一级直接使用。

从工程角度看,硬判决输出更容易验证正确性,毕竟有校验方程可以实时检查。软输出则需要额外的置信度评估手段,一般用互信息或者外信息转移图来评价,这是更高级的接收机设计话题。

4.3 迭代停止条件的工程取舍

max_iter的选择要同时考虑性能和运算量。门限50次时,大多数帧在5到15次迭代内收敛;高信噪比下收敛更快。把max_iter从20提高到50,性能增益主要体现在低信噪比区间,大约能有0.2到0.3dB的改善,但运算量可能翻倍。仿真时可以先跑一次20迭代的BER曲线,再看曲线尾部是否已经出现平台,如果平台明显,再加迭代次数重新跑一遍。

5. 避坑指南:LDPC.M复现中常见的五个翻车点

5.1 高斯消元后GH^T不为零

现象:运行编码矩阵验证代码时,verification矩阵里出现大量非零元素。

原因:最常见的是高斯消元时用了round而不是mod做模2运算,浮点数0.5被round成1,破坏了模2算术规则。另一种原因是消元过程中主元行被提前消掉了,检查消元循环的边界条件。

解决:把消元过程中的所有加法取模操作统一写成mod(x + y, 2),不要用rem也不要直接相加后再mod一次。数学上模2加法等价于异或,更稳妥的做法是用xor函数逐位处理,彻底避开浮点误差。

5.2 白噪声功率算错导致LLR方向相反

现象:仿真曲线在0dB附近的BER就已经接近0.5,改变信噪比后曲线却几乎不变。

原因:初始化LLR用了-2*y/σ²,符号与星座映射不匹配。BPSK映射若是0→+1、1→-1,那么接收值越大代表越可能是0,LLR应为正值,公式是+2*y/σ²。一旦符号反了,解出来的硬判决比特全部取反,校验方程永远不满足。

解决:写一个快速自检函数,不经过信道直接编码并译码,接收符号就是发射符号,加上高斯噪声前先验证LLR初始化符号的正确性。这一步不做后续仿真必翻车。

5.3 稀疏矩阵存储被稠密化

现象:码长2000时运行速度还能接受,换到码长10000后内存直接爆炸。

原因:代码里某个操作符对sparse矩阵做了隐式转换。典型操作包括H_sys([col, pivot], :)的赋值,如果用逻辑索引对sparse矩阵做置换,有时会触发full转换;find返回值是double类型的下标数组,这也是正常的,问题主要出在进行矩阵乘法时两个操作数一个稠密一个稀疏。

解决:凡是涉及H矩阵的操作,统一用sparse类型存储;矩阵乘法保持mod(sparse_G * sparse_H', 2)的调用方式。如果非要在循环里逐行修改sparse矩阵,先转换成普通索引结构,循环结束再一次性转回sparse。

5.4 tanh计算导致消息饱和

现象:高信噪比区间,BP迭代到第3轮突然出现NaN,或者BER曲线出现异常下坠然后回升。

原因:tanh(LLR/2)在LLR较大时趋近于±1,多个接近1的数相乘后,atanh(prod)在prod=1时发散到Inf。标准库的atanh对边界处理并不总是返回Inf,有时会返回NaN。

解决:在每次校验节点更新后增加饱和判断,把prod限制在±(1-1e-15)范围内。这是一个经验值,工程里也常取1-1e-10,差别不大。关键是必须有这层保护,否则高信噪比下必出现数值发散。

5.5 4环未查导致译码平台

现象:BER曲线在中高信噪比区间出现明显平台,与理论性能差异显著,加大迭代次数无法改善。

原因:随机构造的H矩阵存在大量围长为4的短环,BP译码的独立性假设被破坏,迭代收敛到了错误的局部最优状态。

解决:矩阵构造完成后先跑一遍2.3节中的4环检测。检测通过再往下走。如果是标准码库里的H矩阵,比如DVB-S2和5G NR的QC-LDPC矩阵,这个检测可以跳过,因为设计阶段已经保证了围长要求。

6. BER曲线的蒙特卡洛统计:从单帧译码到完整仿真的串联技巧

做一个能发论文或者支撑工程结论的BER曲线,核心是搞清楚需要跑多少帧。统计上误差小于10%的置信区间对应的最小帧数约等于100/(目标BER×误帧率)左右。比如想测BER为1e-4,单帧误码数少于1个时误差会很大,至少需要数万帧的统计量。仿真脚本的数据规模往往被低估,实际跑到高信噪比段时时间长是正常的,不是代码配置有问题。

用代码包里的LDPC.m做BER仿真时,完整流程写成一个main_ber.m文件。外层循环遍历一个信噪比数组,内层帧循环调用编码函数、加噪声、调用译码函数,最后统计误码总数除以发送的总比特数。期间需要巧用每次迭代时校验方程是否通过这个信息,它能告诉你译码器的收敛行为。

% main_ber.m 核心段 EbN0_dB = 0:0.5:5; max_iter = 30; num_frames = 5000; ber = zeros(size(EbN0_dB)); for snr_idx = 1:length(EbN0_dB) EbN0 = 10^(EbN0_dB(snr_idx)/10); % 假设码率0.5,得到噪声方差 sigma2 = 1 / (2 * 0.5 * EbN0); bit_errors = 0; total_bits = 0; for frame = 1:num_frames info_bits = randi([0 1], 1, k); coded = mod(info_bits * G_full, 2); % BPSK映射:0 -> +1, 1 -> -1 tx = 1 - 2 * coded; noise = sqrt(sigma2) * randn(size(tx)); rx = tx + noise; decoded = decodeLDPC_BP(rx, H_sparse, max_iter, sigma2); bit_errors = bit_errors + sum(decoded(1:k) ~= info_bits); total_bits = total_bits + k; end ber(snr_idx) = bit_errors / total_bits; end

这段仿真中总比特数以信息位k计算,如果是编码后比特数得到的是误码率而非BER。噪声方差推到公式的时候要盯紧码率项,我这里用了0.5的码率来计算每比特能量与噪声功率的比值关系,跑曲线时务必与编码参数保持一致。

从实用的角度看,这套LDPC.m的代码包最值钱的地方在于它把完整链路摆在一起了,矩阵生成、编码验证、信道加噪、迭代译码、BER统计,全部在一个文件里能跑通,比MATLAB通信工具箱自带函数的黑匣子模式更适合学习。工具箱里的ldpcEncode和ldpcDecode做性能对比测试时用起来省事,但遇到矩阵构造特殊需求时还得回到这份手写代码的基础上改。从那以后我每次仿真都会强制把校验方程验证和4环检测写进主流程,宁可多花这几秒,也不愿第二天醒来面对一堆不合理曲线重新排查。希望帮到你。

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

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

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

立即咨询