第三十讲:综合项目——点对点通信系统完整链路仿真
好,终于到了最后一讲。说实话,我写到这里还挺感慨的。前面二十九讲,我们从信息论的基础,一路走到编码、调制、信道、解调、解码,每一步都拆开揉碎了讲。现在,是时候把这些零件拼起来,跑一个完整的点对点通信系统了。
这个项目,说白了就是模拟一个最简单的“发消息”过程。从信源产生数据,到信宿正确接收,中间经过编码、调制、传输、解调、解码五个环节。我会用 Python 代码把整个链路串起来,让你亲眼看到比特是怎么飞过去的。
一、系统架构总览
先画一张图,把整个流程看清楚。我个人习惯在做任何系统之前,先画一张架构图,哪怕只是给自己看。这张图能帮你避免“写到一半发现漏了环节”的尴尬。
你看,这个流程其实不复杂。信源产生一串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
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 |
四、避坑指南与个人经验
做这个项目时,有几个坑我踩过,分享给你:
- 数据对齐问题: 编码时如果数据长度不是4的倍数,一定要补零。解码后记得截断,否则误码率会虚高。
- 噪声功率计算: 一定要基于信号的实际功率,而不是假设为1。不同调制方式的信号功率不同。
- 软判决 vs 硬判决: 汉明码用硬判决就够了,但如果是卷积码或 Turbo 码,软判决能多出2-3dB的增益。
- 仿真次数: 单次仿真的结果有随机性。我一般会跑10次取平均,这样曲线更平滑。
好了,这个综合项目到这里就结束了。从信源到信宿,我们走完了完整的一圈。你想想看,其实整个通信系统的核心思想就一句话:在不可靠的信道上,实现可靠的传输。编码、调制、解调、解码,所有技术都是围绕这个目标展开的。
希望这三十讲能帮你建立起对信息传导路径的系统认知。以后你在工作中遇到通信相关的问题,至少知道该从哪里入手,该查什么资料。嗯,那就这样吧,祝你在通信的世界里越走越远。