第十八章:通信系统仿真——用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 核心知识体系

下面这张图,是我梳理的通信系统仿真核心逻辑。你把它记在脑子里,以后做任何通信仿真都跑不出这个框架。

通信系统仿真核心流程 信源生成 调制 加噪 解调 误码率计算 比较原始与解码比特 核心参数:信噪比(SNR)、调制方式、比特数 输出结果:误码率(BER)曲线 关键点 • 信源长度对齐 • 噪声功率归一化 • 仿真次数足够多

这张图把整个流程串起来了。你从信源出发,经过调制、加噪、解调,最后算误码率。虚线部分表示把解码后的比特和原始比特做比较,才能算出误码率。

个人经验: 我建议你先把BPSK跑通,再扩展到QPSK。BPSK的判决逻辑简单,容易验证正确性。等BPSK的误码率曲线和理论值对上了,再去做QPSK。这样出了问题,你知道该怀疑哪个环节。

好了,这一章的内容就到这里。通信系统仿真其实不复杂,关键是把每个模块的输入输出搞清楚。你动手跑一遍代码,比看十遍理论都管用。

无相订单流研究社 微信Lucian808555