
简介面向通信工程、电子信息及信号处理方向的学习者与研究者提供一套完整的四相相移键控调制解调仿真代码。压缩包内代码以十八个脚本文件形式呈现整体约八KB覆盖基带信号生成、正交载波调制、扩频与解扩、伪随机序列产生、低通滤波、时频域变换及同步解调等模块同时包含码元转换、串并变换等辅助函数并附有频谱分析与误差判决相关代码便于从原理到实现逐步验证。脚本函数划分细致例如通过傅立叶变换分析信号频谱利用载波调制与低通滤波还原正交支路再经相位判决恢复二进制序列完整展示了信号生成、传输与解调流程。目前已有153人学习下载适合正在学习数字通信或开展无线通信系统仿真的读者参考。1. QPSK信号调制解调仿真包不是 PPT是一套能直接跑的 MATLAB 链路做通信仿真的人应该都见过这种场景下到一个 QPSK 信号调制解调的压缩包打开一看十几个 .m 文件躺在那没有说明文档敢直接双击运行的人不多。这套 QPSK.rar 不是原理演示动画是 18 个能被 MATLAB 直接调用的脚本从串并转换、IQ 调制、低通滤波到抽样判决链路是完整的。更难得的是包里还混着扩频代码m_sequence、spread_spectrum、despread_spectrum 这几个文件说明它带直扩能力。QPSK 看着是四相本质上是两个 BPSK 在正交载波上叠加。它适合两类人刚接触数字调制、想把课本星座图对应到代码上的学生调误码率、搞不清眼图为什么糊的从业者。把代码跑通比看十遍原理图实在。2. 从比特到相位QPSK 调制的串并转换、星座映射与载波调制2.1 串并转换ser_to_par.m 把比特流拆成 I/Q 两路QPSK 传输的核心是“一个符号携带两个比特”。输入是一串串行比特流第一步永远是串并转换把比特流按 2 比特一组拆分。奇数位进 I 路偶数位进 Q 路这就是包里为什么同时出现 ser_to_par.m 和 par_to_ser.m 两个文件它们互为逆操作。串并转换的粒度是 2 比特不是 1 比特也不是 4 比特这是 QPSK 的定义决定的。如果拿 8PSK 或 16QAM 的代码改 QPSK第一步就要改分组粒度很多人在这里栽跟头。% ser_to_par_demo.m % 生成1000个随机比特作为信源 bits randi([0 1], 1, 1000); % 串行比特按2比特一组拆分 % 第1、3、5...个比特进I路第2、4、6...个比特进Q路 i_bits bits(1:2:end); q_bits bits(2:2:end); % 检查长度多出的奇数个比特直接丢弃 disp([I路长度: , num2str(length(i_bits))]); disp([Q路长度: , num2str(length(q_bits))]);这段代码逻辑很直白randi 生成信源序列通过奇偶下标分离变成两路并行比特。参数上要注意输入比特数必须是偶数否则末尾丢一个比特后面统计误码率时对不齐。实际工程里的 ser_to_par.m 会带缓存对齐和时钟节拍逻辑仿真阶段可以简化但这步不能省。如果跳过串并转换直接查表映射星座图上只会出现两个点I 和 Q 永远相等四个相位退化成两个这就是典型的“相位减半”翻车现场。我见过不少人在这一步出错串并转换做对了但分组起点偏移了一个比特接收端无论怎么调都恢复不出原始序列。所以 ser_to_par.m 和 par_to_ser.m 的起点对齐很关键发射端第一组是 bits(1)、bits(2)接收端合并时也要按同样的顺序交错。调试时先拿一段已知比特流走一遍串并、并串输出和输入完全一致再往下进行。2.2 电平映射与载波调制bi_to_uni.m、carrier_mod.m 和 rf_mod.m 的流水线分离出 I/Q 比特后下一步是电平转换。QPSK 四个相位对应 00、01、11、10通常做法是把 0 映射为 -1、1 映射为 1即双极性电平这个处理在 bi_to_uni.m 里完成。文件名叫 bi_to_uni字面是二进制转单极性但在 QPSK 链路里它实际做的是比特到乘法器电平的预处理。别被名字带偏重点看它输出的电平值是不是 ±1而不是纠结单极性双极性的字面定义。% bi_to_uni_demo.m % 单极性比特转双极性电平便于后续载波相乘 i_symbols 2 * i_bits - 1; % 0 - -1, 1 - 1 q_symbols 2 * q_bits - 1; % 电平序列做插值每个符号填充sps个采样点 sps 16; % 每符号采样点数 i_wave repelem(i_symbols, sps); q_wave repelem(q_symbols, sps);repelem 是零阶保持把每个符号电平保持 sps 个采样点等效于成型滤波前的理想脉冲。sps 参数很关键它决定后续眼图横轴的密集程度仿真里取 16 是为了波形平滑工程上 8 到 32 都是常见取值。取值太小波形台阶感严重频谱旁瓣衰减变差取值太大仿真时间成倍增加。这套代码里没有专门的成型滤波文件扩频和调制之间直接用矩形脉冲成形。矩形脉冲对应频谱旁瓣衰减慢带宽受限信道里需要后续加根升余弦滤波器这份资源胜在链路透明适合先跑通再优化。载波调制在 carrier_mod.m 里完成。I 路乘余弦载波Q 路乘正弦载波两路相加就是 QPSK 复合信号。rf_mod.m 在此基础上加入射频概念相当于把信号搬到更高的载频上。% carrier_mod_demo.m fs 200e3; % 系统采样率 200kHz fc 20e3; % 载波频率 20kHz t (0:length(i_wave)-1) / fs; % 时间轴 % I路乘cosQ路乘sin相位差90度合成为QPSK s_qpsk i_wave .* cos(2*pi*fc*t) ... q_wave .* sin(2*pi*fc*t);这段代码是 QPSK 调制最核心的运算。cos 和 sin 的 90 度相位差就是“正交”来源I 路和 Q 路任意一路单独看都是 BPSK叠加后才是四相。频率参数上fc 相对 fs 要留足频带一般取 fs 的 1/10 到 1/4。fc 取得太小频谱集中在零频附近低通滤波器不好设计取得太大采样率跟不上混叠就直接来了。2.3 文件清单与调用关系18 个 m 文件怎么串成一条链路把 18 个 m 文件按发射链路排一遍关系就清楚了。我整理了一份对应表方便对号入座文件环节作用ser_to_par.m调制前端串行比特转 I/Q 两路并行bi_to_uni.m电平映射比特转双极性电平carrier_mod.m载波调制I/Q 分量合成 QPSK 信号rf_mod.m射频仿真基带信号搬移到载频T2F.m频谱分析时域波形转频域m_sequence.m扩频码生成产生 m 序列spread_spectrum.m扩频数据与扩频码相乘despread_spectrum.m解扩接收端相关解扩kuopinzlp.m直扩链路扩频直扩主流程lpf.m接收前端混频后低通滤波carrier_remod.m载波恢复本地载波重构par_to_ser.m判决输出I/Q 路合并为串行比特剩下几个辅助文件里dpxs.m 负责波形或频谱显示qpskzlp.m 和 qpskzlpbeifen.m 是整链路主程序与其备份版本kuopinzlp1.m 是扩频流程的变体jiekuozlp.m 是解扩环节的另一个实现。整体调用关系是发射端按 ser_to_par → bi_to_uni → carrier_mod → rf_mod 的顺序跑扩频插在映射之后、载波调制之前接收端按混频 → lpf → 判决 → par_to_ser 的顺序收回比特。调试时记住这个顺序就知道该在哪一步打断点。调试这套代码有个实用技巧把 ser_to_par 和 par_to_ser 看作一对黑匣子先单独验证它们是否互逆——输入随机比特经过这两步如果输出和输入完全一致说明链路骨架是通的剩下问题都在中间环节排查范围一下子缩小一大半。3. 解调链路怎么搭混频、低通滤波、判决与并串转换的四步走3.1 同步解调的基本假设本地载波必须和发送端对齐解调端第一反应是反转调制过程收到 QPSK 信号后分别乘上 cos 和 sin 的本地载波再用低通滤波器把两倍载频分量滤掉剩下的就是 I/Q 基带电平。这个方案成立的前提是本地载波与发送端完全同频同相这就是同步解调里“同步”二字的含义。这套代码用的是理想本地载波直接拿发送端的 cos 和 sin 复制一份过来用省去了锁相环设计适合先把链路跑通再考虑非理想因素。% qpsk_demod_mixer.m % 接收信号分别乘上I/Q本地载波完成频谱搬移 r_signal s_qpsk 0.05 * randn(size(s_qpsk)); % 加一点噪声模拟信道 i_down r_signal .* cos(2*pi*fc*t); % I路混频 q_down r_signal .* sin(2*pi*fc*t); % Q路混频混频的数学基础是积化和差信号乘相同频率的载波后产生基带分量和两倍载频分量之后由 lpf.m 滤掉高频。这里 randn 的幅度 0.05 对应信噪比大约 20dB星座图还能看得很清楚。注意发送端用的载波和本地载波必须来自同一个时间轴 t如果两边采样点数不一致混频结果直接是错的。还要区分同步解调和非同步解调。非同步解调在实现上更宽松比如用包络或差分检测但对 QPSK 这类绝对相位调制非相干解调会有固定误码损失大约差 2.3dB。所以工程上绝大多数 QPSK 接收机都走同步解调路线这套代码里的 carrier_remod.m 就是为同步需求准备的。3.2 lpf.m 的参数设计截止频率取符号速率的多少倍低通滤波器是解调质量的分水岭。前面设的载波频率 20kHz符号速率按 sps16 折算为 12.5k 波特LPF 截止频率如果太低符号波形被压扁眼图睁开度变小太高两倍频分量滤不干净星座图外圈出现毛刺。这套代码里 lpf.m 就是个独立脚本输入 x、fs、cutoff内部用巴特沃斯滤波器实现。function y lpf(x, fs, cutoff) % 五阶巴特沃斯低通滤波器 % x: 输入信号, fs: 采样率, cutoff: 截止频率, 单位Hz [b, a] butter(5, cutoff / (fs / 2), low); y filter(b, a, x); end调用时写成 i_baseband lpf(i_down, fs, 15e3)cutoff 取 15kHz略高于符号速率 12.5k明显低于载频 20k留出过渡带。经验选法是取 1.2 到 1.5 倍的符号速率。cutoff 单位和 fs 保持一致千万别混用 kHz 和 Hz。滤波器阶数这里用 5 阶群延迟还在可控范围如果阶数加到 9 以上波形会明显延迟后面抽样时刻要跟着偏移这就是眼图睁着但误码率高的隐形原因。3.3 判决、并串转换与误码统计par_to_ser.m 的反向操作滤波后的 I/Q 信号是连续波形要先抽样得到每个符号的电平值再判决成 0 或 1。抽样时刻通常选在码元中点也就是眼图睁开最大的地方。这一步代码最简单但错在抽样偏移上的人最多。抽样点偏到符号边缘电平接近 0噪声稍大一点就判错。% decision_demo.m % 每个符号取中点采样判决后并串合并 idx round(sps/2) : sps : length(i_baseband); i_hat i_baseband(idx) 0; % 大于0判1否则判0 q_hat q_baseband(idx) 0; rx_bits reshape([i_hat; q_hat], 1, []); % I/Q交替合并成串行流 err_rate sum(rx_bits(1:length(bits)) ~ bits(1:length(rx_bits))) / length(bits);par_to_ser.m 就是这段功能封装。判决门限取 0 是双极性电平的自然选择如果发送端没有做双极性转换而是直接把 0/1 映射到 0/1门限要改成 0.5。合并时 reshape 按行交错确保恢复顺序和发射端 ser_to_par 一致。误码率统计时注意长度对齐rx_bits 可能比原始 bits 短一个符号先截到相同长度再比较。误码率统计这一步看着简单实际上有陷阱如果串并转换起点偏了一位或者抽样时刻偏移了半个符号周期误码率会在 0.5 附近徘徊怎么看都不像随机噪声造成的。遇到这种“数值优美但结果错误”的情况优先检查对齐而不是加噪声。提示如果本地载波不是理想源而是 carrier_remod.m 恢复出来的载波误码率会比理论值略高这是载波恢复环路的正常代价不属于代码 bug。4. 傅立叶变换看 QPSK频谱特征与包里隐藏的直扩链路4.1 T2F.m / F2T.m 的变换逻辑采样率、点数与频域搬移包里有一对函数 T2F.m 和 F2T.m命名是 Time to Frequency 和 Frequency to Time。这类时频转换工具在通信仿真里是通用的不局限于 QPSK但很多人在调用时不知道内部细节画出来的频谱图横轴对不上。% T2F_demo.m function [f, spec] T2F(t, x) fs 1 / (t(2) - t(1)); % 由时间轴反推采样率 N length(x); % 信号长度 f (-N/2 : N/2-1) * fs / N; % 频率轴中心化对称 spec fftshift(fft(x)); % FFT后搬移零频居中 end关键在频率轴构造。MATLAB 的 fft 输出从 0 到 fs想显示双边谱必须用 fftshift 把零频搬到中间。频率分辨率 fs/N想看得更细就加大 N也就是截取更长的时域信号。观察 QPSK 频谱时N 最好取 2 的幂如 4096 或 8192这样频谱图上的栅栏效应弱一些。还有一个常被忽略的参数是时间轴的起点 t 必须从 0 开始均匀递增如果从非零时刻开始频率轴会整体偏移。4.2 QPSK 的频谱特征主瓣宽度、旁瓣滚降与带外泄漏傅立叶变换在 QPSK 中的应用本质是把时域正交性映射到频域观察。教科书上说 QPSK 信号频谱由单边带和载波组成实际操作中看到的是以载频为中心的连续谱所谓单边带是理想滤波器取出的一侧平时仿真看到的是双边对称的。QPSK 信号功率谱和 BPSK 相似主瓣宽度是符号速率的两倍零点出现在 2 倍的符号速率处。因为 I/Q 两路正交频谱上不会出现两个完全独立的载波峰而是合成一个整体形状中心频率处能量集中。观察步骤是把调制好的 s_qpsk 用 T2F 转频域再 plot 幅值看三个点主瓣宽度是否接近 2 倍符号速率旁瓣衰减是否平滑带外有没有杂散尖峰。杂散尖峰通常来自采样率不匹配或时域截断不是 QPSK 本身的特征。傅立叶变换在这里的角色是把时域 IQ 波形映射到频域看看能量分布是否符合理论预期。QPSK 信号呈恒包络特征理想情况下频谱旁瓣按频率的平方倒数衰减如果 I/Q 幅度不平衡比如 I 路增益是 1、Q 路增益是 1.2频谱上会出现原载波位置的残留分量这是判断 IQ 失衡的快速手段。dpxs.m 这个脚本在包里承担的就是把这类频谱特征显示出来的职责跑主流程前先调它看基线频谱比最后统一检查高效。4.3 直扩链路m_sequence.m 生成扩频码spread_spectrum.m 完成扩频这个包真正的价值在黑匣子里藏着m_sequence.m、spread_spectrum.m、despread_spectrum.m 三个文件构成一套直接序列扩频DSSS实现。直扩做法是用高速率伪随机码与低速率的调制符号相乘把信号频谱展宽换来处理增益。% m_sequence_demo.m function seq m_sequence(reg_init, poly) % 线性反馈移位寄存器生成m序列 % reg_init: 寄存器初态, poly: 反馈抽头多项式 N 2^length(reg_init) - 1; seq zeros(1, N); reg reg_init; for k 1:N seq(k) reg(end); % 输出末级 feedback mod(sum(reg(poly)), 2); % 抽头异或 reg [feedback, reg(1:end-1)]; % 寄存器移位 end endm 序列周期由寄存器级数决定3 级寄存器周期 74 级周期 15实际扩频码往往取 31 或 63。poly 是反馈抽头位置比如 [2 3] 表示第 2 和第 3 级抽头异或后反馈到输入端抽头选错序列周期会缩短甚至退化成固定值。扩频操作就是把每个数据符号与伪码序列相乘接收端用同一个 m 序列做相关解扩把能量重新收拢回窄带。despread_spectrum.m 就是相关器实现它要求本地伪码起始位置与发送端严格对齐差一个码片相关峰就出不来。仿真代码里一般直接假设对齐所以跑起来很顺切到真实信道就得用滑动相关搜索做码捕获这是从仿真走向实物的第一个门槛。处理增益的定义是扩频后带宽与信息带宽之比数值上等于码片速率除以符号速率。如果让每个 QPSK 符号乘以 63 位 m 序列处理增益就是 63约 18dB。这个增益在解扩前体现在频谱展宽上解扩后体现在信噪比提升上。你在包里的 kuopinzlp.m 和 kuopinzlp1.m 之间对比一下能看出两个版本在扩频增益上的不同取舍。5. QPSK 仿真的常见问题排查眼图、同步、频谱三个翻车点这套包调试时最常见的翻车点不出五类按出现频率从高到低排列如下每条都按“现象 → 原因 → 解决”来写方便直接对照。5.1 眼图模糊成一团LPF 带宽没有对准符号速率现象用 eyediagram 画解调后的 I 路波形眼图完全糊掉看不出清晰的“眼睛”形状抽样判决点上下抖动很大。原因lpf.m 的截止频率设置不当。截止频率过高时混频后的 2 倍载频分量没有被充分抑制叠加在基带波形上造成纹波截止频率过低时符号的方波边沿被滤成缓坡码间串扰加剧。两种情况都会让眼图睁开度变小。QPSK 眼图比 BPSK 更敏感因为两路正交信号在合成后会互相影响。解决按符号速率来定截止频率取 0.8 到 1.5 倍符号速率。如果符号速率 12.5kfs200kcutoff 取 10k 到 18k 之间的值交替试几次看眼图哪个最清晰。cutoff 取值对照表符号速率(k波特)cutoff 下限(kHz)cutoff 上限(kHz)12.51018252036这组数据以巴特沃斯五阶为例换滤波器类型要重扫。我一般会直接写个 for 循环扫一遍 cutoff把每组的眼图睁开度记下来比手动试快得多。5.2 误码率居高不下本地载波存在相位差现象整体误码率在 10% 附近下不去星座图上四个簇不在标准位置而是整体旋转了一个角度。旋转角小于 45 度时判决还能部分正确大于 45 度就系统性出错。原因QPSK 信号对相位误差有 45 度的容忍边界。理想星座点分布在 0、90、180、270 度本地载波如果偏了 10 度每个簇都跟着偏 10 度判决边界还是 45 度线没越过边界前只是信噪比损失。本地载波用的是 carrier_remod.m 里的重构信号如果这个信号是从接收波形直接估出来的信道噪声会影响估计精度估计噪声等效为相位抖动。解决先在仿真里用理想载波跑通确认误码率和理论值接近再换成 carrier_remod.m 的恢复载波。恢复载波的锁相环带宽设窄一些环路噪声就小但捕获时间会变长。做性能对比时把载波恢复造成的误码损失单独统计出来不要混在信道噪声里算。相位偏差的补偿用导频符号或者判决反馈环都能做先确认偏差来源再去选算法。5.3 频谱出现杂散尖峰采样点数与窗函数选择现象用 T2F.m 画出来的频谱在主瓣两侧有一串等间隔的小尖峰理论上应该是平滑的旁瓣衰减。原因FFT 的隐含假设是信号在时域上周期延拓。如果截取的长度不是信号周期的整数倍截断处产生不连续频谱上就出现泄漏表现为杂散和拖尾。这是数字信号处理的经典问题不是调制代码的 bug。fc20k、fs200k 时信号周期是 0.05msN 取 1000 正好是整数倍关系。解决第一种方法是让采样点数 N 对应的时长正好是载波周期的整数倍。第二种方法更通用给时域信号加汉明窗或海宁窗牺牲主瓣宽度换掉旁瓣泄漏。加窗后的频谱主瓣会宽一点但带外杂散能压下去十几个 dB。仿真里两种都试一下对比加窗前后的频谱包络取带外泄漏小的方案。5.4 扩频之后误码率反而升高解扩伪码没对齐现象在 QPSK 基础上加了 spread_spectrum.m 扩频后误码率不是降低而是升高甚至接近 50%星座图上的点完全散开看不出四个簇。原因直扩系统的抗干扰能力建立在解扩相关峰正确恢复的基础上。despread_spectrum.m 做相关时本地 m 序列与发送端扩频码的相位差超过一个码片相关值接近零数据没有被解扩而只是被再乘了一次伪随机码频谱没有收拢相当于信号变成了噪声。这是直扩仿真里最隐蔽的坑代码不报错但结果完全错误。解决检查两个地方。一是 m_sequence.m 调用时的初始寄存器值发端和收端必须一致二是解扩前有没有做码同步仿真链路省掉捕获过程会默认对齐一旦加噪声或改参数就露馅。建议把发端和收端的 m 序列画在同一个图里核对对齐情况或者互相关跑一次看峰值位置是否在零偏处。5.5 运行报错变量未定义脚本与函数作用域冲突现象双击 qpskzlp.m 运行报错“Undefined function or variable”但文件明明在路径里别的脚本又能正常跑。原因包里有些脚本既包含主流程又定义局部函数MATLAB 版本升级后对脚本中函数的支持规则变了。旧版本允许脚本末尾带函数定义新版本要求函数单独存文件或者把主流程改成 function main 的调用形式。qpskzlpbeifen.m 和 kuopinzlp1.m 这类备份文件里混着多份实现容易让工作区的变量互相覆盖前一次运行的残留变量会干扰下一次。解决先把当前文件夹加入路径用 which 命令确认同名函数没有冲突比如 which spread_spectrum。然后用命令行逐段跑先跑 m_sequence 生成伪码再跑 spread_spectrum最后跑 qpskzlp 主流程。MATLAB 从 R2016b 开始支持脚本局部函数更早的版本就别指望双击一键运行了。这个坑属于环境问题不是算法问题但坑起人来一点不含糊。6. 验证调制质量的实用手段星座图、眼图与误码率三步检查拿到这套代码别急着改参数先建立验证基线。我自己的习惯是三步先画星座图确认映射正确再看眼图确认滤波参数合理最后算误码率确认链路完整。% verify_qpsk.m % 第一步发射端星座图四个簇应该清晰分开 scatterplot(i_symbols 1j*q_symbols); grid on; title(发射端星座图); % 第二步解调后眼图观察判决点睁开度 eyediagram(i_baseband, sps); % 第三步误码率与理论值对比 [err, ber] biterr(bits(1:length(rx_bits)), double(rx_bits)); disp([实际误码率: , num2str(ber)]);scatterplot 是 MATLAB 通信工具箱自带函数没有工具箱就用 plot(real(x), imag(x), .) 代替。eyediagram 第二参数是每符号采样点数 sps取 16 时把 16 个相位点叠加成一个眼图。检查顺序别反星座图不对眼图一定不对眼图睁开度 OK 但误码率高问题基本出在抽样时刻偏移上。只看星座图有一个陷阱星座图只显示抽样时刻的离散点滤波器带宽、码间串扰这些问题它在视觉上不敏感眼图才暴露完整波形质量。所以我习惯上眼图睁开度不够就不谈误码率盲目调噪声幅度只会掩盖问题。从那次被一个看似正常的星座图骗了之后我每次拿到新的调制仿真代码都强制自己把这三步走完再动任何参数——先看星座图再拉眼图最后才算误码率顺序一次都不换。这套 QPSK 包里隐藏的直扩链路参数敏感度比调制部分更高建议你用同样的三步单独验证扩频解扩前后的星座图对比处理增益的实际收益。希望帮到你。本文还有配套的精品资源点击获取