ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

信号与系统:用Python掌握频谱分析、调制、滤波器与采样

信号与系统:用Python掌握频谱分析、调制、滤波器与采样 信号与系统里最值得先花时间搞懂的其实是频域分析。频谱分析不是画一张“看起来很高端”的图而是把时间波形拆解成不同频率成分的组合。后面要学的调制、滤波器、采样本质上都在做同一件事在频域里观察、搬移、选择或判断。这篇博文更像一份可复现笔记我把频谱分析、频域分析、调制、滤波器、采样这几个概念串成一条链路按从简到繁的顺序给出实验设计、代码示例、参数解释和运行中容易踩的坑。适合正在学信号与系统课程、或者想用 Python 做课程实验的同学。我建议先把整条链路拆成几个独立的小实验先生成一个已知信号做频谱分析再观察调制后频谱如何搬移再用滤波器切出目标频带最后验证采样率不够时会发生什么。这样每个环节的输入、输出和判断标准都很明确比直接硬啃公式有效得多。1. 先建立一条主线频谱分析到底在分析什么1.1 为什么频谱分析是信号与系统里的枢纽很多同学刚开始学时只看时域波形正弦波是一个上下摆动的曲线调幅波是包络变化的波形滤波后是变平滑的信号。这种视角没有错但遇到复杂信号时时域波形基本看不出有效信息。比如两路信号混在一起时域上就是一条打乱的曲线很难判断里面包含哪些频率成分。频谱分析把问题转换到频域横轴变成频率纵轴变成该频率上的幅度或能量。这样一来信号里有哪些频率、哪个频率占主导、哪些频率需要保留或滤除都能看得非常清楚。调制、滤波器、采样这三个经典主题本质上都围绕这个视角展开。调制是把频谱搬到另一个位置滤波器是从频谱中保留或删除某些区域采样则在频域里引入了周期性延拓。理解了频谱这几个概念就不再是孤立章节。1.2 时域波形和频谱图怎么对应最容易理解的方式是从正弦叠加开始。假设信号是 50Hz 和 120Hz 两个正弦波的叠加时域上你会看到一个不再规则的波形因为它由两个不同频率、不同幅度的正弦成分组成。转到频域后图里会在 50Hz 和 120Hz 附近出现两个峰值峰值高度和原来正弦波的幅度成正比。这叫“频谱图中每个峰值对应一个正弦分量”。实际使用时还要注意区分几个术语幅度谱、相位谱、双边谱、单边谱。幅度谱是最常用的横轴频率纵轴幅度相位谱用来描述每个频率成分的相位偏移很多场景里不关心双边谱会同时画出正频率和负频率工程上更习惯看单边谱因为负频率通常只是数学对称。第一节课实验不必追求复杂。我一般会用两个正弦叠加先让代码跑通再去看峰值位置和幅度是否和输入一致。这一步验证通过后面所有实验才可信。1.3 傅里叶变换家族怎么选FT、FS、DTFT、DFT很多资料一上来就堆公式容易让人混乱。其实只需要把适用对象分清楚连续周期信号用傅里叶级数频域是离散谱线。连续非周期信号用傅里叶变换频域是连续谱。离散非周期信号用离散时间傅里叶变换频域是连续周期函数。有限长离散序列用离散傅里叶变换计算机可以直接计算。Python 里的fft函数实际计算的是离散傅里叶变换也是数字信号处理里最重要的工具。它做的事是把一段有限长离散序列拆成一组正弦分量。我们不需要手动写 FFT 算法但必须理解它的输入参数和输出含义输入是采样后的离散信号输出是复数数组取模就是幅度谱取角度就是相位谱。注意计算机只能处理离散点不能直接处理连续时间信号。所以任何频谱分析实验里第一步都是把连续信号用离散采样点表示。2. 动手前的准备工具、环境和最小实验脚本2.1 Python 生态还是 MATLAB信号与系统课程里MATLAB 和 Simulink 很常见如果你有学校授权直接用 MATLAB 没问题。不过我更推荐 Python 验证这些理论理由有三安装免费、脚本清晰、后续做机器学习或其他任务也能复用。Python 核心依赖是三件套NumPy 负责数组计算和 FFTSciPy 提供滤波器设计函数Matplotlib 负责画图。这三个库覆盖了本文全部实验。如果只是做课程作业不需要安装大型软件。建议在本地装一个 Python 环境运行 Jupyter Notebook 或 VS Code 都可以。在线环境也能跑但本地环境在调试代码、保存实验记录时更方便。2.2 依赖安装安装命令很简单pip install numpy scipy matplotlib如果下载速度偏慢可以使用学校或企业提供的镜像源这一点不固定以你本地网络条件为准。安装完成后可以用一个最小脚本验证环境是否正常import numpy as np import matplotlib.pyplot as plt from scipy import signal print(numpy:, np.__version__) print(scipy:, signal.__version__ if hasattr(signal, __version__) else scipy ok)能正常打印版本号环境就基本可用。实测时要注意不同版本下个别函数参数可能有细微变化但本文涉及的fft、butter、filtfilt都是稳定接口。2.3 生成第一个信号并画图最简单的实验是生成两路正弦叠加信号采样率设成 1000Hz时长 1 秒频率分别取 50Hz 和 120Hz。import numpy as np import matplotlib.pyplot as plt fs 1000 # 采样率 1000Hz T 1 # 时长 1s t np.arange(0, T, 1/fs) # 时间序列 f1, f2 50, 120 x np.sin(2 * np.pi * f1 * t) 0.5 * np.sin(2 * np.pi * f2 * t) plt.figure(figsize(10, 4)) plt.plot(t[:500], x[:500]) # 只画前 0.5 秒 plt.xlabel(时间 (s)) plt.ylabel(幅度) plt.title(时域波形) plt.grid(True) plt.show()先只看前 500 个点避免整幅图挤在一起看不清。运行正常后下一步才做频谱分析。3. 第一次频域实验从时域波形到频谱图3.1 用 FFT 计算频谱频谱分析的核心代码不复杂但很容易写错。完整流程如下from scipy.fft import fft, fftfreq N len(x) X fft(x) # 复数结果 freqs fftfreq(N, 1/fs) # 对应的频率轴 # 只取单边谱正频率部分 half N // 2 mag np.abs(X[:half]) * 2 / N # 幅度归一化 freqs_half freqs[:half] plt.figure(figsize(10, 4)) plt.plot(freqs_half, mag) plt.xlabel(频率 (Hz)) plt.ylabel(幅度) plt.title(幅度谱单边) plt.xlim(0, 200) plt.grid(True) plt.show()这段代码里有几个关键点必须说清楚。3.2 FFT 参数采样率、序列长度、频谱分辨率第一个参数1/fs是采样间隔fftfreq需要知道这个间隔才能生成正确的横轴。如果写错整个横轴频率就会偏移峰值位置对不上。第二个关键点是序列长度N。频率分辨率约等于fs / N也就是频谱图上两个频率点之间的间隔。比如fs1000HzN1000点时两个频点间隔是 1Hz如果两个输入频率相差只有 0.5Hz1Hz 的分辨率就无法把它们分开频谱图上会合成一个峰。要改善分辨率正确做法是增加信号时长而不是盲目把 FFT 点数填大。有人会直接给fft(x, n8192)以为点数多了分辨率就高。实际只是把原样数据插值到更多频点看起来更平滑但真实分辨率仍由采样时长决定。这个点很多资料没讲透实际测试时很容易被“高分辨率假象”误导。第三个关键点是归一化。fft的原始幅度和输入信号幅度差一个倍数。按abs(X) * 2 / N处理后单边谱的峰值才会和原始正弦波幅度对应。不做这步你会看到幅度值非常离谱但峰值位置仍然是准确的。3.3 怎么判断频谱图是否可信频谱图画出来后不要只看“有峰”要看峰值是否符合预期输入 50Hz 正弦幅度为 1频谱图中 50Hz 处幅度应接近 1。输入 120Hz 正弦幅度为 0.5频谱图中 120Hz 处应接近 0.5。其他地方应接近 0没有明显隆起或拖尾。如果峰值位置和输入频率完全对不上优先检查采样率和fftfreq参数。如果频率对、幅度不对检查归一化系数。如果整条谱线出现很多毛刺可能是信号长度不是整数个周期或者没有加窗。周期截断会导致频谱泄漏会在峰值附近出现旁瓣。对单次实验来说可以选一段完整周期数来验证对更复杂的信号通常会加汉宁窗或汉明窗来降低泄漏。经验第一次做 FFT 时不要同时处理复杂信号。先用两个正弦叠加确认峰值频率和幅度都正确再逐步增加信号复杂度。4. 调制与解调中的频谱变化频率搬移怎么观察4.1 调幅信号生成与可见频谱调制在通信系统里很常见目的之一是把低频信号搬到高频载波上方便天线发射或频分复用。调制后频谱会发生明显搬移用频谱图可以很直观地观察。以常规调幅为例基带信号是低频正弦载波是更高频正弦f_base 5 # 基带信号频率 5Hz f_carrier 500 # 载波频率 500Hz t np.arange(0, 1, 1/fs) base np.cos(2 * np.pi * f_base * t) carrier np.cos(2 * np.pi * f_carrier * t) m 0.5 x_am (1 m * base) * carrier这里m是调制指数控制包络变化深度。调制后信号的频谱不再只在 500Hz 处一条谱线而是在 500Hz 两侧各多出一条边带分别在 495Hz 和 505Hz 处。原因是时域相乘在频域等效为卷积两个频率分量会组合出“和频”和“差频”。4.2 从频谱图判断调制参数是否合理如果调制指数m小于 1包络不会下穿零线解调时不容易失真。如果m大于 1包络会过零标准包络检波会丢失信息解调后的波形明显畸变。在频谱图上过调制的特征通常是边带幅度进一步增大时域包络出现交叉。实际操作中我一般会固定载波频率改变基带频率观察边带间隔是否随之变化。基带 5Hz 时两个边带离载波各 5Hz基带改成 10Hz边带间隔就变成 10Hz。这样能快速验证频谱图是否跟理论一致。4.3 解调时频谱会回到低频吗最简单的方法是相干解调把调幅信号再乘以一个同频同相的载波然后通过低通滤波器。x_demod x_am * carrier # 与本地载波相乘乘法后会产生一个较高频率的镜频分量和一个低频分量。低通滤波器把高频部分去掉剩下的就是与基带信号相关的低频成分。这个过程在频域里相当于把频谱又搬回低频。不过要注意本地载波如果不同频同相解调出来的幅度就会偏小如果频率有偏差解调结果会出现低频振荡。做实验时不要直接拿串口或真实数据去试先用内存里的正弦信号验证。如果你只是想直观展示边带只看频谱图就够了。如果想走完整链路就把解调后的信号频谱再画出来观察高频分量被滤除后低频成分是否恢复。这正好引出滤波器。5. 滤波器的实际设计参数边界与相位问题5.1 用 SciPy 设计一个低通滤波器滤波器的本质是频率选择系统。以低通为例它的目标是保留低频成分衰减高频成分。SciPy 里最常用的是 Butterworth 滤波器设计函数是butter滤波函数可以选择lfilter或filtfilt。下面的代码演示如何设计一个 8 阶低通滤波器截止频率设为 100Hz然后对“50Hz 信号加 300Hz 噪声”做滤波from scipy.signal import butter, filtfilt, lfilter fs 1000 t np.arange(0, 1, 1/fs) x np.sin(2 * np.pi * 50 * t) 0.8 * np.sin(2 * np.pi * 300 * t) def lowpass_butter(data, cutoff, fs, order8): nyq 0.5 * fs normal_cutoff cutoff / nyq b, a butter(order, normal_cutoff, btypelow) y filtfilt(b, a, data) return y y lowpass_butter(x, 100, fs, order8)滤波后信号 50Hz 成分保留300Hz 成分被明显压制。如果只看时域波形会觉得“波形变平滑了”但更严谨的验证方式是看滤波前后的频谱确认 300Hz 峰值下降幅度。5.2 截止频率、阶数、纹波怎么理解滤波器参数之间不是彼此独立的调整任何一个参数都会影响其他特性。参数含义影响截止频率幅度衰减到一定值的频率点决定通带和阻带的边界阶数滤波器复杂度阶数越高过渡带越窄但相位延迟更大纹波通带内幅度波动幅度要求过严会增加设计复杂度不少同学看到“截止频率 100Hz”会以为 100Hz 以上直接变成 0100Hz 以下完全不变。实际任何滤波器都有一个过渡带。100Hz 只是幅度降到某个设计值的位置例如 Butterworth 滤波器通常把截止频率定义为下降 3dB 的位置。高于 100Hz 的部分在过渡带内仍然可能残留一部分强度直到远离截止频率后才接近 0。实测时不要一上来就把阶数设成 50。阶数过高会带来两个问题一是滤波系统可能数值不稳定二是相位失真严重。对于离线分析一般 4 到 10 阶足够用过如果是实时系统每增加一阶都意味着更大的计算延迟。5.3 lfilter 和 filtfilt 怎么选这个点很容易被忽略但实际影响非常大。lfilter是标准因果滤波器输出依赖当前和过去的输入会引入相位延迟。如果你用 8 阶低通滤波滤波后的波形会比原始波形在时间上发生偏移频谱上表现为相位变化。filtfilt是零相位滤波它会把信号正向处理一遍再反向处理一遍抵消相位延迟。离线分析时filtfilt画出来的波形和原始波形在时间上对齐更容易观察幅度变化。但要注意filtfilt不能用于实时系统。因为它需要把整段数据先取到内存而且引入了“未来的数据”。如果项目里要求边采样边滤波只能使用lfilter或更严格的在线滤波器实现。建议课程实验和离线数据处理用filtfilt嵌入式、实时通信或流式任务不要图省事套用在线演示代码。6. 采样与混叠用频谱判断采样率够不够6.1 采样率不足会出现频率“造假”采样定理说要无失真地恢复一个带限信号采样率必须大于信号最高频率的两倍。这个理论很简洁但实操中很多人仍然会踩坑因为采样率不够时频谱图会显示一个“错误频率”。看下面的例子信号频率是 90Hz采样率是 100Hz。按我们熟悉的直觉90Hz 小于 100Hz似乎可以采样。但实际 FFT 后峰值会出现在 10Hz 而不是 90Hz。原因是 90Hz 超过了奈奎斯特频率 50Hz采样后折叠到 10Hz。这个现象叫混叠。fs 100 t np.arange(0, 1, 1/fs) x np.sin(2 * np.pi * 90 * t) X fft(x) freqs fftfreq(len(x), 1/fs) half len(x) // 2 plt.plot(freqs[:half], np.abs(X[:half]))如果你只盯着时域波形可能看不出问题但一旦画出频谱就会看到 10Hz 处出现一个不存在的峰值。这说明采样率不足时频谱图会“欺骗”你。6.2 奈奎斯特频率是必要条件不是充分条件课程里常出现“采样率 2 倍最高频率”的表述实际工程中很少直接卡两倍。原因是真实信号并不是理想带限信号它可能带有噪声、谐波以及你根本不关心的高频分量。如果这些高频分量的能量足够大它们会折叠到低频区间影响有效信号。比如要采集一个 30Hz 的接近正弦的振动信号但环境里混入了一个 900Hz 的电磁干扰。采样率如果是 100Hz900Hz 会被折叠到 100Hz 范围以内你看到的频谱可能一片混乱。这种情况下问题不是采样率不足而是采样前没有把高频干扰滤掉。我一般会按以下思路选择采样率先确认关心信号的最高频率分量是多少。查看信号中是否可能包含更高频率的噪声或谐波。如果有必须加抗混叠滤波器或使用更高采样率留足裕量。工程上可以取最高关心频率的 5 到 10 倍不要卡在 2 倍。如果限制采样率就在采样前用模拟低通滤除高于奈奎斯特频率的成分。这里的数字不是绝对标准但比“2 倍”稳妥得多。6.3 抗混叠滤波要放在采样之前不是之后经常有人问采样后发现频谱乱了能不能先用数字滤波器过滤再重新采样不行。混叠一旦发生原本的高频成分已经折叠到低频区域和有用信号在频域重叠后续任何数字滤波器都无法把它们分开。所以抗混叠滤波必须放在降低采样率之前。现实中ADC 前面会放模拟低通滤波器或者用带抗混叠功能的 ADC数字系统在降采样之前也要先做数字低通滤波。这个顺序不能颠倒。做课程实验时可以用一个简单信号验证把 90Hz 信号和 50Hz 信号叠加采样率设为 100Hz时间拉长到 10 秒。FFT 后50Hz 正常显示90Hz 变成 10Hz 出现在低频区。如果我们用一个截止频率 60Hz 的数字低通后再降采样50Hz 信号能保留90Hz 在降采样前被滤除不再混叠。但这里要强调数字低通必须在降采样之前执行。如果已经在 100Hz 采样率下拿到了混叠信号再滤波也没有意义。7. 频谱图异常时按什么顺序排查7.1 先看输入信号和采样率频谱图不对时不要急着改滤波器参数或加窗函数先按顺序排查。第一看输入信号。信号是否正常生成幅值是否为 0频率参数是否写成了弧度或周期。有人把2 * np.pi * f * t里的f直接写成角频率又乘以2*pi结果频率翻倍频谱图自然不对。这是非常常见的低级错误但不仔细看很难发现。第二看采样率是否超过最高频率。如果采样率比信号频率还低一定会混叠。先算一下最高频率和奈奎斯特频率之间的关系再往下查。第三看信号时长。FFT 分辨率等于fs / N。如果两个频率间隔小于分辨率它们会变成一个峰。解决办法是增加信号时长或者在生成信号时让时长刚好包含整数个周期。7.2 再看 FFT 和绘图参数很多“频谱异常”其实是绘图错误。忘记取模直接plot(X)结果会画出复数实部和虚部交错的奇怪曲线。没有调用fftfreq横轴直接填 0 到 N频率范围和位置全错。没有取单边谱左右镜像被误当成“两个不同频率成分”尤其是初学者看到对称峰常误以为是两个频率。xlim范围太大或太小峰值被挤出视野。幅度标签太小看起来像是没有信号其实只是量纲或归一化问题。查看时建议先画全谱再局部放大。全谱能看到整体形状局部放大能确认峰值频率。第一次跑通之前不要直接用xlim(0, fs/2)把范围框死否则混叠的高频信息可能被忽略。7.3 最后检查代码和工具环境如果输入、采样率、FFT 参数都没问题仍然出怪图那就需要检查环境Python 和库版本是否一致。代码是否在同一个进程中多次覆盖了变量名。内存是否足够如果数组特别大有些机器会中途卡住或结果不完整。是否误用了他人代码里的fs或N没有改成自己的变量。排查时可以加打印语句print(fs , fs) print(N , len(x)) print(max freq , freqs.max()) print(min mag , np.min(np.abs(X)), max mag , np.max(np.abs(X)))先确认基础变量再决定要不要动代码。在信号处理实验里日志和参数打印比“乱调参数”有效得多。7.4 一个适合实验课的最小检查清单我把常用的检查项整理成了一张表检查项正常情况异常时怎么办采样率是否满足奈奎斯特条件最高频率小于 fs/2且有裕量提高采样率或加抗混叠滤波频率分辨率是否足够目标频率间隔大于 fs/N增加采样时长避免盲补零FFT 结果是否取模取np.abs(X)只画实部或复数会出乱图横轴频率是否正确使用fftfreq手动生成频率轴时容易出错幅度是否归一化单边谱幅度接近输入幅度乘以2/N校正滤波器参数是否正确截止频率和阶数符合需求检查normal_cutoff cutoff / nyq是否使用零相位滤波离线分析用filtfilt实时系统不要用filtfilt这套清单写进实验报告里也很有用能证明你确实是按判定标准去验证而不是蒙出来的图。8. 最后留几个长期有用的习惯频谱分析、调制、滤波器、采样看起来是信号与系统课程里的四个章节但放到一起做实验后会发现它们是同一条链路上的环节。掌握这条链路等于掌握了一整套“如何观察和分析信号”的思路。我自己在带新人时会提醒他们养成三个习惯。第一每段实验都要有明确输入和预期输出。生成什么样的信号、采样率多少、预期频谱中峰值在哪里先写下来再跑代码。第二绘图之后一定要做验证不要只看“形状对了”。用find_peaks或直接打印峰值位置把数值和理论值对齐。第三任何参数改动都要形成记录。尤其是滤波器阶数、截止频率、采样率这三个参数改一个就可能导致结果完全变化没有记录很容易迷失。如果只是学习信号与系统的理论默认的fft、butter、filtfilt配置已经够用。如果要扩展到真实设备或批量数据分析就要额外考虑输入信号的格式、实时性要求、输出命名和失败重试。但不管场景怎么扩展核心思路都不会变先把单路信号跑稳再处理复杂任务先确认时域正确再进入频域判断先看频谱再调参数。
返回列表