简介:四相相移键控(QPSK)调制解调完整仿真代码,适合通信工程专业学生、数字通信课程学习者以及无线通信算法工程师参考学习。内容围绕QPSK信号生成、频谱分析与同步解调等核心环节,通过改变载波相位来映射两位比特,具有较高频谱效率,是理解现代数字调制的重要案例。rar压缩包内共18个m文件,体积仅8KB,均为MATLAB脚本,覆盖序列生成、串并变换、载波调制、扩频解扩、低通滤波和误码测试等功能模块,还包含用于傅立叶变换与波形绘制的辅助函数。代码结构清晰、注释简洁,便于按步骤复现QPSK信号从生成到解调的完整链路,直观观察星座图、时域波形与频谱特性,深入理解I/Q调制、载波恢复以及判决门限设置等关键细节。资源已有152人学习下载,既可作为课程设计、毕业设计仿真底稿,也可作为自学入门样例,帮助初学者快速掌握四相相移键控技术的原理与实现方法。
1. QPSK信号调制:为什么你解出来的星座图总在转?
做无线通信仿真或调试SDR链路时,QPSK(Quadrature Phase Shift Keying,正交相移键控)是最常见的起点。它把两个比特映射成四个相位之一,频谱效率高、实现简单,但真正动手做一次信号调制而不只是调用modulate函数时,很多人会发现:星座图在转、误码率下不去、频谱包络对不上理论值。这些问题大多不是算法没学懂,而是调制实现里的采样率、符号映射、载波频率和滤波器参数没有对齐。
这篇内容围绕QPSK信号调制的完整链路展开,从比特到星座点的数学映射讲起,给出可直接运行的Python正交调制实现,再落到解调端的载波同步和定时同步。适合正在做通信仿真、准备用SDR发射QPSK信号,或者做FPGA调制器开发的人。你会看到怎么设采样率、怎么选载波、怎么判断星座图旋转的根源,而不是只得到一个能跑的脚本。
2. QPSK调制原理与映射关系:从比特到星座点的数学
2.1 QPSK的I/Q双通道调制结构
QPSK信号可以看作两个正交BPSK信号的叠加。一个通道叫I路(In-phase),一个叫Q路(Quadrature),两路载波相差90度。把二进制的比特流每两个比特分成一组,其中一个比特控制I路,另一个控制Q路,两路分别做BPSK调制后相加,就得到了QPSK信号。
数学上,一个QPSK符号可以写成:
[ s(t) = I(t) \cos(2\pi f_c t) - Q(t) \sin(2\pi f_c t) ]
其中 ( I(t) ) 和 ( Q(t) ) 是基带脉冲成型后的幅度,取值通常是 (+A) 或 (-A)。四个相位落在 ( \pi/4, 3\pi/4, 5\pi/4, 7\pi/4 )(即45°、135°、225°、315°),正好对应两路BPSK的四种组合。你也可以旋转45度映射到0°/90°/180°/270°,但大多数标准都采用第一种,因为这样I/Q两路的符号判断正好对应格雷码的相邻位。
2.2 格雷映射与星座图判定的边界
格雷映射的核心思想是:相邻相位只差一个比特,这样在噪声影响下,误判成相邻星座点时只错一个比特,而不是两个。QPSK最常用的映射关系如下:
| 输入比特 (b0 b1) | I 路幅度 | Q 路幅度 | 相位 |
|---|---|---|---|
| 00 | +1 | +1 | 45° |
| 01 | +1 | -1 | 315° |
| 11 | -1 | -1 | 225° |
| 10 | -1 | +1 | 135° |
注意这里映射顺序不是简单的二进制递增,而是按格雷码排列:00、01、11、10,保证相邻相位之间只有一位不同。实际工程里,如果直接用00、01、10、11的顺序映射,误码率在高信噪比下会差大约0.5dB,因为误判到对角星座点时会错两个比特。
判定相位时,接收端只需要比较I路和Q路的符号。大于0判为+1,小于0判为-1。这是硬判决。但如果I/Q通道之间有幅度不平衡或者正交偏差,判决边界就会倾斜,误码就多了。这也是为什么后面要讲I/Q不平衡补偿。
2.3 载波信号频率一般设为多少
“载波信号一般为多少”这个问题没有固定答案,取决于系统设计。做仿真时,载波频率必须满足一个条件:远大于符号速率,同时不能超过采样率的一半。常见做法是取采样率的1/4到1/6,比如采样率4MHz,载波设1MHz左右。这样既保证信号不靠近直流,避免直流偏置和低频噪声干扰,又让每个载波周期能采到足够的点,便于后面的I/Q解调。
如果载波频率太接近符号速率,基带信号的频谱和载波的边带会混叠;如果载波频率太高逼近奈奎斯特频率,采样点太少,解调时定时同步会很困难。我一般会先把符号速率定下来,再决定载波频率,而不是反过来。
3. 用Python从零实现QPSK调制器:波形生成与参数设置
3.1 最小可运行代码:生成QPSK基带信号
先不看滤波器,也不乘载波,只生成基带的I/Q符号序列。这段代码是最小可运行的QPSK调制核心:
import numpy as np # 参数设置 sps = 8 # 每个符号的采样点数(与后续成型滤波有关) symbols = 1000 # 发送符号数 bits_per_symbol = 2 # 生成随机比特流 bits = np.random.randint(0, 2, symbols * bits_per_symbol) # 按格雷码映射成I/Q幅度 mapping = { (0, 0): (1, 1), (0, 1): (1, -1), (1, 0): (-1, 1), (1, 1): (-1, -1), } i_symbols = np.zeros(symbols) q_symbols = np.zeros(symbols) for n in range(symbols): b0 = bits[2*n] b1 = bits[2*n + 1] i_val, q_val = mapping[(b0, b1)] i_symbols[n] = i_val q_symbols[n] = q_val这段代码先把随机比特按每组两位拆开,然后通过字典映射得到I路和Q路的幅度。注意这里的mapping字典用的键是(b0, b1),值是对应的I、Q幅度。用字典的好处是映射关系一目了然,后续想换成非格雷码只改这里就行。
i_symbols和q_symbols还是符号级的序列,每个符号只有一个值,还不是真正的波形。要让它们变成带限信号,还需要过采样和成型滤波,这就是下一步。
3.2 关键参数:符号速率、采样率、滚降系数怎么定
符号速率(baud rate)决定了信号的传输速率。比如符号率是1Msps,每个符号带2比特,那么比特率就是2Mbps。采样率必须大于符号速率的2倍,工程上通常取4到16倍。如果每个符号采样点数太少,后面的匹配滤波和定时同步会很难做;太多则运算量大,滤波器阶数也高。
成型滤波用升余弦或根升余弦滤波器,滚降系数(roll-off factor)是一个核心参数。它决定信号的频谱占用和过冲特性:
| 滚降系数 α | 占用带宽(符号率R) | 过冲 | 典型场景 |
|---|---|---|---|
| 0.1 | 1.1R | 很大 | 卫星通信,带宽受限 |
| 0.25 | 1.25R | 较大 | 常规数字微波 |
| 0.5 | 1.5R | 中等 | 一般测试验证 |
| 1.0 | 2R | 很小 | 教学演示,OQPSK |
实际调试中,α越小频谱越紧凑,但滤波器的冲击响应衰减慢,定时抖动敏感度更高,匹配滤波后的眼图开口更小。第一次做QPSK仿真时建议用0.35到0.5,先让链路跑通,再往紧凑方向压。
用根升余弦滤波器做脉冲成型时,滤波器系数可以用commpy或者scipy.signal手动构造。下面给出一个不用专用库的实现:
from scipy import signal def rrc_filter(alpha, span, sps): """生成根升余弦滤波器系数""" N = span * sps + 1 t = np.arange(N) - span * sps / 2 t = t / sps # 归一化到符号周期 h = np.zeros_like(t) for i, ti in enumerate(t): if ti == 0.0: h[i] = 1.0 - alpha + 4 * alpha / np.pi elif np.abs(np.abs(ti) - 1/(4*alpha)) < 1e-8: h[i] = alpha/np.sqrt(2) * ((1+2/np.pi)*np.sin(np.pi/(4*alpha)) + (1-2/np.pi)*np.cos(np.pi/(4*alpha))) else: num = np.sin(np.pi*ti*(1-alpha)) + 4*alpha*ti*np.cos(np.pi*ti*(1+alpha)) den = np.pi*ti*(1-(4*alpha*ti)**2) h[i] = num / den # 归一化 return h / np.sqrt(np.sum(h**2)) alpha = 0.35 span = 6 filter_coeffs = rrc_filter(alpha, span, sps)这个滤波器的关键是让发射端和接收端各用一个根升余弦滤波器,两者级联后等效于一个完整的升余弦滤波器,能在采样点处消除符号间干扰。注意span是滤波器覆盖的符号个数,取6到10比较合适。太小则截断误差大,太大则信号延迟长。
3.3 让信号上载波:混频与正交调制实现
基带符号经过成型滤波后,需要上变频到载波频率。正交调制器用两路正交载波分别乘上I/Q信号再相加。做这个步骤的代码很简单,但坑很多:
def qpsk_modulate(i_wave, q_wave, fc, fs): """正交调制:I路乘cos,Q路乘sin,相加""" n = len(i_wave) t = np.arange(n) / fs carrier_i = np.cos(2 * np.pi * fc * t) carrier_q = -np.sin(2 * np.pi * fc * t) qpsk_signal = i_wave * carrier_i + q_wave * carrier_q return qpsk_signal fs = sps * 1000 # 假设符号率1000,采样率8000 fc = 2000 # 载波频率,取采样率的1/4 i_up = signal.upfirdn(filter_coeffs, i_symbols, up=sps) q_up = signal.upfirdn(filter_coeffs, q_symbols, up=sps) # 补偿滤波器延迟 delay = int(span * sps / 2) i_up = i_up[delay:] q_up = q_up[delay:] tx_signal = qpsk_modulate(i_up, q_up, fc, fs)这里Q路载波用负正弦,是因为前面公式里是减去Q路径的正弦分量。如果你写成加号,星座图会镜像,解调时I/Q翻转,问题很隐蔽。另外upfirdn会引入滤波器延迟,必须截掉前面span*sps/2个点,否则发射信号的起始部分是滤波器填充的零,频谱会不干净。
还有一个容易出错的点:载波频率必须能被采样率的整数分之一描述,但不能是奈奎斯特频率的一半。比如fs=8000时,fc=2000刚好是fs/4,每个载波周期采4个点,I/Q混频系数正好是1,0,-1,0的序列,方便后续调试。实际系统里,载波频率由硬件PLL决定,仿真里这样选只是为了直观。
4. 解调端同步问题:为什么你的QPSK误码率高
4.1 载波同步:Costas环与频偏影响
调制容易,解调难。QPSK解调的第一步是载波同步。如果接收端的本地振荡器和发射端有频率偏差,解调出来的星座图会持续旋转。频偏是几十Hz时,星座图慢慢转;频偏是几百Hz时,星座图糊成圆圈,完全没法判决。
常见的载波同步方法分两类:数据辅助和非数据辅助。非数据辅助里,Costas环是QPSK的标准方案。它的误差信号用I/Q两路的硬判决结果来估计相位偏差:
# 简化版Costas环迭代(逐符号) phase = 0.0 for n in range(len(rx_symbols)): # 去旋转 I = rx_symbols[n].real * np.cos(phase) + rx_symbols[n].imag * np.sin(phase) Q = -rx_symbols[n].real * np.sin(phase) + rx_symbols[n].imag * np.cos(phase) # 硬判决 dI = 1.0 if I > 0 else -1.0 dQ = 1.0 if Q > 0 else -1.0 # 误差信号 error = dI * Q - dQ * I phase += 0.1 * error # 环路增益,需按符号率调整这段代码里,相位更新量等于环路增益乘以误差。环路增益太小则锁定慢,太大则会抖。实际工程中需要按信噪比和符号率调,可能在0.01到0.2之间。而且这个算法在QPSK里有四相位模糊,即使锁定也可能旋转到任意一个90度倍数上,需要差分编码或者在帧头里做相位解模糊。
4.2 定时同步与匹配滤波
除了载波相位,接收机还必须知道每个符号的最佳采样时刻。这是定时同步要解决的问题。如果定时偏差是采样周期的一半,眼图完全闭合,误码率接近50%。
最常见的定时同步算法是Gardner算法,它不用数据辅助,独立于载波相位,适合QPSK。Gardner的误差公式是:
[ e = (y_N - y_{N-1}) \times y_{N-1/2} ]
其中 ( y_N ) 是当前符号采样点,( y_{N-1} ) 是上一个符号采样点,( y_{N-1/2} ) 是两者中间时刻的采样。当采样点正好落在符号峰值时,中间点的期望值为零,误差为零;偏离时产生正负误差信号去调整插值时刻。
实际实现中,接收端先对I/Q信号做根升余弦匹配滤波,再用Gardner环和插值器完成定时恢复。匹配滤波器和发射端的成型滤波器系数相同,这也是为什么前面说根升余弦要分在两端用。如果发射端用了升余弦而接收端也用升余弦,整体就不是奈奎斯特响应,符号间干扰会显现出来。
4.3 星座图旋转诊断:从现象反推故障
看到星座图异常时,先不要调环路参数,先观察现象。
第一种:星座图逆时针匀速旋转。这是载波频偏的典型表现。频偏方向决定旋转方向,频偏大小决定旋转速度。解决方法是先做粗频偏估计,比如用帧前导的已知符号做相关估计,把残余频偏压到环路可以接受的范围。
第二种:星座图不转,但四个点分别拉成四团椭圆斑。这是定时同步或滤波器失配的问题。检查接收端滤波器的采样相位,或者看眼图有没有闭合。也可能是发射端加了滚降系数很大的滤波器,接收端却用了匹配系数。
第三种:星座图只出现两个点,另外两个点重叠。这说明I/Q两路中有一路幅度为零,通常是发射端I路或Q路的信号延迟没对齐,或者混频时少了一路载波。检查I/Q波形的时域图,确认两路波形长度一致、延迟一致。
第四种:星座图整体旋转45度,也就是说点出现在轴正负方向上。这是本地载波相位偏了45度,且环路还没开始收敛。等锁稳定后应该会转回来。如果始终锁在45度,说明Costas环没有正常工作,看误差信号是否周期性过零。
5. 实战技巧:用SDR或仿真验证QPSK调制链路
5.1 用GNURadio搭一个QPSK收发流图
手写代码验证了算法,再用SDR平台做一次无线路测是完整流程。GNURadio里做QPSK收发的流图结构很固定,我已经搭过多次,关键节点如下:
- 发射端:Random Source → Chuck(按2比特分块) → Constellation Encoder(选QPSK格雷映射) → Root Raised Cosine Filter(滚降系数0.35,每符号采样数8) → Throttle → UHD USRP Sink
- 接收端:UHD USRP Source → Root Raised Cosine Filter(和发射端同样系数) → Symbol Sync(算法选Gardner,检测器选Mueller & Muller,环路带宽0.05) → Costas Loop(环路带宽0.02,顺序2阶QPSK) → Constellation Sink
Symbol Sync模块的参数中,Samples per Symbol必须和发射端的Samples per Symbol一致。如果这里配错,定时同步完全无法收敛。Costas Loop的Order选项里,QPSK要用2或4,不是1和3——1和3是BPSK/OOK用的。
5.2 眼图和EVM验证调制质量
最后一步验证不是看误码率,而是看眼图和EVM。EVM(误差矢量幅度)是衡量调制质量的核心指标,它计算理想星座点和实际接收符号之间的误差。5G NR标准里,256QAM要求EVM低于3.5%,QPSK通常要求低于17.5%,但自己验证时,EVM低于10%就算调制链路正常。
在GNURadio里加一个Constellation Sink,勾选EVM选项,就能直接看到数值。如果是自己写Python,EVM的计算方式如下:
# 假设rx_syms是归一化后的接收符号,ref_syms是对齐后的理想符号 error = rx_syms - ref_syms evm = np.sqrt(np.mean(np.abs(error)**2) / np.mean(np.abs(ref_syms)**2)) * 100 print(f"EVM: {evm:.2f}%")如果EVM高于20%,优先检查接收信号功率是否在SDR ADC的最佳范围内。功率太低量化噪声大,EVM恶化;功率太高产生非线性失真,星座图边缘的点会向内收缩。对于常见的SDR前端,把接收增益调到信号幅度约为满量程的60%到70%时,EVM通常最低。
除了EVM,眼图也是快速定位问题的好工具。QPSK的I路和Q路分别画眼图,正常情况下在最佳采样时刻有四个清晰的开口。如果开口处线条很粗,定时抖动大;如果眼图中线交叉位置不是中心点,说明滤波器系数有偏置。
还有一个验证技巧:把滚降系数从0.35改成0.15,重复跑同一条链路,观察EVM和频谱占用变化。如果EVM急剧恶化,说明你的定时同步环路带宽与滤波器的群延迟不匹配,需要把环路带宽调小,同时增加滤波器的span参数。这组对比能帮你快速理解滚降系数和同步性能之间的权衡。
本文还有配套的精品资源,点击获取