简介:面向通信与信号处理方向的MATLAB仿真实战包,围绕pi/4-QPSK调制解调、LDPC编译码与FFT频偏估计展开,完整实现了从随机二进制序列生成、LDPC编码、pi/4-QPSK调制,到AWGN信道传输、FFT频偏估计与补偿、pi/4-QPSK解调、LDPC译码及误码率统计的闭环流程。包内共14个文件,以9个m脚本、4个mat数据文件和1个txt说明为主,m脚本包含主程序、LDPC校验矩阵生成、编译码函数等模块,mat文件用于保存中间矩阵与仿真结果,txt提供操作指引。压缩包整体仅146KB,体量轻但流程完整,便于快速部署与二次开发。目前已有81人学习下载,适合通信工程、电子信息类专业学生及科研人员对照同名博文深入理解同步通信系统的误码率仿真方法。配套中文注释和程序操作视频,可显著降低上手门槛,尤其适合MATLAB初学者逐步复现频偏估计与LDPC译码等关键环节。
1. pi/4-QPSK + LDPC + FFT 频偏估计:这套仿真链路能帮你省下什么
同样是跑 pi/4-QPSK 链路级误码率仿真,有人一个下午出图,有人一周都在和星座图较劲,差距基本都卡在频偏处理上。这套资源把 pi/4-QPSK 调制解调、LDPC 编译码和 FFT 频偏估计同步串成一条端到端链路,收端先用 FFT 粗估频偏并补偿,再做差分检测和 LDPC 译码,最后输出误码率曲线,中间每一级的星座和频谱变化都看得见。
它适合通信工程做毕设、准备链路级仿真项目、或者想快速验证一个同步算法的人。你需要的不只是某个孤立函数,而是一条能改参数、能复现结果、还能对着中文注释反推实现细节的完整链路。下载下来对照自己的场景跑,比从零开始写省下的时间不是一星半点。
2. 仿真链路怎么搭:模块划分、参数约定与运行顺序
2.1 一条完整的收发链路长什么样
先把整体结构摆出来。这套仿真按「发端 → 信道 → 收端」三段组织,信号流向是:发端比特 → LDPC 编码 → pi/4-QPSK 符号映射与差分编码 → 根升余弦成形滤波 → 过采样进信道加 AWGN 和频偏 → 匹配滤波 → FFT 频偏估计与补偿 → 差分检测解调 → LDPC 译码 → 误码率比对。这个顺序不是随便排的,每个模块的位置都有讲究。
LDPC 编码放在调制前面,因为信道编码工作在比特域,pi/4-QPSK 映射工作在符号域,两者靠一个交织接口衔接。FFT 频偏估计放在匹配滤波之后,是因为谱峰搜索对带外噪声敏感,根升余弦匹配滤波先把带外噪声压掉,信噪比提升了再估计,谱峰才站得稳。如果你把频偏估计挪到匹配滤波之前,低信噪比时谱峰大概率选错,后面怎么补偿都救不回来。
误码率统计口径上,整个仿真吃的是 Eb/N0 而不是 SNR。LDPC 引入了冗余比特,同样一个符号里编码后的能量被稀释,直接看 SNR 会把编码增益算错。换算关系是 SNR = Eb/N0 + 10·log10(R·log2(M)),R 是码率,M 是调制阶数。pi/4-QPSK 是 2 比特每符号,码率 1/2 时 R·log2(M)=1,SNR 数值上恰好等于 Eb/N0;码率 3/4 时就要加 10·log10(1.5)≈1.76dB。这条换算我几乎每次都在代码里写注释,防止改了码率后整条曲线莫名偏移。
频偏统一用归一化频偏 fd·Ts 表示,Ts 是符号周期。这样仿真结果与具体符号速率解耦,换一个速率不用重跑全部参数。资源里主脚本头部会先定义 Eb/N0 扫描向量和归一化频偏值,这两个是整条链路的两个主要可变维度。
2.2 核心参数表
跑这套仿真前,我第一件事是核对参数表,它们基本都暴露在主脚本头部,代码里也配了中文注释:
| 参数 | 典型配置 | 说明 |
|---|---|---|
| 调制方式 | pi/4-QPSK | 差分相位集为 ±π/4、±3π/4,无 180° 相位跳变 |
| 成形/匹配滤波 | 根升余弦(RRC) | 滚降系数 alpha 取 0.25 ~ 0.35 |
| 过采样倍数 sps | 8 或 16 | 决定 FFT 频偏估计的谱线分辨率 |
| LDPC 码率 | 1/2、2/3、3/4 | 低码率瀑布区更靠左,频谱效率下降 |
| LDPC 最大迭代 | 30 ~ 50 次 | 超过不收敛就按出错处理,避免死循环 |
| FFT 点数 | 4096 或更长 | 频偏分辨率近似 fs / N_fft |
| Eb/N0 扫描范围 | 0 ~ 10 dB,步进 0.5 或 1 dB | 按观察瀑布区还是平层来定 |
| 蒙特卡洛帧数 | 每点至少 100 帧,或按错误比特数停机 | 详见第 4 章 |
这里面最容易翻车的是 FFT 点数和过采样倍数的组合。有人以为 sps 越大越好,其实 sps 增大后单个符号采样点多,但符号数没变,FFT 的谱峰搜索范围不因此变宽,只是计算量上去了。我一般 sps=8 起步,频偏分辨率不够时优先加 FFT 点数而不是加 sps。LDPC 最大迭代次数同理,30~50 是链路仿真的常见区间,设到 100 几乎不改善误码率,只拖慢仿真。
2.3 程序组织与运行顺序
资源里是中文注释加一段操作视频,我推荐的阅读顺序是「先看视频,再跑主脚本,最后抠子函数」。视频先让你看到最终效果,知道每张图长什么样、曲线应落在哪个位置,再回来看代码,对「哪个变量对应图上哪条线」会清楚很多。直接硬读代码容易卡在变量命名和矩阵维度上。
主脚本负责串链路:生成比特、编码、调制、加噪声加频偏、估计补偿、解调解码、画图。子函数按模块拆,pi4_qpsk_mod.m、ldpc_encode.m、fft_freq_offset_est.m 这类,每个函数头顶有一段注释说明输入输出和注意事项。跑的时候我建议分三个层次推进:
- 层次一:无频偏、无 LDPC 的理想同步基线,验证调制解调链路本身。
- 层次二:加入 LDPC,看编码增益是否落在预期区间。
- 层次三:加入频偏和 FFT 估计补偿,此时曲线相对层次二会出现一小段可接受的损失。
三个层次对应三张图,每一步的增益都能对上号,出了问题定位范围也小。上来直接跑完整链路,误码率不对时你根本不知道是编码问题还是同步问题。配套操作视频我一般先拖到中间看波形,再回开头,因为视频里最值钱的是中间那几帧星座图和频谱图的变化过程,开头那段环境介绍反而可以跳过。
注意:先把主脚本完整跑通一次,再动参数。上来就改 LDPC 码率或 FFT 点数,出了问题你分不清是参数问题还是代码本身没有对齐。
3. pi/4-QPSK 调制解调:差分编码、成形滤波与实现细节
3.1 pi/4-QPSK 为什么在移动通信里站得住
pi/4-QPSK 常被看成 QPSK 和 OQPSK 的折中。QPSK 的相位跳变里有 180°,经过带宽受限信道后包络明显塌陷,对非线性功放不友好;OQPSK 把同相与正交分量错开半个符号,把 180° 跳变拆成两次 90° 跳变,但对定时误差更敏感。pi/4-QPSK 的差分相位集合只有 ±π/4 和 ±3π/4,最大相位跳变 135°,既避免了大包络波动,又保留了用非相干方式解调的可能。蜂窝系统当年选它,就是看中抗频偏和抗非线性的组合优势。
这套仿真里理解 pi/4-QPSK 的关键在于「信息不是映射在绝对相位上,而是映射在相位差上」。当前符号相位等于上一符号相位加上一个经过 Gray 编码的差分相位,这就是名字里 pi/4 的由来——每个符号在前一符号基础上旋转了 pi/4 的整数倍。接收端只要算相邻符号的相位差就能恢复比特,不需要知道载波绝对相位,这一点直接决定了后面的解调方式。
3.2 调制端实现:差分编码与成形滤波
发端代码通常是这个套路,变量名稍有差异但逻辑一致:
sps = 8; % 过采样倍数,每个符号 8 个采样点 alpha = 0.25; % 根升余弦滚降系数 M = 4; % QPSK 星座点数 phase_set = [-3*pi/4, 3*pi/4, -pi/4, pi/4]; % 差分相位集合,Gray 映射 data = randi([0 1], nBits, 1); % nBits 由主脚本传入,一帧比特数 dataSym = bi2de(reshape(data, 2, []).', 'left-msb'); % 比特对转符号索引 0~3 deltaPhase = phase_set(dataSym + 1); % 每个符号对应的差分相位 phase = cumsum(deltaPhase); % 差分编码:绝对相位逐符号累加 sym = exp(1j * phase); % 得到 pi/4-QPSK 复符号序列 rrcFilter = rcosdesign(alpha, 6, sps, 'sqrt'); % 根升余弦滤波器,截断 6 个符号 tx = upfirdn(sym, rrcFilter, sps); % 插值到 sps 倍符号速率并成形逻辑说明:deltaPhase 对应 pi/4-QPSK 的差分相位集合,dataSym 把每两个比特映射到四个相位之一,cumsum 一次性完成差分累加得到绝对相位,sym 是最终复符号序列。rcosdesign 生成根升余弦滤波器,upfirdn(sym, rrcFilter, sps) 先插零再卷积,输出速率是符号速率的 sps 倍。
参数上 alpha 和 sps 最常改。alpha 越小带宽越省,但滤波器冲激响应拖尾更长;sps 决定匹配滤波能压掉多少带外噪声,sps=8 是链路级仿真的常见值,往上收益有限。注意 upfirdn 输出自带滤波器群延迟,符号流前后会有一段滤波器暂态,做误码率比对前要先把这部分截掉,不然开头几十个符号全是乱的。
3.3 收端差分检测:不需要绝对相位
解调端资源里用的是差分检测,核心就几行:
rxSym = rx(1:end-1); % 当前符号 rxSymDelay = rx(2:end); % 延迟一个符号的接收序列 diffPhase = angle(rxSymDelay .* conj(rxSym)); % 相邻符号相位差 rxData = zeros(size(diffPhase)); for k = 1:length(diffPhase) [~, idx] = min(abs(diffPhase(k) - phase_set)); % 最小相位距离判决 rxData(k) = idx - 1; end逻辑说明:rx(1:end-1) 和 rx(2:end) 错开一个符号,共轭相乘后取 angle,等价于 arg(a·conj(b)) = arg(a) - arg(b),相邻符号相位相减得到差分相位,公共的载波相位偏差被消掉。再和 phase_set 做最近距离判决。这套操作只要频偏在几个符号内变化缓慢,就能不用导频解调。代价是相位噪声被两次放大,低信噪比时比相干解调差 2~3dB。这也是为什么整套链路里还必须加一次 FFT 频偏粗估计——差分检测扛得住小频偏,扛不住大频偏,粗估计先把频偏压到零附近,差分检测只负责残余误差。
这里必须强调一个容易误解的点:差分检测不等于不需要频偏校正。归一化频偏在 0.001 量级以下,差分检测确实不敏感;但频偏到 0.05 以上,星座持续旋转,差分相位里的判决区域照样被转混。所以链路顺序必须是「FFT 粗估计并补偿 → 差分检测」,不能拿差分检测当主同步用。
3.4 参数怎么调才不玄学
滚降系数和过采样是最容易被当成玄学的两个参数。我给了三条经验。第一,alpha=0.25 兼顾带宽和误码率平台,0.35 更抗 ISI 但频谱浪费,仿真先固定一个再调。第二,sps 和滤波器截断符号数配套,rcosdesign 里的 6 是滤波器截断的符号数,太短会有截断旁瓣,太长拖慢仿真。第三,差分检测的误码率平台上,alpha 从 0.25 改到 0.35,对曲线的影响通常小于 0.2dB,别指望靠调滚降救回劣化链路,问题多半在频偏估计或 LDPC 译码。
4. LDPC 编译码与误码率统计:码型选型与仿真时长控制
4.1 LDPC 码型选型:为什么链路仿真都用 QC-LDPC
LDPC 是靠稀疏校验矩阵 H 约束的线性分组码,译码用置信传播在因子图上迭代。5G NR 最终选了 QC-LDPC,核心原因是它的 H 矩阵由循环移位子矩阵拼成,结构规则,编译码可以并行加速,MATLAB 里用稀疏矩阵表达也很自然。这套资源里的 LDPC 模块,大概率采用 QC-LDPC 结构,码长几百到两千比特量级。仿真里只需要关注两个旋钮:码率决定编码增益的上限,迭代次数决定译码器离理想性能有多近。
5G 的 LDPC 还分 BG1 和 BG2 两套基础矩阵,BG1 面向大码长、高码率场景,BG2 面向短码、低码率场景。链路仿真选码长时可以参考这个思路:码长短于 500 比特时用低码率,码长上千后再上高码率,不然瀑布区会塌掉。资源里的默认配置一般是中码率、中等码长,调的时候保持码长和码率匹配,不要一边码长 192 一边码率 3/4,那种组合误码率表现会很差。
编码端常见做法是把稀疏 H 高斯消元成系统生成矩阵 G,再算 u·G。这里有两个坑。消元后的 G 往往不再稀疏,码长上千时内存和计算量都上来了,所以很多代码改用基于下三角形式的快速编码替代。另一个坑是 G 不唯一,不同实现算出来的 G 差一个行变换,误码率结果不受影响,但调试时不要拿别人的 G 直接逐行对比。
4.2 译码器怎么调:迭代次数与提前终止
对数域 BP 译码的核心是变量节点和校验节点来回传消息,消息置信度用 LLR(对数似然比)表示。链路仿真里的折中方案是最小和近似:
% maxIter 最大迭代次数,H 为稀疏校验矩阵,channelLLR 来自解调软输出 for iter = 1:maxIter % 变量节点更新:合并信道 LLR 和来自校验节点的外信息 [vMsg, vLLR] = updateVarNode(cMsg, channelLLR); % 校验节点更新:最小和近似代替 tanh 规则,速度更快 cMsg = updateCheckNode(vMsg, H); % 硬判决并检查校验方程 hardBits = double(vLLR < 0); if all(mod(H * hardBits(:), 2) == 0) break; % 所有校验方程满足,提前终止迭代 end end逻辑说明:updateVarNode 把信道 LLR 和来自校验节点的外信息加权求和,权重和信道估计质量相关;updateCheckNode 用最小和近似代替 tanh 运算,性能损失大约 0.2~0.4dB,但迭代速度快了一个量级,这是链路级仿真里非常常见的折中。maxIter 设 30~50 就够,加到 100 几乎不改善误码率。
真正的关键是提前终止。一旦 H·hardBits 全零就跳出循环,没有这行的译码器,高信噪比下每个帧都白跑满全部迭代,整条曲线的时间成本高出一大截。
注意:提前终止的判断一定要用校验方程全零,而不是「本次硬判决和上次一样」。两者在高信噪比时都经常发生,但前者是数学上确认收敛,后者可能只是卡在同一个错误状态。
4.3 误码率统计:帧数怎么定
LDPC 场景下误码率统计比普通调制更讲究。编码后的误码率曲线在瀑布区掉得极陡,每个点只跑固定 100 帧的话,瀑布区中段的误码率会在 1e-3 到 1e-5 之间剧烈抖动,曲线像锯齿,根本原因是这一段错误事件非常稀疏。我的做法是按错误比特数停机,而不是按帧数停机:
targetErr = 100; % 每个信噪比点至少攒 100 个错误比特 totalBits = 0; totalErr = 0; while totalErr < targetErr [errBits, nBits] = runOneFrame(EbN0_dB); % 跑一帧完整链路 totalErr = totalErr + errBits; totalBits = totalBits + nBits; end ber = totalErr / totalBits;逻辑说明:targetErr=100 时,误码率 1e-5 意味着要累积 1e7 个比特。低信噪比下很快,高信噪比下很慢。这个准则保证每个点的统计置信度接近,而不是帧数相等。实际跑的时候 targetErr 建议 50~100,小于 50 时曲线尾部噪声偏大;大于 200 时瀑布区底部几个点会跑很久。遇到高信噪比点,可以先用 targetErr=10 试跑一帧,如果一帧全对,说明这个点大概率已经低于可测范围,没必要硬等。
编码增益的对照方式顺手说一下:同一张图里同时画无编码 pi/4-QPSK 和 LDPC 编码后两条曲线,瀑布区左移的 dB 数就是编码增益。1/2 码率 LDPC 在 BER=1e-4 处一般带来 4~5dB 增益,如果仿真结果只有 1~2dB,优先查编译码接口、交织映射,而不是怀疑同步模块。
5. FFT 频偏估计与同步:算法实现与常见问题排查
5.1 FFT 频偏估计的基本原理
接收信号建模为 r(n) = s(n)·exp(j2π·fd·n·Ts + φ0) + w(n),fd 是载波频偏,Ts 是符号周期。对 r(n) 做 FFT,频谱上的峰值位置就对应 fd。原理是复指数在频域是单根谱线,s(n) 是带限信号,信噪比够的时候谱峰位置就是频偏位置。FFT 估计的特点是捕获范围大、速度快,非常适合做粗同步。
估计精度由 FFT 点数决定,频偏分辨率约等于 fs / N_fft。假设 sps=8、符号速率归一化为 1,fs=8,N_fft=4096,分辨率约 0.002(符号率归一化),换算成每符号相位误差 0.002·2π,差分检测完全扛得住。注意 fs 是采样域频率,谱峰频率换算回符号率归一化频偏时要乘 sps,这一步单位换算错了,后面的补偿全是乱的。
5.2 频谱泄露与插值修正
FFT 估计最经典的敌人是频谱泄露。接收数据被矩形窗截断,等于在频域和 sinc 卷积,真实谱峰的能量扩散到相邻谱线。频偏不在 FFT 栅格点上时,峰值位置会在两根谱线之间摇摆,估计值跳动。常见做法是加窗压低旁瓣,再用插值把主瓣顶点定准:
N_fft = 4096; win = hann(length(rx), 'periodic').'; % 周期 hann 窗,压低频谱泄露旁瓣 X = fft(rx .* win, N_fft); % 加窗后变换到 N_fft 点 [~, k_peak] = max(abs(X(1:N_fft/2))); % 只在正频域搜索谱峰 % 抛物线插值:用峰值左右两根谱线修正谱峰位置,首尾谱线不参与,防止越界 k0 = k_peak - 1 + 0.5 * (abs(X(k_peak-1)) - abs(X(k_peak+1))) / ... (abs(X(k_peak-1)) - 2*abs(X(k_peak)) + abs(X(k_peak+1))); fdTs_est = (k0 - 1) / N_fft * sps; % 换算到符号率归一化频偏逻辑说明:hann 窗旁瓣衰减快,适合频谱泄露占主导的场景,periodic 选项让窗函数首尾相接,和 FFT 的周期延拓假设一致。抛物线插值用峰值左右两根谱线拟合主瓣顶点,能把估计分辨率提升接近一个量级。MATLAB 的 fft 是混合基实现,点数取 2 的幂时计算效率最高,但插值公式不要求 N_fft 是 2 的幂,点数足够就好。
需要强调的是插值公式对谱线幅值敏感,如果加窗后主瓣太宽,插值反而会过拟合,这时优先加长 N_fft 而不是继续抠插值算法。另外补零不能提高真实分辨率,只是让谱线更密,估计精度的上限仍由原始数据长度决定。
5.3 常见问题排查记录
把这条链路上最常遇到的五个问题按「现象 → 原因 → 解决」列出来,都是我实际跑仿真被卡过的地方。
现象 1:频偏估计值在几个固定数值之间来回跳,星座图转得忽快忽慢。
原因:频偏落在 FFT 栅格中间,频谱泄露加旁瓣干扰,峰值选择不稳定。
解决:先加 hann 窗再做抛物线插值;如果还跳,把谱峰搜索范围从全频域限制到 ±0.1·fs 对应的谱线区间,滤掉远处的镜像峰。
现象 2:低信噪比时 FFT 直接选错峰,估计值和真实频偏差一个量级。
原因:Eb/N0 低于 0dB 时噪声谱淹没信号谱峰,全频域最大值搜索失效。
解决:先用差分检测或数据辅助粗略估计频偏区间,把 FFT 搜索范围缩小;或者先做去调制处理剥掉信息再估计。粗搜加细搜两步走,比单次大范围 FFT 可靠得多。
现象 3:MATLAB 2023 打开资源文件,中文注释全部乱码。
原因:文件编码和 MATLAB 默认读取编码不一致,老脚本常用 GBK,新版 MATLAB 默认按 UTF-8 打开。
解决:在 MATLAB 里临时切换编码,或者用编辑器把 .m 文件批量转成 UTF-8 另存,转完重开一次根治。这个问题和仿真本身无关,但会拦住大量第一次跑的人。
现象 4:补偿后误码率仍比理论差 2dB 以上,且随频偏增大逐步恶化。
原因:FFT 粗估计分辨率受限,残余频偏仍在差分检测容忍范围之外。
解决:粗估计后加慢速锁相环或基于判决的残余频偏跟踪。链路里「FFT 粗估计 + PLL 细跟踪」是标准组合,别指望 FFT 一步到位。
现象 5:瀑布区中段曲线抖动,同一 Eb/N0 点重复跑结果差一倍。
原因:蒙特卡洛帧数不够,错误比特样本太少,统计量不稳定。
解决:按错误比特数停机,targetErr 至少 100,或者用置信区间判断当前点是否收敛。曲线平层抖动的锅经常是统计量问题,不是算法问题。
6. 跑出可信误码率曲线的三个验证习惯
把链路跑通只是开始,真正让我省时间的是每次仿真强制走同一套验证流程。第一步是跑理论基线。在加 LDPC 和频偏之前,先让最简单的 pi/4-QPSK 理想同步链路出原始误码率,对照理论值。差分检测的误码率大约比相干 QPSK 差 2dB,MATLAB 里可以直接算相干参考再平移:
EbN0_dB = 0:0.5:10; % 相干 QPSK 理论误码率:Q(sqrt(2*Eb/N0)) berRefQPSK = qfunc(sqrt(2 * db2pow(EbN0_dB))); % 差分检测 pi/4-QPSK 大约比相干差 2dB,作为参考基线 berRef = interp1(EbN0_dB, berRefQPSK, EbN0_dB - 2); semilogy(EbN0_dB, berRef, 'k--'); hold on; % 把仿真逐点折线叠上来,偏差超过 0.5dB 就要回头查仿真曲线和这个参考偏差超过 0.5dB,先别往下走,回头查成形滤波和差分相位映射。第二步是逐级加模块。基线过了加 LDPC,只看编码增益是否落在预期区间;再加频偏,只看同步损耗。每次只动一个环节,出问题立刻定位。我在这上面交过学费——有一回图省事一次把频偏和 LDPC 全加上,误码率高到离谱,排查了半天,最后发现只是差分检测相位集顺序和编码映射没对齐。第三步是保存随机种子和中间量。仿真开头用 rng 固定种子,每个 Eb/N0 点的接收星座、频偏估计值、译码迭代次数存到 .mat 文件。曲线异常时能回到现场,直接看是频偏估计跳了还是译码没收敛。随机种子就是后悔药,丢了种子复现问题全靠运气。从那以后我每次新建仿真工程,第一行就是 rng 固定种子,最后一个模块才画图,中间全部落盘。希望帮到你。
本文还有配套的精品资源,点击获取