☰
STM32F4 FFT测量:高精度信号频率、幅值与相位分析实战
2026/10/20 12:57:54 网站建设 项目流程

简介:本资源是一套基于STM32F4系列MCU实现正弦波信号高精度参数测量的完整嵌入式工程,面向嵌入式开发工程师、电子类专业学生及数字信号处理实践者,解决工业检测、电力监测、传感器校准等场景中对幅值、频率与相位差的实时、精准测量需求。压缩包共109个文件(50个.h头文件定义外设驱动与FFT接口,49个.c源文件含ADC采样、TIM定时控制、LCD显示、RCC时钟配置及核心FFT计算逻辑),辅以Keil MDK工程文件(uvprojx/uvoptx)、批处理脚本(keilkilll.bat)及生成固件(hex),整体体积仅574KB,结构清晰、模块解耦度高,便于移植与二次开发。已有741人学习下载,配套代码已实测运行于STM32F407平台,涵盖从模拟信号采集、抗混叠预处理、2048点FFT频谱分析到峰值搜索与相位解算的全流程,提供可直接编译调试的完整工程框架与关键算法注释。

1. 项目缘起:为什么要在STM32F4上做FFT测量?

在嵌入式开发,尤其是工业控制、电力监测、音频处理等领域,我们常常需要实时分析一个模拟信号的“成分”。比如,你想知道一个来自传感器的正弦波信号,它的电压到底有多大(幅值),它一秒钟内振动了多少次(频率),或者两个信号之间谁先谁后(相位差)。最直接的想法可能是用单片机自带的ADC(模数转换器)采集一些点,然后找最大值、数过零点。这个方法对付理想、干净、单一频率的信号还行,一旦信号里混入了一点噪声,或者信号本身不是完美的正弦波,这种时域分析的方法就很容易“翻车”——幅值测不准,频率分辨率低,相位差更是无从谈起。

这时候,频域分析的工具——FFT(快速傅里叶变换)就派上用场了。它能把一个随时间变化的信号,分解成一系列不同频率、不同幅值、不同相位的正弦波的叠加。对于单一频率的正弦波,FFT的结果会在这个频率点上出现一个明显的“尖峰”,我们读取这个尖峰的属性,就能一次性、高精度地得到幅值、频率和相位信息。STM32F4系列MCU,得益于其Cortex-M4内核和硬件FPU(浮点运算单元),进行一定点数的浮点FFT运算速度相当可观,使得在资源受限的嵌入式系统中实现实时的频谱分析成为可能。

所以,这个项目的核心价值就在于:利用STM32F4的硬件性能,通过FFT算法,实现一种比传统时域方法更抗干扰、更精准的交流信号三要素(幅值、频率、相位)测量方案。这对于需要精确监控交流电压电流、进行振动分析、实现锁相环(PLL)等应用场景,是一个很实用的技能点。

2. FFT测量原理与关键参数设计

在动手写代码之前,我们必须搞清楚FFT是怎么工作的,以及几个关键参数如何影响测量精度。如果你只关心代码,跳过这部分直接去抄,很可能遇到结果不对却不知道如何调试的困境。

2.1 FFT到底输出了什么?

我们给FFT算法输入一段长度为N的时域离散信号序列x[n]。经过计算,它输出一个同样长度为N的复数序列X[k]。这里的k就是频率索引,对应着实际的频率f = k * Fs / N。其中,Fs是我们的采样频率。

X[k]是一个复数,它包含了第k个频率分量的全部信息:

  • 复数的模(Magnitude):|X[k]|。它代表了该频率分量的大小,但不是我们直接想要的信号幅值。
  • 复数的辐角(Phase):arg(X[k])。它代表了该频率分量在时间起点(t=0)时的初始相位。

这里有一个至关重要的转换:对于一个纯正弦波A * sin(2πft + φ),经过FFT后(假设使用常见的库函数),其对应频率点k的复数模|X[k]|与实际信号幅值A的关系是:|X[k]| = (A * N) / 2

这是因为FFT的算法本质是对双边频谱进行计算,能量平均分配在了正负频率上(对于实信号,频谱是共轭对称的)。所以,要得到真实的信号幅值A,计算公式为:A = (2 * |X[k]|) / N

相位φ则直接由arg(X[k])得到,但需要注意,arg()函数(通常用atan2(Im, Re)计算)返回的范围是(-π, π]或(0, 2π],需要根据你的应用进行相位解缠绕或转换。

2.2 采样定理与频率分辨率

这是精度保障的基石。

  • 采样频率Fs:必须大于信号最高频率的两倍(奈奎斯特采样定理)。对于50Hz的工频信号,Fs至少需要100Hz。但为了有好的波形重建和抗混叠,通常取信号频率的10倍甚至更高。例如,测量1kHz正弦波,Fs可以设置为10kHz。
  • 采样点数N:这就是FFT的点数,通常是2的整数次幂(如256, 512, 1024, 2048)。N决定了两个关键指标:
    1. 频率分辨率Δf = Fs / N:这是FFT能区分开的最小频率间隔。Δf越小,你定位信号频率就越精确。如果你的信号频率是50.5Hz,但Δf=1Hz,那么FFT结果可能会在50Hz和51Hz两个点上都有能量(频谱泄漏),导致频率和幅值测量都不准。要想精确测量,信号频率最好是Δf的整数倍。
    2. 计算量与RAM占用:N越大,计算时间越长,需要的RAM也越多。STM32F407的RAM约192KB,做1024点浮点FFT(输入输出数组各需4KB)绰绰有余,但做4096点就要仔细规划内存了。

2.3 频谱泄漏与加窗函数

理想情况下,如果我们采集了整数个信号周期,那么FFT后能量会完美集中在一个频率点上。但现实中,几乎无法做到同步采样(即恰好采集整数个周期)。非整数周期采样会导致信号在截断的边界处不连续,FFT结果中,能量会“泄漏”到旁边的频率点上,造成主瓣展宽、旁瓣增高。

为了抑制泄漏,我们需要对采样到的时域数据加一个“窗函数”(Window Function),在数据两端逐步衰减到0,减少截断带来的突变。常用的窗函数有:

  • 汉宁窗(Hann):最常用的窗,能有效抑制旁瓣,但主瓣稍宽。适用于大多数频谱分析场景。
  • 平顶窗(Flat Top):幅值测量精度最高,因为它的主瓣非常平坦,但对频率分辨率影响最大。如果你最关心幅值精度,平顶窗是首选。
  • 矩形窗(Rectangular):即不加窗。仅当确信是同步采样时使用,否则泄漏严重。

注意:加窗函数在抑制泄漏的同时,也会改变信号的幅值。因此,用加窗后的数据计算幅值时,需要进行窗函数增益补偿。每个窗都有一个固定的相干增益(Coherent Gain)或幅度校正因子(Amplitude Correction Factor),需要查表获得。例如,汉宁窗的补偿因子约为1.63(即计算出的幅值要乘以1.63才能接近真实值)。

3. STM32F4上的工程实现:从ADC到FFT结果

理论铺垫完毕,我们进入实战环节。我将基于STM32CubeMX和HAL库,搭建一个完整的测量系统。

3.1 硬件与软件环境准备

  • MCU:以STM32F407VET6为例(拥有硬件FPU)。
  • 开发环境:STM32CubeIDE 或 Keil MDK。
  • 关键外设:
    • ADC:用于采集模拟信号。配置为连续扫描模式,使用DMA将数据搬运到内存数组,避免CPU干预。
    • 定时器TIM:用于触发ADC采样,确保采样间隔Ts = 1/Fs绝对精确。这是高精度测量的关键,不能用软件延时!
    • 串口USART:用于将计算结果打印到PC端验证。

CubeMX配置要点:

  1. 启用FPU:在Project Manager -> Code Generator中,勾选Copy all used libraries into the project folder和Generate peripheral initialization as a pair of ‘.c/.h’ files。在Project -> Settings -> Target中,确认Floating Point Hardware为Single Precision。
  2. 配置一个定时器(如TIM2)作为主时钟源,产生更新中断,周期设置为(TIM_CLK / (PSC+1)) / ARR = Fs。
  3. 配置ADC1,选择规则通道,设置采样时间(根据信号频率调整,确保采样保持时间足够)。在ADC Settings -> Data Alignment选择Right alignment。在DMA Settings添加一个DMA请求,模式设为Circular(循环模式),这样ADC就会在定时器触发下,源源不断地通过DMA向数组填数据。
  4. 配置一个串口用于调试。

3.2 软件架构与代码解析

工程主要包含以下几个模块:

main.c/主循环:

// 定义FFT点数,必须是2的幂 #define FFT_LENGTH 1024 // 定义采样频率 #define SAMPLING_FREQ 10000.0f // 10kHz // 定义复数结构体,ARM CMSIS-DSP库需要 typedef struct { float32_t real; float32_t imag; } Complex_t; // 声明全局缓冲区 Complex_t fftInputBuf[FFT_LENGTH]; Complex_t fftOutputBuf[FFT_LENGTH]; float32_t adcRawBuf[FFT_LENGTH]; // ADC原始值缓冲区 volatile uint8_t dma_buffer_full = 0; // DMA完成标志 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); MX_TIM2_Init(); // 定时器触发ADC MX_USART1_UART_Init(); // 初始化CMSIS-DSP库的FFT实例(针对实数输入,输出复数) arm_rfft_fast_instance_f32 fft_instance; arm_rfft_fast_init_f32(&fft_instance, FFT_LENGTH); // 启动ADC的DMA传输,目标地址是adcRawBuf,长度FFT_LENGTH HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcRawBuf, FFT_LENGTH); // 启动定时器,开始以精确频率触发ADC HAL_TIM_Base_Start(&htim2); while (1) { // 等待DMA采集完一整帧数据 if(dma_buffer_full) { dma_buffer_full = 0; // 1. 数据预处理:ADC值转电压,并转换为CMSIS-DSP需要的格式 for(int i=0; i<FFT_LENGTH; i++) { // 假设ADC为12位,参考电压3.3V float32_t voltage = (adcRawBuf[i] / 4095.0f) * 3.3f; // 减去直流偏置(假设信号是交流,以0V为中心) // 在实际中,可能需要先计算一段数据的平均值作为直流偏置 static float32_t dc_offset = 1.65f; // 例如,假设偏置在1.65V voltage -= dc_offset; // CMSIS-DSP的实数FFT要求输入数据按特定顺序排列在复数数组中 // 对于arm_rfft_fast_f32,我们直接填充到fftInputBuf的实部,虚部为0 fftInputBuf[i].real = voltage; fftInputBuf[i].imag = 0.0f; } // 2. 可选:加窗处理(以汉宁窗为例) float32_t window[FFT_LENGTH]; arm_hanning_f32(window, FFT_LENGTH); for(int i=0; i<FFT_LENGTH; i++) { fftInputBuf[i].real *= window[i]; } // 3. 执行FFT arm_rfft_fast_f32(&fft_instance, (float32_t *)fftInputBuf, (float32_t *)fftOutputBuf, 0); // 4. 计算幅值谱 float32_t fftMag[FFT_LENGTH/2 + 1]; // 实数FFT的幅值谱长度 arm_cmplx_mag_f32((float32_t *)fftOutputBuf, fftMag, FFT_LENGTH/2 + 1); // 5. 寻找主频峰 uint32_t maxMagIndex; arm_max_f32(fftMag, FFT_LENGTH/2 + 1, &fftMag[maxMagIndex], &maxMagIndex); // 6. 计算频率、幅值、相位 float32_t measured_freq = (float32_t)maxMagIndex * SAMPLING_FREQ / (float32_t)FFT_LENGTH; // 幅值计算:考虑FFT系数和窗函数补偿 float32_t measured_amplitude = (2.0f * fftMag[maxMagIndex]) / (float32_t)FFT_LENGTH; // 汉宁窗补偿因子约为1.63 measured_amplitude *= 1.63f; // 相位计算 float32_t phase = atan2f(fftOutputBuf[maxMagIndex].imag, fftOutputBuf[maxMagIndex].real); // 7. 通过串口打印结果 printf("Freq: %.2f Hz, Amp: %.3f V, Phase: %.3f rad\n", measured_freq, measured_amplitude, phase); // (可选)重新启动DMA,进行下一帧采集 // 由于DMA是循环模式,我们只需要重置标志位,数据会自动覆盖 } } } // ADC DMA传输完成回调函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc->Instance == ADC1) { dma_buffer_full = 1; } }

stm32f4xx_it.c: 需要确保DMA和定时器中断被正确使能和处理。

3.3 核心步骤拆解与注意事项

  1. 精确采样定时:使用定时器触发ADC是必须的。软件触发或不规则采样会导致Fs不稳定,直接摧毁FFT的频率基准,结果毫无意义。
  2. 直流偏置去除:如果信号有直流分量(比如ADC采集的电压范围是0-3.3V,而信号在1.65V上下波动),必须减去这个直流分量,否则巨大的零频(DC)分量会干扰其他频率的测量。可以在预处理时减去一个固定值,或者更精确地,计算采集缓冲区adcRawBuf的平均值作为实时直流偏置。
  3. 使用优化的CMSIS-DSP库:ARM提供的CMSIS-DSP库针对Cortex-M4/M7做了高度优化,特别是使用了SIMD指令,速度比你自己写的FFT代码快一个数量级。务必在工程中正确添加该库。
  4. 幅值计算的细节:
    • 公式A = (2 * |X[k]|) / N是基础。
    • 如果加了窗,一定要乘以该窗的幅度补偿因子。这个因子是窗函数所有点的平均值。汉宁窗是0.5,其倒数就是2,但通常的补偿因子是考虑能量归一化后的值,1.63是个经验近似值,最准确的方法是查表或计算N / sum(window)。
    • 如果你的信号是A * cos(ωt + φ),上述公式适用。如果是A * sin(ωt + φ),相位结果会有一个π/2的固定偏移,需要注意。
  5. 相位测量的参考点:atan2(Im, Re)得到的相位,是相对于本次FFT分析数据块起始时刻(t=0)的相位。如果你要测量两个通道信号(比如电压和电流)的相位差,必须确保两个通道是同步采样的(使用ADC的双重或三重模式,由同一个定时器触发),然后分别计算它们的相位φ1和φ2,相位差Δφ = φ1 - φ2。这样能消除采样启动时间不同步带来的误差。

4. 精度提升实战:解决频谱泄漏与栅栏效应

即使我们按照上述步骤操作,测量结果可能仍然有误差,尤其是在频率和幅值上。问题主要来自“非同步采样”和“栅栏效应”。

4.1 问题现象与根因分析

  • 现象:输入一个频率为1000.5Hz的正弦波,采样频率Fs=10000Hz,点数N=1024。频率分辨率Δf = 10000/1024 ≈ 9.77Hz。1000.5Hz对应的理论谱线位置k = 1000.5 / 9.77 ≈ 102.4,这不是整数。FFT结果只能在k=102(对应997.1Hz) 和k=103(对应1006.8Hz) 两个整数索引上输出,导致:
    1. 频率测量误差:最大可能误差有±Δf/2。
    2. 幅值测量误差:能量分散在两条谱线上,主谱线幅值低于真实值。
    3. 相位测量误差:同样因为能量分散,主谱线相位也不准。

这就是“栅栏效应”——就像通过栅栏看风景,你只能看到特定间隔(Δf)上的情况,看不到栅栏缝隙后的真实峰值。

4.2 解决方案:插值算法

为了解决非整数次谐波问题,我们需要在FFT结果的基础上进行“插值”,估算出真实峰值的频率、幅值和相位。常用方法是双谱线插值法,它利用主峰和其旁边最大的一根谱线(左右均可)进行估算。

假设我们找到的最大幅值谱线索引为k_m,其幅值为y_m。其旁边较大的那根谱线索引为k_{m+1}(或k_{m-1}),幅值为y_{m+1}。令α = y_{m+1} / y_m。

对于不同的窗函数,修正公式不同。以汉宁窗为例:

  1. 频率修正:δ = (2α - 1) / (α + 1)修正后的归一化频率偏移:Δ = δ真实频率:f = (k_m + Δ) * (Fs / N)

    注意:Δ的范围在(-0.5, 0.5)之间,表示真实峰值在两条谱线之间的相对位置。

  2. 幅值修正: 汉宁窗的幅值修正函数相对复杂,但一个常用的近似公式是:A_corrected = (2 * y_m) / (N * (0.5 + 0.5*cos(πΔ)) )这个公式考虑了能量从主谱线泄漏到旁谱线的情况。实际上,更严谨的做法是使用窗函数的频谱函数进行精确计算,但对于汉宁窗,上述近似在|Δ| < 0.5时效果很好。

  3. 相位修正: 相位也需要插值修正。主谱线相位为φ_m,相邻谱线相位为φ_{m+1}。修正后的相位约为:φ_corrected = φ_m + πΔ * (N-1)/N这个修正考虑了因非同步采样造成的相位旋转。

代码实现片段:

// 在找到maxMagIndex后,判断使用左边还是右边的谱线进行插值 uint32_t k_m = maxMagIndex; float32_t y_m = fftMag[k_m]; float32_t y_side; int32_t delta_k; // 偏移方向,1或-1 // 判断主峰旁边的较大谱线 if(k_m == 0) { // 如果是直流分量,只取右边 delta_k = 1; y_side = fftMag[k_m+1]; } else if(k_m == (FFT_LENGTH/2)) { // 如果是奈奎斯特频率点,只取左边 delta_k = -1; y_side = fftMag[k_m-1]; } else { // 比较左右两边,取幅值大的那一边 if(fftMag[k_m-1] > fftMag[k_m+1]) { delta_k = -1; y_side = fftMag[k_m-1]; } else { delta_k = 1; y_side = fftMag[k_m+1]; } } // 计算幅值比α float32_t alpha = y_side / y_m; // 计算频率偏移量Δ (针对汉宁窗) float32_t delta = (2.0f * alpha - 1.0f) / (alpha + 1.0f); // 计算精确频率 float32_t exact_freq = (k_m + delta_k * delta) * SAMPLING_FREQ / FFT_LENGTH; // 计算精确幅值(汉宁窗近似修正) float32_t window_correction = 0.5f + 0.5f * cosf(PI * delta); float32_t exact_amplitude = (2.0f * y_m) / (FFT_LENGTH * window_correction); // 别忘了乘以窗函数的幅度补偿因子 exact_amplitude *= 1.63f; // 计算精确相位(需要获取复数输出) Complex_t X_m = fftOutputBuf[k_m]; Complex_t X_side = fftOutputBuf[k_m + delta_k]; float32_t phase_m = atan2f(X_m.imag, X_m.real); float32_t phase_side = atan2f(X_side.imag, X_side.real); // 简单的线性插值修正(更复杂的修正需要根据窗函数频谱) float32_t exact_phase = phase_m + delta * (phase_side - phase_m);

通过引入双谱线插值,我们可以将频率测量精度提升到远高于Δf的水平,幅值和相位的精度也得到显著改善。这是工程上实现高精度FFT测量的关键一步。

5. 系统优化与常见问题排查

一个能跑起来的Demo和一个稳定可靠的产品之间,还有不少距离。下面分享几个实战中的优化点和踩坑经验。

5.1 内存管理与实时性优化

  • 使用__attribute__((section(".ram2")))或CCM RAM:STM32F4的RAM分为多个块。将大型的FFT输入输出数组(特别是复数数组)放到速度最快的RAM区域(如CCM RAM,如果芯片支持),可以显著提升计算速度,同时避免对主RAM带宽的争抢。
    Complex_t fftInputBuf[FFT_LENGTH] __attribute__((section(".ccmram")));
  • 双缓冲DMA策略:前面的例子使用了循环DMA和标志位,这在处理一帧数据时,下一帧数据可能被覆盖。对于严格的实时连续处理,建议使用双缓冲(Ping-Pong Buffer)。配置DMA为双缓冲模式,或者手动管理两个缓冲区,当DMA填满缓冲区A时,触发中断,CPU处理A,同时DMA向缓冲区B填充数据。
  • FFT计算时间评估:使用1024点浮点FFT(arm_rfft_fast_f32)在STM32F407(168MHz)上大约需要0.5ms。你需要确保一帧数据的采集时间(N/Fs)远大于FFT计算时间,否则系统会跟不上。例如,Fs=10kHz, N=1024,采集一帧需要102.4ms,计算只用0.5ms,绰绰有余。

5.2 抗混叠滤波与ADC配置

  • 硬件抗混叠滤波器:这是必须的!如果信号中包含了高于Fs/2的频率成分,它们会“混叠”到低频段,污染你的测量结果。必须在ADC输入端加入一个低通滤波器(RC电路或有源滤波器),其截止频率略低于Fs/2。
  • ADC采样时间:ADC_SampleTime必须设置合理。采样时间太短,ADC内部的采样保持电容充电不足,会导致精度下降。采样时间太长,则限制了可用的最高采样频率。需要根据信号源阻抗和精度要求,参考数据手册计算。一个经验值是,对于中等阻抗源(<10kΩ),将采样时间设置为ADC时钟周期的几个周期(如84MHz ADC时钟下,设置SAMPLETIME_15CYCLES)通常能取得较好效果。

5.3 调试技巧与问题排查清单

当你发现测量结果不对时,可以按以下清单排查:

  1. 信号源本身是否正确?用示波器直接测量输入到MCU引脚上的信号,确认其幅值、频率、波形是否如预期。
  2. ADC采集的数据对吗?在预处理之前,通过串口将adcRawBuf的原始值打印出来,画成波形图(可以用Excel、Python的Matplotlib或专业的串口绘图工具)。检查波形是否连续,幅值范围是否在ADC量程内,有没有明显的畸变。
  3. 采样频率Fs准确吗?检查定时器的配置计算是否正确。用示波器测量定时器触发ADC的引脚(如果可用),或者测量一个GPIO翻转的周期,来验证Fs是否精确。
  4. FFT输入数据格式对吗?确认你传递给arm_rfft_fast_f32的数组格式是否正确。对于实数FFT,输入数组是长度为N的浮点数组,输出是长度为N的复数数组(实部虚部交错存储)。仔细阅读CMSIS-DSP文档。
  5. 幅值计算公式用对了吗?再次核对公式:A = (2 * |X[k]|) / N。确认|X[k]|是复数模,而不是实部或虚部。确认是否加了窗以及是否做了窗补偿。
  6. 频率索引k找对了吗?打印出整个fftMag数组,找到最大值的位置k,计算其对应的频率f = k * Fs / N,看是否在信号频率附近。
  7. 频谱泄漏严重吗?观察fftMag的频谱图。如果主峰很“胖”,向两边拖尾很长,说明泄漏严重。尝试增加采样点数N,或者确保信号频率接近Δf的整数倍,或者使用更合适的窗函数。
  8. 相位结果合理吗?相位对噪声非常敏感。如果信号信噪比不高,相位结果可能会跳动很大。可以尝试对多帧数据的相位结果进行平均,或者提高信号幅值(在ADC量程内),降低噪声。

5.4 从单频信号到多频信号与谐波分析

本项目聚焦于单一正弦波测量,但FFT的能力远不止于此。如果你的信号包含多个频率成分(比如电网谐波分析),上述方法依然适用,只需遍历fftMag数组,找到所有超过一定阈值的峰值点,对每个峰值点分别应用幅值、频率、相位的计算公式和插值修正即可。

对于谐波分析,特别需要注意:

  • 动态范围:基波(如50Hz)的幅值可能远大于高次谐波(如250Hz的5次谐波)。为了准确测量小信号谐波,需要确保ADC有足够的位数(如16位ADC比12位好),并且FFT计算使用浮点数以保持精度。
  • 同步采样仍然关键:对于谐波分析,非同步采样造成的频谱泄漏会导致谐波间相互干扰。此时,使用锁相环(PLL)电路来锁定信号频率,并动态调整ADC采样时钟,使其始终是信号频率的整数倍,是最高精度的方案,当然也增加了硬件复杂度。

我个人在做一个电机电流谐波分析项目时,就曾因为忽略抗混叠滤波器,导致高频开关噪声混叠到低频段,误以为是低频谐波,排查了很久。另一个教训是,相位测量时,两个ADC通道的采样保持时间如果存在微小差异(即使由同一个定时器触发),也会引入固定的相位误差,需要在软件中进行校准。这些细节,往往是书本上不会写,但实际项目中一定会遇到的“坑”。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询