
简介本资源是一套面向通信工程专业本科生及入门级仿真学习者的数字与模拟调制技术实践代码包聚焦ASK、FSK、PSK、AM、PM、FM六种核心调制方式的原理验证与MATLAB实现。资源共5个.m文件分别对应各调制方式的完整仿真流程——包括载波生成、信号调制、时频域可视化及基础解调逻辑代码简洁规范注释清晰便于理解调制参数设置、波形特征与抗噪特性差异。压缩包仅2KB轻量易用适合作为课程实验补充、课程设计参考或通信原理复习工具。目前已有1591人学习下载读者可直接运行脚本观察不同调制下基带信号、已调信号及频谱图的变化规律快速建立调制类型与性能指标如带宽、功率效率、解调复杂度之间的直观关联为后续深入学习现代通信系统打下坚实基础。 做通信原理课设的时候第一次看到“实现ASK、FSK、PSK、AM、PM、FM六种调制方式的仿真”这个题目估计不少人都跟我当初一样第一反应是去翻MATLAB的modulate函数文档。结果翻完更迷茫——函数确实能直接调但画出来的波形对不对、频谱长什么样、参数为什么这么设完全说不清楚。这次我换了条路用Python把六种调制从消息信号到已调波形完整手写了一遍再做频谱验证、星座图、解调恢复和误码率测试整套代码两百行左右全部跑通。这篇文章就是这次仿真的完整记录包括参数怎么选、代码怎么写、波形怎么验证、以及我踩过的几个坑。适合正在做通信原理仿真实验的在校学生也适合工作中需要快速理解调制原理、又不想被MATLAB工具箱黑盒化的工程师。1. 先理清家族关系六种调制不是六个孤立东西很多教程喜欢把ASK、FSK、PSK、AM、PM、FM六个名词并列摆放好像它们是六个完全不同的问题。但实际做一遍仿真就会明白它们之间的关系不是并列而是有两层逻辑。1.1 模拟调制和数字调制的真正区别AM、PM、FM属于模拟调制消息信号是连续的模拟波形载波的幅度、相位、频率分别随着消息信号连续变化。ASK、FSK、PSK属于数字调制消息信号是离散的0/1比特载波参数不是在连续变化而是在几个离散状态之间跳变。从数学上看数字调制其实是模拟调制的“特例”——把消息信号换成方波而已。ASK本质上就是AM用0/1方波做调制信号FSK就是FM用方波做调制信号PSK就是PM用方波做调制信号。想通这一点代码结构就能大大复用模拟调制写好三个公式数字调制只需要把消息源从正弦波换成比特序列再做少量适配。1.2 从“消息到波形”的两种映射逻辑另一个容易混淆的点是AM/ASK控制的是“发多大”FM/FSK控制的是“发多快”PM/PSK控制的是“从哪个相位开始发”。具体来说AM/ASK载波幅度随消息变化包络形状就是消息波形本身。广播调幅、遥控、光通信常用。FM/FSK载波频率随消息变化波形疏密程度代表消息。调频广播、蓝牙、对讲机常用。PM/PSK载波相位随消息变化波形看起来疏密差不多但过零点位置在偏移。卫星通信、WiFi、蜂窝通信常用。对照这个表格后面写代码时思路会非常清晰调制方式受控的载波参数数字版本典型应用AM幅度ASKOOK中短波广播、遥控FM频率FSK调频广播、蓝牙PM相位PSK卫星通信、WiFi2. 仿真参数怎么定采样率、载波和码元速率的选择做仿真最容易栽的跟头不是公式写错而是参数选得离谱导致波形一片糊然后开始怀疑自己公式抄错了。这里先交代我的参数体系和背后的理由。2.1 为什么用Python而不是MATLABMATLAB的通信工具箱确实提供了modulate、demod这些现成函数但对做原理验证的人来说黑盒化是个大问题——你很难知道函数内部到底做了什么假设。Python NumPy SciPy Matplotlib这套组合免费、开源每个公式都可以直接写出来跑完还能顺手用Matplotlib排版出图。对于要写实验报告、或者想真正搞懂调制原理的人我强烈建议用Python手写一遍公式。2.2 一套能通吃六种调制的基础参数我选参数的原则只有一条既要满足采样定理又要让波形在时域图上能被肉眼直接看懂。很多人喜欢把载波设成1MHz采样率设成10MHz结果画出来的图全是密密麻麻的竖线什么都看不清。我最终定下的参数如下参数取值说明采样率 fs10 kHz远大于最高频率分量留足余量载波频率 fc800 Hz高于消息频率一个数量级以上又远小于fs/2模拟消息频率 fm50 Hz方便在时域看出包络变化总时长 T0.2 s能展示足够多的载波周期码元速率 Rb50 Baud每个码元0.02s总时长内放10个bit每码元采样数 sps200每个bit内足够画清楚波形这里有几个关键比例需要说清楚。第一采样率10kHz而最高信号频率是载波800Hz加上调频产生的扩展大概到1.3kHz左右远远小于奈奎斯特频率5kHz不会混叠。第二800Hz载波对50Hz的消息一个消息周期内有16个载波周期包络效果非常明显。第三对数字调制来说800Hz载波对应50Baud码元速率每个bit内有16个载波周期能清楚看出“键控”的开关效果。参数是这样在代码里落地的import numpy as np import matplotlib.pyplot as plt from scipy.signal import hilbert, butter, lfilter fs 10000 # 采样率 Hz fc 800 # 载波频率 Hz fm 50 # 模拟消息频率 Hz T 0.2 # 总时长 s N int(T * fs) # 总采样点数 t np.arange(N) / fs # 时间向量用arange(N)避免浮点误差3. 先做模拟调制AM、PM、FM的公式落地模拟调制是基础三个公式都标准、直接但真正写代码时各有各的注意点。3.1 AM调幅调制系数是第一个必须搞懂的概念AM的时域表达式是s_AM(t) A_c[1 μ·m(t)]·cos(2πf_c t)其中μ是调幅系数取值在0到1之间。m(t)是归一化后的消息信号最大幅度不超过1。代码实现非常直接mu 0.6 mt 0.8 * np.cos(2 * np.pi * fm * t) # 模拟消息 s_am (1 mu * mt) * np.cos(2 * np.pi * fc * t)这里最值得强调的是μ的作用。包络的表达式是1 μ·m(t)当μ·m(t)的最小值大于-1时包络始终为正幅度信息完整保留。如果μ超过1包络会出现过零和反相这就是“过调幅”后面用包络检波解调时会产生严重失真。实际调试时有一个很实用的技巧直接从波形上量最大幅度A_max和最小幅度A_min然后用公式μ (A_max - A_min) / (A_max A_min)反推调制系数。如果算出来大于1说明过调幅了把μ调小即可。我在实验里用这个办法快速校验了自己生成的波形是否正常。3.2 PM和FM相位与频率是积分关系PM的表达式是s_PM(t) A·cos(2πf_c t K_p·m(t))FM的表达式是s_FM(t) A·cos(2πf_c t 2πK_f·∫m(τ)dτ)注意看FM比PM多了一个积分运算。也就是说FM本质上就是对消息的积分再做相位调制。这个关系我建议刻在脑子里PM直接调相位FM先积分再调相位。PM代码kp np.pi / 4 s_pm np.cos(2 * np.pi * fc * t kp * mt)FM代码里最关键的步骤是用np.cumsum近似积分kf 300 # 频偏常数 phase_fm 2 * np.pi * kf * np.cumsum(mt) / fs s_fm np.cos(2 * np.pi * fc * t phase_fm)这里的cumsum(mt)/fs就是对m(t)从0到t做数值积分。很多人第一次写FM代码时容易忘掉除以fs导致累积相位被放大了几千倍瞬时频率偏得离谱频谱完全乱套。这个坑我在后面“踩坑实录”里会再强调一次。3.3 三种模拟调制的波形怎么看把三个波形画出来对比观察角度完全不同AM看包络。载波的轮廓就是1 μ·m(t)和消息形状一致最直观。FM看疏密。消息幅度大时瞬时频率高波形密集消息幅度小时瞬时频率低波形稀疏。调频广播“抗干扰能力强”的特性就来源于此——幅度噪声很难影响频率信息。PM看相位偏移。肉眼看疏密变化不明显因为PM改变的是相位瞬时频率其实是消息的导数。想看PM的效果最好叠加原始载波对比或者直接用瞬时频率来分析。画图代码建议用下面这种方式六个子图排成一列方便实验报告里直接对比fig, axs plt.subplots(6, 1, figsize(12, 10)) axs[0].plot(t, s_am) axs[0].set_title(AM) # 其余类似... plt.tight_layout()4. 再做数字调制ASK、FSK、PSK的比特到波形数字调制和模拟调制的差别只在消息源一个是连续正弦波一个是0/1的比特序列。所以先要把比特序列变成和t长度一致的波形。4.1 数字基带信号怎么生成先生成10个随机比特再用np.repeat把每个比特扩展成sps个采样点np.random.seed(42) # 固定随机种子保证结果可复现 n_bits 10 Tb 0.02 # 码元时长 s对应50 Baud sps int(Tb * fs) # 每码元采样点数 200 bits np.random.randint(0, 2, n_bits) bit_seq np.repeat(bits, sps) t_d np.arange(len(bit_seq)) / fs # 数字信号的时间向量4.2 三种键控的执行代码ASK最简单就是比特序列直接乘载波s_ask bit_seq * np.cos(2 * np.pi * fc * t_d)比特为1时发完整载波比特为0时幅度为0这种形式也叫OOKOn-Off Keying。FSK用瞬时频率映射。我把两个频率定义为载波加减频偏然后用累积相位法生成这样得到的是相位连续的FSKf_dev 250 f1 fc - f_dev f2 fc f_dev freq_inst np.where(bit_seq 1, f2, f1) phase_fsk 2 * np.pi * np.cumsum(freq_inst) / fs s_fsk np.cos(phase_fsk)PSK用相位翻转BPSK的表达式是s_psk np.cos(2 * np.pi * fc * t_d np.pi * bit_seq)比特为1时相位加π相当于波形反相比特为0时相位不变。等价写法是(1 - 2·bit_seq)·cos(2πf_c t)。4.3 FSK相位连续性问题这里展开讲一下FSK生成方式的选择。很多教材给的FSK定义是两个频率的正弦交替拼接# 不推荐的分段拼接写法 s_fsk_bad np.where(bit_seq 1, np.cos(2*np.pi*f2*t_d), np.cos(2*np.pi*f1*t_d))这种写法在码元边界上会出现相位跳变。表面上看波形也没错但你去做FFT频谱会发现频谱旁瓣明显抬高能量泄漏严重会给后续滤波和解调带来不必要的麻烦。而使用累积相位法生成的相位连续FSK码元切换时相位是平滑过渡的频谱要干净得多。这也是为什么我在代码里选择了cumsum相位累积的方式。为了直观对比我建议你把两种方式都跑一遍对同一个比特序列分别画波形在码元边界处放大看相位跳变的效果非常明显。这个对比本身就是一份很好的实验素材。三种数字调制的代码核心逻辑汇总如下调制关键操作核心代码2ASK幅度乘比特bit_seq * cos(2πfc·t)2FSK频率随比特切换cos(2π·∫(fcΔf·bit)dt)BPSK相位随比特翻转cos(2πfc·t π·bit)5. 频域验证FFT频谱和星座图让效果说话时域波形肉眼看个热闹真正判断调制有没有做对必须看频域。这张“身份证”能告诉你带宽、谱线位置、有没有离散载波等信息。5.1 六种调制频谱各自的特征写一个统一的频谱函数def plot_spectrum(ax, sig, title): N len(sig) freq np.fft.rfftfreq(N, 1 / fs) amp np.abs(np.fft.rfft(sig)) amp amp / np.max(amp) # 归一化 ax.plot(freq, amp) ax.set_title(title) ax.set_xlabel(Frequency (Hz))我用这套函数对六种调制逐一做了频谱分析总结如下AM载波谱线最强两侧各有一条边带位于fc±fm处总带宽2fm100Hz。FM载波两侧出现多条边带谱线间隔等于消息频率50Hz带宽用卡森公式估算B ≈ 2(Δf fm)其中Δf K_f·max|m(t)|。我这里的Δf 300 × 0.8 240Hz理论带宽约580Hz和FFT结果基本吻合。PM谱线分布类似FM但边带幅度由贝塞尔函数决定和调制指数K_p直接相关。ASK和AM结构类似载波谱线存在但因为是随机方波调制边带是连续的sinc形状谱主瓣宽度与码元速率有关。FSK频谱在f1和f2附近各有一个主峰两峰距离为2Δf500Hz。我记得最初看到这个结果时第一反应就是验证两峰间隔是否和设置的频偏一致这是检查FSK参数是否正确的最快方法。BPSK频谱呈sinc²形状没有离散的载波谱线。这是BPSK和ASK在频谱上的核心区别——ASK因为包含0和1的非等幅状态载波分量还在BPSK是等概率的±1双极性信号载波分量被抵消了。5.2 星座图怎么画出来的星座图是数字调制特有的验证手段做法是把已调信号做希尔伯特变换得到解析信号再乘以本地载波的共轭把信号搬移到零频附近得到复基带信号analytic hilbert(s_psk) baseband analytic * np.exp(-1j * 2 * np.pi * fc * t_d) plt.figure() plt.scatter(baseband.real, baseband.imag, s4) plt.axhline(0, colorgray, lw0.5) plt.axvline(0, colorgray, lw0.5) plt.title(BPSK Constellation) plt.gca().set_aspect(equal)BPSK画出来应该是两个聚类点一个在(A, 0)附近一个在(-A, 0)附近对应相位0和π。如果两个点没有聚集在一条直线上而是出现旋转或发散说明载波频率设置或抽样时刻有问题。星座图对后面理解QPSK、QAM这些高阶调制也很有帮助。5.3 从频谱判断参数是否合理这里有个经验性规则如果频谱出现明显的混叠——比如高频分量被折叠到低频区域或者频谱在fs/2附近还有很高的能量——那就是采样率不够需要调高fs。如果载波谱线和边带完全看不清可能是FFT点数太少或没做归一化。另外FFT之前建议不做窗函数处理因为我们的仿真信号是整周期截断的直接FFT结果就足够干净如果是非整周期截断才需要考虑加汉宁窗抑制频谱泄漏。6. 解调回放信号恢复出来才算闭环仿真做到波形生成、频谱验证其实已经能交差了。但我建议多做一步解调因为只有解调成功才说明调制过程没有隐藏错误。而且一旦后面要测误码率解调器是必须有的。6.1 包络检波对付AM和ASKAM的解调用包络检波。希尔伯特变换可以直接提取实信号的包络env_am np.abs(hilbert(s_am)) m_est_am (env_am - 1) / mu提取出来的env_am约等于1 μ·m(t)减去直流分量再除以μ就恢复出了消息。我实测恢复出来的正弦波和原始消息几乎完全重合误差来自数值计算精度肉眼不可见。ASK的包络检波原理相同但最后多一步判决env_ask np.abs(hilbert(s_ask)) bit_energy env_ask.reshape(n_bits, sps).mean(axis1) # 每个码元取平均 bits_ask_est (bit_energy 0.5).astype(int)这里选择了每个码元周期内取平均然后和0.5比较。为什么不直接逐点判决因为载波本身有周期性过零直接逐点判决会在载波谷值附近产生大量抖动误判。先平均再判决等价于在码元周期内做能量积分抗噪能力好得多。6.2 希尔伯特变换解FM和PMFM解调通过瞬时频率恢复消息。具体做法是对FM信号做希尔伯特变换得到解析信号取相位、解缠、求导就得到瞬时频率减去载波频率再除以K_fanalytic_fm hilbert(s_fm) inst_phase np.unwrap(np.angle(analytic_fm)) inst_freq np.diff(inst_phase) * fs / (2 * np.pi) m_est_fm (inst_freq - fc) / kf注意np.diff会让结果比原信号少一个点绘图时要做对齐处理。PM解调同理但少一步求导直接提取相位并减去载波相位分量analytic_pm hilbert(s_pm) phase_pm np.unwrap(np.angle(analytic_pm)) m_est_pm (phase_pm - 2 * np.pi * fc * t) / kpPM解调有一个边界要注意K_p·m(t)的峰值相位偏移如果超过±πunwrap解缠时会产生跳变恢复出来的信号就会多出一个2π的台阶。所以PM仿真的K_p取值要克制我取π/4就很安全。6.3 相关解调处理FSK和PSKFSK用两个相关器做判决。思路很简单分别把接收信号和对应0、1两个频率的本地载波相乘再在每个码元周期内积分哪个输出大就判哪个r1 s_fsk * np.cos(2 * np.pi * f2 * t_d) r0 s_fsk * np.cos(2 * np.pi * f1 * t_d) z1 r1.reshape(n_bits, sps).mean(axis1) z0 r0.reshape(n_bits, sps).mean(axis1) bits_fsk_est (z1 z0).astype(int)PSK相干解调更简单BPSK信号乘以本地同频同相载波后落在正半轴的判为比特1落在负半轴的判为比特0r_psk s_psk * np.cos(2 * np.pi * fc * t_d) z_psk r_psk.reshape(n_bits, sps).mean(axis1) bits_psk_est (z_psk 0).astype(int)这里必须提一句PSK相干解调在实际系统里需要载波同步否则本地载波和接收信号之间存在相位偏差判决结果会整体错误。仿真代码里直接用原始载波是“完美同步”的假设真实工程里要加Costas环或锁相环。这个区别在写实验报告时值得单独说明也是很多面试官喜欢问的点。6.4 加噪声后的简单误码率测试为了进一步验证解调链路的正确性我以BPSK为例加了一组高斯白噪声测试。对每个信噪比生成噪声叠加到BPSK信号上走一遍解调判决统计误码率def ber_bpsk(snr_db, n_trials200): n_bits 1000 sps_local 200 bit_len n_bits * sps_local bits np.random.randint(0, 2, n_bits) bit_seq np.repeat(bits, sps_local) t_local np.arange(bit_len) / fs s np.cos(2*np.pi*fc*t_local np.pi*bit_seq) sigma np.sqrt(np.mean(s**2) / (10**(snr_db/10))) err 0 for _ in range(n_trials): noise sigma * np.random.randn(bit_len) r s noise z (r * np.cos(2*np.pi*fc*t_local)).reshape(n_bits, sps_local).mean(axis1) est (z 0).astype(int) err np.sum(est ! bits) return err / (n_trials * n_bits)理论误码率公式是Q(√(2·Eb/N0))把仿真结果和理论曲线画在一起信噪比在0~10dB范围内应该贴合得很好。如果仿真和理论差很多优先检查噪声功率的计算是不是用了信号总功率而不是每比特能量。7. 踩坑实录做完一遍仿真最容易翻车的细节这一节是我真实踩过的坑汇总。如果你准备照着上面的代码自己跑一遍以下问题大概率会至少遇到一个。7.1 载波频率选太高波形和频谱全废最典型的失败案例是把fc设成100kHzfs设成1MHzT设成1秒。结果时域图画出100000个载波周期屏幕上直接变成一片黑色的实心区域。频谱图也因为频率轴范围太大边带细节全部被压缩成一条线。演示用的仿真参数第一优先级是“能看清”不是“贴近真实射频”这两件事是不同场景下的不同选择。7.2 数组长度对不上t、bit_seq和sps的恩怨这是我见过最多人遇到的问题。np.arange(0, T, 1/fs)生成的数组长度可能不是整数而np.repeat(bits, sps)的长度确定是n_bits×sps。两个数组长度不一致时直接相乘就会报错或者悄悄截断。解决办法是在生成时间向量时统一用np.arange(N)/fs这种形式确保长度严格等于N。数字调制部分单独用t_d np.arange(len(bit_seq))/fs和模拟部分的时间向量分开管理就不会混。7.3 cumsum积分忘除以fsFM的相位累积是2π·K_f·Σm(t)/fs这个fs的分母绝对不能省。我调试时有一次把cumsum(mt)直接当成积分用结果瞬时频率偏移被放大了一万倍画出来的FM信号频谱在0到5kHz之间铺满怎么看怎么不对。检查了半天才想起积分要除以采样间隔。建议把这个除法当成本能反应任何用求和近似积分的代码都要同步除以采样率。7.4 相位unwrap和瞬时频率的边界np.angle返回的相位范围是[-π, π]FM信号的总相位是持续累积的一定会超过这个范围所以必须用np.unwrap。但unwrap也不是万能的如果相邻采样点之间的真实相位变化超过πunwrap会错误地把跳变当成2π翻转结果越解越乱。这要求采样率足够高确保每个采样点之间相位增量远小于π。按照我之前fs10kHz、fc800Hz的参数每个载波周期有12.5个采样点相位增量约0.5弧度很安全。7.5 随机比特不固定导致的结果不可复现做误码率测试时每次运行生成的比特序列都不同结果每次BER曲线都有一点差异。这不是BUG但没有对照价值。调试阶段一定要用np.random.seed固定随机数种子需要对比不同信噪比时应保证同一个比特序列。做完正确性验证后再放开随机种子统计平均性能。7.6 频谱和星座图的细节问题画频谱图时FFT结果幅度很大不归一化的话弱小的边带会被压成一条看不见的直线。另外要使用rfftfreq和rfft取正频率部分直接用fftfreq会把负半轴的频谱也画出来图面多出一倍内容还容易引起混淆。画星座图时记得plt.gca().set_aspect(equal)否则x轴和y轴比例不一致圆形的星座会被拉成椭圆影响判断。最后说点我做完这套仿真的体会。其实六种调制方式分开看都不难难的是把它们放到同一个工程框架里用同一套参数、同一套验证方法去处理。当你做到用频谱、星座图、误码率三个维度去交叉验证一个波形时对调制的理解会从“背公式”变成“有直觉”。这套代码后续还可以往几个方向扩展把BPSK升级成QPSK或QAM加上根升余弦成型滤波器抑制带外泄漏或者给FSK加一个完整的锁相环解调链路。每次扩展都是一次新的踩坑也是把原理吃得更透的机会。本文还有配套的精品资源点击获取