第十八章:通信系统仿真——用Python搭一套完整的信号链路
通信系统仿真,说白了就是模拟信号从发送到接收的完整旅程。我刚开始做这个的时候,总觉得不就是发个信号嘛,能有多复杂?结果第一次仿真跑出来,误码率高达50%,跟抛硬币差不多。嗯,从那以后我再也不敢小看信道的影响了。
这一章,我们亲手搭建一套完整的通信系统仿真链路。包括信源生成、调制、加噪、解调,最后算误码率。你想想看,这其实就是数字通信最核心的骨架。
核心流程: 信源 → 调制 → 加噪 → 解调 → 误码率计算
18.1 信源生成——比特流从哪里来?
通信系统的第一步,是产生原始数据。在实际项目中,信源可能是语音、图像或者文本。但仿真时,我们通常用随机比特流来模拟。
我个人习惯用 NumPy 的随机数生成器。为什么?因为可复现。设置一个随机种子,每次跑的结果都一样,调试起来方便很多。
import numpy as np
def generate_bits(num_bits, seed=42):
np.random.seed(seed)
bits = np.random.randint(0, 2, num_bits)
return bits
# 生成1000个随机比特
bits = generate_bits(1000)
print(bits[:20]) # 看看前20个
小技巧: 我在项目中遇到过一个问题——信源长度必须是符号长度的整数倍。比如QPSK调制,每个符号携带2个比特,那么比特数最好是2的倍数。否则最后会多出几个比特没法处理。
18.2 调制——把比特变成波形
调制,就是把数字比特映射到模拟波形上。最常见的调制方式有BPSK和QPSK。BPSK每个符号携带1个比特,QPSK每个符号携带2个比特。
说白了,BPSK就是0映射到-1,1映射到+1。QPSK稍微复杂一点,用四个相位表示00、01、10、11。
def bpsk_modulate(bits):
# BPSK: 0 -> -1, 1 -> +1
symbols = 2 * bits - 1
return symbols
def qpsk_modulate(bits):
# QPSK: 每2个比特映射一个符号
# 00 -> -1-1j, 01 -> -1+1j, 10 -> +1-1j, 11 -> +1+1j
if len(bits) % 2 != 0:
raise ValueError("比特数必须是偶数")
symbols = []
for i in range(0, len(bits), 2):
if bits[i] == 0 and bits[i+1] == 0:
symbols.append(-1 - 1j)
elif bits[i] == 0 and bits[i+1] == 1:
symbols.append(-1 + 1j)
elif bits[i] == 1 and bits[i+1] == 0:
symbols.append(+1 - 1j)
else:
symbols.append(+1 + 1j)
return np.array(symbols)
注意: 我曾经犯过一个低级错误——QPSK映射时把实部和虚部搞反了。结果解调出来的比特全是错的。调试了整整一下午才发现。所以建议你写完之后,先手动验证几个映射关系。
18.3 加噪——模拟真实信道
信号在信道中传输,一定会受到噪声干扰。最常用的模型是加性高斯白噪声(AWGN)。噪声的强度用信噪比(SNR)来控制,单位通常是dB。
这里有个关键点:信噪比的定义要搞清楚。对于复信号,噪声功率要分配到实部和虚部上,各占一半。
def add_awgn(symbols, snr_db):
# 计算信号功率
signal_power = np.mean(np.abs(symbols)**2)
# 将dB转换为线性值
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 生成噪声,没考虑功率归一化。结果信噪比完全对不上。记住:randn 生成的是单位功率的噪声,需要根据信噪比缩放。
18.4 解调——从噪声中恢复比特
解调是调制的逆过程。对于BPSK,判决规则很简单:接收符号大于0判为1,小于0判为0。QPSK则需要分别判决实部和虚部。
def bpsk_demodulate(received):
# 判决:>0 判为1,<=0 判为0
bits = (received.real > 0).astype(int)
return bits
def qpsk_demodulate(received):
bits = []
for sym in received:
# 判决实部
if sym.real > 0:
bits.append(1)
else:
bits.append(0)
# 判决虚部
if sym.imag > 0:
bits.append(1)
else:
bits.append(0)
return np.array(bits)
嗯,这里要注意:QPSK解调时,实部和虚部的判决顺序必须和调制时一致。否则比特顺序就乱了。
18.5 误码率计算——仿真结果怎么看?
误码率(BER)是衡量通信系统性能的核心指标。说白了,就是发出去的比特和收到的比特有多少不一样。
def calculate_ber(original_bits, received_bits):
if len(original_bits) != len(received_bits):
raise ValueError("比特长度不一致")
errors = np.sum(original_bits != received_bits)
ber = errors / len(original_bits)
return ber
完整仿真链路:
def run_simulation(num_bits=10000, snr_db=10, modulation='bpsk'):
# 1. 信源生成
bits = generate_bits(num_bits)
# 2. 调制
if modulation == 'bpsk':
symbols = bpsk_modulate(bits)
elif modulation == 'qpsk':
symbols = qpsk_modulate(bits)
else:
raise ValueError("不支持的调制方式")
# 3. 加噪
received = add_awgn(symbols, snr_db)
# 4. 解调
if modulation == 'bpsk':
decoded_bits = bpsk_demodulate(received)
elif modulation == 'qpsk':
decoded_bits = qpsk_demodulate(received)
# 5. 误码率计算
ber = calculate_ber(bits, decoded_bits)
return ber
# 测试不同信噪比下的误码率
for snr in [0, 2, 4, 6, 8, 10]:
ber = run_simulation(100000, snr, 'bpsk')
print(f"SNR={snr}dB, BER={ber:.6f}")
18.6 仿真结果分析
跑完仿真,你会看到:信噪比越高,误码率越低。这是符合理论预期的。但有个细节——仿真次数要足够多。我一般至少跑10万个比特,否则统计结果不稳定。
| 信噪比 (dB) | BPSK 误码率 | QPSK 误码率 |
|---|---|---|
| 0 | 0.0786 | 0.1587 |
| 4 | 0.0125 | 0.0251 |
| 8 | 0.0002 | 0.0004 |
| 10 | 0.0000 | 0.0001 |
你看,BPSK和QPSK在相同信噪比下,误码率差了一倍左右。这是因为QPSK每个符号携带的信息更多,抗噪声能力相对弱一些。
18.7 核心知识体系
下面这张图,是我梳理的通信系统仿真核心逻辑。你把它记在脑子里,以后做任何通信仿真都跑不出这个框架。
这张图把整个流程串起来了。你从信源出发,经过调制、加噪、解调,最后算误码率。虚线部分表示把解码后的比特和原始比特做比较,才能算出误码率。
个人经验: 我建议你先把BPSK跑通,再扩展到QPSK。BPSK的判决逻辑简单,容易验证正确性。等BPSK的误码率曲线和理论值对上了,再去做QPSK。这样出了问题,你知道该怀疑哪个环节。
好了,这一章的内容就到这里。通信系统仿真其实不复杂,关键是把每个模块的输入输出搞清楚。你动手跑一遍代码,比看十遍理论都管用。