news 2026/9/14 4:14:44

QPSK调制解调全流程详解:星座映射、脉冲成形与误码率仿真

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QPSK调制解调全流程详解:星座映射、脉冲成形与误码率仿真

简介:一份面向无线通信与信号处理初学者、通信专业学生及相关从业者的 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)相位偏移复数符号符号索引
0045°(0.7071, 0.7071)0
01135°(-0.7071, 0.7071)1
11225°(-0.7071, -0.7071)2
10315°(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的rcosdesignDDPLL搭链路,但本质步骤可以写成下面的代码。为了便于对照,这里的基带模型不做射频搬移,只讨论复数基带符号如何在受扰后恢复。

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)

这段代码中符号索引与调制端的映射表必须一致,否则解调正确但比特全错。实际代码中最好把gridbit_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
07.86e-2
32.28e-2
62.39e-3
93.36e-5
121.90e-8

表格用于校验仿真曲线走向。很多工程师在对比时忘记把Es/N0换成Eb/N0,直接用符号信噪比对照公式,结果偏差很大。QPSK的Es/N0Eb/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是复噪声每个维度的方差,乘到两个独立高斯随机变量上之后,复数噪声总方差恰好是Nober_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调制解调实现是否可信:

  1. 调制端先直接输出星座点,不经过信道,确认四个簇中心落在±0.7071±0.7071j或代码自己约定的位置。
  2. 在AWGN信道下依次设Eb/N0为0、3、6dB,每个点跑至少10万符号,观察误码率是否落在理论曲线附近。少于1万符号的统计波动太大,不能说明问题。
  3. 加一个符号率1%到5%的载波频偏,观察解调器是否还能维持正常星座;如果散成一圈,说明环路捕获范围不够,需要把压控振荡器增益加大或先做扫频。

这三个步骤分别验证基带映射、链路预算和同步性能。真正到硬件上,还需要把仿真得到的EVM和频谱仪实测数值对比,偏差超过2dB就从滤波器频响和混频器前级找原因。也可以把第2章的成型滤波器和第3章的定时同步一起环回,检查收端匹配滤波器拿到的信噪比损失是否在0.1dB以内。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 4:13:21

如何用 EAS 构建 Ignite 应用的开发、预览与生产构建包?

如何用 EAS 构建 Ignite 应用的开发、预览与生产构建包&#xff1f; 【免费下载链接】ignite Infinite Reds battle-tested React Native project boilerplate, along with a CLI, component/model generators, and more! 9 years of continuous development and counting. 项…

作者头像 李华
网站建设 2026/9/14 4:11:31

keep 开源告警平台部署实战:从选型到上线的完整指南

keep 开源告警平台部署实战&#xff1a;从选型到上线的完整指南 【免费下载链接】keep The open-source AIOps and alert management platform 项目地址: https://gitcode.com/GitHub_Trending/kee/keep 监控系统、CI/CD、SRE 工具各喊各的&#xff0c;值班同学每天在几…

作者头像 李华
网站建设 2026/9/14 4:11:25

中文操作系统内核开发:假持久化与USB协议栈实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华