ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python六合一调制仿真:ASK/FSK/PSK/AM/PM/FM从公式到波形与误码率

Python六合一调制仿真:ASK/FSK/PSK/AM/PM/FM从公式到波形与误码率 简介这份资源面向通信原理学习者与数字信号处理入门者聚焦ASK、FSK、PSK、AM、PM、FM六种调制方式的仿真实现帮助读者在MATLAB环境中理解模拟与数字调制的原理差异、波形特征及解调思路。压缩包共5个文件均为.m脚本分别对应六种调制方式的仿真代码整体约2KB体量轻便便于直接运行与逐行研读。目前已有1595人学习下载说明其在通信课程实验与自学场景中具有一定参考价值。读者可借助这些脚本观察载波幅度、频率、相位随基带信号变化的规律对比二进制键控与模拟调制的实现差异并在此基础上修改参数、扩展多进制调制或加入噪声分析为课程设计、实验报告与通信系统仿真练习提供可复用的代码骨架。1. 六种调制仿真从一条公式到六张波形图中间隔着多少坑做通信基带或者射频算法的朋友迟早会撞上同一个需求把 ASK、FSK、PSK、AM、PM、FM 这六种调制方式在同一个框架里仿真出来对比它们的时域波形、频谱和抗噪性能。听起来像是教科书第一章的内容真动手写的时候才发现六种调制背后是两套完全不同的数学体系——线性的幅度调制和非线性的角度调制而 FSK 又卡在中间既不是纯线性也不是纯角度。更麻烦的是很多教程只给公式不给可运行的代码或者只给代码不解释参数怎么影响结果。这篇笔记就按我实际做过的路径从零搭一个能跑通六种调制的仿真框架把每一步的参数含义、常见翻车点和验证方法都摊开讲。适合正在做通信课程设计、基带算法验证或者想用 Python 快速对比调制方案优劣的工程师。2. 六种调制的数学骨架先分清线性与非线性再谈代码2.1 幅度类调制ASK 和 AM 的载波幅度怎么变ASK 和 AM 本质上都是让载波的幅度随调制信号变化区别在于调制信号的类型。ASK 的调制信号是二进制序列只有 0 和 1 两个电平所以载波幅度在两个值之间跳变AM 的调制信号是连续模拟信号比如正弦波或音频载波幅度连续变化。数学上ASK 的表达式可以写成s(t) A * m(t) * cos(2πf_c t)其中 m(t) 是二进制基带信号取值 0 或 1A 是载波幅度f_c 是载波频率。实际仿真时为了让 0 和 1 都有载波输出通常把 m(t) 映射成 -1 和 1或者保留 0 和 1 但加直流偏置。我一般用 0 和 1 直接乘这样 0 码元期间输出为零频谱更干净解调时包络检波也简单。AM 的表达式多了一个直流分量s(t) A * [1 m_a * m(t)] * cos(2πf_c t)这里 m_a 是调幅系数也叫调制深度通常取 0 到 1 之间。m_a 超过 1 就会过调幅包络出现反相用简单的包络检波会严重失真。仿真时我习惯把 m_a 设成 0.8留一点余量波形好看且解调稳定。代码实现上ASK 和 AM 共用一套载波生成逻辑只是基带信号不同。下面这段代码生成两种调制的时域波形import numpy as np import matplotlib.pyplot as plt # 基本参数 fs 10000 # 采样率 T 0.1 # 信号时长 t np.arange(0, T, 1/fs) fc 1000 # 载波频率 fm 50 # 调制信号频率 # 载波 carrier np.cos(2 * np.pi * fc * t) # ASK二进制序列每个码元持续 0.01 秒 bit_rate 100 bits np.random.randint(0, 2, int(bit_rate * T)) bit_duration 1 / bit_rate baseband_ask np.zeros_like(t) for i, b in enumerate(bits): start int(i * bit_duration * fs) end int((i 1) * bit_duration * fs) baseband_ask[start:end] b ask_signal baseband_ask * carrier # AM正弦调制调幅系数 0.8 m_a 0.8 baseband_am np.sin(2 * np.pi * fm * t) am_signal (1 m_a * baseband_am) * carrier # 绘图 fig, axes plt.subplots(2, 1, figsize(10, 6)) axes[0].plot(t[:500], ask_signal[:500]) axes[0].set_title(ASK 波形前 500 点) axes[1].plot(t[:500], am_signal[:500]) axes[1].set_title(AM 波形前 500 点) plt.tight_layout() plt.show()这段代码里fs必须满足奈奎斯特采样定理至少是载波频率的两倍我一般取 10 倍以上波形才平滑。bit_rate决定 ASK 的码元宽度码元越窄占用带宽越宽。AM 的m_a是唯一需要调的参数超过 1 就会看到包络凹陷那是过调幅的标志。2.2 角度类调制PM 和 FM 的相位与频率怎么联动PM 和 FM 都是让载波的角度发生变化PM 改变的是相位FM 改变的是频率。两者在数学上可以互相转换——对调制信号积分再做 PM 就是 FM对调制信号微分再做 FM 就是 PM。仿真时我通常先实现 PM再用积分关系推 FM。PM 的表达式s(t) A * cos(2πf_c t k_p * m(t))k_p 是相位灵敏度单位是弧度每伏。m(t) 的幅度决定相位偏移量k_p 越大相位摆动越剧烈频谱展宽越明显。FM 的表达式s(t) A * cos(2πf_c t 2πk_f * ∫m(τ)dτ)k_f 是频率灵敏度单位是赫兹每伏。积分项在离散仿真里用累加代替累加前要乘以采样间隔 dt否则量纲不对。下面代码实现 PM 和 FM调制信号用同一个正弦波# PM 和 FM k_p 2.0 # 相位灵敏度弧度/伏 k_f 200.0 # 频率灵敏度Hz/伏 # PM pm_signal np.cos(2 * np.pi * fc * t k_p * baseband_am) # FM先积分再调相 integral np.cumsum(baseband_am) / fs fm_signal np.cos(2 * np.pi * fc * t 2 * np.pi * k_f * integral) fig, axes plt.subplots(2, 1, figsize(10, 6)) axes[0].plot(t[:500], pm_signal[:500]) axes[0].set_title(PM 波形前 500 点) axes[1].plot(t[:500], fm_signal[:500]) axes[1].set_title(FM 波形前 500 点) plt.tight_layout() plt.show()这里k_p和k_f是最容易设错的两个参数。k_p 太大相位会绕好几圈波形看起来像噪声k_f 太大瞬时频率会跑到负值虽然数学上没问题但物理上意味着频谱搬移超出了载波范围。我一般先估算最大频偏 Δf k_f * max|m(t)|确保 Δf 远小于 fc否则会出现频谱混叠。2.3 FSK 和 PSK数字角度调制的两种实现路径FSK 和 PSK 是数字调制调制信号是二进制序列。FSK 用两个不同频率的载波分别表示 0 和 1PSK 用两个不同相位的载波表示 0 和 1。FSK 可以看成 FM 的特例PSK 可以看成 PM 的特例但工程实现上它们有独立的解调方法。FSK 的表达式s(t) A * cos(2πf_1 t) 当码元为 1 s(t) A * cos(2πf_2 t) 当码元为 0f_1 和 f_2 的间隔 Δf |f_1 - f_2| 决定抗噪性能。Δf 越大两个频率越容易区分但占用带宽也越宽。常见做法是让 Δf 等于码元速率的一半这样两个频率在一个码元周期内正交。PSK 的表达式s(t) A * cos(2πf_c t φ) 当码元为 1 s(t) A * cos(2πf_c t) 当码元为 0φ 通常取 π也就是 BPSK两个相位相差 180 度。相位差越大解调时判决门限越宽抗噪越好。代码实现# FSK f1 1200 f2 800 fsk_signal np.zeros_like(t) for i, b in enumerate(bits): start int(i * bit_duration * fs) end int((i 1) * bit_duration * fs) freq f1 if b 1 else f2 fsk_signal[start:end] np.cos(2 * np.pi * freq * t[start:end]) # BPSK phase_shift np.pi bpsk_signal np.zeros_like(t) for i, b in enumerate(bits): start int(i * bit_duration * fs) end int((i 1) * bit_duration * fs) phase phase_shift if b 1 else 0 bpsk_signal[start:end] np.cos(2 * np.pi * fc * t[start:end] phase) fig, axes plt.subplots(2, 1, figsize(10, 6)) axes[0].plot(t[:500], fsk_signal[:500]) axes[0].set_title(FSK 波形前 500 点) axes[1].plot(t[:500], bpsk_signal[:500]) axes[1].set_title(BPSK 波形前 500 点) plt.tight_layout() plt.show()FSK 的f1和f2要避开载波频率的整数倍否则频谱会重叠。BPSK 的相位跳变点要精确对齐码元边界否则解调时会出现码间干扰。我一般用np.cos直接算不用查表这样相位连续频谱旁瓣更低。3. 用 Python 搭一套六合一仿真框架参数、频谱与误码率3.1 统一参数空间采样率、载波频率、码元速率怎么配六种调制放在一个框架里参数必须统一否则对比没有意义。我一般固定三个核心参数采样率 fs、载波频率 fc、码元速率 Rb。fs 取 100 kHzfc 取 10 kHzRb 取 1 kbps。这样每个码元有 100 个采样点载波有 10 个周期波形细节足够。对于模拟调制 AM、PM、FM调制信号频率 fm 取 500 Hz比 Rb 低保证模拟信号的连续性。调幅系数 m_a 取 0.8相位灵敏度 k_p 取 2 rad/V频率灵敏度 k_f 取 2 kHz/V。这些值不是随便定的m_a 留余量防过调k_p 和 k_f 保证最大频偏不超过 fc 的 20%。参数表参数符号取值说明采样率fs100 kHz至少 10 倍载波频率载波频率fc10 kHz六种调制共用码元速率Rb1 kbps数字调制用调制信号频率fm500 Hz模拟调制用调幅系数m_a0.8防过调相位灵敏度k_p2 rad/VPM 用频率灵敏度k_f2 kHz/VFM 用FSK 频偏Δf500 Hz码元速率一半这套参数跑出来的波形ASK 和 AM 的包络清晰PM 和 FM 的相位连续FSK 和 PSK 的码元边界锐利。如果换参数比如 fs 降到 20 kHz载波只有 2 个周期波形会失真频谱会出现混叠。3.2 频谱分析FFT 的窗函数与分辨率怎么选六种调制的频谱形状差异很大。ASK 和 AM 是双边带谱载波两侧各有一个边带PM 和 FM 的频谱展宽边带数量取决于调制指数FSK 有两个峰分别对应 f1 和 f2PSK 的频谱主瓣宽旁瓣衰减慢。用 FFT 看频谱窗函数和点数决定分辨率。我一般用汉宁窗点数取 2 的整数次幂比如 8192 或 16384。汉宁窗旁瓣衰减快适合看弱边带矩形窗主瓣窄适合看频率间隔。点数越多频率分辨率越高但计算越慢。代码from scipy.fft import fft, fftfreq from scipy.signal import get_window def plot_spectrum(signal, fs, title): N 8192 window get_window(hann, N) spectrum fft(signal[:N] * window) freq fftfreq(N, 1/fs) magnitude np.abs(spectrum) / N plt.plot(freq[:N//2], magnitude[:N//2]) plt.title(title) plt.xlabel(频率 (Hz)) plt.ylabel(幅度) plt.grid(True) # 对六种信号分别画频谱 signals [ask_signal, fsk_signal, bpsk_signal, am_signal, pm_signal, fm_signal] titles [ASK, FSK, BPSK, AM, PM, FM] fig, axes plt.subplots(3, 2, figsize(12, 10)) for ax, sig, title in zip(axes.flat, signals, titles): plt.sca(ax) plot_spectrum(sig, fs, title) plt.tight_layout() plt.show()这里N取 8192频率分辨率是 fs/N ≈ 12.2 Hz足够分辨 FSK 的 500 Hz 频偏。窗函数用汉宁窗旁瓣抑制在 -31 dB 以下能看到 AM 的边带细节。如果换成矩形窗旁瓣只有 -13 dB弱边带会被淹没。3.3 误码率对比加高斯白噪声后六种调制谁更抗噪误码率是数字调制的核心指标。ASK、FSK、PSK 的误码率公式不同但都可以用蒙特卡洛仿真验证。我一般加高斯白噪声逐步提高信噪比统计误码数。ASK 的误码率近似为 Q(sqrt(SNR/2))FSK 为 Q(sqrt(SNR))BPSK 为 Q(sqrt(2*SNR))。从公式看BPSK 最抗噪ASK 最差。仿真时用相干解调ASK 用包络检波FSK 用两个带通滤波器BPSK 用乘法器加低通滤波。代码from scipy.special import erfc def awgn(signal, snr_db): snr 10 ** (snr_db / 10) power np.mean(signal ** 2) noise_power power / snr noise np.random.normal(0, np.sqrt(noise_power), len(signal)) return signal noise def ber_ask(snr_db): # 简化理论值 snr 10 ** (snr_db / 10) return 0.5 * erfc(np.sqrt(snr / 4)) def ber_bpsk(snr_db): snr 10 ** (snr_db / 10) return 0.5 * erfc(np.sqrt(snr)) snr_range np.arange(0, 12, 1) ber_ask_vals [ber_ask(s) for s in snr_range] ber_bpsk_vals [ber_bpsk(s) for s in snr_range] plt.semilogy(snr_range, ber_ask_vals, o-, labelASK) plt.semilogy(snr_range, ber_bpsk_vals, s-, labelBPSK) plt.xlabel(SNR (dB)) plt.ylabel(误码率) plt.legend() plt.grid(True) plt.show()这段代码只算了理论值实际仿真要在解调后逐位比较。ASK 的误码率在 10 dB 时约 10^-2BPSK 在 10 dB 时约 10^-5差距三个数量级。FSK 介于两者之间非相干解调时比 ASK 好比 BPSK 差。4. 避坑与排查六种调制仿真里最容易翻车的五个地方4.1 采样率不够导致频谱混叠现象ASK 和 AM 的频谱出现对称的镜像峰FSK 的两个频率峰分不开。原因采样率低于载波频率的两倍或者低于最高边带频率的两倍。解决把 fs 提高到 fc 的 10 倍以上或者用带通采样。我一般先算信号的最大频率分量AM 的最大频率是 fc fmPM 和 FM 的最大频率是 fc Δf取其中最大值的 2.5 倍作为 fs。4.2 积分器忘记乘采样间隔现象FM 的波形频率偏移随仿真时间线性增大最后完全失真。原因离散积分用 cumsum 时没有乘 dt导致积分结果量纲错误时间越长误差越大。解决integral np.cumsum(m) / fs或者np.cumsum(m) * dt。这个坑我踩过两次第一次以为是 k_f 设大了调了半天才发现是积分器的问题。4.3 相位不连续导致频谱旁瓣抬高现象BPSK 和 FSK 的频谱旁瓣衰减慢带外泄漏严重。原因码元切换时相位跳变引入了高频分量。解决用连续相位调制比如 CPFSK 或 MSK或者在码元边界做相位平滑。我一般用相位累加器保证每个码元的起始相位等于上一个码元的结束相位。4.4 调幅系数超过 1 导致包络反相现象AM 的包络在调制信号负峰处出现凹陷包络检波后波形失真。原因m_a 1载波幅度在某个时刻变成负值包络不再跟随调制信号。解决把 m_a 降到 0.8 以下或者用同步检波代替包络检波。仿真时可以先画包络看到凹陷就调小 m_a。4.5 FFT 点数太少导致频率分辨率不足现象FSK 的两个频率峰看起来像一个宽峰PM 的边带数不清。原因FFT 点数 N 太小频率分辨率 fs/N 大于频率间隔。解决增大 N 到 8192 或 16384或者用补零的方法提高分辨率。补零不增加信息量但能让频谱曲线更平滑适合做报告插图。5. 进阶技巧用瞬时频率和相位差验证解调结果仿真做完怎么确认六种调制的波形和频谱是对的我一般用两个验证手段瞬时频率和相位差。对 FM 和 FSK瞬时频率应该等于载波频率加上频偏对 PM 和 PSK相位差应该在码元边界跳变。瞬时频率用希尔伯特变换求from scipy.signal import hilbert def instantaneous_frequency(signal, fs): analytic hilbert(signal) phase np.unwrap(np.angle(analytic)) inst_freq np.diff(phase) / (2 * np.pi) * fs return inst_freq # 对 FM 信号求瞬时频率 inst_freq_fm instantaneous_frequency(fm_signal, fs) plt.plot(t[1:1000], inst_freq_fm[:999]) plt.title(FM 瞬时频率) plt.xlabel(时间 (s)) plt.ylabel(频率 (Hz)) plt.grid(True) plt.show()FM 的瞬时频率应该在 fc ± k_f * max|m(t)| 之间摆动如果超出这个范围说明 k_f 设大了。FSK 的瞬时频率应该只有两个值f1 和 f2如果出现中间值说明码元边界有过渡带需要加窗或者用连续相位。相位差用相邻码元的相位差验证def phase_difference(signal, fs, fc, bit_duration): analytic hilbert(signal) phase np.unwrap(np.angle(analytic)) # 去掉载波相位 phase phase - 2 * np.pi * fc * t # 每个码元取中点 diffs [] for i in range(len(bits) - 1): mid1 int((i 0.5) * bit_duration * fs) mid2 int((i 1.5) * bit_duration * fs) diffs.append(phase[mid2] - phase[mid1]) return diffs # 对 BPSK 求相位差 diffs phase_difference(bpsk_signal, fs, fc, bit_duration) plt.stem(diffs[:20]) plt.title(BPSK 相邻码元相位差) plt.xlabel(码元序号) plt.ylabel(相位差 (rad)) plt.grid(True) plt.show()BPSK 的相位差应该只有 0 和 π 两个值如果出现中间值说明相位跳变不干净可能是码元边界对齐有问题。我一般把码元速率设成载波频率的整数分之一这样每个码元包含整数个载波周期相位跳变点正好在过零点。这套验证方法帮我省了很多调试时间。有一次 FSK 的误码率始终降不下来用瞬时频率一看发现 f1 和 f2 的间隔只有 200 Hz比设计的 500 Hz 小了一半原因是码元速率算错了。改过来之后误码率曲线立刻和理论值对上了。做仿真最怕的就是“看起来对”波形画出来像那么回事一算误码率全错。我的习惯是每写一个调制函数先画瞬时频率或相位差确认数学关系对了再往下做解调。这个习惯让我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表