☰
QPSK已调信号生成:从比特到复基带样本的完整流程与避坑指南
2026/10/14 10:13:20 网站建设 项目流程

简介:这份资源面向通信原理课程学习者与MATLAB仿真入门者,聚焦西电B测中的QPSK调制实验,帮助读者在无噪声条件下完整复现已调信号的生成与观测流程。包内共6个文件,以4份docx实验报告、测试记录与评分表为主,辅以1份m源码和1份pptx汇报演示,压缩包约592KB,结构紧凑,便于对照源码与文档同步理解。实验要求源码采用周期63比特的m序列,比特速率与载波频率数值相等且可在5至100间取整,需在MATLAB中完成QPSK调制仿真,并依次观测源码时域波形、基带发射端星座图、已调信号时域波形以及功率谱或频谱。已有1990人学习下载,读者可借此获得可直接运行的M代码、完整的实验报告撰写框架、评分标准参考与演示汇报思路,适合作为课程设计、线上测试或自学通信调制的实操范本。

1. 从一份 QPSK 已调信号生成包说起:它到底能帮你省掉哪段重复劳动

做数字通信链路仿真的人,几乎都绕不开 QPSK 这个调制方式。它频谱效率够用、实现复杂度不高、抗噪性能在中等信噪比区间表现稳定,所以从教学实验到工程预研,QPSK 的已调信号生成几乎是每个链路仿真项目的必经环节。但真正动手写过的人都知道,从原始比特流到最终那串可以送进信道模型的复数基带样本,中间要处理串并转换、星座映射、脉冲成型、载波调制这一连串步骤,每一步都有参数需要确认,每一步写错都会让后面的误码率曲线变成一团看不懂的玄学。

这份「QPSK已调信号生成.zip」解决的就是这个环节的重复劳动问题。它把 QPSK 已调信号生成的完整流程封装成可直接调用的代码包,你不需要从零推导星座映射表,也不需要反复调试根升余弦滤波器的滚降系数和截断长度。拿到手之后,改几个参数就能生成符合自己链路需求的已调信号序列,直接送进后续的 AWGN 信道、瑞利衰落信道或者接收端解调模块。适合正在做通信系统仿真、数字信号处理课程设计、或者需要快速验证接收算法的人。如果你之前用 MATLAB 或 Python 手写过 QPSK 调制但总觉得哪里不对,这份资源可以当作一个可对照的参考实现。

2. QPSK 已调信号生成的内部流程:从比特到复基带样本

2.1 串并转换与星座映射的对应关系

QPSK 的本质是把每两个比特映射到一个复平面上的星座点上。这四个点通常取 $(\pm1 \pm j)/\sqrt{2}$ 或者 $(\pm1 \pm j)$,区别在于是否做归一化。归一化的目的是让平均符号能量为 1,方便后续信噪比计算。很多新手在这里翻车:映射表写反了,比如把 00 映射到第一象限、01 映射到第二象限,结果解调出来的比特顺序全乱,但星座图看起来又是对的,排查起来非常痛苦。

常见做法是采用格雷码映射,让相邻星座点之间只差一个比特,这样在信噪比不高时误比特率会低一些。具体映射关系如下表:

输入比特对同相分量 I正交分量 Q星座点
00+1+1第一象限
01-1+1第二象限
11-1-1第三象限
10+1-1第四象限

这张表看着简单,但实际写代码时,比特流的读取顺序、串并转换时哪个比特在前,都会影响最终映射结果。我一般会在代码里加一个断言,用已知的测试比特序列验证映射输出是否和预期一致,确认无误后再跑长数据。

2.2 脉冲成型滤波器的参数选择

星座映射出来的符号是冲激序列,直接送信道会占用无限带宽。所以需要脉冲成型,把每个符号的能量集中在一个可控的频带内。最常用的是根升余弦滤波器,因为它满足奈奎斯特第一准则,在接收端匹配滤波后能消除码间干扰。

关键参数有三个:滚降系数、符号周期、滤波器截断长度。滚降系数一般取 0.2 到 0.5 之间,取值越小频谱效率越高,但时域拖尾越长,对定时同步要求越苛刻。符号周期由符号速率决定,比如符号速率是 1 MHz,那符号周期就是 1 微秒。滤波器截断长度通常取 6 到 10 个符号周期,太短了频域特性不理想,太长了计算量大且边缘效应不明显。

下面是一段 Python 示例,展示如何用根升余弦滤波器对 QPSK 符号做脉冲成型:

import numpy as np from scipy.signal import firwin def rrc_filter(beta, sps, span): """ 生成根升余弦滤波器系数 beta: 滚降系数 sps: 每符号采样点数 span: 滤波器跨度的符号数 """ N = span * sps t = np.arange(-N/2, N/2 + 1) / sps # 根升余弦时域表达式 with np.errstate(divide='ignore', invalid='ignore'): num = np.sin(np.pi * t * (1 - beta)) + 4 * beta * t * np.cos(np.pi * t * (1 + beta)) den = np.pi * t * (1 - (4 * beta * t) ** 2) h = num / den h[np.isnan(h)] = 1.0 # t=0 处的极限值 h = h / np.sqrt(np.sum(h ** 2)) # 能量归一化 return h # 参数设置 beta = 0.35 sps = 8 span = 10 h_rrc = rrc_filter(beta, sps, span) print(f"滤波器长度: {len(h_rrc)} 个采样点")

这段代码里,beta控制滚降程度,sps决定每个符号被过采样多少倍,span决定滤波器拖尾覆盖多少个符号。能量归一化那一步很关键,不做的话后续加噪声时信噪比计算会偏。我见过有人忘了归一化,结果误码率曲线整体平移了好几 dB,查了半天才发现是滤波器增益没处理。

2.3 载波调制与复基带等效

脉冲成型后的信号还是基带信号,要搬到射频载波上才是真正的「已调信号」。但在仿真里,为了降低采样率、避免高频载波带来的计算负担,通常采用复基带等效模型。也就是说,把 $I(t)\cos(2\pi f_c t) - Q(t)\sin(2\pi f_c t)$ 这个实信号表示成 $I(t) + jQ(t)$ 的复数形式,载波频率在等效模型里被隐去了。

这样做的前提是接收端也做相干解调,并且载波同步由后续模块处理。如果你要生成的信号是直接送 DAC 做实际发射,那就需要保留实信号形式,把复基带的实部和虚部分别乘以余弦和正弦载波再相加。这一步在代码里通常用一个开关控制,输出复基带还是实带通。

def qpsk_modulate(bits, sps, beta, span, output_complex=True): """ QPSK 已调信号生成主函数 bits: 输入比特流,长度为偶数 sps: 每符号采样点数 beta: 根升余弦滚降系数 span: 滤波器跨度 output_complex: True 输出复基带,False 输出实带通 """ # 串并转换 I_bits = bits[0::2] Q_bits = bits[1::2] # 星座映射:0 -> +1, 1 -> -1 I_symbols = 1 - 2 * I_bits Q_symbols = 1 - 2 * Q_bits symbols = I_symbols + 1j * Q_symbols # 上采样 upsampled = np.zeros(len(symbols) * sps, dtype=complex) upsampled[::sps] = symbols # 脉冲成型 h_rrc = rrc_filter(beta, sps, span) baseband = np.convolve(upsampled, h_rrc, mode='same') if output_complex: return baseband else: # 实带通输出,载波频率取 0.1 倍采样率作为示例 fc = 0.1 t = np.arange(len(baseband)) / sps carrier_cos = np.cos(2 * np.pi * fc * t) carrier_sin = np.sin(2 * np.pi * fc * t) return baseband.real * carrier_cos - baseband.imag * carrier_sin

这段代码把前面几个步骤串起来了。bits[0::2]和bits[1::2]完成串并转换,1 - 2 * bits实现 0 到 +1、1 到 -1 的映射。上采样时注意upsampled[::sps] = symbols这个写法,它把符号放在每个符号周期的起始位置,卷积之后会有span/2个符号的延迟,这是滤波器因果性带来的,后续做定时同步时要考虑这个延迟。

3. 把生成包跑起来:环境配置与参数调优的实操路径

3.1 运行环境与依赖确认

拿到压缩包之后,先别急着跑主脚本。第一步是确认运行环境。这类信号生成包通常依赖 NumPy 和 SciPy 做数值计算和滤波器设计,如果包含绘图功能还会依赖 Matplotlib。我一般会先建一个干净的虚拟环境,避免和系统里已有的包版本冲突。

python -m venv qpsk_env source qpsk_env/bin/activate # Windows 下用 qpsk_env\Scripts\activate pip install numpy scipy matplotlib

装完之后,用pip list确认一下版本。NumPy 建议 1.20 以上,SciPy 建议 1.6 以上,这两个版本之后的 API 比较稳定,不容易出现np.float被移除之类的兼容性问题。如果压缩包里有requirements.txt,直接pip install -r requirements.txt更省事。

环境好了之后,先跑一遍包里的示例脚本。通常这类资源会带一个demo.py或者example.py,用默认参数生成一段信号并画出星座图和时域波形。先看默认参数下能不能跑通、图对不对,再改参数做自己的实验。这一步别跳过,我见过有人直接改主函数参数,结果因为环境问题报了一堆错,以为是代码有 bug,其实是 SciPy 版本不对导致滤波器设计函数行为变了。

3.2 关键参数的调整逻辑与验证方法

默认参数跑通之后,就要根据自己的链路需求调参数了。最常调的是符号速率、每符号采样点数、滚降系数这三个。

符号速率决定了信号占用的带宽。QPSK 的符号速率等于比特速率的一半,因为每个符号携带 2 比特。比如你有个 2 Mbps 的比特流,那符号速率就是 1 MHz,基带带宽大约是 1 MHz 乘以 $(1+\beta)$。如果滚降系数取 0.35,那占用带宽就是 1.35 MHz 左右。这个关系在选参数时一定要心里有数,不然生成出来的信号可能和你后续信道模型的带宽不匹配。

每符号采样点数影响仿真精度和计算量。采样点数越多,时域波形越平滑,但数据量也越大。一般取 4 到 16 之间。取 4 的时候,一个符号周期内只有 4 个采样点,脉冲成型的效果会比较粗糙,但跑长数据快。取 16 的话波形精细,适合做频谱分析,但内存占用明显上升。我一般先用 8 跑通流程,确认无误后再根据具体分析需求调整。

滚降系数前面提过,0.2 到 0.5 之间。如果你做的是带宽受限场景,比如卫星通信,可能会取 0.2 甚至更低。如果是地面移动通信,0.35 左右比较常见。调这个参数的时候,重点看成型后信号的频谱是不是在预期带宽内衰减到了足够低的电平,以及时域上有没有明显的码间干扰。

验证方法很简单:生成一段已知的比特序列,比如[0,0,1,1,0,1,1,0],手动算一下映射后的符号应该是什么,然后看代码输出的前几个符号对不对。再画个星座图,看四个点是不是落在预期位置。最后加一点高斯白噪声,看解调后的误码率是不是随着信噪比升高而下降。这三步走完,基本能确认生成包工作正常。

3.3 输出数据的格式与后续对接

生成包输出的通常是复数数组,每个元素是一个复基带样本。这个格式可以直接送进大多数信道仿真模块。如果你用的是 MATLAB,可能需要转成.mat文件;如果用 Python 生态,直接np.save存成.npy就行。

# 生成信号并保存 bits = np.random.randint(0, 2, 10000) signal = qpsk_modulate(bits, sps=8, beta=0.35, span=10, output_complex=True) np.save('qpsk_baseband.npy', signal) print(f"信号长度: {len(signal)} 个样本") print(f"前 5 个样本: {signal[:5]}")

存的时候注意数据类型。复数默认是complex128,如果后续处理对内存敏感,可以转成complex64,精度损失在仿真里通常可以接受。另外,如果你要把信号送进硬件或者做定点仿真,还需要做量化,这一步生成包一般不管,得自己加。

对接信道模型时,信噪比的定义要确认清楚。有的信道模型按符号信噪比算,有的按比特信噪比算,两者差一个 $10\log_{10}(2)$ 的关系。搞错了的话,误码率曲线整体偏移 3 dB,你会以为算法有问题,其实是信噪比定义没对齐。

4. 避坑与排查:QPSK 信号生成里最容易翻车的五个地方

4.1 星座图看起来对但误码率居高不下

现象:生成的信号星座图四个点清清楚楚,但送进解调器后误码率就是降不下去,信噪比再高也没用。

原因:最常见的是映射表和解映射表不一致。生成端用格雷码映射,解调端用了自然二进制映射,星座图上看不出来,但比特顺序全错。另一种可能是串并转换时 I 路和 Q 路的比特顺序搞反了,生成端把第一个比特给 I,解调端把第一个比特给 Q。

解决:在生成端和解调端用同一段已知比特序列做回环测试,对比输入和输出。如果误码率是 50%,基本就是映射或顺序问题。把映射表打印出来,逐项核对。

4.2 脉冲成型后信号功率异常

现象:成型后的信号平均功率和预期差很多,导致加噪声时信噪比计算错误。

原因:根升余弦滤波器系数没有做能量归一化,或者归一化方式不对。有的实现按峰值归一化,有的按能量归一化,两者差一个系数。另外,上采样时如果符号插入位置不对,卷积后的信号幅度也会异常。

解决:在滤波器设计函数里显式做能量归一化,即h = h / np.sqrt(np.sum(h**2))。生成信号后,用np.mean(np.abs(signal)**2)检查平均功率,和理论值对比。QPSK 归一化星座点的平均符号能量是 1,成型后如果滤波器能量归一化了,信号平均功率应该也在 1 附近。

4.3 滤波器截断长度不够导致频谱泄漏

现象:生成信号的频谱在带外衰减很慢,旁瓣很高,送进信道后对相邻频段造成干扰。

原因:根升余弦滤波器的截断长度太短,时域拖尾被硬切了,频域对应的是旁瓣抬升。一般span小于 6 的时候这个问题比较明显。

解决:把span加到 10 甚至 12,再看频谱。代价是计算量增加,但频谱特性会好很多。如果计算资源实在紧张,可以换用频域成型方法,但实现复杂度会高一些。

4.4 采样率不匹配导致定时误差

现象:接收端解调时星座点发散,不是四个清晰的点,而是模糊的一团。

原因:生成端的每符号采样点数和接收端假设的不一致。比如生成端用了 8 倍过采样,接收端按 4 倍处理,定时同步就会出错。另一种可能是生成端和接收端的符号速率定义不同,一个按符号周期算,一个按采样周期算。

解决:在生成包的输出里明确标注sps参数,接收端严格按这个值做下采样和定时同步。如果要做多速率处理,用重采样函数显式转换,不要靠猜。

4.5 复数输出被当成实数处理

现象:信号送进后续模块后报类型错误,或者虚部被静默丢弃,导致 Q 路信息丢失。

原因:有些信道模型或绘图函数只接受实数数组,复数传进去会被自动取实部,虚部直接没了。这种情况不会报错,但结果完全错误。

解决:在对接每个模块之前,确认它接受的数据类型。如果只接受实数,就把复基带的实部和虚部分开处理,或者转成实带通形式。别指望模块自己帮你处理复数。

5. 进阶用法:用生成包做端到端链路验证与参数扫描

生成包跑通之后,它的价值不止于生成一段信号。我一般会把它嵌进一个端到端链路里,做参数扫描和性能验证。具体做法是写一个外层循环,遍历不同的滚降系数和信噪比组合,每个组合下生成信号、加噪声、解调、统计误码率,最后画出一组误码率曲线。

import numpy as np import matplotlib.pyplot as plt def awgn(signal, snr_db): """加高斯白噪声,按符号信噪比计算""" signal_power = np.mean(np.abs(signal) ** 2) noise_power = signal_power / (10 ** (snr_db / 10)) noise = np.sqrt(noise_power / 2) * (np.random.randn(len(signal)) + 1j * np.random.randn(len(signal))) return signal + noise def qpsk_demodulate(signal, sps): """简单解调:下采样后取符号判决""" downsampled = signal[::sps] I_bits = (downsampled.real < 0).astype(int) Q_bits = (downsampled.imag < 0).astype(int) bits = np.empty(2 * len(I_bits), dtype=int) bits[0::2] = I_bits bits[1::2] = Q_bits return bits # 参数扫描 betas = [0.2, 0.35, 0.5] snr_range = np.arange(0, 11, 2) bits = np.random.randint(0, 2, 20000) for beta in betas: ber_list = [] for snr in snr_range: signal = qpsk_modulate(bits, sps=8, beta=beta, span=10, output_complex=True) noisy = awgn(signal, snr) received_bits = qpsk_demodulate(noisy, sps=8) # 对齐长度后统计误码率 min_len = min(len(bits), len(received_bits)) ber = np.mean(bits[:min_len] != received_bits[:min_len]) ber_list.append(ber) plt.semilogy(snr_range, ber_list, marker='o', label=f'beta={beta}') plt.xlabel('SNR (dB)') plt.ylabel('BER') plt.legend() plt.grid(True) plt.show()

这段代码里,awgn函数按符号信噪比加噪声,qpsk_demodulate做最简单的硬判决解调。注意解调时下采样的位置要和生成端上采样的位置对应,否则会有定时偏移。实际链路里还会有匹配滤波和定时同步,这里为了演示省略了,所以误码率会比理论值差一些,但趋势是对的。

跑完这个扫描,你能直观看到滚降系数对误码率的影响。在低信噪比区间,不同滚降系数的误码率差别不大,因为噪声是主要因素。但在高信噪比区间,滚降系数小的信号因为带宽效率高,如果接收端匹配滤波做得好,误码率反而可能更低。这个结论和很多人的直觉相反,但实际仿真里确实会出现,原因是滚降系数影响的是频谱形状,而误码率主要由星座点之间的欧氏距离决定,归一化之后这个距离是一样的。

从那以后我每次拿到一个新的信号生成包,都会先跑一遍这个参数扫描,确认它的误码率曲线和理论值对得上,再拿去做更复杂的链路验证。这个习惯帮我省了很多后悔药,因为一旦生成端有问题,后面所有模块的调试都是在错误的基础上进行的。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询