第三十讲:综合项目——点对点通信系统完整链路仿真

好,终于到了最后一讲。说实话,我写到这里还挺感慨的。前面二十九讲,我们从信息论的基础,一路走到编码、调制、信道、解调、解码,每一步都拆开揉碎了讲。现在,是时候把这些零件拼起来,跑一个完整的点对点通信系统了。

这个项目,说白了就是模拟一个最简单的“发消息”过程。从信源产生数据,到信宿正确接收,中间经过编码、调制、传输、解调、解码五个环节。我会用 Python 代码把整个链路串起来,让你亲眼看到比特是怎么飞过去的。

项目目标: 实现一个从信源到信宿的完整仿真链路,包含汉明码编码、BPSK调制、AWGN信道、软判决解调、维特比译码(或硬判决纠错),最终对比误码率。

一、系统架构总览

先画一张图,把整个流程看清楚。我个人习惯在做任何系统之前,先画一张架构图,哪怕只是给自己看。这张图能帮你避免“写到一半发现漏了环节”的尴尬。

信源 编码 调制 信道 解调 解码 二进制数据 汉明码(7,4) BPSK AWGN 软判决 纠错 最终对比:编码 vs 未编码 误码率曲线

你看,这个流程其实不复杂。信源产生一串0和1,经过编码增加冗余,调制到载波上,通过有噪声的信道,接收端解调回来,最后解码纠错,得到原始数据。嗯,这里要注意:编码和调制是两个不同的概念,编码加冗余是为了纠错,调制是为了让信号能在信道上传输。

二、各模块实现细节

2.1 信源:生成随机比特

信源没什么好说的,就是随机产生一串二进制数据。我一般用 numpy 的随机数生成器,方便控制长度和种子。

import numpy as np

def generate_source(n_bits=1000, seed=42):
    np.random.seed(seed)
    return np.random.randint(0, 2, n_bits)
小技巧: 固定随机种子能让实验结果可复现。我在调试时一定会设种子,不然每次跑出来的结果不一样,根本没法对比。

2.2 编码:汉明码 (7,4)

汉明码 (7,4) 的意思是:每4个信息位,加上3个校验位,组成7个码字。它能纠正1个比特错误。为什么选它?因为它简单,适合教学。我在项目中用过更复杂的 LDPC 码,但原理是一样的——用冗余换可靠性

def hamming_encode(bits):
    # 输入:4位信息位
    # 输出:7位码字
    d = bits
    p1 = d[0] ^ d[1] ^ d[3]
    p2 = d[0] ^ d[2] ^ d[3]
    p3 = d[1] ^ d[2] ^ d[3]
    return np.array([d[0], d[1], d[2], d[3], p1, p2, p3])

def encode_all(data):
    # 将数据按4位分组编码
    encoded = []
    for i in range(0, len(data), 4):
        block = data[i:i+4]
        if len(block) < 4:
            block = np.pad(block, (0, 4-len(block)), 'constant')
        encoded.extend(hamming_encode(block))
    return np.array(encoded)

2.3 调制:BPSK

BPSK 是最简单的数字调制方式。0 映射到 -1,1 映射到 +1。为什么用 BPSK?因为它抗噪声能力强,而且实现简单。我记得第一次做通信仿真时,老师就说:“先把 BPSK 玩明白,其他的调制方式都是它的变种。”

def bpsk_modulate(bits):
    # 0 -> -1, 1 -> +1
    return 2 * bits - 1

2.4 信道:加性高斯白噪声 (AWGN)

信道模型用 AWGN,这是最经典的模型。噪声的强度由信噪比 SNR 控制。SNR 越低,噪声越大,误码率越高。

def awgn_channel(signal, snr_db):
    # 计算噪声功率
    signal_power = np.mean(signal**2)
    snr_linear = 10**(snr_db / 10)
    noise_power = signal_power / snr_linear
    noise = np.sqrt(noise_power) * np.random.randn(len(signal))
    return signal + noise
注意: 噪声功率的计算一定要基于信号功率。我曾经犯过一个错误,直接假设信号功率为1,结果在高 SNR 时仿真结果完全不对。后来排查了半天才发现是噪声功率算错了。

2.5 解调:软判决

解调有两种方式:硬判决和软判决。硬判决直接判断符号的正负,输出0或1。软判决则保留接收到的连续值,给解码器提供更多信息。软判决的纠错能力更强,但计算量也更大。

def bpsk_demodulate_soft(received):
    # 软判决:直接返回接收值
    return received

def bpsk_demodulate_hard(received):
    # 硬判决:大于0判为1,否则0
    return (received > 0).astype(int)

2.6 解码:汉明码纠错

解码时,先计算校验子,然后定位错误位置并纠正。这里我用的是硬判决解码,因为汉明码本身设计就是针对硬判决的。

def hamming_decode(codeword):
    # 输入:7位码字
    # 输出:4位信息位
    c = codeword
    s1 = c[4] ^ c[0] ^ c[1] ^ c[3]
    s2 = c[5] ^ c[0] ^ c[2] ^ c[3]
    s3 = c[6] ^ c[1] ^ c[2] ^ c[3]
    syndrome = s1 + 2*s2 + 4*s3
    
    # 错误位置映射
    error_pos = {3:0, 5:1, 6:2, 7:3, 1:4, 2:5, 4:6}
    if syndrome in error_pos:
        pos = error_pos[syndrome]
        c[pos] ^= 1  # 翻转错误比特
    
    return c[0:4]  # 返回信息位

三、完整链路仿真与误码率对比

现在,我们把所有模块串起来,跑一个完整的仿真。我会对比“编码+硬判决”和“未编码”两种方案的误码率。

def simulate(snr_db, n_bits=10000):
    # 生成信源
    data = generate_source(n_bits)
    
    # 编码
    encoded = encode_all(data)
    
    # 调制
    modulated = bpsk_modulate(encoded)
    
    # 信道
    received = awgn_channel(modulated, snr_db)
    
    # 解调(硬判决)
    demodulated = bpsk_demodulate_hard(received)
    
    # 解码
    decoded = []
    for i in range(0, len(demodulated), 7):
        block = demodulated[i:i+7]
        if len(block) == 7:
            decoded.extend(hamming_decode(block))
    decoded = np.array(decoded)
    
    # 计算误码率
    min_len = min(len(data), len(decoded))
    ber = np.sum(data[:min_len] != decoded[:min_len]) / min_len
    return ber

# 运行不同SNR下的仿真
snr_range = np.arange(0, 10, 1)
ber_coded = []
ber_uncoded = []

for snr in snr_range:
    ber_coded.append(simulate(snr))
    # 未编码:直接调制解调
    data = generate_source(10000)
    modulated = bpsk_modulate(data)
    received = awgn_channel(modulated, snr)
    demodulated = bpsk_demodulate_hard(received)
    ber_uncoded.append(np.sum(data != demodulated) / len(data))

print("SNR(dB) | 编码BER | 未编码BER")
print("-" * 40)
for i, snr in enumerate(snr_range):
    print(f"{snr:5.1f}  | {ber_coded[i]:.4e} | {ber_uncoded[i]:.4e}")

跑完这段代码,你会看到类似下面的结果:

SNR (dB) 编码 BER 未编码 BER
0 1.2e-1 1.8e-1
2 5.6e-2 1.1e-1
4 1.8e-2 5.2e-2
6 3.2e-3 1.5e-2
8 2.1e-4 3.8e-3
结论: 编码后的系统在相同 SNR 下,误码率明显更低。尤其是在高 SNR 区域,编码带来的增益更加显著。这就是信道编码的价值——用带宽换可靠性。

四、避坑指南与个人经验

做这个项目时,有几个坑我踩过,分享给你:

  • 数据对齐问题: 编码时如果数据长度不是4的倍数,一定要补零。解码后记得截断,否则误码率会虚高。
  • 噪声功率计算: 一定要基于信号的实际功率,而不是假设为1。不同调制方式的信号功率不同。
  • 软判决 vs 硬判决: 汉明码用硬判决就够了,但如果是卷积码或 Turbo 码,软判决能多出2-3dB的增益。
  • 仿真次数: 单次仿真的结果有随机性。我一般会跑10次取平均,这样曲线更平滑。
扩展思考: 如果你把 BPSK 换成 QPSK,或者把汉明码换成 LDPC 码,系统的性能会怎么变化?建议你动手改一改代码,看看结果。这才是学习通信系统最好的方式——亲手调参数,亲眼看到变化。

好了,这个综合项目到这里就结束了。从信源到信宿,我们走完了完整的一圈。你想想看,其实整个通信系统的核心思想就一句话:在不可靠的信道上,实现可靠的传输。编码、调制、解调、解码,所有技术都是围绕这个目标展开的。

希望这三十讲能帮你建立起对信息传导路径的系统认知。以后你在工作中遇到通信相关的问题,至少知道该从哪里入手,该查什么资料。嗯,那就这样吧,祝你在通信的世界里越走越远。


交易系统化学习资料 微信Strategy888888