简介:这是一份面向通信工程专业学生与研究人员的MATLAB仿真资源,围绕16QAM调制下HARQ与LDPC编码的完整实现展开,适合用于无线通信课程设计、毕业设计或相关科研入门。压缩包内共包含19个文件,其中16个.mat文件保存不同码率与码长的LDPC校验矩阵(如H_matrix_*系列),覆盖从短码到长码、不同码率的对照场景;3个.m脚本则分别负责校验矩阵生成(genH.m)、全调制方式下的LDPC编码发送(transmit_ldpc_all_mod.m)以及ARQ主循环(ARQmain_ldpc_all_mod.m),整体压缩后仅18KB,内容轻量易读。目前已有211人学习下载,代码结构清晰,便于按模块拆解与研究。通过这套程序,读者可快速掌握HARQ重传合并流程、LDPC编译码在16QAM链路中的调用方式,并借助多种校验矩阵对比不同码长、码率参数下的纠错性能,后续还可在此基础上修改调制阶数或替换校验矩阵进行扩展实验。
1. 在MATLAB里把HARQ、LDPC、16QAM串成一条链路:先解决“能跑”,再谈“跑得快”
做无线通信物理层仿真的人,大多经历过这个阶段:单独写一个16QAM解调、单独调一个LDPC译码器都能跑通,但一旦把HARQ重传机制加进去,代码就从“脚本”变成“状态机”。HARQ、LDPC、16QAM这三个词的组合在MATLAB生态里很常见,网上能搜到不少类似“HARQ_LDPC_16QAM_6”的压缩包,但真正打开后你会发现,大部分资源要么只给出发送端代码,要么只把HARQ做成无脑循环重发,完全没考虑合并增益。这篇文章的目标,是给出一个可以直接在MATLAB里复现的LDPC编码、16QAM软解调、HARQ重传合并的仿真链路,并且说清楚每个参数的物理含义,以及最容易被忽略的噪声方差匹配问题。适合做无线链路预算、算法验证、毕业设计,以及想从“跑通”走向“跑准”的工程师。
2. LDPC编码和16QAM调制在MATLAB里的基础实现
2.1 用comm.LDPCEncoder和dvbs2ldpc搭最小编码链路
MATLAB通信系统工具箱提供了现成的LDPC编解码对象。常见做法是先用dvbs2ldpc生成一个符合DVB-S2标准的稀疏校验矩阵,再把它传给comm.LDPCEncoder和comm.LDPCDecoder。之所以选DVB-S2矩阵而不是随手构造的稀疏矩阵,是因为它的码长、行重列重都是经过工程优化的,译码收敛快,而且不同MATLAB版本之间行为一致,不需要自己手工检查H矩阵是否满秩。
% 配置 LDPC 编码器,码长 64800,码率 1/2 H = dvbs2ldpc(1/2); ldpcEncoder = comm.LDPCEncoder(H); ldpcDecoder = comm.LDPCDecoder(H, 'MaximumIterationCount', 50); % 信息位长度由 H 的列数减行数决定 K = size(H, 2) - size(H, 1); % 32400 infoBits = randi([0 1], K, 1); % 编码得到 64800 个比特 codedBits = ldpcEncoder(infoBits);这里的H是DVB-S2标准里的校验矩阵,size(H,1)是校验位个数,K用总列数减去行数得到。MaximumIterationCount控制译码迭代次数,5G NR场景下一般取8到20;DVB-S2码长较长,设50能获得更好性能,但仿真时间会明显上升。如果你用的是老版本MATLAB,例如R2018b之前,dvbs2ldpc只支持少数固定码率,这种情况下可以改用ldpcQuasiCyclicMatrix手动构造准循环LDPC码。
注意infoBits必须是列向量,randi生成的正好满足要求。在循环仿真里,这行代码建议放到for循环外,避免重复生成H矩阵。我在实际项目中会把编解码器对象封装成一个简单的struct,这样切换码率时只改H就好。
2.2 16QAM调制应该用bit输入还是symbol输入
16QAM调制在MATLAB里有两种等价写法:一种是把比特分组后查表映射成十进制符号,再用qammod的symbol输入;另一种是直接把比特列向量交给qammod并指定'InputType','bit'。后者会自动按Gray顺序映射,省掉手动处理坐标映射的中间代码。
% 单位平均功率下的 16QAM 调制 modSym = qammod(codedBits, 16, 'InputType', 'bit', 'UnitAveragePower', true); % 经过 AWGN 信道,snr 为 dB 值 rxSym = awgn(modSym, snr, 'measured'); % 计算每个比特的 LLR 软量 llr = qamdemod(rxSym, 16, 'OutputType', 'llr', ... 'UnitAveragePower', true, 'NoiseVariance', 10^(-snr/10));解码端使用llr输出比硬判决好很多,因为LDPC译码器内部是概率域迭代译码,输入软比特能保留信道置信度信息。这里NoiseVariance必须是线性噪声方差,如果给的是dB值,译码性能会迅速恶化。常见的错误是把snr直接填进去,或者开了UnitAveragePower却忘了星座平均功率不是1。qamdemod输出的LLR默认没有做符号功率归一化,但对于固定调制阶数,这个缩放不影响LDPC译码的判决结果,因为LLR之间是等比例变化的。
2.3 LDPC译码输入必须是LLR,不要用硬判决绕过
很多初学者为了“省事”,直接在qamdemod里用OutputType','bit',再喂给LDPC译码器。这样做其实也能跑,但误码性能会损失1.5到2dB。HARQ场景下硬判决就更不适用了:Chase合并要求把多次接收的软信息相加,硬判决只能做投票,信息量远低于LLR累加。
所以在搭链路之前,建议单独写一个软解调函数:
function llr = softDemod16QAM(rxSym, noiseVar) llr = qamdemod(rxSym, 16, 'OutputType', 'llr', ... 'UnitAveragePower', true, 'NoiseVariance', noiseVar); end这样在HARQ循环里每次调用都只用传两个参数,避免那一长串qamdemod参数把主循环淹没。另外,在AWGN信道下,如果信噪比定义为符号能量与噪声功率之比,那么噪声方差就是10^(-snr/10);如果定义为比特能量信噪比Eb/N0,还需要除以log2(16)再乘以编码率。这个换算关系会在整个仿真里反复用到,建议写注释标注清楚,否则以后回看脚本会一头雾水。
表:LDPC+16QAM链路里最常调的参数
| 参数 | 常见取值 | 影响 |
|---|---|---|
| ParityCheckMatrix | dvbs2ldpc(1/2) | 决定码率、码长和误码性能 |
| MaximumIterationCount | 8~50 | 迭代越多译码越准,仿真越慢 |
| UnitAveragePower | true | 星座均方根为1,噪声方差直接对应SNR |
| NoiseVariance | 10^(-snr/10) | 必须与星座平均功率匹配,否则LLR失真 |
3. 在MATLAB里搭建HARQ重传:从“重发”到真正“合并”
3.1 Type-I 和 Chase 合并的取舍
HARQ分两大类:Type-I是出错就丢弃,接收端只解当前收到的帧,重传与之前没有关系;Type-II里最常用的是Chase合并,重传的是相同符号,接收端把每次LLR逐位累加后统一译码。对于LDPC+16QAM链路,几乎所有人都会选Chase合并,因为LDPC译码器本身就输入LLR,合并只是一个加法器。增量冗余IR需要改变每次重传的编码速率,MATLAB里得重新配置LDPC矩阵,复杂度高但增益通常只有零点几个dB,除非你在复现5G NR标准协议,否则不建议一上来就做IR-HARQ。
这里有一个容易混淆的点:HARQ与ARQ的区别。ARQ不合并,重传的帧如果还错就再丢弃;HARQ会利用之前没有解出来的软信息。仿真时必须把合并逻辑写在解码之前,不能“解码失败就清零接收缓冲”。我在不少示例代码里见过类似错误,重传次数一多,噪声反复累加,性能反而比重传一次还差。
3.2 用状态循环模拟发送端缓冲区和接收端LLR累加
发送端需要把调制符号保存下来,接收端要维护一个列向量rxLLRSum来累计软信息。下面这段代码是完整的Chase合并HARQ核心逻辑:
maxRetries = 4; txBuffer = modSym; % 保存待重传符号 rxLLRSum = zeros(length(modSym), 1); % LLR 合并缓冲 retryCount = 0; ackFlag = false; while ~ackFlag && retryCount < maxRetries % 每次重传都经过独立 AWGN 信道 rxSym = awgn(txBuffer, snr, 'measured'); % 软解调得到当前帧 LLR llr = qamdemod(rxSym, 16, 'OutputType', 'llr', ... 'UnitAveragePower', true, 'NoiseVariance', 10^(-snr/10)); % Chase 合并:直接累加 rxLLRSum = rxLLRSum + llr; % 用合并后的 LLR 解码 decodedBits = ldpcDecoder(rxLLRSum); % 用误码判断 ACK/NACK;实际系统更严格,会加 CRC if isequal(decodedBits, infoBits) ackFlag = true; else retryCount = retryCount + 1; % 如果达到最大重传仍然失败,一般直接宣告失败 if retryCount == maxRetries break; end end end这里的isequal在仿真里模拟物理层CRC校验。实际LDPC译码即使没有正确收敛,也有可能因为迭代次数限制而返回一组不满足校验方程的比特;在纯仿真环境下,用信息比特逐位对比是最可靠的方式。注意rxLLRSum不能清零,否则第二次重传和第一次发送完全独立,HARQ就退化成了ARQ。
表:HARQ仿真关键参数
| 参数 | 含义 | 建议值 |
|---|---|---|
| maxRetries | 最大重传次数 | 2~5,再大吞吐量上不去 |
| 合并方式 | LLR逐位相加 | Chase |
| ACK延迟 | 模拟反馈时延 | 0,静态仿真可不考虑 |
| 重传粒度 | 按码字还是按符号重发 | 按码字,方便对齐LLR |
如果要在仿真中加入反馈时延,最简单的方法是在awgn循环里插入固定延迟,例如每轮重传前先发送若干空闲帧。对于纯性能评估,这种延迟可以忽略,因为它不改变合并后的信噪比,只影响吞吐量计算的分子分母。
3.3 LLR合并之前要确认符号对齐和比特交织顺序
Chase合并假设不同重传的比特到LLR的位置是一一对应的。如果你的链路里加了随机交织器,重传时必须使用相同的交织表,否则LLR加在错误的位置上。另一个常见坑是qammod的InputType','bit'会自动把比特按列优先映射,如果之前手动做了比特分组,顺序可能不一致。保险做法是发送端把调制符号存下来,重传时直接复用同一个txBuffer,这样就从根上避免符号错位。
4. 完整链路仿真:SNR扫描、误码率统计和“假收敛”排查
4.1 主循环怎么写才能既准又快
完成单帧HARQ逻辑后,下一步就是把它放进SNR扫描循环里。统计指标一般有两个:误码率BER和吞吐量。BER用信息比特错误数除以信息比特总数,吞吐量用成功解码的码字数除以总发射次数——重发次数越多,吞吐量越低。
snrVec = 5:2:15; maxFrame = 500; targetErr = 50; for snrIdx = 1:length(snrVec) snr = snrVec(snrIdx); totalBitErr = 0; totalBitTx = 0; totalFrameTx = 0; successFrame = 0; for frame = 1:maxFrame % 生成随机信息比特 infoBits = randi([0 1], K, 1); codedBits = ldpcEncoder(infoBits); modSym = qammod(codedBits, 16, 'InputType', 'bit', ... 'UnitAveragePower', true); % 发送一次原始码字 txCount = 1; rxLLRSum = zeros(length(modSym), 1); needRetx = true; while needRetx && txCount <= maxRetries rxSym = awgn(modSym, snr, 'measured'); llr = qamdemod(rxSym, 16, 'OutputType', 'llr', ... 'UnitAveragePower', true, 'NoiseVariance', 10^(-snr/10)); rxLLRSum = rxLLRSum + llr; decodedBits = ldpcDecoder(rxLLRSum); if isequal(decodedBits, infoBits) needRetx = false; successFrame = successFrame + 1; else txCount = txCount + 1; end end % 统计误比特,即使重传失败也算错误 totalBitErr = totalBitErr + sum(decodedBits ~= infoBits); totalBitTx = totalBitTx + K; totalFrameTx = totalFrameTx + txCount; end ber(snrIdx) = totalBitErr / totalBitTx; throughput(snrIdx) = successFrame * K / totalFrameTx; end这段代码中各变量的作用:txCount记录当前码字实际发射次数,successFrame用于计算成功接收的帧数,totalFrameTx则把所有重发次数加总。吞吐量的单位是“每个传输时隙成功的信息比特数”,如果你知道具体符号速率,再乘上符号速率就是bps。这里没有把HARQ反馈等待时隙算进去,如果仿真目的是对比协议开销,需要额外加一个等待因子。
4.2 常见假象:无噪声不收敛、高SNR反而变差
下面这张表整理了我在调试HARQ+LDPC+16QAM链路时最常碰到的问题。它们会伪装成“算法不收敛”或者“SNR高但性能差”。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 无噪声时误码率约为0.5 | LLR正负号反了 | 检查qamdemod星座映射方向,或尝试翻转所有LLR |
| 高SNR出现误码地板 | LLR噪声方差不准 | 确认NoiseVariance是线性值,且与UnitAveragePower匹配 |
| 重传次数越多,BER越高 | 每次重传前清零了LLR缓冲 | 确保rxLLRSum只在码字开始时清零 |
| 吞吐量在低SNR反而高于高SNR | 统计公式分母用了编码比特而不是信息比特 | 检查throughput计算里是否乘以了编码率 |
| 解码结果时好时坏,不稳定 | LDPC迭代次数太少 | 把MaximumIterationCount提高到20以上 |
其中“重传次数越多,BER越高”这个现象尤其隐蔽。如果rxLLRSum被意外清零,每次重传只是独立尝试,多次尝试后误码率确实会下降,但吞吐量下降很多,性能曲线看起来像没有HARQ增益。另外,使用parfor并行SNR扫描时,要确保随机数种子独立,否则多轮仿真结果完全一致,统计没有意义。
4.3 用单次全噪声仿真正确性
在跑完整SNR曲线前,建议先固定一个SNR(比如10dB),只跑100帧,观察每一帧的重传次数和误码数。把disp加进主循环,可以直观看到哪些帧需要重传两次,哪些帧一次就过。如果所有帧一次就过,说明SNR点取得太高,不能体现HARQ增益;如果几乎所有帧都重传4次以上,说明SNR点过低。用这个简单步骤就能确定SNR扫描范围是否合理,省得整条曲线跑完才发现数组全是0。
5. 让HARQ+LDPC仿真“可信”的三个验证技巧
LDPC、16QAM和HARQ组合在一起,可以调节的参数非常多,因此仿真结果必须先验证再使用。我一般会做以下三件事:无噪闭环测试、理论曲线对照、用吞吐量而不是BER展示HARQ增益。
无噪闭环测试是把snr设成100dB或直接在信道之前把接收信号赋值为发送信号。此时LLR应该非常大,LDPC一次译码必成功。如果这个测试失败,问题一定出在qammod/qamdemod参数或LLR正负号上,而与HARQ逻辑无关。这个测试还能确认K的计算是否正确:如果信息位数算错,isequal永远不会成立。
理论曲线对照是让LLR计算回归基础。在没有HARQ的普通LDPC+16QAM链路上,误码率曲线应该与文献里同码长同码率的性能曲线趋势一致。如果仿真的BER在10dB时还在0.1以上,多半是噪声方差或星座功率没有对齐。这时候不要急着找HARQ的问题,先让基础链路净增益看起来正常。常见做法是找一个不含HARQ的简单误码率脚本,仅替换LDPC矩阵作对比。
第三个技巧关于HARQ效果验证。BER曲线只能说明重传后每比特错误率下降,但无法反映重传的资源浪费。正确指标是“吞吐量 vs SNR”曲线。当SNR低时,重传次数增多,吞吐量随之下跌;SNR高时,吞吐量趋于K除以一次发射。通过这条曲线能够直观看出:在哪个SNR条件下引入HARQ是划算的。具体代码只需在主循环保存totalFrameTx和successFrame,不需要额外改动链路结构。
举个例子:仿真后如果发现SNR=8dB时,平均每个码字发射1.8次,而SNR=10dB时平均1.1次,两点的吞吐量差异比BER差异更明显。报告这种结果时,我会把表头写成“平均发射次数”和“归一化吞吐量”,这样读者可以快速评估HARQ增益。最终,你得到的不仅是一条能跑的MATLAB链路,而是一套能够支撑链路预算和算法选型决策的仿真工具。
本文还有配套的精品资源,点击获取