第二题:QPSK仿真——从理论到代码的完整实现
各位同学,今天我们来聊聊QPSK仿真。说实话,QPSK(正交相移键控)是我个人非常喜欢的一种调制方式。为什么?因为它简单、高效,而且在工程中特别实用。我记得刚入行那会儿,第一个独立负责的项目就是基于QPSK的卫星通信系统。那时候踩了不少坑,今天我把这些经验都揉进代码里,咱们一起走一遍。
一、QPSK调制原理——先别急着写代码
QPSK本质上就是把二进制比特流映射到四个相位点上。每个符号携带2个比特,所以传输效率比BPSK翻了一倍。你想想看,同样的带宽,数据速率直接翻倍,这买卖划算吧?
具体来说,QPSK的四个星座点分别对应:
- 00 → 相位 45°(π/4)
- 01 → 相位 135°(3π/4)
- 11 → 相位 225°(5π/4)
- 10 → 相位 315°(7π/4)
这里有个细节要注意:映射顺序不是随便定的。格雷编码(相邻星座点只差1个比特)能有效降低误码率。我在项目中吃过这个亏——第一次做QPSK时用了自然编码,结果误码率比理论值高了0.5dB。后来改成格雷编码,立马就对了。
核心公式:
QPSK信号可以表示为:s(t) = I(t)·cos(2πfct) - Q(t)·sin(2πfct)
其中I(t)是同相分量,Q(t)是正交分量。说白了,就是把两个BPSK信号叠加在一起,一个在cos上,一个在sin上。
二、Python实现QPSK调制——动手吧
好,理论讲完了,咱们直接上代码。我个人习惯用NumPy处理数组,用Matplotlib画图。先写一个QPSK调制函数:
import numpy as np
import matplotlib.pyplot as plt
def qpsk_modulate(bits):
"""
QPSK调制函数
bits: 输入比特流,长度必须为偶数
返回: 复数形式的QPSK符号
"""
# 确保比特数为偶数
if len(bits) % 2 != 0:
raise ValueError("比特数必须为偶数")
# 将比特流分组,每2个比特映射为一个符号
symbols = []
for i in range(0, len(bits), 2):
bit_pair = bits[i:i+2]
# 格雷编码映射
if bit_pair == [0, 0]:
symbols.append(complex(1/np.sqrt(2), 1/np.sqrt(2))) # 45°
elif bit_pair == [0, 1]:
symbols.append(complex(-1/np.sqrt(2), 1/np.sqrt(2))) # 135°
elif bit_pair == [1, 1]:
symbols.append(complex(-1/np.sqrt(2), -1/np.sqrt(2))) # 225°
elif bit_pair == [1, 0]:
symbols.append(complex(1/np.sqrt(2), -1/np.sqrt(2))) # 315°
return np.array(symbols)
嗯,这里要注意:我用了1/√2作为幅度归一化。为什么?因为每个符号的能量要归一化到1。如果不做归一化,后面加噪声时信噪比计算会出问题。我曾经见过有人直接用(1,1)作为星座点,结果误码率曲线怎么都对不上理论值——就是归一化没做好。
三、添加AWGN噪声——模拟真实信道
信号发出去,总要经过信道。AWGN(加性高斯白噪声)是最基本的信道模型。说白了,就是在信号上叠加一个高斯分布的随机噪声。
这里有个关键点:噪声功率怎么算?根据信噪比SNR来算。SNR = 10·log10(信号功率/噪声功率)。既然我们信号功率归一化为1,那么噪声功率就是10(-SNR/10)。
def add_awgn(symbols, snr_db):
"""
添加AWGN噪声
symbols: 输入符号(复数)
snr_db: 信噪比,单位dB
返回: 加噪后的符号
"""
# 计算信号功率
signal_power = np.mean(np.abs(symbols)**2)
# 根据SNR计算噪声功率
snr_linear = 10**(snr_db/10)
noise_power = signal_power / snr_linear
# 生成复高斯噪声
noise = np.sqrt(noise_power/2) * (np.random.randn(len(symbols)) +
1j * np.random.randn(len(symbols)))
return symbols + noise
避坑指南:我曾经犯过一个错误——直接用np.random.randn生成噪声,忘了除以2。结果噪声功率翻倍,误码率曲线整体右移了3dB。找了半天才发现问题。记住:复噪声的实部和虚部各占一半功率。
四、QPSK解调——把信号变回比特
解调就是调制的逆过程。收到信号后,我们要判断每个符号属于哪个象限,然后映射回比特对。这里用的是最小欧氏距离判决法——说白了,就是看接收到的符号离哪个星座点最近。
def qpsk_demodulate(received_symbols):
"""
QPSK解调
received_symbols: 接收到的符号(复数)
返回: 解调后的比特流
"""
# 定义四个星座点
constellation = [
complex(1/np.sqrt(2), 1/np.sqrt(2)), # 00
complex(-1/np.sqrt(2), 1/np.sqrt(2)), # 01
complex(-1/np.sqrt(2), -1/np.sqrt(2)), # 11
complex(1/np.sqrt(2), -1/np.sqrt(2)) # 10
]
# 对应的比特映射
bit_mapping = [[0,0], [0,1], [1,1], [1,0]]
bits = []
for symbol in received_symbols:
# 计算到四个星座点的距离
distances = [np.abs(symbol - ref) for ref in constellation]
# 找到最近的点
idx = np.argmin(distances)
# 映射回比特
bits.extend(bit_mapping[idx])
return np.array(bits)
五、误码率计算——验证你的系统
误码率(BER)是衡量通信系统性能的核心指标。计算很简单:统计错误比特数除以总比特数。但要注意,一定要对比发送的原始比特和解调后的比特,不能自己跟自己比。
def calculate_ber(original_bits, demodulated_bits):
"""
计算误码率
"""
if len(original_bits) != len(demodulated_bits):
raise ValueError("比特长度不一致")
errors = np.sum(original_bits != demodulated_bits)
ber = errors / len(original_bits)
return ber
我建议你跑一个完整的仿真链路:生成随机比特 → QPSK调制 → 加噪声 → QPSK解调 → 计算BER。然后改变SNR,画出BER曲线。你会发现,当SNR从0dB增加到10dB时,BER会从大约0.1降到10-4以下。
六、星座图可视化——一眼看出问题
星座图是调试通信系统最直观的工具。没有之一。我每次做实验,第一件事就是画星座图。它能告诉你:噪声大不大?有没有相位偏移?有没有IQ不平衡?
def plot_constellation(symbols, title="QPSK Constellation"):
"""
绘制星座图
"""
plt.figure(figsize=(8, 8))
plt.scatter(symbols.real, symbols.imag, alpha=0.6, s=20)
plt.grid(True, alpha=0.3)
plt.xlabel("In-phase (I)")
plt.ylabel("Quadrature (Q)")
plt.title(title)
plt.axis('equal')
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.show()
重要提醒:画星座图时一定要用plt.axis('equal')。如果不加这行,I轴和Q轴的比例可能不一样,星座点看起来会是椭圆形的,容易误判。我见过有人因为这个设置,把正常的QPSK信号误判成IQ不平衡。
七、完整仿真流程——串起来跑一遍
好了,所有模块都写好了。咱们把它们串起来,跑一个完整的仿真:
# 参数设置
num_bits = 100000 # 发送10万个比特
snr_db_range = np.arange(0, 15, 2) # SNR从0到14dB,步进2dB
# 存储结果
ber_results = []
for snr_db in snr_db_range:
# 1. 生成随机比特
tx_bits = np.random.randint(0, 2, num_bits)
# 2. QPSK调制
tx_symbols = qpsk_modulate(tx_bits)
# 3. 添加AWGN噪声
rx_symbols = add_awgn(tx_symbols, snr_db)
# 4. QPSK解调
rx_bits = qpsk_demodulate(rx_symbols)
# 5. 计算误码率
ber = calculate_ber(tx_bits, rx_bits)
ber_results.append(ber)
print(f"SNR = {snr_db} dB, BER = {ber:.6f}")
# 绘制BER曲线
plt.figure(figsize=(10, 6))
plt.semilogy(snr_db_range, ber_results, 'bo-', label='Simulated QPSK')
plt.grid(True, alpha=0.3)
plt.xlabel('SNR (dB)')
plt.ylabel('Bit Error Rate (BER)')
plt.title('QPSK BER Performance in AWGN Channel')
plt.legend()
plt.show()
跑完这段代码,你会得到一条漂亮的BER曲线。把它和理论值对比一下——如果吻合得很好,说明你的实现没问题。如果不吻合,嗯,那就得回头检查了。我建议你先从高SNR开始测试,比如15dB,这时候BER应该非常低(接近0)。如果高SNR下BER还很高,那肯定是代码有bug。
八、知识体系总览
为了让你对整个QPSK仿真流程有个全局认识,我画了一张流程图:
这张图把整个流程串起来了。你写代码的时候,就按这个顺序来。每一步的输出都是下一步的输入,逻辑非常清晰。
九、总结与经验分享
做QPSK仿真,说白了就是三件事:调得好、噪得准、解得对。调得好指星座点映射要正确;噪得准指噪声功率计算要精确;解得对指判决门限要合理。
我个人建议你多做几组对比实验:
- 对比格雷编码和自然编码的BER差异
- 对比不同SNR下的星座图变化
- 对比理论BER曲线和仿真BER曲线
这些对比做下来,你对QPSK的理解会深入很多。我记得当年做这些实验时,光是星座图就画了几十张。每张图都记录下SNR和BER,最后总结出一张完整的性能表。那种从数据中看到规律的感觉,真的很爽。
好,QPSK仿真就讲到这里。代码都给你了,赶紧动手试试吧。有问题随时交流。
公众号:蓝海资料掘金营,微信deep3321