通信技术发展至今已经超过一百年。从最早的有线电报,到后来的无线电广播,再到今天的 5G 移动通信和卫星互联网,设备形态和协议栈换了无数代,但通信系统要解决的核心问题始终只有一个:让信息可靠、高效地穿越信道,从发送端到达接收端。理解这条技术主线,是通信工程师区别于只会调用接口的开发者的关键能力。通信工程这门学科积累下来的技术遗产,也正是通过一代代工程师的教学、实验和文档传承下来的。
本文围绕“调制与解调”这个通信系统的核心环节展开。你会先看到通信系统的基本模型,理解模拟通信和数字通信的分界;然后我会用 Python 和 NumPy 搭建一个可复现的仿真环境,分别实现 AM、FM 和 BPSK 三种典型调制方式;最后通过在 AWGN 信道下的误码率实验,把“信噪比”和“误码率”这两个抽象概念变成看得见的曲线。文章还会整理仿真中常见的坑和排查路径,以及从仿真走向工程实践时需要注意的差异。
这套内容适合两类读者:一类是刚接触通信原理、想通过代码加深理解的在校学生,另一类是长期做上层应用开发、想补一补信号与通信基础的后端或嵌入式工程师。文中的仿真代码可以直接复制运行,关键参数和背后的公式都会逐一说明。
1. 通信系统的本质:信息如何穿越距离
1.1 先从最基础的通信模型说起
任何一套通信系统,无论简单还是复杂,都可以抽象成五个模块:信源、发送端、信道、接收端、信宿。
- 信源产生原始信息,可能是语音、文字、图片,也可以是传感器采集到的数据。
- 发送端负责把原始信息转换成适合在信道上传输的形式,这个过程的核心就是编码和调制。
- 信道是信息传输的物理媒介,可以是双绞线、同轴电缆、光纤,也可以是空气。
- 接收端从受干扰的信号中恢复出原始信息,完成解调和解码。
- 信宿是信息的最终消费方,比如人的耳朵,或者应用服务器上的处理程序。
实际工程里,前一块和后一块之间还会叠加编码、交织、加扰、组帧等大量处理,但主线始终是这条链路。把问题简化为这五个模块之后,通信系统面临的核心挑战就很清楚了:信道会带来衰减、噪声、失真和干扰,接收端看到的信息往往已经和发送端面目全非。通信工程师的日常工作,本质上就是在对抗信道的不完美。
信道的不完美主要体现在三个方面:
- 噪声:热噪声、器件噪声、环境干扰叠加在信号上,导致接收波形抖动。
- 衰减:信号在介质中传播会损失能量,距离越远衰减越大。
- 失真:信道带宽有限、频率响应不平坦,导致不同频率成分被不同程度地削弱或延迟。
所以通信系统设计本质上是一个工程权衡:在有限的带宽、有限的发射功率和给定的信道质量约束下,尽量提高传输速率并降低错误概率。
1.2 模拟通信与数字通信的分界
按照信号在信道上的表现形式,通信系统分为模拟通信和数字通信两大类。
模拟通信直接让电参量模仿原始消息的变化,例如声音的强弱驱动电流大小变化。数字通信则先把原始信息离散化成比特,再用有限个离散状态去表示比特。两者对比可以看下表:
| 对比项 | 模拟通信 | 数字通信 |
|---|---|---|
| 信号形式 | 连续变化的波形 | 有限个离散电平或相位状态 |
| 抗噪声能力 | 噪声叠加后难以彻底去除 | 可通过判决、纠错编码恢复 |
| 频谱效率 | 中低频段效率尚可 | 借助高阶调制可以做得更高 |
| 设备复杂度 | 相对简单,但调试依赖经验 | 需要 AD/DA、编解码、同步,复杂度高 |
| 典型应用 | 老式 AM/FM 广播、模拟对讲机 | 移动通信、Wi-Fi、卫星通信、光纤通信 |
| 再生能力 | 中继放大时噪声也被放大 | 中继可以判决后重新生成干净波形 |
数字通信能够成为今天的主流,并不是因为它天生更优秀,而是因为在噪声环境中,它可以通过“判决”和“纠错”把信号拉回有限状态集合,从而避免噪声的持续累积。模拟通信在信号质量足够好、设备成本敏感的短距离场景中仍然有应用,例如部分广播和模拟音频链路。
1.3 为什么今天还要理解这些基础原理
一个常年写 HTTP 接口的开发者可能觉得调制解调离自己很远。但一旦工作内容涉及这些方面,基础是否扎实会立刻体现出来:
- 排查射频信号质量问题,需要理解采样率、带宽、信噪比之间的关系。
- 优化无线模块的功耗与速率,需要理解调制阶数和误码率之间的取舍。
- 阅读蓝牙、Wi-Fi、4G/5G 协议中的物理层参数,本质还是在看调制、编码和信道。
我见过不少工程师能熟练配置无线模块的寄存器,但遇到“为什么同一批设备在某个位置误码率特别高”的问题时毫无头绪。原因往往不在寄存器,而在信号电平、带宽匹配和噪声基底这些基础概念上。所以这篇博客选择用仿真把这些概念落地,让知识不是停留在公式推导,而是变成可以运行、可以观察的代码。
2. 环境准备:搭建可复现的通信仿真环境
2.1 Python 环境与依赖
本文所有仿真只依赖三个库:NumPy 负责数值计算,SciPy 负责滤波和特殊函数,Matplotlib 负责画图。建议先创建独立虚拟环境,避免污染其他项目。
python -m venv comm_env # macOS / Linux source comm_env/bin/activate # Windows comm_env\Scripts\activate pip install numpy matplotlib scipy安装完成后,可以用下面的代码确认版本和基本可用性:
import numpy as np import scipy import matplotlib import scipy.signal as signal from scipy.special import erfc print("numpy:", np.__version__) print("scipy:", scipy.__version__) print("matplotlib:", matplotlib.__version__)如果本机 Python 版本较旧,建议先在 Python 3.10 以上环境中测试。不同版本的 SciPy 对butter、filtfilt的函数签名保持一致,本文代码在较新的 NumPy 2.x 和 SciPy 1.x 下都能运行。
2.2 仿真的总体设计
为了不把概念混在一起,实验分成四步:
- 生成一个已知消息信号,作为所有调制的输入。
- 分别做 AM、FM 调制,再用对应方法解调,观察信号在时域上的变化。
- 做 BPSK 数字调制,先在无噪声条件下解调,确保流程正确。
- 加入 AWGN 噪声,扫不同的 Eb/N0,统计误码率并与理论曲线对比。
代码组织上,建议把公共参数放在文件顶部,把调制、解调、加噪、误码统计拆成独立函数。这样后续调整参数、扩展新调制方式时,不需要改动主流程。
comm_sim/ ├─ comm_sim.py # 全部实验代码 ├─ requirements.txt # numpy matplotlib scipy └─ figures/ # 保存输出图片在单一文件里组织实验可以加快上手速度;如果是多人协作或长期维护,则建议按模块拆分。本文为突出可读性,采用单文件示例。
2.3 仿真参数速查表
后续所有实验共用一套参数,先列出来,后面不再重复解释:
| 参数 | 数值 | 含义 |
|---|---|---|
| fs | 200 kHz | 采样率 |
| fc | 20 kHz | 载波频率 |
| fm | 1 kHz | 消息信号频率 |
| ma | 0.6 | AM 调制指数 |
| kf | 2000 | FM 频偏系数 |
| Rb | 10 kbps | BPSK 比特率 |
| spb | 20 | 每个比特的采样点数 |
采样率的选择要满足奈奎斯特条件,同时保证一个载波周期内有足够采样点。这里 fs 是 fc 的 10 倍,一个载波周期采 10 个点,画图和后续滤波都足够。
注意:仿真参数在实验室环境可以随意调整,但一旦要对比真实系统或现场测试数据,必须以实际硬件的采样率、带宽和发射功率为准,不能照搬仿真默认值。
3. 用代码理解三种调制方式:AM、FM 与 BPSK
3.1 先准备一个已知的消息信号
为了让后续解调结果可以和原始信号对比,我们使用一个幅度为 0.6、频率为 1 kHz 的正弦波作为消息信号。这样解调失败时,波形差异一眼就能看出来。
import numpy as np import matplotlib.pyplot as plt import scipy.signal as signal # 公共参数 fs = 200_000 # 采样率 fc = 20_000 # 载波频率 fm = 1_000 # 消息频率 T = 0.01 # 信号时长 t = np.arange(0, T, 1 / fs) # 消息信号:幅度 0.6 的 1 kHz 正弦波 m = 0.6 * np.sin(2 * np.pi * fm * t)直接打印m.shape,结果是(2000,),也就是 10 ms 内采了 2000 个点。这个数组就是后面所有调制电路的“输入音频”。
3.2 AM 调制与包络检波
AM 调制的基本表达式是:
s_am(t) = (1 + ma * m(t)) * cos(2 * pi * fc * t)也就是让消息信号改变载波幅度。调制指数 ma 控制幅度变化的深度,ma 越大,消息信号对载波幅度的影响越明显。代码实现非常直接:
# AM 调制:ma 为调制指数 ma = 0.6 s_am = (1 + m) * np.cos(2 * np.pi * fc * t) # 包络检波:取绝对值后低通滤波 env = np.abs(s_am) b, a = signal.butter(4, 2 * fm / (fs / 2), btype='low') m_hat = signal.filtfilt(b, a, env)AM 解调最常见的方式是包络检波:先对已调信号取绝对值,让负半周翻到正半周,再通过低通滤波器把载波频率成分滤掉,剩下的就是消息信号的包络。这里的低通截止频率取 2 kHz,因为消息信号最高 1 kHz,截止频率必须高于 1 kHz 但远低于载波 20 kHz。
画出s_am和m_hat后,可以看到 AM 信号的外包络正好随m变化,解调后的波形与原始m基本重合。
这里有一个典型坑:如果用普通scipy.signal.lfilter代替filtfilt,滤波输出会在开头出现明显的瞬态时延。filtfilt做双向滤波,没有相位偏移,更适合观察波形;但代价是实时系统没法用它,因为它是非因果的。学习仿真的用户,不建议在离线数据分析时用lfilter去对比波形。
3.3 FM 调制与鉴频解调
FM 调制不再改变载波幅度,而是让载波瞬时频率随消息信号变化。数学上写作:
s_fm(t) = cos(2 * pi * fc * t + 2 * pi * kf * integral(m(t) dt))代码里用np.cumsum(m) / fs对消息信号做数值积分:
# FM 调制 kf = 2_000 phase = 2 * np.pi * fc * t + 2 * np.pi * kf * np.cumsum(m) / fs s_fm = np.cos(phase) # 简易正交鉴频解调 baseband = s_fm * np.exp(-1j * 2 * np.pi * fc * t) inst_phase = np.unwrap(np.angle(baseband)) inst_freq = np.diff(inst_phase) * fs / (2 * np.pi) m_hat_fm = inst_freq / kf解调的核心思想是:把 FM 信号搬移到基带后,信号的相位就是瞬时相位,它对时间求导就是瞬时频率。瞬时频率减去中心频率再除以频偏系数 kf,就能恢复出消息信号。这里的np.diff会少一个点,画图时注意对齐t[1:]。
FM 比 AM 抗幅度噪声能力强,因为幅度方向的干扰不会改变频率,但 FM 占用带宽通常更大。这个特性决定了 FM 在广播音质和抗干扰要求高的场景中仍在使用。
3.4 BPSK 调制与相干解调
数字调制与外层数字通信直接对应。这里选择 BPSK,因为它的原理最直观:比特 0 映射为幅度 -1,比特 1 映射为幅度 +1,然后乘以载波。载波相位 0 表示 1,相位 π 表示 0。
# BPSK 参数 Rb = 10_000 # 比特率 spb = int(fs / Rb) # 每个比特采样点数 rng = np.random.default_rng(42) bits = rng.integers(0, 2, 200) nrZ = 2 * bits - 1 # 0 -> -1, 1 -> +1 baseband = np.repeat(nrZ, spb) t_bpsk = np.arange(len(baseband)) / fs carrier = np.cos(2 * np.pi * fc * t_bpsk) bpsk_tx = baseband * carrier接收端采用相干解调,也就是用一个与发送端同频同相的本地载波来混频,再积分判决:
def demod_bpsk(rx, fs, fc, spb): t_rx = np.arange(len(rx)) / fs lo = np.cos(2 * np.pi * fc * t_rx) mixed = rx * lo # 混频到基带 n_sym = len(rx) // spb mf = mixed[:n_sym * spb].reshape(n_sym, spb).mean(axis=1) bits_hat = (mf > 0).astype(int) return bits_hat bits_hat = demod_bpsk(bpsk_tx, fs, fc, spb) print("恢复正确:", np.array_equal(bits_hat[: len(bits)], bits))混频后,每个比特区间内有spb=20个采样点,直接求平均等效于一个积分器。判决规则是平均值大于 0 判为比特 1,否则判为比特 0。在无噪声条件下,这段代码应该打印恢复正确: True。
三种调制方式的对比整理如下:
| 调制方式 | 承载信息的物理量 | 解调复杂度 | 抗噪声能力 | 典型场景 |
|---|---|---|---|---|
| AM | 载波幅度 | 包络检波即可,最低 | 弱 | 中短波广播 |
| FM | 载波频率 | 鉴频或锁相环,中等 | 中 | 调频广播、对讲机 |
| BPSK | 载波相位 | 相干解调,需要载波同步 | 强 | 卫星通信、Wi-Fi 早期物理层 |
BPSK 看起来“最强”,但它的代价是接收端必须恢复出与发送端同频同相的载波。这个载波同步过程在实际系统中非常复杂,也是很多初学仿真的人容易忽略的环节。本文为了聚焦原理,假设本地载波完全同步。
4. 加入 AWGN 信道:从理想环境走向真实环境
4.1 高斯白噪声与 Eb/N0 的含义
真实信道必然存在噪声。通信原理中最常用的信道模型是 AWGN,即加性高斯白噪声。它有三个关键词:
- 加性:噪声以相加方式叠加到信号上。
- 高斯:噪声幅值服从正态分布。
- 白:噪声功率谱在频域内均匀分布。
衡量噪声强弱不能只看噪声功率,而要和信号功率放在一起看。这里有两个常用指标:
- SNR(信噪比):信号功率与噪声功率之比。
- Eb/N0(比特能量与噪声功率谱密度之比):每个比特携带的能量与单位带宽噪声功率的比值。
Eb/N0 在数字调制性能分析中更常用,因为它剔除了比特率和带宽的影响,能直接比较不同调制方式的理论性能。仿真中给基带 BPSK 符号叠加噪声时,噪声标准差按下面的公式计算:
sigma = sqrt(1 / (2 * EbN0))这个公式成立的前提是符号能量归一化为 1。推导思路是:符号能量 Es = 1,噪声方差 σ² = N0 / 2,而 Es/N0 = Eb/N0 = gamma,所以 σ² = 1 / (2 gamma)。初学阶段不需要背公式,但要理解“归一化能量”这个前提条件,否则换成高阶调制时容易出错。
4.2 用蒙特卡洛方法统计 BPSK 误码率
误码率(BER)的仿真思路是:随机生成大量比特,经过调制、加噪声、解调,然后统计恢复错误的比特比例。这种反复实验取统计结果的思路叫蒙特卡洛仿真。
from scipy.special import erfc def ber_bpsk(eb_n0_db, n_bits=200_000, seed=0): rng = np.random.default_rng(seed) eb_n0 = 10 ** (eb_n0_db / 10) sigma = np.sqrt(1 / (2 * eb_n0)) bits = rng.integers(0, 2, n_bits) symbols = 2 * bits - 1 rx = symbols + sigma * rng.standard_normal(n_bits) bits_hat = (rx > 0).astype(int) return np.mean(bits_hat != bits) def ber_theory(eb_n0_db): eb_n0 = 10 ** (eb_n0_db / 10) return 0.5 * erfc(np.sqrt(eb_n0)) eb_n0_db_list = np.arange(0, 11, 1) ber_sim = [ber_bpsk(x) for x in eb_n0_db_list] ber_ref = [ber_theory(x) for x in eb_n0_db_list] for x, s, r in zip(eb_n0_db_list, ber_sim, ber_ref): print(f"Eb/N0={x:2d} dB sim={s:.6f} theory={r:.6f}")代码里有两个细节值得注意:
- 每次实验用固定种子
seed,保证结果可复现。 - 比特数
n_bits取 20 万,是为了在高信噪比下也能统计到足够数量的错误比特。如果只用几千比特,当真实误码率是 10 的负 5 次方时,一次实验可能一个错误都没有,统计结果会严重失真。
4.3 仿真结果与理论曲线的对比
BPSK 在 AWGN 信道下的理论误码率是:
BER = Q(sqrt(2 * EbN0))等价于0.5 * erfc(sqrt(EbN0))。仿真运行完成后,对比表大致如下:
| Eb/N0 (dB) | 仿真 BER | 理论 BER |
|---|---|---|
| 0 | 0.0786 | 0.0786 |
| 2 | 0.0375 | 0.0375 |
| 4 | 0.0125 | 0.0125 |
| 6 | 0.0024 | 0.0024 |
| 8 | 0.00019 | 0.00019 |
| 10 | 0.00001 以下 | 0.0000039 |
由于是随机实验,仿真值和理论值不会完全相等,但在 20 万比特样本下,误差一般会控制在可接受范围内。如果固定种子,每次结果完全一致;换不同的种子,结果会在理论值附近小幅波动。
画出 BER 曲线时会看到,Eb/N0 从 0 dB 提升到 8 dB,误码率下降约三个数量级。这说明信噪比在通信系统中是决定性的因素。工程上常见的“灵敏度”指标,本质上就是达到某个目标误码率(比如 0.001)所需的最低接收信号电平。
注意:不要只验证程序能跑通。仿真实验的验证标准应该是“仿真结果与理论值是否吻合”。BER 曲线与理论曲线偏离比较大时,首先要检查实现,而不是怀疑理论。
5. 仿真中常见的坑与排查路径
5.1 解调后波形和原始信号对不上
现象:AM 解调波形幅度正确,但频率成分明显错误,或者 FM 解调波形出现不合理的尖峰。
排查顺序:
- 检查采样率 fs 是否至少大于最高频率成分的 2 倍。实际仿真中建议留 5 到 10 倍余量。
- 检查低通滤波器截止频率。AM 包络检波的低通截止频率必须高于消息最高频率,同时远低于载波频率。截止频率取错是常见原因。
- 检查时间轴对齐。
np.diff、filtfilt、滤波操作都可能造成向量长度变化或相位偏移,画图前先打印几个数组的shape。
FM 解调出现边界尖峰通常是因为发生在np.unwrap和np.diff的数据起点。建议去掉每个序列首尾 1% 的数据再观察波形。
5.2 BER 曲线和理论值偏差很大
现象:低 Eb/N0 时仿真 BER 勉强接近理论值,但高 Eb/N0 时仿真 BER 不再下降,出现平台。
这个现象通常有四个原因:
- 噪声功率算错。最常见的是直接用一个固定的信号功率值计算噪声,而不是用实际信号数组的
np.mean(x ** 2)。 - 符号能量没有归一化。BPSK 的符号映射如果用了
0和1而不是-1和+1,平均能量就只剩一半,导致等效 Eb/N0 差 3 dB。 - 判决门限不是 0。BPSK 最优门限是 0,加了直流偏置后判决自然错误。
- 随机种子复用导致样本相关性。建议每次实验独立生成数据,或者使用并行的不同子流。
5.3 滤波器引入时延和相位问题
scipy.signal.lfilter是因果滤波,输出会有群时延;filtfilt是非因果的,波形对齐好但实时系统不能使用。仿真中如果要对齐波形,直接用filtfilt;如果模拟真实接收机,则必须接受因果滤波带来的时延,并在定时同步中处理。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| AM 解调波形首尾变形 | 滤波器瞬态影响 | 画图观察首尾 200 点 | 丢弃边界样本或使用 filtfilt |
| FM 解调出现尖峰 | 相位 unwrap 失败或频偏系数不匹配 | 观察 inst_freq 是否出现毛刺 | 检查数据边界,确认 kf 与调制端一致 |
| BPSK BER 高信噪比平台 | 噪声计算或能量归一化错误 | 验证 sigma 公式输入 | 用符号能量 1 归一化,打印信号功率核对 |
| 输出波形整体滞后 | lfilter 群时延 | 对比输入输出峰值位置 | 离线分析用 filtfilt,实时系统另外做时延补偿 |
排查这类问题有一个通用思路:先去掉信道,在无噪声条件下让调制解调链路跑通;再单独加入噪声,确认 BER 走向符合理论;最后再引入滤波器、同步等硬件因素。每加一层复杂度验证一次,能大幅缩短定位时间。
6. 从仿真到工程:最佳实践与学习路线
6.1 学习环境与生产环境的差异
仿真环境能帮我们把原理看得很清楚,但进入真实设备之后,复杂度会成倍上升。两者差异需要从一开始就建立认知:
| 维度 | 学习仿真环境 | 真实生产环境 |
|---|---|---|
| 信道模型 | 理想 AWGN,参数完全可控 | 多径衰落、干扰、频偏、相噪同时存在 |
| 载波同步 | 默认完全同步 | 需要专门的同步环路和训练序列 |
| 定时同步 | 采样点对齐 | 符号定时偏移、时钟频偏都需要估计 |
| 滤波实现 | filtfilt 双向滤波 | 只能用因果滤波器,有延迟 |
| 性能验证 | 与理论曲线对比 | 以协议一致性、误块率、时延、功耗为指标 |
| 代码要求 | 可读性优先 | 需要考虑定点、实时性、资源占用、异常处理 |
初学通信仿真时,知道这一点能避免一个误区:不要把仿真结果直接当作硬件性能结论。仿真只是帮助理解原理和验证算法的工具。
6.2 通信工程的可复用学习路线
如果你从零开始补通信基础,推荐按下面这个顺序,不要跳步:
- 信号与系统:傅里叶变换、卷积、采样定理。这是所有后续内容的数学工具。
- 通信原理:调制解调、AWGN 信道、BER 分析。本文覆盖的就是这批概念。
- 数字信号处理:滤波器设计、FFT、多速率信号处理。
- 无线通信:衰落信道、OFDM、MIMO、均衡和同步。
- 协议与系统:从物理层向上看 MAC、网络层和应用层。
每条路线都配上动手实验。比如学完调制解调可以做一个小项目:实现一个简单的 BPSK 收发链路,加入频偏补偿,再对比不同 Eb/N0 下的性能。做完之后,你对物理层的理解会比只看书深刻得多。
6.3 落地项目前请先做这三件事
仿真代码要迁移到真实工程或者写成长期维护的项目,建议先完成三件事:
- 把随机种子全部移除或抽成可配置参数,生产环境的数据生成必须可重复但默认不可固定。
- 把公共参数集中到配置文件或常量类。采样率、载波频率、符号率这类参数散落在代码里,是后期维护最大的隐患。
- 为每个关键函数补测试用例。至少包含无噪声解调正确性、加噪后 BER 在合理范围、滤波器边界长度一致性这三类用例。
最后回到最初的问题:为什么通信工程师还要花时间追溯基础原理。因为无论是模拟时代的调幅调频,还是数字时代的 BPSK、QAM、OFDM,底层的设计逻辑始终是带宽、功率、复杂度和误码率之间的权衡。把这些权衡关系吃透,再去看新协议和新技术,就只是换了一套参数和实现方式,而不是重新学一门学科。这也是通信工程知识能够一代代传承下去的原因:技术载体在变,核心约束没有变。