资讯动态

STM32标准库FFT实战:精准测量正弦信号幅值、频率与相位差

发布时间:2026/10/17 14:08:17 来源:尧图企业网站定制
简介本资源面向嵌入式开发工程师与信号处理学习者提供在STM32F407平台上基于标准库实现FFT算法的完整工程用于对正弦波信号进行频域分析精准测量幅值、频率与相位差适用于电机控制、音频分析与无线通信等实时信号处理场景。压缩包共283个文件约9.98MB以C源码与头文件为核心包含标准外设库驱动、工程配置与编译产物并附带hex固件、map映射文件及少量说明文档便于直接编译验证与二次开发。资源已积累1665人学习下载具备一定参考热度。工程完整呈现了从ADC采样、窗函数处理到FFT变换与频谱分析的实现链路读者可据此掌握频域测量方法理解频谱泄露抑制与结果输出思路并借助STM32F407的FPU加速复杂运算快速搭建自己的信号分析原型。1. 从一块跑不出正确相位的板子说起STM32 标准库 FFT 测幅值、频率与相位差前阵子帮一个做电机控制的朋友看代码他用 STM32F103 采两路正弦信号想同时算出幅值、频率和两路之间的相位差结果频率和幅值勉强能看相位差却像玄学一样乱跳换个采样率又变一个值。问题不在 FFT 本身而在于他直接套了网上那份“FFT 测幅频相”的模板没处理实信号频谱的对称性、没做窗函数补偿、也没把相位差归一到同一参考点。这份资源就是围绕这个场景来的在 STM32 上用标准库Standard Peripheral Library实现 FFT对正弦波信号做频域分析输出幅值、频率和两路信号的相位差。它适合正在做电参数测量、音频分析、振动检测又不想上 DSP 库或换 HAL 的开发者。整套代码基于定点/浮点混合的 FFT 实现配合 ADC 采样和串口输出能直接烧进板子跑。2. 项目定位与技术选型为什么是标准库 定点 FFT2.1 这个项目到底解决什么问题它要解决的核心问题是在资源有限的 STM32 上不依赖外部 DSP 芯片也不依赖 ARM 的 CMSIS-DSP 库仅用标准外设库和一片 ADC完成对正弦信号的频域参数提取。具体输出三个量——幅值、频率、相位差。幅值和频率来自 FFT 结果的峰值谱线相位差来自两路信号在同一频率点上的相位角之差。这类需求在电参数测量里很常见比如测交流信号的电压电流相位差来算功率因数或者测两路正弦的相位关系判断传感器是否同步。用 FFT 而不是过零检测好处是抗噪能力强能同时拿到多个频率分量缺点是计算量和内存占用上来了。所以这个项目的定位很明确不是做通用频谱仪而是做“已知是正弦信号”的精准参数测量。2.2 为什么选标准库而不是 HAL 或 CMSIS-DSP标准库在 STM32F1/F4 的老项目里存量极大很多产线代码、教学代码、电机控制代码都是标准库写的。换 HAL 意味着重写外设初始化换 CMSIS-DSP 意味着引入额外依赖和版本适配。这个项目选择标准库是为了让读者能直接嵌进现有工程不用动底层框架。FFT 算法本身没有用 CMSIS-DSP 的arm_cfft_f32而是自己实现了一套基 2 的定点/浮点混合 FFT。原因有两个一是标准库工程里加 CMSIS-DSP 需要手动配头文件路径和宏容易翻车二是自己实现能控制精度和内存方便在 F103 这种 20KB RAM 的片子上跑 256 点或 512 点 FFT。常见做法是采样点数取 2 的整数次幂用float存旋转因子用int16_t存 ADC 采样值蝶形运算时转成float计算最后再转回。2.3 关键参数与资源占用参数典型值说明采样点数 N256 / 512 / 1024必须是 2 的幂N 越大频率分辨率越高ADC 分辨率12 位标准库 ADC 默认 12 位采样值 0~4095采样率 fs可配如 10kHz由定时器触发 ADC 决定频率分辨率fs/N例如 10kHz/512 ≈ 19.5HzRAM 占用N×4×2 字节左右实部虚部512 点约 4KB输出接口串口波特率 115200打印幅值/频率/相位差这个资源包里通常包含FFT 核心算法文件、ADC 采样配置、定时器触发配置、串口打印、主循环处理逻辑。文件数不多但每一块都卡在测量精度的关键点上。3. 核心模块拆解从 ADC 采样到相位差输出的完整链路3.1 ADC 定时器采样别用软件延时触发采样环节最容易出的问题是采样率不准。很多人用delay或主循环里读 ADC结果采样间隔抖动FFT 出来的谱线泄漏严重。正确做法是用定时器触发 ADCDMA 搬运保证等间隔采样。// 定时器 TIM2 配置为采样率触发源 void TIM2_Config(u16 arr, u16 psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period arr; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler psc; // 预分频 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 更新事件触发 ADC TIM_Cmd(TIM2, ENABLE); } // ADC 配置为外部触发 DMA void ADC_Config(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; // ... 时钟使能省略 DMA_InitStructure.DMA_PeripheralBaseAddr (u32)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (u32)adc_buf; // 采样缓冲区 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize N; // 采样点数 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); }逻辑说明TIM2 的更新事件作为 ADC 的外部触发每次更新启动一次转换DMA 自动把结果搬到adc_buf。这样采样间隔由定时器决定精度远高于软件循环。参数上arr和psc共同决定采样率fs 72MHz / ((arr1)*(psc1))。比如要 10kHz可以取psc0, arr7199。注意 ADC 时钟不能超过 14MHz标准库下一般用 PCLK2 的 6 分频或 8 分频。3.2 FFT 核心旋转因子与蝶形运算FFT 部分通常包含三个函数旋转因子表生成、位反转、蝶形运算。下面是一个 256 点浮点 FFT 的典型实现片段。#define N 256 float twiddle_real[N/2], twiddle_imag[N/2]; // 生成旋转因子表 void FFT_Init(void) { for (int i 0; i N/2; i) { twiddle_real[i] cosf(2 * PI * i / N); twiddle_imag[i] -sinf(2 * PI * i / N); } } // 位反转重排 void BitReverse(float *real, float *imag) { int j 0; for (int i 0; i N - 1; i) { if (i j) { float tr real[i]; real[i] real[j]; real[j] tr; float ti imag[i]; imag[i] imag[j]; imag[j] ti; } int k N / 2; while (k j) { j - k; k / 2; } j k; } } // 基 2 蝶形运算 void FFT_Compute(float *real, float *imag) { BitReverse(real, imag); for (int len 2; len N; len 1) { int half len 1; int step N / len; for (int i 0; i N; i len) { for (int j 0; j half; j) { int tw j * step; float wr twiddle_real[tw]; float wi twiddle_imag[tw]; float ur real[ijhalf] * wr - imag[ijhalf] * wi; float ui real[ijhalf] * wi imag[ijhalf] * wr; real[ijhalf] real[ij] - ur; imag[ijhalf] imag[ij] - ui; real[ij] ur; imag[ij] ui; } } } }逻辑说明FFT_Init预计算旋转因子避免每次蝶形运算都调cos/sin这在 STM32 上能省大量时间。BitReverse把输入序列按位反转顺序重排是基 2 FFT 的标准步骤。FFT_Compute三层循环外层控制蝶形级数中层按块处理内层做蝶形。参数N必须是 2 的幂twiddle表长度是N/2。注意 ADC 采样值是 0~4095 的无符号数送进 FFT 前要减去直流偏置通常 2048变成有符号浮点否则频谱里会出现巨大的直流分量把交流分量淹没。3.3 幅值、频率、相位差的计算FFT 输出的是复数数组需要进一步算模值和相位。// 计算幅值谱和相位谱 void Calc_MagPhase(float *real, float *imag, float *mag, float *phase) { for (int i 0; i N/2; i) { mag[i] sqrtf(real[i]*real[i] imag[i]*imag[i]); phase[i] atan2f(imag[i], real[i]); } } // 找峰值谱线算频率和幅值 void Find_Peak(float *mag, float *freq, float *amp) { int max_idx 1; for (int i 2; i N/2; i) { if (mag[i] mag[max_idx]) max_idx i; } *freq (float)max_idx * SAMPLE_RATE / N; *amp mag[max_idx] * 2.0f / N; // 单边谱幅值补偿 }逻辑说明Calc_MagPhase对每个频点算模和相位。Find_Peak在N/2个频点里找最大模值对应的索引频率就是索引 × fs / N。幅值要乘2/N因为 FFT 结果是对称的单边谱需要补偿。相位差的计算是对两路信号分别做 FFT找到同一峰值频率索引取两路在该索引上的相位角相减再归一到[-π, π]。注意相位差对采样起始点敏感如果两路 ADC 不是严格同步采样相位差会有固定偏移需要在硬件上保证两路同时采样或者用双 ADC 同步模式。4. 避坑与排查相位差乱跳、幅值不准、频率对不上的真实原因4.1 相位差每次上电都不一样现象两路信号明明是同频同相测出来的相位差每次上电都在变或者随采样起始点漂移。原因FFT 的相位是相对于采样窗口起点的如果两路信号的采样不是严格同时开始或者 ADC 采样有通道间延迟相位差就会引入一个与频率相关的偏移。另外如果两路信号不是在同一时刻启动采样起始相位就不一致。解决用双 ADC 同步模式STM32 的 ADC1 和 ADC2 可以配置为同步规则模式确保两路同时采样。如果硬件不支持至少在软件上让两路采样共用同一个定时器触发并且 DMA 搬运到两个独立缓冲区。相位差计算时取同一频率索引的相位角相减不要用不同索引。4.2 幅值测出来只有实际的一半或两倍现象输入 1V 峰峰值的正弦测出来幅值要么是 0.5要么是 2。原因FFT 结果的幅值补偿系数没搞对。对于实信号单边谱幅值 2 × |X[k]| / N。如果忘了乘 2结果就是一半如果乘了 2 又没除以 N结果就偏大。另外如果 ADC 前端有分压或放大还要乘回硬件增益。解决统一公式amp 2.0f * sqrt(real*real imag*imag) / N。如果用了窗函数比如汉宁窗还要除以窗函数的相干增益汉宁窗约 0.5。建议先用信号发生器输入一个已知幅值的正弦校准一次把系数固定下来。4.3 频率分辨率不够两个靠近的频率分不开现象输入两个频率相近的正弦FFT 后只看到一个峰或者峰值频率偏离实际值。原因频率分辨率 fs / N。如果 N 太小或 fs 太高分辨率就不够。比如 fs10kHzN256分辨率是 39Hz两个相差 20Hz 的信号就分不开。另外如果信号频率不是分辨率整数倍会有频谱泄漏峰值也会偏移。解决增大 N 或降低 fs。N 受 RAM 限制F103 上 1024 点浮点 FFT 需要约 8KB RAM比较紧张。可以改用定点 FFT 或降低采样率。如果信号频率已知范围可以用带通采样或欠采样。加窗能减少泄漏但会加宽主瓣分辨率反而下降要权衡。4.4 串口打印乱码或数据不对现象串口助手上看到乱码或者幅值频率都是 0。原因串口波特率不对或者 FFT 计算时间太长导致主循环阻塞DMA 采样缓冲区被覆盖。另外如果adc_buf是u16类型送进 FFT 前没转成float计算结果全是整数除法精度丢失。解决确认串口波特率和时钟配置一致。FFT 计算放在 DMA 采样完成中断里或者用双缓冲DMA 填一半时处理另一半。adc_buf转float时记得减直流偏置。打印用printf重定向到串口或者自己写UART_SendFloat。4.5 标准库工程里加 FFT 文件后编译报错现象提示undefined reference to sqrtf或cosf或者 RAM 不够。原因标准库工程默认没开浮点数学库或者没勾选Use MicroLIB。另外F103 没有硬件浮点单元所有浮点运算都是软件模拟速度慢RAM 占用也大。解决在 Keil 的 Target 选项里勾选Use MicroLIB并在链接器里加入m库如果用的是 GCC。如果 RAM 不够把 FFT 点数降到 256 或 128或者改用q15定点格式。常见做法是旋转因子用float采样数据用int16_t蝶形运算时转float算完再转回。5. 二次开发与精度提升从能跑到测得准的改造思路5.1 用窗函数抑制频谱泄漏如果信号频率不是fs/N的整数倍直接做 FFT 会有泄漏峰值幅值和相位都会偏。加汉宁窗是常见做法。// 汉宁窗生成与应用 void Apply_Hanning(float *buf, int n) { for (int i 0; i n; i) { float w 0.5f * (1.0f - cosf(2 * PI * i / (n - 1))); buf[i] * w; } }加窗后幅值要除以窗函数的相干增益。汉宁窗的相干增益是 0.5所以幅值补偿系数变成2.0f / (N * 0.5f)。相位也会受窗函数影响但两路信号用同一个窗相位差不受影响。注意加窗会让主瓣变宽频率分辨率下降如果两个频率很近加窗反而更分不开。5.2 用插值法提高频率和幅值精度峰值谱线不一定正好落在信号频率上用抛物线插值可以估计真实峰值位置。// 抛物线插值估计真实峰值 float Interpolate_Peak(float *mag, int idx) { float alpha mag[idx - 1]; float beta mag[idx]; float gamma mag[idx 1]; float p 0.5f * (alpha - gamma) / (alpha - 2 * beta gamma); return (idx p) * SAMPLE_RATE / N; // 插值后的频率 }p是峰值偏移量范围在[-0.5, 0.5]。插值后的频率精度可以提高到分辨率的三分之一左右。幅值也可以用同样的插值公式修正。这个方法在信号频率接近分辨率整数倍时效果最好如果泄漏严重插值也会偏。5.3 用双缓冲 DMA 半传输中断做连续测量单次采样 计算会阻塞主循环没法连续测。改成双缓冲DMA 填前半缓冲时CPU 处理后半缓冲填后半时处理前半。#define BUF_SIZE (N * 2) u16 adc_buf[BUF_SIZE]; void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HTIF1) ! RESET) { // 前半满处理后半 Process_FFT(adc_buf[N], N); DMA_ClearITPendingBit(DMA1_IT_HTIF1); } if (DMA_GetITStatus(DMA1_IT_TCIF1) ! RESET) { // 后半满处理前半 Process_FFT(adc_buf[0], N); DMA_ClearITPendingBit(DMA1_IT_TCIF1); } }这样采样和计算并行能连续输出测量结果。注意Process_FFT里不要用printf直接打印最好把结果存到全局变量主循环里再打印避免中断里耗时太长。5.4 校准与验证方法改造完怎么验证我一般会做三步第一步用信号发生器输入一个已知频率和幅值的正弦比如 1kHz、1V 峰峰值看测出来的频率和幅值误差是否在 1% 以内。第二步输入两路同频同相的信号看相位差是否接近 0再输入两路相差 90 度的信号看相位差是否接近 90 度。第三步改变信号频率从 100Hz 扫到 5kHz看全频段内幅值和频率的波动。如果某个频段误差突然变大多半是采样率或窗函数的问题。从那以后我每次调 FFT 测量代码都强制先跑一遍“已知信号校准”把幅值系数、相位偏移、频率插值参数固定下来再上真实信号。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑