简介:基于STM32F103ZET6的频谱分析与波形识别系统完整工程包,适合电子竞赛备赛、嵌入式初学者及信号处理爱好者参考。系统通过ADC采样信号,调用DSP库FFT函数进行频谱变换,并将结果实时显示于TFTLCD屏,同时给出波形参数与类型识别结果。资源包含195个文件,压缩包4.76MB,核心源码以C文件(40个)与H头文件(42个)为主,附带编译生成的hex、axf、map等构建文件,以及uvprojx/uvoptx工程配置,便于直接打开Keil工程查看和重新编译。此外还包括LCD、ADC、定时器等底层驱动代码,以及部分调试辅助文件,整体结构清晰。已有4717人学习下载,适合需要快速上手STM32+FFT实战项目的读者,可从中获取硬件连接思路、软件设计框架和完整可参考的工程实现。
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接口OLED,128x64分辨率,用来显示频谱柱状图和波形识别结果。
- 供电部分:AMS1117-3.3稳压,支持5V或锂电池输入。
信号调理这块我多说一句。很多初学者直接拿信号怼到PA0引脚上,如果信号有负半轴或者电压超过3.3V,轻则采样数据截断失真,重则烧引脚。我用的方案是先用运放做一级电压跟随,再用两个电阻分压把信号抬到1.65V中心电平附近,确保整个波形落在ADC量程中央。
2.3 软件架构规划
软件部分分三个模块:数据采集层、算法处理层、显示交互层。
数据采集层用定时器触发ADC,DMA搬运数据到内存缓冲区,做到采样不占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 SCL | D0 | PB6 |
| OLED SDA | D1 | PB7 |
| 信号输入 | OUT | PA0 (ADC1_IN0) |
| 供电 | VCC/GND | 3.3V/GND |
OLED用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
代码我拆成三段讲,方便你直接抄作业。
第一段是ADC+DMA的初始化,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*i+1] = 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引脚是否被拉高(BOOT0=1会进入串口下载模式,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里还能稳定运行。你需要在精度、速度和资源占用之间做各种取舍,而这些取舍的经验,光看书是学不来的,只有真刀真枪地调一遍才能体会。
本文还有配套的精品资源,点击获取