
简介基于STM32F103ZET6的频谱分析与波形识别系统完整工程包适合电子竞赛备赛、嵌入式初学者及信号处理爱好者参考。系统通过ADC采样信号调用DSP库FFT函数进行频谱变换并将结果实时显示于TFTLCD屏同时给出波形参数与类型识别结果。资源包含195个文件压缩包4.76MB核心源码以C文件40个与H头文件42个为主附带编译生成的hex、axf、map等构建文件以及uvprojx/uvoptx工程配置便于直接打开Keil工程查看和重新编译。此外还包括LCD、ADC、定时器等底层驱动代码以及部分调试辅助文件整体结构清晰。已有4717人学习下载适合需要快速上手STM32FFT实战项目的读者可从中获取硬件连接思路、软件设计框架和完整可参考的工程实现。1. 项目概述与核心需求拆解先说个背景。早先我做振动监测和音频信号处理相关的项目一直是靠PC端的上位机软件搬离线数据来做频谱分析和波形观察。确实是能出结果但每次都得等人把设备拿回实验室、连上串口、导完数据再分析折腾不说现场根本没法实时判断设备状态。后来转念一想——能不能直接在设备端就把频谱算出来、把波形认出来这篇文章要聊的就是我基于STM32做的一套频谱分析和波形识别系统。硬件核心是一块STM32F103C8T6也就是大家常说的最小系统板外接一个模拟信号源用ADC去采集信号在片内完成FFT运算再把频谱和波形识别的结果通过OLED屏显示出来。整个项目从ADC采集、FFT算法到特征分类全部在MCU端跑完不依赖上位机。这个项目解决的核心痛点有两个一是把采集—分析—展示这条链路压缩到嵌入式端做到现场实时反馈二是给只有单一频点特征的设备提供一个廉价的故障预判手段——比如电机振动异常、电源纹波超标这类场景识别出波形类型和主频分布基本就能定位问题。如果你是正在做嵌入式或者电子设计相关项目、手头有STM32基础板的同学这篇文章会很有参考价值。我不仅会把关键模块的设计思路讲透还会放出核心代码片段并把我实际调试中踩过的坑一并交代清楚。老实说这套东西做完以后让我对MCU能做多少事有了新的认识——用一颗几十块钱的芯片跑出实时频谱这件事本身的性价比就足够高了。2. 整体设计方案与选型思路2.1 为什么选STM32而不是更高性能的处理器这是最早需要想清楚的问题。做频谱分析很多人的第一反应是上树莓派或者直接用DSP性能完全够开发也省心。但我的诉求很明确低功耗、低成本、小体积并且要能跑在电池供电的场景里。STM32F103C8T6主频72MHz带12位ADC、多路DMA和定时器完全可以满足入门级频谱分析的算力需求。有人会问72MHz跑FFT会不会吃力以256点FFT为例Cortex-M3内核做一次完整的基-2 FFT大概需要1~3ms如果采样率是10kHz那每256个采样点也就25.6ms的窗口周期算力占用不到10%剩下的时间还能做特征提取和控制逻辑。即便是1024点FFT也就十几毫秒的事对秒级刷新的显示需求来说完全够用。2.2 硬件方案组成整个系统的硬件链路是这样的STM32F103C8T6最小系统板主控负责ADC采集、FFT运算和显示驱动。信号调理电路一个简单的运放跟随器加上分压保护电路把输入信号的幅值钳位在0~3.3V保护ADC引脚。OLED显示模块0.96寸IIC接口OLED128x64分辨率用来显示频谱柱状图和波形识别结果。供电部分AMS1117-3.3稳压支持5V或锂电池输入。信号调理这块我多说一句。很多初学者直接拿信号怼到PA0引脚上如果信号有负半轴或者电压超过3.3V轻则采样数据截断失真重则烧引脚。我用的方案是先用运放做一级电压跟随再用两个电阻分压把信号抬到1.65V中心电平附近确保整个波形落在ADC量程中央。2.3 软件架构规划软件部分分三个模块数据采集层、算法处理层、显示交互层。数据采集层用定时器触发ADCDMA搬运数据到内存缓冲区做到采样不占CPU。算法处理层拿DMA缓冲区的数据做定点FFT然后提取频谱特征、识别波形类型。显示交互层负责把频谱数据映射成柱状图同时显示识别结果。这里有个关键设计选择FFT库我选的是ST官方的DSP库libarm_cortexM3l_math.a里面的arm_cfft_f32和arm_cmplx_mag_f32两个函数组合使用比自己手写FFT效率高很多而且代码经过了严格优化数值稳定性也有保证。如果不想用ST的库也有纯C的FFT实现可以替代但处理速度会明显慢一些。3. 核心算法解析从ADC采样到FFT频谱3.1 采样参数选型采样率、采样点数与窗函数频谱分析有个最基本的约束叫奈奎斯特定理——采样率至少要是信号最高频率的两倍否则会发生频谱混叠。我在这个项目里设置ADC采样率为10kHz有效分析带宽是5kHz对常见的音频信号和机械振动信号来说已经够用。采样点数选了256这带来的频率分辨率频率分辨率 采样率 / 采样点数就是 10000 / 256 39.06Hz。意味着在频谱上两个频率间隔小于39Hz的峰值是无法区分开的。如果你的应用场景需要分辨更接近的频率分量就需要增大采样点数或者降低采样率两者本质上是同一个公式在权衡。窗函数这一点很容易被忽略但对频谱分析的准确性影响极大。直接对截断后的有限长信号做FFT频谱会出现严重的泄漏效应——一个纯净的正弦波可能变成一片裙边。标准做法是先对时域数据乘一个窗函数再送进FFT。我这边用的是汉宁窗Hann它的主瓣稍宽但旁瓣衰减好对大多数频谱分析场景是最稳的选择。加窗之后有个代价幅度会被衰减大约一半所以做幅值校准时需要乘一个补偿系数。汉宁窗的恢复系数是2也就是说算出来的幅值乘以2才是真实幅值。这个细节不处理的话频谱图上的幅值会系统性偏低6dB左右。3.2 FFT计算流程在STM32上的落地整个FFT计算的流程可以拆成下面几步第一步从DMA缓冲区取256个采样点逐点减去直流分量。直流分量是所有采样值的算术平均值如果不剔除它会变成频谱上0Hz处巨大的峰值把其他频段的信息全部压扁。第二步对时域数据乘汉宁窗系数。这里可以用查表法预先算好256个窗系数存在Flash里运行时逐个乘进去避免实时计算三角函数的开销。第三步调用arm_cfft_f32做复数FFT。注意这个函数输入是交错排列的复数数组也就是实部虚部交替存需要先把实序列包装成复数数组格式。ST库的FFT输入输出都是原地操作节省内存。第四步用arm_cmplx_mag_f32取模值得到每个频率点的幅值。幅值需要除以采样点数/2才是真实的幅度值。第五步把0到128点对应0到5kHz的数据做峰值检测找出幅度最大的几个频点作为后续波形识别和显示的特征输入。3.3 波形识别不只靠FFT还要看时域特征波形识别单靠FFT是不够的。举例来说一个理想的方波和一个理想的正弦波如果基频相同它们在频谱上的差异主要体现在谐波分量上——方波含有丰富的高次奇次谐波但正弦波几乎只有基波。不过实际采集到的信号往往含有噪声谐波占比可能会有波动单靠谐波数量判断容易误判。我的方案是时域特征频域特征联合判断具体用到了三个维度过零率单位时间内信号穿越零电平的次数。正弦波大约2次/周期方波也是2次/周期但三角波也是2次/周期所以过零率只能区分频率不能区分波形。峰值因数信号峰值与有效值RMS之比。正弦波峰值因数是1.414方波是1三角波是1.73这个参数识别波形非常有效。谐波能量占比计算频谱上2次及更高次谐波的能量占总能量的比例。方波的理论谐波占比很高正弦波几乎为零三角波介于中间。三者综合再加一个简单的决策树分类器就能较好地识别出正弦波、方波、三角波和噪声这四种基本类型。识别周期设为1秒更新一次每次取最近时间内统计的特征做判断避免单帧数据上的偶发误判。我用了一个比较简单的方法来算峰值因数在时域里记录256个采样点的最大值和RMS值直接相除。实测下来即便信号有一定底噪这几种波形的峰值因数区分度依然足够。4. 实操过程与关键代码实现4.1 硬件搭建与接线说明接线这部分比较直接。模块引脚STM32引脚OLED SCLD0PB6OLED SDAD1PB7信号输入OUTPA0 (ADC1_IN0)供电VCC/GND3.3V/GNDOLED用IIC模式跳线电阻焊在IIC侧。信号输入端和PA0之间串一个1kΩ电阻做限流保护同时并联一个0.1uF电容做简单的抗混叠滤波。这个RC低通滤波的截止频率是1/(2πRC)算下来是1.59kHz如果只分析1.5kHz以内的信号就够用。要分析更高频率的话电容值要相应减小。4.2 STM32CubeMX配置要点我习惯用STM32CubeMX生成初始化代码然后在这个基底上改逻辑。关键配置项有这么几个ADC1: 开启IN0通道采样时间设为最大239.5周期提高采样精度。开启连续转换模式开启DMA请求。定时器2: 设置为PWM生成模式产生10kHz的触发信号接到ADC的触发引脚上实现定时触发采样。DMA: 配置为循环模式数据宽度半字16位方向外设到内存。256个采样点的缓冲数组。系统时钟: 外部8MHz晶振PLL倍频到72MHz。这里有个容易踩的坑DMA必须配置成循环模式否则采满256个点后DMA会自动停止后续数据就断了。循环模式下DMA会持续搬运新数据覆盖旧数据CPU在任意时刻都可以拿最近一帧数据去做FFT这是一种典型的双缓冲思想的简化版。4.3 核心代码实战ADC采样与FFT代码我拆成三段讲方便你直接抄作业。第一段是ADCDMA的初始化CubeMX生成的基础代码之外我自己补了启动DMA传输的代码// 启动ADC DMA采集buffer为uint16_t数组长度256 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, FFT_SIZE); // 同时启动定时器开始产生触发脉冲 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3);有了这两行ADC就会按照10kHz的节奏持续往缓冲区里灌数据主循环里只需要去读这个缓冲区就行。第二段是FFT处理的封装函数void process_fft(uint16_t *adc_data, float *mag_output) { static float fft_input[FFT_SIZE * 2]; static float window_coeff[FFT_SIZE]; float mean 0.0f; // 1. 计算直流分量并构建复数输入 for (int i 0; i FFT_SIZE; i) { mean adc_data[i]; } mean / FFT_SIZE; for (int i 0; i FFT_SIZE; i) { fft_input[2*i] (adc_data[i] - mean) * window_coeff[i]; fft_input[2*i1] 0.0f; } // 2. 执行FFT变换 arm_cfft_f32(arm_cfft_sR_f32_len256, fft_input, 0, 1); // 3. 计算幅值谱 arm_cmplx_mag_f32(fft_input, mag_output, FFT_SIZE); // 4. 幅值校准除以(N/2)并乘窗恢复系数2 for (int i 0; i FFT_SIZE/2; i) { mag_output[i] mag_output[i] / (FFT_SIZE / 2) * 2.0f; } }第三段是峰值检测和主频提取这里有一个小技巧找峰值时不能只做全局最大值搜索要做局部极大值检测——逐个点比较相邻三点如果中间点比两边都高就记为一个候选峰。这样做能同时识别出基波和多次谐波为后续的波形识别提供特征数据。4.4 波形识别算法的实现细节峰值因数计算这一步其实很便宜——在ADC数据不断更新的同时可以在一个统计周期内维护最大值和平方累加和周期结束时直接算RMS再和最大值相除。波形识别的决策逻辑我用了一段简洁的C代码实现核心判断顺序是先看峰值因数再看谐波占比typedef enum { WAVE_SINE, WAVE_SQUARE, WAVE_TRIANGLE, WAVE_NOISE } WaveType; WaveType classify_waveform(float peak_factor, float harmonic_ratio) { // 噪声通常峰值因数大于2.2且谐波结构混乱 if (peak_factor 2.2f) { return WAVE_NOISE; } // 正弦波峰值因数接近1.414谐波占比极低 if (peak_factor 1.6f harmonic_ratio 0.1f) { return WAVE_SINE; } // 方波峰值因数接近1.0谐波占比很高 if (peak_factor 1.3f harmonic_ratio 0.3f) { return WAVE_SQUARE; } // 三角波介于中间 if (peak_factor 1.5f peak_factor 2.0f) { return WAVE_TRIANGLE; } return WAVE_NOISE; }实测这套逻辑在输入信号比较干净的实验室环境下识别准确率很高但如果环境噪声偏大建议在分类前先对时域信号做一次简单的滑动平均滤波削掉高频毛刺识别的稳定性会提升一个档次。显示部分就比较简单了——把FFT幅值谱做对数压缩映射成柱状图按从低到高画到OLED上同时在右上角显示识别到的波形类型。OLED刷新我控制到5帧/秒太小看不到频谱动态太大会有明显的闪烁感。5. 常见问题与排查技巧实录5.1 STM32点不亮、连接不上调试过程中我碰到最多的一个问题就是报错error: no stm32 target found! if your product embeds debug authentication。这不是STM32坏了而是调试接口状态异常。优先检查这几处BOOT0引脚是否被拉高BOOT01会进入串口下载模式SWD调试会被断开供电是否稳定很多最小系统板用USB供电时电流不够OLED一亮起来电压就掉到3V以下SWDIO和SWCLK接线是否接反这两根线接反是新手最容易犯的错误。另外我要特别提醒一点在Keil里如果之前烧录过程序且开启了读保护后面就无法直接连接调试目标需要用ST-Link Utility先解除读保护再重新烧录。5.2 delay函数卡死问题的排查写这套代码时我用了一个HAL_Delay结果发现在FFT运算的循环里调用会直接卡死。排查之后发现是优先级冲突FFT运算是在主循环里的但SysTick中断的优先级低于ADC的DMA中断DMA一直抢占CPU导致SysTick无法及时响应HAL_Delay就无限等待了。这个问题其实有个更简单的解法尽量避免在中断服务函数里面做耗时的HAL调用DMA传输完成回调里只置一个标志位主循环轮询到这个标志位再做FFT和显示。这样既不会卡死也给系统留出了足够的刷新余量。5.3 虚拟串口识别异常在串口调试阶段我碰上过STM32的Virtual COM Port在设备管理器里显示黄色感叹号的情况。这是因为ST的USB虚拟串口驱动和系统自带的CDC驱动冲突导致的。解决办法是去ST官网下载最新的STM32 Virtual COM Port驱动手动更新设备驱动指定到下载目录不要用系统的自动搜索。5.4 FFT结果直流分量过大做FFT后频谱图上0Hz处巨高无比其他频点几乎看不见。这个问题几乎可以肯定是没做直流分量消除。有些信号源本身可能带直流偏置ADC采集到的原始值一定不是以零为中心的所以必须在进FFT之前先减去平均值。我在前面的代码里专门加了一步均值计算就是为了解决这个问题。5.5 波形识别频繁误判如果你的信号源不稳定波形识别结果会来回跳。我做过两个改进一是把识别周期拉长到1秒短时间内的抖动被平滑掉二是加入滞回比较——比如当前判断是正弦波只有连续多帧都显示方波特征才切换类型防止在边界处反复跳变。6. 实测效果与进一步扩展思路整套系统跑起来之后我用信号发生器做了几组验证。1kHz正弦波输入时频谱图上基频位置只有一个干净的单峰波形识别显示SINE峰值因数稳定在1.41左右。切到1kHz方波后频谱图在3kHz和5kHz处出现了明显的奇次谐波谱线波形识别切到SQUARE。三角波的谐波衰减更快识别器也能正确区分。这套系统的上限并不止于此。如果再加上点功夫有几个方向完全值得试一下第一在F103上把采样点数升到1024频率分辨率会降到9.7Hz能分辨更细的频率分布代价是单次FFT时间变长但实时性依然可以接受。第二加一个简单的阈值报警功能——当某个频段的幅值超过预设值就在OLED上显示警告直接当成小型设备故障预判器用。第三把FFT结果通过串口或蓝牙发到手机APP上显示变成一个完整的移动端频谱仪。我在做这套系统的过程中最大的感受是算法本身不算难难的是把算法放进一个资源受限的MCU里还能稳定运行。你需要在精度、速度和资源占用之间做各种取舍而这些取舍的经验光看书是学不来的只有真刀真枪地调一遍才能体会。本文还有配套的精品资源点击获取