简介:一套基于pi/4-QPSK调制解调与Turbo编译码的通信链路MATLAB误码率仿真程序,适合通信工程、电子信息类专业学生及科研人员用于课程设计、毕业设计或算法验证。仿真流程完整覆盖随机二进制序列生成、Turbo编码、pi/4-QPSK调制、AWGN信道加噪、解调、译码及误码率统计等环节,代码配有中文注释,并附带程序操作视频,便于快速掌握运行环境和参数配置,降低调试门槛。
资源包共10个文件,其中7个m脚本包含主程序与Turbo编解码函数,2个mat数据文件用于保存参考结果,另有1个txt说明文档,压缩包整体仅41KB,轻量清晰。目前已有113人学习下载,适合在MATLAB 2022a等环境下直接运行,通过调整信噪比即可观察误码率变化,也可将Turbo编码与pi/4-QPSK解调模块分离复用,为通信链路级仿真提供可扩展的实现框架。
1. 基于pi/4-QPSK+Turbo的Matlab误码率仿真:这套链路到底解决什么问题
通信链路仿真的价值不在省掉硬件,而在把调制、编码、信道、解调、译码每个环节的损伤单独暴露出来。pi/4-QPSK是一种相位差分调制方式,相比标准QPSK,它的信号点相位跳变被限制在±π/4和±3π/4之间,不会出现180度突变,对非线性功放和衰落信道更友好;Turbo码则通过两个递归系统卷积码(RSC)和迭代译码逼近信道容量。把这两个技术放在一条链路里,是卫星移动通信、数字集群系统里非常经典的物理层方案。
这篇笔记要讲的就是怎么在Matlab里把这条链路完整搭起来,跑出误码率曲线,并且拿到带中文注释的程序和操作视频,让链路可复现、可修改。适合两类人:一是通信方向的学生,课程设计或毕业设计需要一套能跑通的误码率仿真;二是做物理层算法验证的工程师,需要快速评估pi/4-QPSK配合不同Turbo参数在AWGN信道下的性能边界。
2. pi/4-QPSK调制解调:差分相位为什么能省掉载波同步
2.1 pi/4-QPSK和QPSK的差别:相位跳变被压住了
标准QPSK的四个星座点按π/2间隔分布,相邻符号之间可能出现180度相位跳变。这个跳变带来的问题是:信号包络会在过零点被压缩到接近0,经过带限滤波器后包络起伏明显变大,射频端非线性功放会把这种幅度起伏转化为频谱扩展,恶化邻信道泄漏。pi/4-QPSK的改进思路是把星座点分成两组,A组在0、π/2、π、3π/2,B组在π/4、3π/4、5π/4、7π/4,奇偶符号从不同组里取点。这样相邻符号的相位差只能是π/4或3π/4,最大跳变限制在135度以内,包络波动比QPSK小很多。
另一个实际收益是解调方式更灵活。因为相位差携带信息,接收端可以用差分检测,不需要恢复绝对载波相位,也不存在QPSK相干解调里的四象限相位模糊问题。代价是差分检测在AWGN信道下有大约2到3dB的性能损失,这在后面Turbo码配合时要格外注意。
2.2 发射端Matlab实现:差分编码和相位累加
我一般把发射端分成三步:比特映射、差分相位累加、脉冲成形。输入数据先按2比特一组映射成相位增量,然后和前一个符号的相对相位累加得到当前符号的绝对相位,这个绝对相位再对应到星座点坐标。关键代码模块如下:
% 发射端参数 M = 4; % QPSK Rb = 9600; % 信息比特率 9600 bps Fs = 19200; % 采样率 19200 Hz(匹配后续滤波) sps = Fs / Rb; % 每比特采样数,这里等于2 % 比特流转相位增量:00->0 01->pi/2 11->pi 10->-pi/2 % 这里采用格雷映射,相邻相位差只有1比特不同 gray_map = [0 1 3 2]; % 二进制到格雷码顺序 dibit = bi2de(reshape(bits(1:end-mod(length(bits),2)), [], 2), 'left-msb'); delta_phase = (pi/2) * gray_map(dibit+1); % 相位增量序列 % 差分相位累加:当前绝对相位 = 上一绝对相位 + 当前相位增量 abs_phase = cumsum(delta_phase); % 星座映射,取B组基准,保证最大跳变135度 tx_sym = exp(1j * abs_phase);逻辑说明:bi2de把每2个比特转成十进制索引,gray_map保证相邻符号只差1比特,有利于差分检测时单比特错误不会一次带偏两个比特。cumsum完成相位累加,这里不需要mod 2π,Matlab的exp(1j*theta)会自动包裹相位。要注意输入比特长度必须是偶数,代码里reshape之前的截断处理就是做这个约束。
参数说明:Rb和Fs的比例决定了plt里时域波形和频谱的分辨率,sps=2时一个符号只有2个采样点,后续做匹配滤波前如果需要观察眼图或星座更细致的分布,可以把Fs提高到Rb的8到16倍,代价是仿真速度下降。成形滤波器我习惯用根升余弦(RRC),滚降系数0.35,这个系数在带宽和峰值平均功率比(PAPR)之间取平衡。
2.3 接收端差分解调:相位差判决和软信息输出
接收端最常见的做法是把下变频后的基带信号做匹配滤波,然后按符号周期采样得到复数符号序列。差分解调利用当前符号和上一符号的共轭相乘得到相位差,再判决到最近的π/2整数倍相位上:
% 基带差分相位检测 % rx_sym是匹配滤波后按符号周期采样的复数序列 phase_diff = angle(rx_sym(2:end) .* conj(rx_sym(1:end-1))); % 映射判决:把相位差映射到0、pi/2、pi、-pi/2 % 这里利用复数旋转简化:乘exp(-j*pi/4)后按四象限判决 rotated = exp(1j * (phase_diff - pi/4)); dec_dibit = zeros(size(phase_diff)); dec_dibit(real(rotated) >= 0 & imag(rotated) >= 0) = 0; % 00 dec_dibit(real(rotated) < 0 & imag(rotated) >= 0) = 1; % 01 dec_dibit(real(rotated) < 0 & imag(rotated) < 0) = 3; % 11 dec_dibit(real(rotated) >= 0 & imag(rotated) < 0) = 2; % 10逻辑说明:conj(rx_sym(1:end-1))是上一符号的共轭,复数相乘的相位就是当前符号相对上一符号的相位差。旋转π/4是为了把pi/4-QPSK的两组星座点映射到标准QPSK的四个象限上;让判决边界落在坐标轴上,这样四象限判决直接可用。
参数说明:这段代码输出的是硬判决比特。如果后面接的是普通卷积码,硬判决够用;但接Turbo译码器时,硬判决会丢软信息,Turbo的迭代增益基本发挥不出来。所以实际链路里我会在这里输出对数似然比(LLR),而不是硬判决,具体做法在第四章讲Turbo软输入时说明。要注意差分解调的参考点是前一符号,所以一个符号出错可能影响两个比特的判决,这也是差分检测性能损失的一部分来源。
3. Turbo编译码:迭代译码为什么能逼近香农限
3.1 Turbo码的核心结构:两个RSC和一个交织器
Turbo码的编码器由两个递归系统卷积码(RSC)通过交织器并联而成。第一个RSC直接编码信息序列,第二个RSC编码交织后的信息序列。译码端用两个软输入软输出(SISO)译码器迭代交换外信息,每轮迭代让两个译码器互相修正对方的置信度,这是Turbo码逼近信道容量的根本机制。
选择RSC而不是普通卷积码的原因在于递归结构让编码器的输出权重谱更稀疏,低重量码字少,迭代译码时错误平台(error floor)更低。RSC的生成多项式一般用八进制表示,比如反馈多项式37、前向多项式21(八进制),也就是[1 0 0 0 1 1]和[1 0 0 0 0 1]对应的生成序列。实际工程里不同标准选的生成多项式不同,但仿真阶段用这一组足够验证算法。
3.2 编码器Matlab实现:RSC分量码和删除矩阵
% 生成多项式:反馈37(oct),前向21(oct) % 对应二进制:g0 = [1 0 0 0 1 1] g1 = [1 0 0 0 0 1] g0 = [1 0 0 0 1 1]; % 反馈多项式 x^5+x^2+x+1 g1 = [1 0 0 0 0 1]; % 前向多项式 x^5+1 % RSC编码函数(单分量) function [sys, par] = rsc_encode(bits, g0, g1) state = zeros(1, length(g0)-1); % 初始状态 sys = zeros(size(bits)); par = zeros(size(bits)); for k = 1:length(bits) sys(k) = bits(k); % 反馈位:当前比特和状态位异或 fb = mod(sum(bits(k) * g0(2:end) .* state), 2); % 校验位:反馈位和状态加权异或 par(k) = mod(sum(fb * g1(2:end) .* state), 2); % 状态更新:右移,反馈位进入最高位 state = [fb, state(1:end-1)]; end end % 编码器主流程:码率1/3,不删余 bits_inter = interleaver(bits_in); % 随机交织,种子固定便于复现 [sys1, par1] = rsc_encode(bits_in, g0, g1); [sys2, par2] = rsc_encode(bits_inter, g0, g1); % 码率1/3:系统位 + 两个校验位 encoded = [sys1; par1; par2]; % 每一列对应一个信息比特的编码输出逻辑说明:RSC编码器里反馈位fb同时参与校验位计算和状态更新,这是递归结构的关键。par(k)是当前时刻的校验输出,和普通卷积码最大区别在于反馈位影响后续所有时刻的状态,这是Turbo码低重量码字减少的原因。
参数说明:交织器长度直接影响Turbo码性能。我做过对比,长度256时误码率曲线在10^-4左右出现平台,长度1024时平台明显下降,长度4096时在仿真信噪比范围内基本看不到错误平台。随机交织器的种子要固定,否则每次仿真结果不同,不利于调试。删余矩阵可以按码率需求调整:码率1/3不删余,码率1/2时交替保留par1和par2的偶数/奇数位置,恢复顺序时最容易出错,后面避坑章专门讲。
3.3 译码器Matlab实现:Max-Log-MAP迭代译码
% 软输入软输出译码(Max-Log-MAP近似) % Lc为信道置信度,L_a为先验LLR,L_e为外信息 function L_e = siso_decode(rec_sys, rec_par, L_a, g0, g1) % 前向度量、后向度量用格状图递推 % 这里用归一化简化版本,状态数为 2^(约束长度-1) num_states = 2^(length(g0)-1); % 分支度量计算:根据接收软信息和先验LLR gamma = zeros(num_states, 2); for k = 1:length(rec_sys) for s = 1:num_states gamma(s, 1) = exp( ... ); % 输入0的分支度量 gamma(s, 2) = exp( ... ); % 输入1的分支度量 end % 前向递推 alpha % 后向递推 beta % 计算后验LLR,减去先验得到外信息 end end % 迭代译码主循环 L_e1 = zeros(size(encoded_bits)); % 分量译码器1的外信息初始为0 for iter = 1:max_iter L_a2 = interleaver(L_e1); % 外信息交织后作为译码器2先验 L_e2 = siso_decode(rec_sys2, rec_par2, L_a2, g0, g1); L_a1 = deinterleaver(L_e2); % 交织器逆操作 L_e1 = siso_decode(rec_sys1, rec_par1, L_a1, g0, g1); end L_total = deinterleaver(L_e2) + L_e1; % 硬判决前组合所有软信息逻辑说明:这里的关键是外信息的流向。第一个SISO译码器处理系统位和校验位1,输出外信息;外信息交织后成为第二个SISO译码器的先验LLR;第二个SISO处理交织后的系统位和校验位2,输出外信息再解交织回去;如此往复。每一轮迭代,两个分量译码器都在利用对方给出的独立信息修正判决,这正是Turbo码名称的由来。
参数说明:max_iter一般取5到8,超过8轮后性能提升非常有限,反而线性增加仿真时间。Max-Log-MAP比Log-MAP少算指数和对数,每个符号的译码延迟能降低一半以上,性能损失在0.2到0.3dB左右。仿真阶段我建议先用Max-Log-MAP把链路跑通,确认无误后再切Log-MAP看性能上界。
3.4 Turbo码关键参数速查表
| 参数 | 推荐范围 | 对性能的影响 |
|---|---|---|
| 迭代次数 | 5 ~ 8 | 少于4轮时增益不够,大于8轮收益可忽略 |
| 交织器长度 | 1024 ~ 4096 | 长度越大,错误平台越低,但延迟和内存线性增加 |
| 生成多项式 | 反馈37、前向21(八进制) | 决定距离谱,换多项式需重新仿真验证 |
| 码率 | 1/3 或基于删余的 1/2 | 码率越低,编码增益越大,但频谱效率下降 |
| 交织器类型 | 随机交织(固定种子) | 规则交织在部分信噪比下会出现周期性错误 |
这个表是我调试链路时最常回头看的一张表。特别是交织器长度和迭代次数这两个参数,新手最容易一上来就取最大值,结果是仿真跑了几小时,曲线还不如长度1024迭代6次的好看。仿真不是做极限测试,是找性能和复杂度的平衡点。
4. 完整通信链路搭建与误码率仿真流程
4.1 链路模块划分和函数组织
一条完整的pi/4-QPSK+Turbo仿真链路,我习惯拆成五个独立模块:信源、Turbo编码器、pi/4-QPSK调制器、AWGN信道、pi/4-QPSK差分解调器、Turbo迭代译码器。每个模块一个函数文件,主脚本只负责组装和循环。模块划分干净的一个直接好处是:后面想换成瑞利信道或换成LDPC码,只需要改对应模块的接口,其他部分完全不动。
文件组织上,我一般按功能命名:main_ber_simulation.m是主脚本,pi4qpsk_mod.m和pi4qpsk_demod_soft.m是调制解调,turbo_encoder.m和turbo_decoder.m是编译码,rsc_encode.m和siso_decode.m是底层的分量函数。中文注释写在每个文件头部的说明块里,标注输入输出格式、是否需要归一化、以及常见调用方式,方便后面回读和给其他人交接。
4.2 主脚本:EbN0循环和误码率统计
% 主仿真脚本:pi/4-QPSK + Turbo 误码率仿真 % 输出:BER曲线、星座图、迭代收敛图 clear; clc; close all; % 系统参数 EbN0_dB = 0:0.5:4; % 信息比特信噪比范围 max_iter = 6; % Turbo迭代次数 frame_len = 1024; % 交织器/帧长度 num_frames = 50; % 每个信噪比下的仿真帧数 code_rate = 1/3; % Turbo码率(不删余) for idx = 1:length(EbN0_dB) EbN0 = 10^(EbN0_dB(idx)/10); N0 = 1 / EbN0; % 归一化噪声功率谱密度 total_bit_err = 0; total_bit_cnt = 0; for frm = 1:num_frames % 信源:随机比特 bits_in = randi([0 1], frame_len, 1); % Turbo编码,码率1/3 encoded_bits = turbo_encoder(bits_in, g0, g1, interleaver); % pi/4-QPSK调制,输出符号序列 tx_sym = pi4qpsk_mod(encoded_bits); % AWGN信道:符号能量归一化为1,噪声方差N0/2 noise = sqrt(N0/2) * (randn(size(tx_sym)) + 1j*randn(size(tx_sym))); rx_sym = tx_sym + noise; % 差分解调,输出软LLR llr = pi4qpsk_demod_soft(rx_sym); % Turbo译码 decoded_bits = turbo_decoder(llr, max_iter); % 统计误码 err = sum(decoded_bits ~= bits_in); total_bit_err = total_bit_err + err; total_bit_cnt = total_bit_cnt + frame_len; end ber(idx) = total_bit_err / total_bit_cnt; end % 画图 semilogy(EbN0_dB, ber, 'b-o'); grid on; xlabel('Eb/N0 (dB)'); ylabel('BER');逻辑说明:循环结构上一开始有两个嵌套循环,外层扫信噪比,内层做多次蒙特卡洛。每帧重新生成信源比特,避免同一帧数据在不同信噪比下重复使用导致统计相关性。N0的计算用了归一化假设,信号功率为1,那么噪声方差就是N0/2,前提是调制星座的能量已经归一化。
参数说明:frame_len取1024是交织器长度和帧长统一,为的是让统计巧合。num_frames取50帧时每个信噪比下统计10万多比特,误码率在10^-4量级已经比较可信;如果想看10^-5以下的误码率,需要把帧数提高到200以上,否则曲线末尾会抖动。EbN0_dB范围要按Turbo码的性能估计,码率1/3时误码率降到10^-4大约需要1.5到2dB,范围取0到4dB比较合适。
4.3 解调器输出软信息的关键实现
前面提到Turbo译码必须吃软信息。pi/4-QPSK差分解调器的LLR输出,我采用的方法是把判决区域的相位差转换成对应两条比特路径的似然比,近似用相位的正弦和余弦分量加权:
% pi/4-QPSK软输出差分解调 % 输入:匹配滤波后符号序列rx_sym % 输出:每个编码比特的LLR(对数似然比) function llr = pi4qpsk_demod_soft(rx_sym) % 差分相位 phase_diff = angle(rx_sym(2:end) .* conj(rx_sym(1:end-1))); % 两个编码比特分别对应相位平面的I/Q分量 % 对差分相位做旋转,使判决边界落在坐标轴上 rotated = exp(1j * (phase_diff - pi/4)); % 第一个比特由实部决定,第二个比特由虚部决定 % 用确定性近似LLR:幅值越大,置信度越高 llr_bit1 = sqrt(2) * real(rotated) / (N0/2); % 归一化噪声方差 llr_bit2 = sqrt(2) * imag(rotated) / (N0/2); % 按发射端的比特交织顺序恢复编码比特 llr = reshape([llr_bit1(:).'; llr_bit2(:).'], [], 1); end逻辑说明:rotated把星座旋转到标准QPSK坐标后,实部和虚部的符号直接对应两个编码比特的硬判决,幅度则反映了判决可靠程度。LLR用幅度除以噪声方差,本质是匹配滤波输出的软判决统计量。虽然这个近似不如真正推导的APP概率精确,但实践里配合Max-Log-MAP迭代译码,性能损失在0.2dB以内,实现简单得多。
参数说明:最后一行reshape的作用是按发射端的比特顺序输出,保证和Turbo译码器的输入顺序一致。如果调制端做的是正交比特映射,这里交错读取的顺序必须严格匹配,否则译码性能会直接崩掉,这个细节是我自己在调试中踩过的坑,当时误码率突然从10^-3跳到0.5,排查了一整天才发现是比特顺序反了。
4.4 操作视频对应的运行路径
配套的操作视频本质上就是把上面这套流程演示一遍:打开Matlab,切到工程目录,运行主脚本,观察星座图和误码率曲线,再修改几组参数重跑对比。实操里建议按三个步骤来:第一步只跑一个信噪比点,比如2dB,确认单帧能正常译码,避免一上来跑全曲线浪费时间;第二步跑完整曲线,观察趋势是否单调下降;第三步改交织器长度或迭代次数,对比性能差异。这个习惯看着简单,但能避免大部分低级错误带来的无效仿真。
5. 误码率仿真避坑清单:5个最容易翻车的环节
5.1 误码率曲线在高信噪比时进入平台期不再下降
现象:EbN0超过3dB后,BER曲线不再下降,像被一个水平线托住一样,和理论预期差一个量级以上。原因是多方面的,最常见的两个:一是每帧数据量太少,统计误差导致误码率下限被抬高;二是差分检测带来的固有性能限制,pi/4-QPSK差分解调的BER在10^-5附近会明显变缓。
解决:先增加帧数到200,确认是否是统计抖动;然后对照相干解调的误码率,看差值是否稳定在2~3dB范围内。如果增加帧数后曲线仍然平,再从编码侧入手,检查是否出现了迭代不收敛导致的错误平台。
5.2 删余矩阵恢复顺序错位导致译码全错
现象:码率从1/3改成1/2后,误码率直接从良好变成0.5左右,有时改一个参数就好了,有时怎么调都不对。原因几乎总是删余后的校验位在接收端恢复原始顺序时错位。
解决:我后来给自己定了个规矩,删除矩阵用独立的函数管理,同一份矩阵同时用于删除和恢复,杜绝手写两遍可能引入的不一致。恢复后立刻做一次静态检查,比对删余位置和恢复位置是否一一对应,确定无误再进入译码流程。
5.3 解调器输出了硬判决,Turbo迭代失效
现象:链路里有Turbo码但增益不明显,误码率曲线和没用编码时差不多,迭代次数改了也没用。原因是解调器输出的是硬判决比特而不是软信息,Turbo译码器的输入置信度全部相同,外信息迭代等于空转。
解决:在解调器输出端检查数据类型,硬判决是0/1整数,软LLR是浮点数。如果发现是硬判决,按第四章的pi4qpsk_demod_soft改成软输出。这个坑特别隐蔽,因为链路能跑通,只是性能差,看起来像某个参数不对,实际是信息量在接口处被丢了。
5.4 信噪比口径不统一:EbN0和EsN0混用
现象:误码率曲线的横轴和论文对不上,特别是在改码率之后,曲线整体平移了约1.8dB。原因是EbN0和EsN0混用,码率1/3时EsN0比EbN0低10*log10(3)≈4.77dB,差异非常大。
解决:统一用信息比特信噪比EbN0作为横轴。编码后每符号的信噪比EsN0和EbN0的换算关系是EsN0 = EbN0 + 10log10(code_rate * bits_per_symbol)。pi/4-QPSK每个符号2个比特,码率1/3时每个信息比特对应3个编码比特,所以EsN0 = EbN0 + 10log10(2/3) ≈ EbN0 - 1.76dB。我在主脚本里专门留了一段注释记录这个换算。
5.5 交织器种子不固定导致结果不可复现
现象:同样的参数,两次运行得到的误码率曲线不同,差别在小信噪比时尤其明显。原因是交织器使用随机数生成且未固定种子。Turbo码对交织模式敏感,不同的交织器产生的性能差异在低信噪比下可能超过0.5dB。
解决:在代码最开头固定随机数种子,rng(2024),并关闭对交织器的随机扰动。这里要特别提醒,如果别人要求复现你的仿真结果,这一步比参数表还重要,否则任何一次运行都是新结果。
6. 让仿真结果更有说服力的三个进阶技巧
验证链路正确性不能只靠一条BER曲线。我常用的第一个技巧是加一条无编码QPSK的理论误码率曲线做对照。无编码QPSK在AWGN信道下的理论误码率是0.5 * erfc(sqrt(EbN0_lin)),把这条线画在同一张图上,Turbo链路的增益一眼可见,一般码率1/3迭代6次在BER=10^-4时能比无编码提升4到5dB。
第二个技巧是观察迭代收敛行为。在2dB信噪比下分别记录迭代1、2、4、6次后的误码率,如果前两次迭代误码率下降明显而后面趋缓,说明译码器工作正常;如果迭代到第4次仍然没有收敛趋势,大概率是交织器或软信息链路有问题。这个方法比看最终BER曲线更早暴露问题,也更容易定位是哪一级链路异常。
第三个技巧是星座图检查。在解调器输入端和差分检测后各画一张星座图,前者应该看到围绕pi/4-QPSK星座点的噪声云且没有明显的相位旋转,后者应该看到旋转到标准QPSK象限的四个清晰簇。如果第二张图的簇出现45度偏移,说明差分相位计算的参考方向有问题。
我自己的习惯是在交付前跑一遍参数扫描表,把交织器长度、迭代次数、码率三个参数的组合结果整理成小表格,和主要BER图放在一起。这样看的人能直观理解每个参数的影响,也不至于误以为只有一组参数能复现。
最后说一个血泪教训:有一次我把码率从1/3改到1/2,但没同步修改接收端的噪声方差归一化,导致曲线整体偏移了快2dB,排查了两天才发现是信道置信度Lc的公式在换码率后没更新。从那以后我把所有参数推算过程都写成注释放在主脚本头部,每次改参数先看注释,再动代码。希望这些经验和坑能帮到你,让你的pi/4-QPSK+Turbo仿真链路一次跑通,少走我走过的弯路。
本文还有配套的精品资源,点击获取