
简介一套基于STM32的FFT频谱分析与波形识别完整工程资料面向单片机开发者、数字信号处理初学者及电子竞赛备赛人群。工程包含基础版与可触屏调节采样频率的进阶版两个目录进阶版通过触摸屏修改采样频率来优化频率分辨率使50Hz~200Hz范围的频谱更精确并直接给出基频及3、5、7次谐波峰值波形识别模块能区分正弦波、方波、锯齿波、三角波硬件基于正点原子精英版3.5寸TFTLCD烧写后即可运行。资源共395个文件约9.78MB核心代码以81个C源文件和85个H头文件为主同时包含o/d/crf等编译中间文件、uvprojx工程文件、hex/axf烧录文件及bat辅助脚本便于查看配置、重新编译与烧写。已有3.3万余人学习下载两个版本对比使用既能快速复现实验现象也能深入理解采样率、FFT频率分辨率与波形特征提取的具体实现。1. 基于STM32的FFT频谱分析波形识别一块板子把信号看明白手头有一台信号发生器输入正弦波、方波、三角波、锯齿波想不用台式示波器、不接电脑就在一块STM32开发板上实时把波形类型认出来顺便把频谱画出来。这个项目就是干这件事的用STM32内置ADC做同步采样把采样数据送进FFT变换拿到幅度谱之后通过谐波特征反向推断输入的波形类型。整体不依赖外部专用芯片一块最小系统板加一个OLED屏就能跑适合做信号处理课程设计、仪器仪表入门也适合想搞懂FFT算出来到底怎么用的开发者。这里把从采样、变换到识别、上屏的完整链路拆开讲清楚。2. 系统架构与关键技术选型为什么ADC采样率直接决定频谱能看多宽2.1 硬件组成与信号链路先看这个项目的硬件构成它沿用的是一套非常典型的嵌入式信号采集链路信号源 → 调理电路 → STM32 ADC → 内存缓冲 → FFT计算 → 特征提取 → 显示。信号源方面常见接法是用信号发生器输出峰峰值不超过3.3V的波形直接接入STM32的ADC引脚。严格一点的做法是先经过一个电压跟随器做阻抗匹配再用电容隔直把信号垫到1.65V左右的中位电压避免负半周被ADC截掉。多数教学场景下用DAC模块或者直接拿信号发生器输出带直流偏置的信号也没问题。核心控制器的选型建议优先选用带FPU的Cortex-M4内核芯片比如STM32F407或STM32F103系列都能跑但F4系列做浮点FFT有明显优势。官方DSP库里的arm_cfft_f32依赖硬件浮点加速在F4上做1024点复FFT大约只需要几十微秒级别的时间F103没有FPU跑浮点FFT会慢很多。如果手里只有F103可以改用Q15定点格式的arm_cfft_q15速度大幅提升代价是精度略降幅度谱的幅值计算需要做定标处理。显示部分用0.96寸OLEDSSD1306或者1.3寸TFT屏都可以。OLED的特点是刷新简单用SPI或I2C接口都能驱动适合显示频谱柱状图和波形类型名想看得更细就用TFT能同时画出时域波形和频谱但驱动复杂度和内存占用会明显增加。2.2 软件架构三段式流水线软件部分不搞复杂操作系统整体是一个前后台结构的循环核心任务分成三个模块采样模块、频域变换模块、波形识别模块。系统的运行节奏是这样设计的定时器产生固定频率的触发信号每次触发启动一次ADC转换转换完成后通过DMA把结果直接搬运到内存缓冲区完全不占用CPU。采集完一整帧数据后主循环里做FFT运算得到幅度谱然后送入识别算法判断波形类型最后把频谱和识别结果显示到屏幕上。这种三段式结构的关键点在于采样和计算是解耦的。采样靠硬件定时器DMA自动完成主循环只负责等数据就绪、做计算、出结果。代码结构上也按这个边界拆文件。2.3 目录结构与文件职责一个可运行的工程文件组织如下project_root/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── adc_sampling.h │ │ ├── fft_process.h │ │ ├── waveform_detect.h │ │ └── oled_display.h │ └── Src/ │ ├── main.c │ ├── adc_sampling.c │ ├── fft_process.c │ ├── waveform_detect.c │ └── oled_display.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F4xx_HAL_Driver/ ├── DSP/ │ └── Lib/ │ └── libarm_cortexM4lf_math.a └── signal_data/ ├── test_sine.csv ├── test_square.csv └── test_triangle.csvadc_sampling.c负责配置ADC、定时器、DMA并提供get_signal_frame()函数取出完整的一帧采样点。fft_process.c封装了FFT初始化、窗函数施加、频谱幅值计算和峰值搜索。waveform_detect.c是识别算法的核心从频谱中提取特征量并输出波形类型。oled_display.c负责把频谱和结果画到屏幕上。这个结构的好处是每一层都可以单独测试可以先往fft_process.c里喂一组已知频率的合成正弦信号验证FFT正确性再对接ADC采集。实际测试时我习惯先用signal_data目录下的CSV离线数据跑通识别算法确认算法逻辑没问题再上板联调能省掉大量在硬件上猜问题的时间。3. 核心模块拆解ADC采样、FFT变换与波形识别三段管线3.1 ADC同步采样定时器触发加DMA搬运先看采样模块这是整个系统数据质量的源头。这里的核心配置是定时器TIM2产生PWM输出信号该信号接到ADC的触发输入端实现精确间隔采样。/* adc_sampling.c - 定时器触发ADC采样关键配置 */ void ADC_Sampling_Init(uint32_t sample_rate) { /* 计算定时器分频和重载值产生指定频率的触发脉冲 */ TIM_HandleTypeDef htim2 {0}; htim2.Instance TIM2; htim2.Init.Prescaler 84 - 1; /* 84MHz / 84 1MHz计数频率 */ htim2.Init.Period (1000000 / sample_rate) - 1; /* 产生sample_rate频率的触发 */ htim2.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim2); TIM_OC_InitTypeDef sConfigOC {0}; sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse (htim2.Init.Period 1) / 2; /* 50%占空比脉冲 */ sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); /* ADC配置开启DMA循环模式数据直接写入内存数组 */ ADC_HandleTypeDef hadc1 {0}; hadc1.Instance ADC1; hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.ContinuousConvMode DISABLE; /* 非连续转换等待外部触发 */ hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; hadc1.Init.ScanConvMode DISABLE; HAL_ADC_Init(hadc1); }这段配置里最关键的是ContinuousConvMode DISABLE和ExternalTrigConvEdge TIM2_TRGO的组合。ADC不再自己反复转换而是等待定时器通过TRGO事件发出触发脉冲每来一次脉冲转换一次采样间隔完全由定时器控制。Prescaler 84-1的前提是系统时钟为84MHz若跑在168MHz主频则不同需要按实际总线时钟调整。Period决定了采样频率比如要配10kHz采样率Period设为99即可。采样率的选择直接影响频谱分析结果后面单独讲。DMA配置为循环模式直接将ADC转换结果写入内存数组数组长度等于FFT点数#define FFT_SIZE 1024 uint16_t adc_buf[FFT_SIZE]; void ADC_DMA_Start(void) { HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buf, FFT_SIZE); }每次主循环检查adc_buf是否被DMA完整填充了一次。细节是DMA为循环模式写满1024个点后自动从头开始覆盖所以主循环要配合一个传输完成标志位来判断数据新旧的边界。3.2 FFT频谱计算从时域到频域的完整变换流程采样得到的是时域波形FFT把它变成频域的信息。工程上直接使用STM32官方DSP库不自己写蝴蝶算法。使用浮点FFT前需要对ADC原始数据做一次预处理/* fft_process.c - 加窗 FFT 幅度谱计算 */ float32_t fft_input[FFT_SIZE * 2]; /* 复数数组实部虚部交替存储 */ float32_t fft_output[FFT_SIZE]; /* 存储幅度谱 */ float32_t window[FFT_SIZE]; /* 窗函数系数表 */ static void Apply_Window(void) { /* 汉宁窗降低频谱泄漏主瓣展宽换旁瓣衰减 */ for (int i 0; i FFT_SIZE; i) { window[i] 0.5f * (1.0f - cosf(2.0f * PI * i / (FFT_SIZE - 1))); /* 实际输入范围adc_buf是12位值域0~4095先减去中点以消除直流分量 */ fft_input[2 * i] (adc_buf[i] - 2048.0f) / 2048.0f * 3.3f * window[i]; fft_input[2 * i 1] 0.0f; /* 虚部置零 */ } } void FFT_Compute(void) { Apply_Window(); /* 执行基4浮点FFT1024点 */ arm_cfft_f32(fft_instance, fft_input, 0, 1); /* 计算幅度谱sqrt(re*re im*im)只保留前FFT_SIZE/2个有效频点 */ arm_cmplx_mag_f32(fft_input, fft_output, FFT_SIZE / 2); }arm_cfft_f32处理复数数据输入数组需要按实部、虚部交替排列。arm_cmplx_mag_f32把复数结果转换为幅度值输出数组长度只要FFT_SIZE/2因为实信号频谱是对称的后半部分没有额外信息。这里有一个工程上很重要的细节ADC原始数据是12位整型必须先转成浮点并减去直流分量否则频谱图里0Hz处会出现一个巨大的直流峰把其他频率的幅度压缩得几乎看不见。实际调试时很多人会先看到频谱图上一根柱子从0频顶到天花板往往就是忘了做直流去除这一步。3.3 波形识别算法用谐波比例特征判断波形类型FFT把幅度谱算出来了识别就建立在每种波形的谐波组成是固定的这一规律上。正弦波只有基波方波有奇次谐波且幅度按1/n规律衰减三角波也是奇次谐波但按1/n²衰减锯齿波则所有整数次谐波都存在幅度按1/n衰减。/* waveform_detect.c - 基于谐波幅度特征的波形识别 */ typedef enum { WAVE_UNKNOWN, WAVE_SINE, WAVE_SQUARE, WAVE_TRIANGLE, WAVE_SAWTOOTH } WaveType; WaveType Waveform_Detect(float32_t *mag_spec, int fft_size, uint32_t sample_rate, float *freq_out) { /* 第一步找到最大峰值的频点判定为基波频率 */ int peak_idx 0; float peak_mag 0.0f; for (int i 1; i fft_size / 2; i) { if (mag_spec[i] peak_mag) { peak_mag mag_spec[i]; peak_idx i; } } /* 频率分辨率为 sample_rate / fft_size */ float freq_res (float)sample_rate / fft_size; *freq_out peak_idx * freq_res; /* 第二步计算基波幅值和谐波能量占比 */ float fundamental_mag mag_spec[peak_idx]; float harmonic_ratio_sum 0.0f; int harmonic_cnt 0; for (int k 2; k 10; k) { int harmonic_idx peak_idx * k; if (harmonic_idx fft_size / 2) break; float h_mag mag_spec[harmonic_idx]; harmonic_ratio_sum h_mag / fundamental_mag; if (h_mag fundamental_mag * 0.05f) { harmonic_cnt; } } /* 第三步根据特征量判定波形 */ if (harmonic_ratio_sum 0.1f) return WAVE_SINE; /* 几乎没有谐波分量判为正弦 */ if (harmonic_cnt 4 || peak_idx 1) return WAVE_SAWTOOTH; /* 谐波数量多且幅度均匀判为锯齿波 */ /* 检查偶次谐波存在则排除方波/三角波 */ int even_harmonic 0; for (int k 2; k 10; k 2) { int h_idx peak_idx * k; if (h_idx fft_size / 2) break; if (mag_spec[h_idx] fundamental_mag * 0.05f) even_harmonic 1; } if (!even_harmonic) { /* 只有奇次谐波进一步区分方波与三角波 */ float h3_ratio mag_spec[peak_idx * 3] / fundamental_mag; if (h3_ratio fundamental_mag * 0.25f) return WAVE_SQUARE; /* 三次谐波幅度高方波特征明显 */ else return WAVE_TRIANGLE; } return WAVE_UNKNOWN; }识别算法的阈值是整个项目里调试量最大的地方。harmonic_ratio_sum 0.1f判正弦在信噪比高的时候非常稳定但输入信号若有少量噪声这个值可能会跳到0.15以上造成正弦误判。解决办法是加一个带通滤波器或者人为提高判定阈值到0.2附近代价是微弱失真的正弦信号会被漏掉。谐波数量的判定逻辑里peak_idx * k直接取峰值点的整数倍位置严格来说应该搜索该谐波附近小邻域内的局部最大值因为频率分辨率有限谐波峰值可能偏离整数倍位置几个bin。我一般会在实际代码里加一个±2个频率bin的搜索窗口识别鲁棒性有明显提升。3.4 主循环调度与OLED频谱绘制主程序的运行逻辑是一个不断轮询的循环结构/* main.c - 主循环逻辑 */ int main(void) { HAL_Init(); SystemClock_Config(); OLED_Init(); ADC_Sampling_Init(10000); /* 10kHz采样率 */ FFT_Instance_Init(); /* 初始化FFT实例 */ ADC_DMA_Start(); for (;;) { if (dma_transfer_complete_flag) { dma_transfer_complete_flag 0; FFT_Compute(); float freq 0.0f; WaveType type Waveform_Detect(fft_output, FFT_SIZE, 10000, freq); OLED_Clear(); OLED_DrawSpectrum(fft_output, FFT_SIZE / 2, freq_res); OLED_ShowWaveType(type, freq); } } }10kHz采样率、1024点FFT频率分辨率约为9.77Hz这意味着能分辨的最低频率差接近10Hz低于10Hz的相邻频率成分会在频谱上混在一起。要观察低频细节可以提高FFT点数到2048或4096要观察更高频的信号则要提高采样率但FFT点数不变时频率分辨率会同步变差。OLED频谱绘制时直接把幅度值映射到屏幕纵轴高度。这里有一个显示上的坑未做对数压缩的幅度谱小信号在屏幕上几乎看不见。建议绘制前做一次10*log10(mag)或者sqrt(mag)的变换频谱的视觉呈现会更均匀。4. 避坑排查ADC采样、FFT计算与波形识别中的三个翻车现场4.1 ADC采样值全是0或者乱跳现象程序烧进去后屏幕上频谱图完全没反应要么是一根水平直线在底部要么是密密麻麻的噪声毛刺。原因这个坑最常见于ADC的触发源没配置对。很多人直接拿示例工程改示例里ADC是软件触发连续转换模式换成定时器触发之后却没有重新配置ExternalTrigConv字段导致ADC实际仍在等待软件触发DMA缓冲区里一直是旧数据或者全零。还有一种情况是信号源没有直流偏置负半周被ADC前端钳位采样值只落在0附近很小的范围内看起来像是死了。解决先确认配置里hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO这一项已经写上。然后断开信号输入在代码里手动给adc_buf填充一组正弦波测试数据确认FFT和显示链路是通的。最后再接入真实信号把信号发生器的输出直流偏置调到1.65V左右幅度控制在0.1V~3.3V范围内。4.2 频谱图上基波频率对不上现象信号发生器输出1kHz正弦波屏幕上显示的峰值频率却在980Hz或者1025Hz附近差了几十个Hz。原因这属于频率分辨率的正常现象。1024点FFT配合10kHz采样率频率分辨率是9.77Hz1kHz落在频率网格里确实不是精确的整数倍频点FFT出来会在相邻几个频率bin上都有能量幅度最大的bin可能偏向某一侧。如果信号源本身存在频率漂移偏差会更大。解决提高FFT点数从1024改为2048或4096分辨率跟着减半或再减半或者调整采样率使目标频率恰好落在整数bin上比如采样率设为1024Hz时分辨率正好是1Hz。更通用的做法是峰值位置做抛物线插值取相邻三个bin的幅值估算真实频率精度能到亚bin级别。4.3 波形识别在方波和三角波之间反复跳变现象输入方波时识别正确但把幅值调低一点再测结果变成了三角波输入三角波时偶尔又跳成方波。原因方波和三角波都是奇次谐波结构区别靠三次谐波相对幅值——方波的三次谐波是基波的三分之一三角波只有九分之一。当信号幅值降低或者输入源输出阻抗较大导致信号畸变三次谐波的实测比例就会落在两者之间的模糊地带阈值判断就会翻车。解决加一个识别结果的软件滞回比较历史上连续判定为方波且三次谐波比例变化不大就锁定为方波只有等判据明确改变时才切换结果。把阈值区间改成分段判断三次谐波比例大于0.3判方波、小于0.12判三角波、两者之间沿用上一次结果能大幅减少闪烁。另外实际采样到的方波如果带边沿过冲其高频谐波会被ADC带宽限制削掉导致谐波比例偏移建议软件里对频谱做一次补偿修正。5. 二次开发把固定波形识别改造成一台实时频谱分析小工具这套系统做完了固定功能之后往更实用的方向扩展思路其实很清晰。先看最直接的改进方向采样参数动态化。目前采样率和FFT点数在编译期是固定的但实际上信号频率未知时有很大的提升空间。把采样率改成可配置参数用户通过按键切换10kHz、50kHz、100kHz三档FFT点数固定1024频率分析范围也随之变化。代码层面只需要把ADC_Sampling_Init()里的定时器Period改成变量暂停DMA采样后重新计算并配置定时器即可。以后我再做这类项目默认就会把采样率做成运行时变量而不是宏常量省得每次改配置都要重新编译烧录。其次是加串口上位机这是逃不开的需求。板子上保留一段UART输出每完成一次分析就把频率峰值、谐波比例、识别结果用格式化文本发出去上位机用串口助手就能看。想让显示更直观可以把幅度谱打包成自定义协议发到PC端绘制实时频谱图PC端用Python的pyqtgraph或matplotlib的交互模式就能轻松展示动态曲线。这个改造能明显拓展板子的使用场景。再进一步识别算法本身也有可玩性。当前判定准则依赖谐波比例对含有噪声或叠加干扰的信号容易失效。一个更鲁棒的方案是引入过零率检测或自相关函数做时域预判先粗分波形类型再用频谱细节确认双通道判断互相印证。对于频率已知但波形未知的测试场景还可以利用FFT的相位信息方波的相位谱有个奇特的跳跃模式利用相位跳变特征可以更可靠地区分方波和三角波。最后建议做一个校准程序开机时自动对基准信号比如板载DAC输出1kHz正弦波执行一次FFT把计算出的各谐波幅度保存为校准系数后续识别时用这个系数修正实测幅度相当于给整个信号链路做了出厂标定。这样即使换了板子、换了信号源阻抗识别准确率也能保持稳定。这次写这套项目代码我在采样率切换上被改宏重新编译折腾了几轮从那以后凡是涉及采集参数配置我都强制自己走一遍结构体参数化运行时初始化的路子宁可多写几行配置代码也不让一个常量卡住一个功能。希望帮到你。本文还有配套的精品资源点击获取