STM32单片机FFT信号处理实战:从ADC采样到频谱分析

📅 2026/7/29 11:43:29
STM32单片机FFT信号处理实战:从ADC采样到频谱分析
1. 项目概述当单片机遇上信号处理在嵌入式开发领域STM32系列单片机因其强大的性能和丰富的外设早已成为工程师手中的“瑞士军刀”。但很多时候我们用它来处理的任务还停留在按键扫描、LED闪烁、串口收发这些基础层面。你有没有想过让这块小小的芯片去做一些更“聪明”的事情比如让它“听懂”一个复杂信号里到底藏着什么秘密。这就是我们今天要聊的核心在STM32单片机上使用傅里叶变换解析信号。简单来说傅里叶变换就像是一个“信号翻译官”它能把一个随时间变化的信号时域信号分解成一系列不同频率、不同振幅的正弦波频域信号。想象一下你听到一段混杂的音乐傅里叶变换能帮你分析出里面到底有哪几个音符在响每个音符有多大声。对于单片机而言这意味着它能从传感器采集到的一堆看似杂乱无章的电压数据中提取出有用的频率成分、幅值大小甚至判断是否存在特定频率的干扰。这有什么用应用场景太多了。在工业现场你可以用它来分析电机轴承的振动频谱提前发现异常磨损在智能家居中可以用来识别特定的声音指令实现非接触控制在电力监测领域可以分析电网的谐波含量甚至可以用在简单的音乐频谱显示或者心电信号的特征提取上。STM32特别是带有FPU浮点运算单元和DSP指令集的型号为实时进行傅里叶变换这种计算密集型任务提供了可能。这篇文章就是带你从零开始在STM32上实现一个实用的信号频域分析功能。我会避开枯燥的数学推导聚焦于工程实现如何选择算法、如何准备数据、如何调用库函数、如何优化性能以及如何解读结果。无论你是正在做相关项目的学生还是希望为产品增加高级诊断功能的工程师这篇基于实际踩坑经验的总结都能给你提供一条清晰的路径。2. 核心思路与方案选型为什么是FFT在单片机上实现傅里叶变换我们几乎唯一的选择就是快速傅里叶变换FFT。这是傅里叶变换的一种高效计算算法能将计算复杂度从O(N²)降低到O(N log N)让实时处理成为可能。我们的核心思路可以概括为ADC采样 - 数据预处理 - FFT计算 - 结果后处理与解读。2.1 算法选型实数FFT vs. 复数FFTFFT算法通常处理复数序列。但我们的ADC采样值都是实数。这里就有两个主流方案直接使用实数FFTRFFT这是最直接的优化。STM32的CMSIS-DSP库直接提供了针对实数输入的FFT函数如arm_rfft_fast_f32。它内部会通过巧妙的打包方式利用复数FFT的蝶形结构来计算实数序列的FFT效率最高内存占用也相对较少。对于绝大多数只需要分析幅值谱的应用这是首选。使用复数FFTCFFT将ADC采集的实数序列的虚部全部置零形成一个复数序列然后调用复数FFT函数如arm_cfft_f32。这种方法更通用可以同时得到幅值和相位信息但计算量和内存占用都是实数FFT的两倍。除非你的应用明确需要相位信息如计算阻抗角、进行相关分析否则不推荐。实操心得一开始我为了“功能全面”使用了复数FFT结果在STM32F103这种没有FPU的芯片上计算256点就感到吃力。后来切换到arm_rfft_fast_f32不仅速度提升一倍内存也节省了一半。所以明确你的核心需求是幅值分析就果断用实数FFT。2.2 点数选择256512还是1024FFT的点数N决定了频率分辨率Δf 采样频率Fs / N和能分析的最高频率奈奎斯特频率Fs/2。点数越多频率分辨率越高但计算量也越大。计算量FFT计算时间大致与 N log₂(N) 成正比。256点比1024点快很多。分辨率若Fs10kHz256点FFT的Δf≈39Hz512点Δf≈19.5Hz1024点Δf≈9.8Hz。你需要根据待分析信号的最小频率间隔来选择。内存一个N点的实数FFT需要约N个float型数据的内存。1024点就是4KB对于资源紧张的芯片需要考虑。一个实用的权衡策略先从256点开始如果发现频率分辨率不够再提升到512点。对于电机振动分析基频几十到几百Hz256或512点通常足够。对于音频分析20Hz-20kHz可能需要1024或2048点并考虑使用更高主频和带DSP的型号如STM32F4/F7/H7。2.3 库的选择CMSIS-DSP是王道STM32生态中CMSIS-DSP库是进行FFT等数字信号处理的不二之选。它是ARM官方优化的库针对Cortex-M内核尤其是M4/M7/M33的DSP指令和FPU做了大量汇编级优化效率远超自己手写的C语言FFT代码。如何获取与添加如果你使用STM32CubeMX初始化工程在Software Packs中选择ARM::CMSIS-DSP即可自动添加。如果手动添加需要将CMSIS-DSP的源码通常位于CubeIDE或CubeMX安装目录的Drivers/CMSIS/DSP下加入工程并包含头文件路径。最关键的一步在编译器中开启FPU支持对于F4/F7等型号并添加预定义宏ARM_MATH_CM4或对应内核的宏。3. 硬件与软件环境搭建3.1 硬件平台选择并非所有STM32都适合做FFT。主要考量两点FPU浮点运算单元FFT涉及大量浮点乘加运算。有FPU如STM32F4/F7/H7系列比没有FPU如STM32F1/F0系列的速度可以快几十倍。强烈建议选择带FPU的型号。主频与内存主频越高计算越快。FFT运算需要缓冲区点数越多对RAM需求越大。例如1024点实数FFT输入输出缓冲区就需要至少1024 * 4字节 4KB的RAM使用float类型。推荐入门型号STM32F407或STM32F429。它们主频高168MHz以上带有FPU和DSP指令RAM足够192KB性价比高资料丰富。3.2 软件工程配置以STM32CubeIDE和HAL库为例关键配置步骤如下时钟树配置将系统时钟SYSCLK配置到芯片允许的最高频率以提升整体性能。ADC配置采样频率Fs根据奈奎斯特定理Fs必须大于你感兴趣的最高信号频率的两倍。例如想分析1kHz以下的信号Fs至少需要2kHz。通常设置到信号最高频率的5-10倍以获得较好波形。在CubeMX中这通过ADC的时钟分频和采样周期来设定。触发方式推荐使用定时器触发。配置一个定时器如TIM2使其溢出频率等于你想要的Fs。然后将ADC的触发源设置为该定时器的触发输出如TIM2_TRGO。这样可以实现精准、等间隔的采样是保证FFT结果准确性的基础。DMA配置必须启用DMA将ADC配置为DMA循环模式Circular。这样ADC会在定时器的驱动下自动将转换结果通过DMA搬运到你指定的内存数组中完全不需要CPU干预实现了极低开销的数据采集。开启FPU在Project - Properties - C/C Build - Settings - MCU Settings中将Floating-point hardware设置为FPv4-SP-D16对于F4系列。添加CMSIS-DSP库如前所述通过CubeMX添加或手动添加源码。4. 代码实现与核心环节解析4.1 数据采集模块这是整个系统的源头必须稳定可靠。// 宏定义 #define FFT_LENGTH 512 // FFT点数 #define ADC_BUFF_LEN (FFT_LENGTH * 2) // DMA缓冲区长度通常是FFT点数的两倍用于乒乓操作 // 全局变量 float32_t adc_buffer[ADC_BUFF_LEN]; // ADC原始值缓冲区 float32_t fft_input[FFT_LENGTH]; // FFT输入缓冲区 float32_t fft_output[FFT_LENGTH]; // FFT输出缓冲区 volatile uint8_t half_buffer_flag 0; // 半缓冲区标志 // ADC DMA半传输/传输完成中断回调函数 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { half_buffer_flag 1; // 前半缓冲区就绪 } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { half_buffer_flag 2; // 后半缓冲区就绪 } // 主循环中的数据搬运 void Process_ADC_Data(void) { if(half_buffer_flag ! 0) { uint32_t start_idx (half_buffer_flag 1) ? 0 : ADC_BUFF_LEN/2; // 将ADC原始值0-4095转换为电压值或直接用于FFT的浮点数 for(int i0; iFFT_LENGTH; i) { // 方法1直接使用如果关心相对幅值 // fft_input[i] (float32_t)adc_buffer[start_idx i]; // 方法2转换为电压更直观 // fft_input[i] ((float32_t)adc_buffer[start_idx i]) / 4095.0f * 3.3f; // 方法3去除直流分量非常重要 static float32_t dc_offset 1.65f; // 假设1.65V是直流偏置 fft_input[i] ((float32_t)adc_buffer[start_idx i]) / 4095.0f * 3.3f - dc_offset; } half_buffer_flag 0; // 清除标志 // 此时fft_input中已经准备好了待处理的数据可以触发FFT计算 Do_FFT_Calculation(); } }注意事项去除直流分量去趋势是FFT前至关重要的一步。ADC采集的信号往往包含一个直流偏置比如信号在1.65V上下波动。这个直流分量在频域会体现在0Hz处一个巨大的谱线可能会淹没你关心的低频小信号。简单的做法是减去采集信号的平均值。4.2 FFT计算模块这是核心处理单元。#include arm_math.h #include arm_const_structs.h // 包含FFT结构体定义 // 定义FFT实例结构体 arm_rfft_fast_instance_f32 fft_instance; // FFT初始化 void FFT_Init(void) { arm_rfft_fast_init_f32(fft_instance, FFT_LENGTH); } // 执行FFT计算 void Do_FFT_Calculation(void) { // 执行实数FFT // fft_input 是输入实数数组长度FFT_LENGTH // fft_output 是输出复数数组长度FFT_LENGTH但其存储格式是特殊的 arm_rfft_fast_f32(fft_instance, fft_input, fft_output, 0); // 最后一个参数0表示正向FFT // 计算幅值谱 Compute_Magnitude(); }4.3 幅值谱计算与解读FFT的直接输出fft_output是一个复数数组存储格式为[实部0, 实部1, 虚部1, 实部2, 虚部2, ..., 实部N/2, 虚部N/2]。其中实部0对应直流分量0Hz实部N/2对应奈奎斯特频率分量Fs/2。其余点共轭对称所以我们只需要前N/21个点。float32_t fft_mag[FFT_LENGTH/2 1]; // 幅值谱数组 void Compute_Magnitude(void) { // 计算直流分量0Hz的幅值 fft_mag[0] fft_output[0] / FFT_LENGTH; // 注意需要除以N // 计算中间频率点的幅值 for(uint16_t i1; i FFT_LENGTH/2; i) { float32_t real fft_output[2*i]; float32_t imag fft_output[2*i 1]; // 幅值 sqrt(real^2 imag^2) / (N/2) // 除以N/2是为了将幅值校正为实际物理幅值对于实数FFT arm_sqrt_f32(real*real imag*imag, fft_mag[i]); fft_mag[i] fft_mag[i] * 2.0f / FFT_LENGTH; } // 计算奈奎斯特频率点Fs/2的幅值 fft_mag[FFT_LENGTH/2] fft_output[1] / FFT_LENGTH; // 此时fft_mag[i] 对应的频率是freq i * (Fs / FFT_LENGTH) }如何解读fft_mag数组假设采样频率Fs 1000 HzFFT_LENGTH 512。fft_mag[0]对应0Hz直流分量的幅值。fft_mag[1]对应1 * (1000/512) ≈ 1.95 Hz的频率分量的幅值。fft_mag[10]对应10 * (1000/512) ≈ 19.53 Hz的频率分量的幅值。fft_mag[256]对应256 * (1000/512) 500 Hz即Fs/2的频率分量的幅值。如果你输入的是一个标准的1kHz正弦波但注意根据奈奎斯特定理1kHz对于1000Hz的Fs来说已经无法被正确采样你应该会在对应的频点看到一个尖峰。4.4 主程序流程整合int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); // 配置ADC和定时器触发 MX_TIM2_Init(); // 配置触发定时器 FFT_Init(); // 初始化FFT // 校准ADC可选但推荐 HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED); // 启动ADC开始DMA采集 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUFF_LEN); HAL_TIM_Base_Start(htim2); // 启动触发定时器 while (1) { Process_ADC_Data(); // 处理采集到的数据 // 可以将fft_mag数组通过串口发送到上位机显示或者本地进行门限判断等 Send_Spectrum_To_UART(); HAL_Delay(100); // 控制频谱更新速率 } }5. 性能优化与精度提升技巧在资源受限的单片机上做FFT优化是永恒的主题。5.1 计算速度优化使用CMSIS-DSP库并开启优化确保编译器优化等级设置为-O2或-O3。CMSIS-DSP库在开启优化后会大量使用内联函数和SIMD指令。合理选择数据类型对于有FPU的芯片全程使用float单精度浮点。对于没有FPU但需要一定速度的场合CMSIS-DSP也提供了q15和q31格式的定点数FFT函数如arm_rfft_q15但需要处理数据的定标复杂度较高。减少非必要计算幅值谱计算中的开方运算arm_sqrt_f32比较耗时。如果只需要比较幅值大小如寻找峰值频率可以直接比较real^2 imag^2的值省去开方和除法这在实时性要求高的场景非常有效。利用双缓冲区与DMA如前所述使用DMA双缓冲区乒乓缓冲区可以实现采集与处理的完全并行CPU只需在缓冲区切换时搬运数据最大化利用计算时间。5.2 频率精度与频谱泄漏处理频谱泄漏是FFT的一个固有现象。如果输入信号的频率不是频率分辨率的整数倍其能量会“泄漏”到相邻的频点上导致谱线变宽、幅值不准。解决方案加窗函数在FFT前将采样数据乘以一个窗函数如汉宁窗Hamming、汉明窗Hanning可以抑制频谱泄漏。// 应用汉宁窗 for(int i0; iFFT_LENGTH; i) { float32_t window 0.5f * (1.0f - arm_cos_f32(2 * PI * i / (FFT_LENGTH - 1))); // 汉宁窗公式 fft_input[i] fft_input[i] * window; } // 然后再进行FFT注意加窗会降低幅值精度并且需要额外的计算。通常对于周期性明显的信号或者进行精确幅值测量时使用。对于单纯的频率检测可以不加窗或使用较简单的窗。5.3 提高频率分辨率Zoom-FFT思路如果关心的信号频带很窄比如只关心50Hz±5Hz但采样频率又必须很高比如10kHz以抗混叠直接做1024点FFT分辨率也只有约9.8Hz。这时可以采用Zoom-FFT的思路将原始信号下变频数字混频到基带。进行低通滤波和抽取降低有效采样率。对降采样后的信号做FFT。 这样可以在不增加FFT点数的情况下大幅提高目标频带内的频率分辨率。但这在单片机上实现较为复杂需要额外的滤波器和抽取器。6. 常见问题与调试实录在实际操作中你肯定会遇到各种奇怪的现象。下面是我踩过的一些坑和解决方法。6.1 频谱结果全是噪声或不对现象可能原因排查步骤与解决方案频谱杂乱没有明显峰值1. ADC采样时钟或触发不稳定2. 信号本身噪声太大3. 未去除直流分量1.检查定时器配置确保定时器触发ADC的间隔绝对均匀。用示波器测量ADC的转换完成引脚或使用一个GPIO在采样中断中翻转来观察节奏。2.检查信号源输入一个已知频率和幅值的纯净正弦波如信号发生器产生进行测试。3.在fft_input赋值前增加一行减去平均值的代码。频谱峰值频率与预期严重不符1. 采样频率Fs计算错误2. 频率索引计算错误1.重新计算FsFs 定时器时钟 / (PSC1) / (ARR1)。用逻辑分析仪确认ADC的实际采样间隔。2.检查幅值谱下标i到频率freq的转换公式freq i * Fs / FFT_LENGTH。只有一个巨大的0Hz峰值其他频率幅值很小直流分量过大严格进行去直流处理。计算fft_input数组的平均值并减去。确保信号是交流耦合的或者在硬件上使用隔直电容。频谱出现镜像频率如输入100Hz在900Hz也出现峰值发生了频谱混叠确保采样频率Fs大于信号最高频率的2倍。在ADC前端增加一个抗混叠滤波器低通滤波器其截止频率略低于Fs/2。6.2 计算速度慢系统卡顿问题执行FFT时其他中断响应变慢或者幅值计算耗时过长。排查检查优化等级确认工程编译选项为-O2。使用性能分析在FFT函数前后用GPIO翻转用示波器测量高电平时间即为FFT计算耗时。检查数据类型确认使用的是单精度浮点float32_t并且链接了FPU库。解决如果FFT计算耗时超过采样周期考虑降低FFT点数如从1024降到512。幅值计算中如非必要省去开方和除法只计算模的平方进行比较。将FFT计算放在低优先级任务或主循环中确保高优先级中断如电机控制不被阻塞。6.3 内存不足问题编译通过但运行时报错或行为异常可能是堆栈溢出。排查在CubeIDE的Project - Properties - C/C Build - Settings - Tool Settings - MCU Settings中查看和调整Heap Size和Stack Size。FFT的大数组是全局变量在.bss段不占用堆栈但库函数内部可能会用到动态内存或较大的栈空间。解决适当增加堆栈大小例如将Stack Size从0x400增加到0x1000。减少FFT点数。将大型数组如fft_input,fft_output用__attribute__((section(.ccmram)))放到CCM RAM如果芯片有中可以加速访问并减轻主RAM负担。6.4 与上位机联调技巧将计算出的fft_mag数组通过串口发送到电脑用PythonMatplotlib或LabVIEW等工具绘图是直观的调试方法。// 简单的串口发送函数 void Send_Spectrum_To_UART(void) { printf(FFT_MAG_START\n); // 发送起始标志 for(int i0; iFFT_LENGTH/21; i) { printf(%.4f\n, fft_mag[i]); // 发送幅值每行一个 } printf(FFT_MAG_END\n); // 发送结束标志 }在电脑端你可以编写一个脚本不断读取串口数据识别START和END标志然后将中间的数据绘制成动态的频谱图。这能让你实时观察信号频谱的变化对于验证算法和调试硬件问题无比重要。7. 进阶应用从频谱到实际功能得到频谱只是第一步如何利用它才是关键。7.1 峰值检测与频率估计找出幅值谱fft_mag中的前N个最大值点其对应的索引通过公式换算即可得到主要频率成分。为了提高频率估计精度特别是当真实频率落在两个FFT频点之间时可以采用插值算法如重心法、抛物线拟合法。// 简单的峰值检测寻找最大值 uint16_t find_peak_frequency(float32_t *mag, uint16_t len, float32_t fs, uint16_t *peak_index) { float32_t max_val 0.0f; uint16_t max_idx 0; // 忽略直流分量0Hz从第1个点开始找 for(uint16_t i1; ilen; i) { if(mag[i] max_val) { max_val mag[i]; max_idx i; } } *peak_index max_idx; // 计算峰值频率 float32_t peak_freq (float32_t)max_idx * fs / FFT_LENGTH; return (uint16_t)peak_freq; }7.2 谐波分析对于电力或电机应用基波如50Hz的谐波100Hz, 150Hz...含量是重要指标。在得到基波频率f0后只需在频谱中查找2*f0,3*f0等对应频点附近的幅值即可。计算总谐波畸变率THD需要将所有谐波幅值的平方和开方再除以基波幅值。7.3 基于频谱的简单分类你可以为不同的“声音”或“振动”模式建立一组“特征频谱模板”。当设备运行时计算实时频谱并与模板频谱进行比对如计算欧氏距离、相关系数找出最匹配的模板从而实现状态分类或故障诊断。这需要离线学习建立模板并在单片机上实现简单的模式匹配算法。在STM32上实现傅里叶变换最难的不是调用那个arm_rfft_fast_f32函数而是理解整个数据流的来龙去脉以及如何让ADC、定时器、DMA、内存和CPU协同工作稳定地生产出可供FFT处理的“原料”。另一个难点是对结果的解读和优化频谱泄漏、栅栏效应这些概念需要在实际的调试波形中才能真正理解。我的建议是先用信号发生器产生一个干净的正弦波确保你的系统能正确地在频谱上显示一个单峰这是所有后续工作的基石。然后再逐步引入更复杂的真实信号去处理噪声、泄漏和实时性的挑战。这个过程本身就是对嵌入式系统开发能力的一次极好锻炼。