简介:QPSK正交相移键控调制解调的MATLAB仿真资源,适合通信工程、电子信息类专业学生及刚接触数字调制技术的研发人员,用于理解QPSK映射、相位调制、噪声信道与解调恢复的完整流程。压缩包共12个文件,以m脚本为主,附有asv版本备份和一份doc格式的QAM调制解调系统定时与载波恢复仿真报告,整体仅214KB,便于快速下载与阅读。已有175人学习浏览,可作为课程设计或通信仿真的入门参考。仿真代码涵盖随机二进制数据流生成、相位映射、复数载波调制、awgn加噪、相干解调、误码率统计等环节,并包含QPSK调制、脉冲成形、星座图、LMS均衡、早迟门定时恢复、零交叉定时恢复、四倍频载波恢复等独立脚本,便于分模块调试与对比;对应报告还对QAM系统中的定时与载波恢复算法做了梳理,帮助读者从实验现象深入到算法原理,快速搭建自己的通信系统仿真链路。
1. 从 qpsk.zip 说起:一份压缩包背后的完整数字通信链路
收到qpsk.zip这种命名风格的文件,基本可以断定里面装的不是零散笔记,而是一个围绕 QPSK(Quadrature Phase Shift Keying,正交相移键控)的工程或仿真项目。QPSK 在数字通信中的地位有点像冒泡排序在算法里——不是最花哨的,但它是理解所有高阶调制方式的地基。它同时调制载波的相位和正交分量,用 4 个相位点传递 2 bit 信息,频谱效率比 BPSK 翻倍,抗噪声能力比 8PSK、16QAM 这类高阶调制更稳健。所以无论是卫星链路、LTE 物理层、Wi-Fi 的某些模式,还是雷达通信一体化波形,QPSK 都是最先上板验证的候选方案。
这份 zip 对你有没有价值,取决于你打开它之后想干什么。如果是想跑通一个完整的调制解调链路,你需要关注发端的符号映射和脉冲成形,收端的定时同步、载波同步和判决;如果是想做性能评估,核心就落在 BER 曲线和星座图散点上。一个能交付的 QPSK 压缩包,通常不会只给一个qpsk.m或qpsk.py,而是包含完整的仿真框架、参数配置和结果图。这篇文就顺着这个预期,把 QPSK 从理论到可运行的工程实现讲透。
2. QPSK 的调制原理与星座映射:两路正交载波承载 2 bit
2.1 相位平面上的 4 个点:QPSK 为什么是 I/Q 两路
QPSK 的本质是用载波的 4 个相位状态来携带信息。标准的 QPSK 星座点分布在单位圆的 4 个象限,每个点对应一个相位偏移(45°、135°、225°、315°)。这 4 个相位点分别映射到 2 bit 组合:00、01、11、10。为什么非要落在对角线上?因为这样 I/Q 两路的幅值都非零,接收端可以从两个正交分量里各自提取信息,能量利用率最高。
QPSK 可以看作两个正交 BPSK 的叠加:I 路(同相分量)和 Q 路(正交分量)各自独立传输 1 bit,两路共享同一个载波频率,但相位相差 90°。这种结构直接决定了 QPSK 的实现方式——不需要复杂的相位调制器,只要两个 BPSK 调制器分别驱动 I/Q 两路,再在射频端合路即可。
星座映射有格雷码和非格雷码两种。格雷码的关键优势是相邻相位点只差 1 bit,当噪声把符号判决到相邻星座点时,只产生 1 bit 误码而非 2 bit,这在工程上能显著降低 BER。IEEE 802.11、DVB-S 等标准都采用格雷映射。我建议你在 zip 里的代码中先确认它用的是哪种映射,因为这直接关系到仿真结果和理论 BER 曲线做对比时的口径。
2.1.1 用 Python 验证 QPSK 星座映射
import numpy as np import matplotlib.pyplot as plt # 格雷码映射 gray_map = np.array([0b00, 0b01, 0b11, 0b10]) constellation = np.array([1+1j, -1+1j, -1-1j, 1-1j]) / np.sqrt(2) bits = np.random.randint(0, 2, 1000 * 2).reshape(-1, 2) symbols = np.zeros(len(bits), dtype=complex) for i, (b0, b1) in enumerate(bits): idx = (b0 << 1) | b1 symbols[i] = constellation[gray_map == idx][0] plt.scatter(symbols.real, symbols.imag, s=5, alpha=0.5) plt.plot([-1.2, 1.2], [0, 0], 'k--', linewidth=0.5) plt.plot([0, 0], [-1.2, 1.2], 'k--', linewidth=0.5) plt.xlabel('In-Phase') plt.ylabel('Quadrature') plt.title('QPSK Constellation (Gray Coded)') plt.axis('equal') plt.grid(True) plt.show()这段代码先定义了格雷映射表和标准功率归一化的 QPSK 星座点。/ np.sqrt(2)的作用是把星座点归一化到单位能量,这是工程里必须的一步——如果不做归一化,后面加噪声时 SNR 与 Eb/N0 的换算就对不上。1000 * 2表示生成 1000 个 QPSK 符号,每个符号对应 2 bit 输入,I 路和 Q 路的取值在{-1/√2, +1/√2}之间跳变。运行后你会看到 4 个散点分别落在复平面的 4 个象限中心,这就是 QPSK 的星座图,也是后续判断调制质量和同步效果的最直观工具。
2.2 符号映射与串并转换:比特流如何变成符号流
在实际通信系统中,发端的数据是以比特流形式进来的(比如编码后的语音帧或 IP 包),要想把比特流映射到 QPSK 星座点上,必须先把串行的比特流切割成 2 bit 一组。这个操作叫串并转换(Serial-to-Parallel),它是 QPSK 调制中从比特域进入符号域的分水岭。切割之后的每一组 2 bit,通过查表的方式映射成对应的复数符号。
串并转换在 FPGA 实现中就是一个移位寄存器加计数器,而在 Python/Matlab 仿真里就是一次 reshape。需要注意的是边界条件:如果输入的比特总数不是 2 的整数倍,仿真代码会报错或者丢弃尾比特。工程上通常会加比特填充(bit padding),在帧尾补 0 并记录有效比特数。你这边的验证手段是检查输出符号个数是否严格等于输入比特数的一半。
2.2.1 串并转换与符号映射参数说明
| 参数 | 取值 | 说明 |
|---|---|---|
| 比特数 | 必须为偶数 | 否则末尾比特丢弃或补零 |
| 映射方式 | Gray / Natural | Gray 在 AWGN 下 BER 更低 |
| 符号速率 Rs | 取决于系统带宽 | 决定脉冲成形滤波器的滚降系数 |
| 载波频率 fc | 仿真中通常设中频或基带 | 基带仿真可以省去上变频 |
2.3 脉冲成形与滚降系数:带宽从哪里来
GQPSK 星座点只是基带符号序列,如果直接把矩形脉冲调制到载波上,频谱会在频域产生无限延展的旁瓣,这在带限信道里会对相邻信道产生干扰。所以 QPSK 发射机里几乎总要加一个脉冲成形滤波器,最常用的是升余弦(Raised Cosine, RC)或根升余弦(Root Raised Cosine, RRC)滤波器。
RRC 滤波器在收发两端各放一个,匹配滤波之后的联合响应就是升余弦频谱,这样既满足了奈奎斯特无码间干扰(ISI)条件,又让接收端的匹配滤波达到最大信噪比输出。滚降因子 α 决定带宽扩展:α=0 时是理想矩形频谱,带宽最小但滤波器不可实现;α=1 时带宽是符号速率的两倍,实现最容易。实际系统里 α 取 0.2~0.35 很常见,LTE 下行 PDSCH 用的是 0.1 左右的扩展因子,但那是 OFDM 体系,时域单载波系统里 0.22 和 0.35 是主流选择。
3. 用 Python 搭建完整 QPSK 仿真链路:从发端到 BER 统计
3.1 仿真链路整体框图和模块划分
设计 QPSK 仿真链路时,我一般按信号处理顺序分成发端、信道、收端三个部分。发端包含比特生成、串并转换、星座映射、上采样和 RRC 成形滤波;信道部分最基础的就是加性高斯白噪声(AWGN),进阶再加频偏、相偏和多径;收端包含匹配滤波、定时同步、载波同步、星座判决和 BER 统计。
这个链路框架的意义在于:如果 zip 包里提供的代码按模块拆分了函数,你可以在任意两个模块之间插入探针。比如在成形滤波之后看频谱,在匹配滤波之后看眼图,在判决之后看星座散点。模块化是仿真代码组织的核心原则,函数式或类式组织远比单脚本堆叠好调试。
3.2 发端实现:RRC 滤波与信号生成
3.2.1 根升余弦滤波器设计与滤波操作
def rrc_filter(sps, alpha, num_taps=101): """ 设计根升余弦滤波器 sps: number of samples per symbol alpha: roll-off factor num_taps: 滤波器抽头数,建议为奇数 """ n = np.arange(num_taps) - (num_taps - 1) / 2 h = np.zeros(num_taps) for i, t in enumerate(n): if t == 0: h[i] = 1 - alpha + 4 * alpha / np.pi elif abs(t) == sps / (4 * alpha) and alpha != 0: 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 * t / sps * (1 - alpha)) + \ 4 * alpha * t / sps * np.cos(np.pi * t / sps * (1 + alpha)) den = np.pi * t / sps * (1 - (4 * alpha * t / sps) ** 2) h[i] = num / den return h / np.sqrt(sps) # 参数设置 sps = 8 alpha = 0.35 num_symbols = 1000 # 生成随机符号 bits = np.random.randint(0, 2, num_symbols * 2) gray_map = np.array([0b00, 0b01, 0b11, 0b10]) constellation = np.array([1+1j, -1+1j, -1-1j, 1-1j]) / np.sqrt(2) symbols = np.zeros(num_symbols, dtype=complex) for i in range(num_symbols): idx = (bits[2*i] << 1) | bits[2*i+1] symbols[i] = constellation[gray_map == idx][0] # 上采样:每符号插入 (sps-1) 个零 upsampled = np.zeros(num_symbols * sps, dtype=complex) upsampled[::sps] = symbols # 成形滤波 h_rrc = rrc_filter(sps, alpha) tx_signal = np.convolve(upsampled, h_rrc, mode='same')上采样操作是理解这段代码的关键:upsampled[::sps] = symbols把每个符号放在时间轴上相隔sps个采样点的位置,其余位置补零。这样做的目的是把符号率提高到采样率,让数字滤波器可以工作在固定的采样频率上。rrc_filter函数返回的滤波器系数用np.sqrt(sps)做了能量归一化,保证滤波后的信号功率和符号序列功率一致。如果这里不归一化,后续 SNR 计算会系统性偏移几个 dB。
滤波器抽头数的选择有个经验:101 个抽头在 sps=8 时可覆盖 12 个符号周期,旁瓣衰减已经足够;你如果要追求更快仿真,降到 51 也能跑,但滤波器的频响会稍微偏离理想 RRC。更关键的是mode='same'带来的群延迟问题——卷积输出比输入延迟(num_taps-1)/2个采样点,后面匹配滤波还会再引入一次同样的延迟,收端符号判决时必须补偿这个总延迟,否则 BER 会很高但找不到原因。
3.3 AWGN 信道:Eb/N0 与 SNR 的正确换算
很多人仿真 QPSK 时第一步就踩坑:噪声方差加错。问题出在 Eb/N0、SNR、符号能量和采样率之间的区分。
Eb/N0 是每比特能量与噪声功率谱密度的比值,它是 BER 曲线横轴的标准单位。对 QPSK 来说,每个符号携带 2 bit,符号能量 Es 与 Eb 的关系是Es = 2 * Eb。SNR 是信号功率与噪声功率的比值,它和 Eb/N0 的关系是:
SNR = Eb/N0 + 10*log10(k) - 10*log10(sps)
这里 k 是每符号比特数(QPSK 取 2),sps 是过采样倍数。减掉10*log10(sps)是因为噪声功率分布在信号带宽内,而信号经过过采样后每个采样点携带的功率变低了。
3.3.1 噪声添加的正确公式
def add_awgn(signal, snr_db, sps, k=2): """ signal: 复基带信号 snr_db: 目标信噪比(dB) sps: samples per symbol k: bits per symbol, QPSK=2 """ # 信号平均功率 signal_power = np.mean(np.abs(signal) ** 2) # 从 SNR(dB) 换算到噪声功率 snr_linear = 10 ** (snr_db / 10) # 噪声单边功率谱密度 N0 = Es / (SNR * k) # Es 为符号能量,这里用信号功率除以符号率得到 symbol_rate = 1.0 # 归一化 N0 = signal_power / (k * snr_linear * symbol_rate) noise_power = N0 * sps / 2 # 每个采样点的复噪声方差 noise = np.sqrt(noise_power / 2) * ( np.random.randn(len(signal)) + 1j * np.random.randn(len(signal)) ) return signal + noise这段代码里最容易忽略的是噪声功率为什么除以 2。复基带信号的实部和虚部各占一半噪声功率,总噪声功率为noise_power时,每个分量的方差是noise_power/2。np.random.randn生成标准正态分布随机数,乘以np.sqrt(noise_power/2)后实部和虚部的功率各为noise_power/2,相加即为noise_power。经过前面的发端滤波,信号带宽受限;matlab 或 Python 中噪声在采样率上均匀分布,所以把 N0 乘以 sps 得到实际采样带宽内的噪声总功率。
验证噪声是否加对的方法很简单:把信号和噪声分别做功率谱估计,看噪声功率谱密度是否等于理论 N0;或者在无滤波直接映射符号加噪声时,用判决后的 BER 对比理论值。QPSK 在 AWGN 下的理论 BER 是Q(sqrt(2*Eb/N0)),可以用from scipy.special import erfc配合0.5 * erfc(np.sqrt(eb_n0_linear))快速比对。
3.4 接收端匹配滤波与符号判决
接收端第一件合法的事就是匹配滤波。匹配滤波器的冲击响应是发射端成型滤波器的时域反转共轭,在收发两端都用 RRC 滤波器时,接收端的滤波器系数与发端完全相同(实数滤波器)。匹配滤波的本质是最大化采样时刻的 SNR,同时把符号间的干扰压到零。
经过匹配滤波后,信号需要按符号间隔抽样。抽样时刻的选择依赖定时同步,但在链路仿真的第一阶段,如果发端上采样是严格整数倍且没有多普勒频偏,你直接取rx_signal[::sps]就能得到每个符号的判决量。不过要注意前面卷积引入的群延迟:总延迟是num_taps - 1个采样点(发端和收端各一半),所以正确的抽样索引应该是rx_signal[num_taps-1::sps]。
判决阶段就是看复平面上的符号落在哪个象限。用np.sign(symbol.real)和np.sign(symbol.imag)可以分别判 I/Q 两路,但注意星座点在对角线上,判决阈值是 0,落在哪个象限决定 bit 值。误码统计要做的是把判决后的符号重新映射回 bit 序列,按gray_map逆映射和发端比特逐位对比,得到误比特数除以总比特数即为 BER。
4. 同步与信道损伤:频偏、定时和衰落下的 QPSK 实战
4.1 载波频偏对 QPSK 的影响与纠正方法
实际射频链路里收发双方的晶振频率不可能完全一致,Wi-Fi 和 LTE 终端都有 ±20ppm 甚至更差的初始晶振误差。在 2.4GHz 载波上,20ppm 意味着高达 48kHz 的频偏,而 QPSK 符号速率通常只有几十 kSps 到几 MSps,这个频偏会让星座图高速旋转,判决完全失败。仿真相对于射频的差异在于:你不会真的去调 VCO,而是往基带复信号上乘一个复指数exp(j*2*pi*f_offset*t)模拟频偏。
频偏的估计和补偿在突发通信里常用数据辅助的前馈算法(如 L&R 估计器或 Fitz 算法),在连续流传输里则用 Costas 环或判决反馈 PLL。I/Q 支路各用一个鉴相器的 Costas 环对 QPSK 是经典方案,它的误差信号是I*Q,在星座点收敛到对角线时误差为零,偏移时误差驱动 NCO 调整本振频率。
def costas_loop(rx, sps, loop_bw=0.01): n = len(rx) out = np.zeros_like(rx) phase = 0.0 freq = 0.0 for i in range(n): # 旋转输入信号 rotated = rx[i] * np.exp(-1j * phase) out[i] = rotated # QPSK Costas 误差:I*Q 符号驱动的四阶鉴相器 err = rotated.real * rotated.imag # 环路滤波器:比例+积分 freq += loop_bw * loop_bw * err phase += freq + loop_bw * err return out这是数字 Costas 环的最简实现。loop_bw参数控制环路带宽,带宽越窄抗噪声越好,但捕获速度越慢。实际工程里loop_bw取 0.001~0.05 之间,你需要根据符号速率和频偏大小去折中。对突发通信(比如 wifi 的 preamble)来说,用已知导频序列做数据辅助频偏估计更常用,因为它可以在几百个符号内完成估计并一次性补偿,不必等环路收敛。
4.2 定时同步与 Gardner 算法:抽样时刻偏了怎么办
如果接收端抽样时刻偏离最佳采样点,码间串扰会抬升误码率。基带仿真里最常见的是故意让接收端采样时刻偏移delta + 0.3 * sps,然后让定时恢复模块把它纠正回来。Gardner 算法是符号率定时的经典选择,它只需要每符号两个采样点(即 sps=2),用相邻三个采样的组合产生误差信号:err = (x[n] - x[n-2]) * x[n-1]的实部或模值。
Gardner 误差信号的性格很有意思:在最佳采样时刻误差为零,偏差正负方向误差符号不同。环路滤波后驱动数控振荡器(NCO)调整插值位置,实现小数延迟插值。为什么不用更简单的早迟门?因为 Gardner 对载波相位不敏感,可以在载波同步之前工作,这是个工程上的大优势。现实中 QPSK 接收机的顺序通常是:AGC → 粗频偏纠正 → 定时同步 → 细频偏/相位同步 → 判决。
4.3 衰落信道下的性能演进:多径、均衡与分集
QPSK 在 AWGN 里的 BER 曲线可以闭式推导,但无线信道里的多径效应会让这个理论值失真。频率选择性衰落信道下,不同延迟径的叠加会引入严重的频率选择性畸变——某一频率分量深度衰落,而另一频率分量被增强。信道相干带宽小于信号带宽时,均衡器是必须的。
最简单的均衡策略是零迫(ZF)均衡器,它把接收信号除以信道频响,噪声在深衰落频点上被严重放大;MMSE 均衡器加入了噪声功率统计信息,在噪声放大和符号干扰之间取平衡,实际工程里几乎只用 MMSE。如果你 zip 里的场景涉及移动通信或 OFDM,还可以考虑在频域做单抽头均衡。Rake 接收机则适合 CDMA 系统,它把多径当成有用的能量收集,每条径用相关器独立解调后最大比合并。
分集是另一条对抗衰落的路径:空间分集(多天线)、时间分集(交织+编码)、频率分集(跳频)。两路独立衰落分支的 QPSK 在最大比合并下的 BER 比单支路好两个数量级以上,这正是 MIMO 系统为什么能对抗衰落的理论基础。
5. 验证与调优:眼图、星座图、BER 曲线的判读技巧
5.1 眼图到底在看什么
在匹配滤波输出端,把连续几个符号周期的波形叠加显示,就是眼图。QPSK 波形可以用实部单独看,也可以用复数画。眼图的核心判读点有四个:眼睛张开的高度代表噪声容限;交叉点的厚度反映定时抖动;眼皮的模糊程度体现码间串扰;过零点位置是否居中眼图大致可以看到定时是否有偏。
零 ISI 系统睁眼最大、眼图中央的水平线厚度为零(无码间串扰时,眼图在采样时刻的散点是收敛的一个点)。如果看到眼图张不开或交叉点有分叉,优先检查脉冲成形的滚降系数是否和收发端匹配,再看定时采样时刻是否落在最佳位置。
5.2 用 BER 曲线验证仿真正确性
AWGN 下 QPSK 理论 BER 是0.5 * erfc(sqrt(Eb/N0)),仿真曲线应该在 Eb/N0 高于 4dB 时和理论值贴合。如果曲线与理论分离,按下面顺序排查:
- 噪声功率是否换算正确:重点看 Es 与 Eb 的关系,QPSK 很容易忘记乘 2
- 群延迟补偿是否到位:
num_taps-1个采样点的偏差会让 BER 在曲线尾部出现地平线(error floor) - 判决阈值是否正确:QPSK 判决条件是实部虚部分别和 0 比较
- 符号数是否足够:BER 在 10^-4 量级时,至少需要 10^5 个符号才能有统计意义
蒙特卡洛仿真里还有一个常见误区:spa过小导致滤波器冲激响应截断严重。sps=4 和 α=0.35 的组合会引入几 dB 的恶化的例子不少见,稳妥取 sps=8 或以上。
5.3 从仿真到实物:一条现实的验证路径
仿真跑通不等于能上板。我会在仿真链路末尾接一个文件接口:仿真输出 IQ 数据写入.bin或.npy,硬化前检查 FPGA 定点化前后的星座图和 BER 差异。QPSK 的定点化通常关注相位的量化误差与成形滤波器系数的量化,bit 宽度选 12bit 大概率够用,16bit 余量更足。这个环节能提前暴露浮点仿真掩盖的天花板效应,是压缩包代码升级为可交付工程的最短路径。
本文还有配套的精品资源,点击获取