简介:一份面向无线通信与信号处理初学者、通信专业学生及相关从业者的 QPSK 调制解调 MATLAB 仿真资源包,覆盖从二进制序列生成、符号映射、载波调制、加噪到解调判决的完整链路,适合通信原理课程实验或入门项目参考,能帮助快速理解 QPSK 从信息序列到恢复序列的完整过程。压缩包内共 2 个 .m 文件,大小仅 566B,代码精简,便于逐行阅读和二次修改,其中分别实现了调制与解调主流程,展示了复数乘法、匹配滤波、相位检测等关键算法。目前已有 147 人学习/下载,适合想通过可运行示例验证理论模型的读者。借助这份小体积示例,可直观对照 QPSK 的 0°、90°、180°、270° 四相位映射,并理解载波恢复、硬判决与软判决等概念。整体结构清楚,便于扩展信噪比分析、相位模糊处理等实验,是衔接教科书与工程仿真的实用素材。
1. 为什么QPSK调制解调总会以一个zip包的形式出现
打开一个qpsk.zip,最常见的场景是:你要在MATLAB或Python里搭一条调制解调链路,但分不清哪个脚本先跑,也不明白星座图上落在哪些点才算正确。这个压缩包的核心往往就是几段脚本:一个调制器、一个AWGN信道、一个解调器,外加误码率统计。QPSK是四相相移键控,每个符号携带2比特,频谱利用率比BPSK高一倍,又比8PSK更容易解调,因此在卫星通信、数字微波和Wi-Fi的802.11a/g OFDM子载波调制中都能看到它。这篇文章按“调制端映射—接收端解调—误码率仿真—EVM验证”这条链路,把zip里该写的东西逐段讲清楚。适合刚接手通信算法的工程师,也适合需要在SystemVerilog里做基带参考模型的人。
2. QPSK信号调制端的星座图映射与脉冲成形细节
2.1 为什么QPSK信号调制比BPSK复杂但比8PSK实用
QPSK相当于两个正交BPSK的叠加,它将串行比特流拆分成同相支路(I路)和正交支路(Q路),两条支路各用二分之一符号速率发送。这样在相同比特速率下,符号率是BPSK的一半,带宽也就压缩了一半,因此单位频带能传2 bit/s。和8PSK相比,QPSK的星座点之间角度差是90°,抗噪声能力中等;8PSK虽有3 bit/s/Hz的频谱效率,但45°的相位间隔要求更高的信噪比,一般链路预算吃不消。所以很多调制解调器把QPSK当作“既有带宽收益又不过分损耗灵敏度”的折中档位。
实际工程里,QPSK信号调制有两种常见实现习惯:一种是把比特先变成极性码,再用两个乘法器把I/Q路基带分量搬到中频或载波上;另一种是在数字域直接生成复数基带波形,后面交给数字上变频。zip包里的参考代码多数是后者,因为MATLAB和Python都习惯用复数表示基带信号,I路对应实部,Q路对应虚部。
2.2 I/Q拆分与格雷映射:QPSK的符号表
QPSK调制器的第一步是把每2个比特映射成1个复数符号。为了降低相邻符号误判时的比特错误数,工程上几乎都使用格雷码。以最常见的星座图为例,四个星座点位于单位圆上,相位分别为45°、135°、225°、315°,对应的输入比特和复数符号如表格所示。
| 输入比特 (b0 b1) | 相位偏移 | 复数符号 | 符号索引 |
|---|---|---|---|
| 00 | 45° | (0.7071, 0.7071) | 0 |
| 01 | 135° | (-0.7071, 0.7071) | 1 |
| 11 | 225° | (-0.7071, -0.7071) | 2 |
| 10 | 315° | (0.7071, -0.7071) | 3 |
注意这个表使用了相邻符号只有1比特翻转的格雷编码。为了方便计算能量,通常让每个符号的模为1,即实部和虚部都是±1/√2,这样平均符号能量Es=1。有些代码里为了省去√2运算,直接用±1,那Es=2,后面加高斯噪声时噪声方差要相应放大,这是一个很容易踩的坑。我一般会先计算一次星座点的平均功率,再决定Es/N0换算,而不是假设某种归一化。
提示:星座图的具体相位旋转不影响误码率,但会改变判决区域。只要发送端和接收端的映射表保持一致即可。
2.3 用根升余弦脉冲成形生成QPSK基带信号
数字QPSK调制中,无限阶跃变化的星座点在频域会产生很宽的主瓣和旁瓣,直接发送会干扰邻近信道。常见做法是加脉冲成形滤波器,通常使用根升余弦滤波器(RRC)。发送端和接收端各用一个根升余弦滤波器,级联后等效为升余弦滤波器,这样在最佳采样点不存在符号间干扰。
以下代码用Python生成随机比特,先做格雷映射,再经过根升余弦成形。你可以直接拿它当zip里的参考实现。
import numpy as np from scipy import signal def rrc_pulse(sps=8, beta=0.35, span=6): # 生成根升余弦滤波器系数 N = span * sps + 1 t = np.arange(-span/2, span/2 + 1/sps, 1/sps) h = np.zeros(N) for i, ti in enumerate(t): if abs(ti) < 1e-8: h[i] = 1.0 - beta + 4 * beta / np.pi elif abs(abs(ti) - 1/(4*beta)) < 1e-8: h[i] = (beta/np.sqrt(2)) * ( (1 + 2/np.pi) * np.sin(np.pi/(4*beta)) + (1 - 2/np.pi) * np.cos(np.pi/(4*beta))) else: num = (np.sin(np.pi*ti*(1 - beta)) + 4*beta*ti*np.cos(np.pi*ti*(1 + beta))) den = np.pi*ti*(1 - 16*beta**2*ti**2) h[i] = num / den h = h / np.linalg.norm(h) return h def bits_to_qpsk_symbols(bits, gray=True): # 每2个比特映射为一个复数QPSK符号 n_syms = len(bits) // 2 pairs = bits[:2*n_syms].reshape(-1, 2) const_map = { (0,0): (0.7071+0.7071j), (0,1): (-0.7071+0.7071j), (1,1): (-0.7071-0.7071j), (1,0): (0.7071-0.7071j), } return np.array([const_map[tuple(p)] for p in pairs]) # 基础参数 fs = 8e6 # 采样率 8 MHz sym_rate = 1e6 # 符号率 1 MBd sps = int(fs // sym_rate) # 每符号采样数 8 n_symbols = 128 rng = np.random.default_rng(42) bits = rng.integers(0, 2, n_symbols * 2) syms = bits_to_qpsk_symbols(bits) # 插零上采样 up = np.zeros(n_symbols * sps, dtype=complex) up[::sps] = syms # 脉冲成形 taps = rrc_pulse(sps=sps, beta=0.35, span=6) tx_signal = signal.upfirdn(taps, up)这段代码的关键在于up[::sps] = syms:每两个采样点之间插入sps-1个零,等效于用离散冲激串承载符号。rrc_pulse生成的抽头长度是span*sps+1,span取6表示滤波器覆盖6个符号周期,抽头越多频域滚降越陡。beta=0.35是滚降系数,决定过渡带宽度;QPSK信号占用带宽约为(1+beta)*sym_rate/2,按这里就是0.675MHz。若采样率降为4倍符号速率,过渡带容易折叠,建议每符号采样数至少取4,接收端再做定时恢复时余量也更大。
3. QPSK解调端的符号同步、载波恢复与判决实现
3.1 接收端解调链路:从载波到符号
模拟或数字接收机里,QPSK解调的顺序分两步:先用本地振荡器把中频信号搬回基带,得到一个复基带信号;再用匹配滤波器和采样器把每个符号对应的I/Q值恢复出来。理想情况下,接收机知道载波频率和定时边界,因此只需要在最佳采样点判决。但实际系统中,本地载波与发送端存在频率偏移和相位偏移,符号时钟也有采样抖动,所以解调器必须包含定时同步与载波同步环路。
很多zip包用MATLAB的rcosdesign和DDPLL搭链路,但本质步骤可以写成下面的代码。为了便于对照,这里的基带模型不做射频搬移,只讨论复数基带符号如何在受扰后恢复。
3.2 匹配滤波与最佳采样的硬判决
接收信号经过与发送端相同的根升余弦滤波后,按采样点抽样获得每符号的复数星座点。这里假设收发两端定时已经对齐,且每个符号取一个采样点。然后对每个复数符号计算它离四个星座点的欧氏距离,取最小距离点对应的索引,再翻转成比特。
def qpsk_demod(rx_syms): # rx_syms: 长度为N的复数数组,已对齐到最佳采样时刻 grid = np.array([ 0.7071+0.7071j, -0.7071+0.7071j, -0.7071-0.7071j, 0.7071-0.7071j ]) idx = np.argmin(np.abs(rx_syms[:, None] - grid[None, :]), axis=1) bit_map = {0: (0,0), 1: (0,1), 2: (1,1), 3: (1,0)} results = [] for i in idx: results.extend(bit_map[i]) return np.array(results)这段代码中符号索引与调制端的映射表必须一致,否则解调正确但比特全错。实际代码中最好把grid和bit_map定义成调制、解调共用的常量,避免两处各写一份。硬判决只输出0/1,如果要交给低密度奇偶校验或极化码译码器,则要改成软值输出,它需要计算对数似然比,而不是直接判决。
3.3 载波相位偏移对判决的影响与Costas环思想
若本地载波与发送端的相位差Δθ恒定,接收符号会被旋转Δθ。当Δθ=0时判决无误;当Δθ=90°时,符号点被旋转到下一点,如果没有差分编码,输出的比特会完全不可用。QPSK常用的载波恢复方式有四项Costas环、判决反馈环和极性判决环,它们的核心都是用符号功率驱动环路滤波器,再反馈给NCO调整本地载波相位。
下面给出一段简化的判决反馈相位估计循环,它比完整Costas环更快理解原理:
def phase_recovery(rx_syms, step=0.05): # 简单判决反馈:先硬判决,再估计残余相位 theta_hat = 0.0 out = np.zeros_like(rx_syms, dtype=complex) grid = np.array([0.7071+0.7071j, -0.7071+0.7071j, -0.7071-0.7071j, 0.7071-0.7071j]) for i, y in enumerate(rx_syms): y_rot = y * np.exp(-1j * theta_hat) est = grid[np.argmin(np.abs(y_rot - grid))] err = np.angle(est * np.conj(y_rot)) theta_hat += step * err out[i] = y_rot return out, theta_hat该环路只适合慢变化的相位噪声,步长step过大会导致环路振荡,过小则跟踪慢。实际QPSK解调器还需要一个二阶环路滤波器,这里用一阶来演示。另外,接收端要先把直流偏置去掉,否则环路会偏向某个正电平。若载波频偏很大,例如超过符号率的5%,建议先在频域或时域做扫频粗同步,再交给锁相环细调。
3.4 定时同步:最大平均功率与Gardner算法
定时同步常被忽略,但QPSK的符号定时偏差会导致眼图闭合,误码率立刻抬高。简单做法是先让每个符号保留多个采样点,计算每个候选定时偏移下的平均功率,功率最大的点就是最佳采样时刻。对于突发通信,这个首估足够;连续的流式传输则要利用Gardner算法,它每个符号使用两个采样值,一个在中点,一个在判决点,利用两者差值更新定时误差。
Gardner算法实现时要注意不受载波相位影响,因此可以在载波恢复之前工作。环路更新公式是err = Re[ (x(n+1)-x(n)) * conj(x(n+0.5)) ],符号速率越高,环路带宽要留得越窄,否则定时抖动会被放大。
提示:很多zip包里解调器漏了匹配滤波的归一化,导致星座图发散。两个根升余弦滤波器级联后,在最佳采样点的幅度应为1,检查滤波器系数的L2范数即可。
4. AWGN信道下的QPSK误码率仿真与Eb/N0换算
4.1 QPSK误码率公式与BPSK等价的原因
在直角坐标下,QPSK的I路和Q路相互正交,各携带一半能量,每一路的误码率等同于BPSK。总误码率为BER = Q(sqrt(2*Eb/N0))。要注意这个公式输出的是误码率,不是误符号率;每个QPSK符号由两个比特组成,误符号率近似为2*Q(sqrt(2*Eb/N0)),但因为格雷码映射,一个符号错误通常只对应1比特错误,所以误码率仍约为误符号率的一半。下面是几个常见Eb/N0点的理论BER值。
| Eb/N0 (dB) | 理论BER |
|---|---|
| 0 | 7.86e-2 |
| 3 | 2.28e-2 |
| 6 | 2.39e-3 |
| 9 | 3.36e-5 |
| 12 | 1.90e-8 |
表格用于校验仿真曲线走向。很多工程师在对比时忘记把Es/N0换成Eb/N0,直接用符号信噪比对照公式,结果偏差很大。QPSK的Es/N0比Eb/N0大3.01dB,因为每个符号携带2个比特。
4.2 蒙特卡罗仿真:加噪声的方差如何设置
仿真时通常直接对复基带符号加高斯白噪声。若QPSK星座点模为1,平均符号能量Es=1,每比特能量Eb=Es/log2(4)=0.5。噪声是复高斯过程,实部和虚部独立,总噪声方差等于N0,因此给定Eb/N0时,N0=Eb/(Eb/N0)。代码里先算一次真实符号能量,再按这个值折算噪声功率,可以避免做法归一化错误。
import numpy as np def add_awgn(syms, ebn0_db, bits_per_symbol=2): # 按实际符号能量给复数符号加AWGN ebn0 = 10**(ebn0_db / 10) Es = np.mean(np.abs(syms)**2) Eb = Es / bits_per_symbol No = Eb / ebn0 n = np.sqrt(No/2) * (np.random.randn(len(syms)) + 1j * np.random.randn(len(syms))) return syms + n def ber_sim(ebn0_db, n_symbols=100000): rng = np.random.default_rng(1) bits = rng.integers(0, 2, n_symbols*2) syms = bits_to_qpsk_symbols(bits) rx = add_awgn(syms, ebn0_db) hard_bits = qpsk_demod(rx) return np.mean(hard_bits != bits) for db in [0, 3, 6, 9, 12]: print(f"Eb/N0={db:2d} dB, BER={ber_sim(db):.4e}")这个仿真没有经过脉冲成形,直接在一个符号一个采样点的离散模型上运行。add_awgn里的No/2是复噪声每个维度的方差,乘到两个独立高斯随机变量上之后,复数噪声总方差恰好是No。ber_sim的接收端没有匹配滤波过程,因为发送端也没有成型滤波,二者是配套的。若把第2章的RRC成形引入,这里也要在接收端加RRC匹配,否则符号间干扰会直接抬高误码率。
4.3 仿真结果与理论曲线的核对
运行三次后,3dB附近的仿真BER约在2.2e-2到2.5e-2之间,和表格接近。如果偏差很大,先按下面几点查:收发映射表的索引是否一致;随机比特生成是否每组都用独立种子;噪声方差是否写成了sqrt(No)而不是sqrt(No/2);统计的是比特错误还是符号错误。符号错误率在高信噪比下约为比特错误率的两倍,这在曲线对比时要留意。
除此之外,帧长太短也会造成抖动,10万符号在BER=1e-4时大约只有10个误码,统计波动约30%。想要曲线平滑到1e-6,至少需要1000万符号,配合循环仿真和并行计算才划算。
5. 用EVM和星座图验证QPSK调制质量
5.1 EVM的计算与门限
低误码率下误码率曲线到不了太深的区间,工程上常用误差矢量幅度(EVM)来量化调制质量。EVM是实际符号点到理想星座点距离的均方根与理想符号幅度的百分比。QPSK的EVM门限一般要求小于10%,部分系统要求5%以下。计算代码如下:
def evm_db(rx_syms, ref_syms): err = rx_syms - ref_syms rms_err = np.sqrt(np.mean(np.abs(err)**2)) rms_ref = np.sqrt(np.mean(np.abs(ref_syms)**2)) return 20 * np.log10(rms_err / rms_ref)如果返回值是-20dB,代表EVM=10%;-30dB对应3.16%。参考符号除了用理想星座点,还可以用突发头部训练序列进行信道估计后重建,这样能去掉公共相位误差的影响,看到真正的调制器失真。
5.2 星座图异常特征与定位
把实际符号画在复平面上,可以快速判断故障性质。下表汇总了几个典型特征和对应原因。
| 星座图表现 | 可能原因 | 检查项 |
|---|---|---|
| 整体旋转固定角度 | 载波相位未收敛 | Costas环路初始相位、NCO线性度 |
| 四个簇围绕原点旋转 | 载波频偏未扣除 | AFC环路、扫频捕获范围 |
| 点簇呈环状分裂 | 定时偏差或滚降系数不匹配 | 示波器眼图、采样时刻 |
| I/Q幅度不一致 | 模拟混频器增益失衡 | 收发增益校准 |
| 原点附近有亮点 | 直流偏置 | 零中频接收机DCOC |
这五类问题在EVM上都会表现为数值变大,但星座图的形态能直接告诉你该调哪一级。比如环状分裂多半出在定时恢复,I/Q增益失衡则在数字基带加一个复数均衡器即可。
5.3 验证qpsk.zip工程的三个步骤
拿到一个压缩包,我建议按以下顺序跑通并确认里面的QPSK调制解调实现是否可信:
- 调制端先直接输出星座点,不经过信道,确认四个簇中心落在±0.7071±0.7071j或代码自己约定的位置。
- 在AWGN信道下依次设Eb/N0为0、3、6dB,每个点跑至少10万符号,观察误码率是否落在理论曲线附近。少于1万符号的统计波动太大,不能说明问题。
- 加一个符号率1%到5%的载波频偏,观察解调器是否还能维持正常星座;如果散成一圈,说明环路捕获范围不够,需要把压控振荡器增益加大或先做扫频。
这三个步骤分别验证基带映射、链路预算和同步性能。真正到硬件上,还需要把仿真得到的EVM和频谱仪实测数值对比,偏差超过2dB就从滤波器频响和混频器前级找原因。也可以把第2章的成型滤波器和第3章的定时同步一起环回,检查收端匹配滤波器拿到的信噪比损失是否在0.1dB以内。
本文还有配套的精品资源,点击获取