news 2026/10/17 14:07:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32标准库实现FFT:精准测量正弦波幅值、频率与相位差

简介:本资源面向嵌入式开发工程师与信号处理学习者,提供在STM32F407平台上基于标准库实现FFT算法的完整工程,用于对正弦波信号进行频域分析,精准测量幅值、频率与相位差,适用于电机控制、音频分析与无线通信等实时信号处理场景。压缩包共283个文件,约9.98MB,以C源码与头文件为核心,包含stm32f4xx系列外设驱动、工程配置与编译产物,并附带少量说明文档与烧录脚本,结构完整可直接复用。资源围绕ADC采样、窗函数处理、FFT变换、频谱分析与结果输出等环节展开,借助Cortex-M4的浮点运算单元提升运算效率。目前已有1665人学习下载,适合希望快速搭建FFT测量框架、理解频域分析流程并对照工程结构查漏补缺的开发者参考。

1. 从一块“跑不准”的板子说起:STM32 上做 FFT 测正弦波到底难在哪

前阵子帮一个做电机控制的朋友看代码,他用 STM32 采集一路正弦波信号,想实时算出幅值、频率和相位差,结果频率总在 ±2Hz 上下跳,相位差更是飘得没法看。他一开始以为是 ADC 采样不准,换了基准源、加了滤波电容,折腾一周没解决。问题其实出在 FFT 本身——点数选少了频率分辨率不够,点数选多了标准库那点 RAM 又扛不住,再加上没做整周期采样,频谱泄漏直接把峰值糊成一片。这个项目就是冲着这类场景来的:在 STM32 上用标准外设库实现 FFT,把正弦波信号的幅值、频率、相位差三个参数稳定测出来。它适合正在做电力监测、音频分析、传感器解调这类需要频域信息的嵌入式开发者,尤其是那些不想上 DSP 芯片、只想用现有 STM32 资源把活干完的人。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序,把这份代码拆开讲一遍。

2. 项目定位与技术选型:为什么是标准库 + 定点 FFT

2.1 这个项目解决的具体问题

先把需求说清楚。输入是一路正弦波信号,经过 ADC 采样变成离散序列,输出是三个数:幅值(V 或 ADC 码值)、频率(Hz)、以及两路信号之间的相位差(度)。听起来简单,但嵌入式里做这件事有几个硬约束:RAM 通常只有几十 KB,主频几十到一百多 MHz,没有浮点单元或者浮点很慢。所以这个项目的核心不是“实现一个 FFT”,而是“在资源受限的 STM32 上,用可接受的精度把这三个参数测出来”。

它覆盖的典型场景包括:电机电流环里的谐波分析、音频信号的基频检测、传感器激励与响应的相位差测量。这些场景的共同点是信号频率不高(通常几百 Hz 到几十 kHz),但对实时性和稳定性有要求。

2.2 技术栈拆解:标准库、CMSIS-DSP 与定点运算

项目用的是 STM32 标准外设库(Standard Peripheral Library),不是 HAL 也不是 LL。这一点很关键——标准库在中小容量型号上代码体积小、执行路径短,很多老项目和新项目都还在用。FFT 部分通常有两种做法:一是自己写基 2 的蝶形运算,二是调用 CMSIS-DSP 里的arm_cfft_q15或arm_rfft_q15。这个项目走的是 CMSIS-DSP 定点路线,用 Q15 格式(16 位定点,范围 -1 到 1-2^-15)来存采样数据。

为什么用定点而不是浮点?STM32F1、F0 这类没有 FPU 的型号,浮点 FFT 一次 1024 点要几十毫秒,定点版本能压到几毫秒。即使有 FPU 的 F4,定点在批量运算时功耗和确定性也更好。代价是动态范围受限,需要做输入缩放,这个后面避坑章节会细说。

选型项方案理由
外设库标准库体积小、路径短、老项目兼容
FFT 实现CMSIS-DSP Q15定点快、RAM 占用可控
采样方式ADC + DMA + 定时器触发保证等间隔采样
点数1024 点(可配 256/512)兼顾分辨率与 RAM
窗函数汉宁窗抑制频谱泄漏

2.3 目录结构与模块划分

一份典型的工程目录大概长这样,我按功能把文件归了类:

project/ ├── CMSIS/ # 内核与 DSP 库 │ ├── core_cm3.h │ └── DSP_Lib/ │ └── Source/ # arm_cfft_q15.c 等 ├── Libraries/ │ └── STM32F10x_StdPeriph_Driver/ ├── User/ │ ├── main.c # 主流程 │ ├── adc_dma.c # 采样配置 │ ├── fft_process.c # FFT 与参数计算 │ ├── fft_process.h │ └── stm32f10x_it.c # 中断 └── Output/

adc_dma.c负责把定时器触发的 ADC 数据通过 DMA 搬进缓冲区,fft_process.c是核心,做加窗、FFT、求模、找峰、算相位。这种划分的好处是采样和算法解耦,换 ADC 型号或者换 FFT 点数时改动范围小。

3. 核心模块拆解:从 ADC 采样到三个参数输出

3.1 采样链路:定时器触发 + DMA 搬运

采样最怕两件事:采样间隔不均匀、CPU 被搬运数据拖死。所以标准做法是用定时器产生固定频率的触发信号,ADC 每次转换完由 DMA 自动搬走,CPU 完全不参与。下面是一段配置示例:

// adc_dma.c #define FFT_SIZE 1024 #define SAMPLE_RATE 10000 // 采样率 10kHz __IO uint16_t adc_buf[FFT_SIZE]; void ADC_DMA_Init(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; TIM_TimeBaseInitTypeDef TIM_InitStructure; // 1. 定时器:产生采样触发,频率 = SAMPLE_RATE RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Period = SystemCoreClock / SAMPLE_RATE - 1; TIM_InitStructure.TIM_Prescaler = 0; TIM_InitStructure.TIM_ClockDivision = 0; TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); // 更新事件触发 ADC // 2. ADC:外部触发,规则通道 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); // 3. DMA:循环模式,搬 FFT_SIZE 个半字 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = FFT_SIZE; 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_Cmd(ADC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }

这段代码的关键参数有三个:SAMPLE_RATE决定频率分辨率(分辨率 = 采样率 / 点数),FFT_SIZE决定 RAM 占用和运算时间,ADC_SampleTime要匹配信号源阻抗。DMA 用循环模式,采满一缓冲后自动从头覆盖,主循环里通过判断半传输或传输完成标志来决定何时处理数据。注意 ADC 的触发源必须和定时器对应,F1 系列里 T2_TRGO 是常见选择,换型号时要查参考手册的触发映射表。

3.2 FFT 运算:Q15 格式、加窗与位反转

采样数据是 12 位无符号整数,直接丢给 Q15 的 FFT 会出问题——Q15 是有符号的,而且要求输入在 -1 到 1 之间。所以要先做直流偏置去除和归一化,再转成 Q15。加窗是为了减小频谱泄漏,汉宁窗是最常用的折中方案。

// fft_process.c #include "arm_math.h" #define FFT_SIZE 1024 q15_t fft_in[FFT_SIZE * 2]; // 复数格式:实部虚部交替 q15_t fft_out[FFT_SIZE]; q15_t window[FFT_SIZE]; void FFT_Process(void) { uint32_t i; float32_t mean = 0.0f; // 1. 求直流均值 for (i = 0; i < FFT_SIZE; i++) { mean += adc_buf[i]; } mean /= FFT_SIZE; // 2. 去直流 + 归一化到 Q15 + 加汉宁窗 for (i = 0; i < FFT_SIZE; i++) { float32_t v = ((float32_t)adc_buf[i] - mean) / 2048.0f; // 12位ADC float32_t w = 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); fft_in[2 * i] = (q15_t)(v * w * 32767.0f); // 实部 fft_in[2 * i + 1] = 0; // 虚部 } // 3. 调用 CMSIS-DSP 复数 FFT arm_cfft_instance_q15 cfft; arm_cfft_init_q15(&cfft, FFT_SIZE); arm_cfft_q15(&cfft, fft_in, 0, 1); // 0=正变换, 1=位反转 // 4. 求模,取前 N/2 个点 arm_cmplx_mag_q15(fft_in, fft_out, FFT_SIZE); }

这里有几个容易翻车的点。arm_cfft_q15的输入是复数交织数组,长度是 2×FFT_SIZE,很多人第一次用会只分配 FFT_SIZE 导致越界。arm_cfft_init_q15在较新版本的 CMSIS-DSP 里才有,老版本要手动填arm_cfft_instance_q15结构体。归一化系数 2048 是按 12 位 ADC 满量程 4096 的一半取的,如果你的 ADC 是 10 位或 16 位,这个数要跟着改,否则幅值算出来差一个固定倍数。

3.3 参数计算:幅值、频率、相位差的提取

FFT 出来的是频谱,要变成物理量还得做几步。幅值看峰值点的模值,频率看峰值点对应的 bin 序号,相位差则要比较两路信号在同一 bin 上的相位角。

// 幅值、频率、相位差计算 #define SAMPLE_RATE 10000.0f #define FFT_SIZE 1024 float32_t get_amplitude(q15_t *mag, uint32_t peak_bin) { // Q15 模值转电压:除以 32767 还原,再乘 2/N 补偿 FFT 增益 // 汉宁窗相干增益 0.5,所以再乘 2 float32_t amp = (float32_t)mag[peak_bin] / 32767.0f; amp = amp * 2.0f / FFT_SIZE * 2.0f; return amp * 3.3f; // 参考电压 3.3V } float32_t get_frequency(uint32_t peak_bin) { return peak_bin * SAMPLE_RATE / FFT_SIZE; } float32_t get_phase_diff(q15_t *fft_a, q15_t *fft_b, uint32_t bin) { // 实部虚部取出,算各自相位角再相减 float32_t re_a = fft_a[2 * bin]; float32_t im_a = fft_a[2 * bin + 1]; float32_t re_b = fft_b[2 * bin]; float32_t im_b = fft_b[2 * bin + 1]; float32_t ph_a = atan2f(im_a, re_a); float32_t ph_b = atan2f(im_b, re_b); float32_t diff = (ph_a - ph_b) * 180.0f / PI; if (diff < 0) diff += 360.0f; return diff; }

找峰值 bin 的时候别只比较模值大小,最好加一个抛物线插值,否则频率分辨率被 FFT_SIZE 卡死。比如 1024 点、10kHz 采样,bin 间隔约 9.77Hz,不插值的话频率误差可能到 ±5Hz。插值公式用峰值点和左右邻点的模值做二次拟合,能把误差压到 1Hz 以内。相位差计算要求两路信号用同一个触发源同步采样,否则相位基准对不上,算出来没有意义。

4. 避坑与排查:那些让 FFT 结果“飘”的常见原因

4.1 频谱泄漏导致幅值偏小、频率跳动

现象是幅值测出来比实际小一截,频率在真实值附近来回跳。原因通常是非整周期采样——信号频率不是频率分辨率的整数倍,能量从主瓣泄漏到旁瓣。解决办法有两个:一是加窗,汉宁窗能把泄漏压下去但主瓣变宽;二是做整周期采样,用信号过零点触发采样起始,保证采到整数个周期。实际项目里我一般先加窗,如果精度还不够再上过零触发。

4.2 Q15 溢出导致频谱出现虚假峰值

现象是频谱上冒出一些不该有的峰,或者底噪明显抬高。原因是输入信号幅度超过 Q15 范围,蝶形运算中间结果溢出。Q15 的 FFT 每一级都会放大,1024 点大概有 10 级,理论增益 2^10。所以输入不能满量程,通常要右移几位或者把归一化系数调小。CMSIS-DSP 的arm_cfft_q15有个bitReverseFlag参数,但缩放得自己控制。稳妥做法是输入幅度控制在满量程的 1/4 以内。

4.3 采样率与定时器配置不匹配

现象是频率算出来整体偏大或偏小一个固定比例。原因是定时器实际触发频率和代码里写的SAMPLE_RATE对不上。比如系统时钟 72MHz,预分频和重装载值算错,实际采样率可能是 9.8kHz 而不是 10kHz。排查方法是拿示波器量定时器触发引脚,或者用一个已知频率的信号源反推实际采样率。这个坑我踩过,调了一下午才发现是预分频少减了 1。

4.4 相位差测量不同步

现象是相位差每次测量结果都不一样,或者两路信号明明同相却测出几十度偏差。原因是两路 ADC 没有用同一个触发源,或者 DMA 搬运顺序导致采样时刻错开。解决方法是让两路 ADC 工作在双重模式(同步规则模式),由同一个定时器触发,这样两个通道的采样时刻严格对齐。如果型号不支持双重模式,那就用两个 ADC 但共用一个触发源,并且保证 DMA 配置一致。

4.5 浮点运算拖慢实时性

现象是主循环处理一帧数据要几十毫秒,跟不上采样速度。原因是加窗和归一化用了浮点,在没有 FPU 的型号上很慢。优化方向是把窗函数表预先算好存成 Q15 数组,归一化用整数移位代替浮点除法。如果必须用浮点,至少把arm_cos_f32换成查表。实测在 F103 上,把加窗改成查表后单帧处理时间从 18ms 降到 6ms。

5. 二次开发与改造:把这份代码变成你自己的测量工具

拿到这份代码后,最直接的改造是换 FFT 点数。256 点 RAM 占用小但分辨率低,2048 点分辨率高但 F103 上跑起来吃力。我的经验是:信号频率低于 1kHz 用 1024 点,1kHz 到 5kHz 用 512 点,再高就用 256 点配合过零触发。改点数时记得同步改arm_cfft_init_q15的参数、窗函数表长度和 DMA 缓冲大小,这三处漏一个就会跑飞。

第二个改造方向是加实时性。如果要在中断里做 FFT,建议用 DMA 的半传输中断——采满一半就开始处理前一半,同时 DMA 继续采后一半,这样处理和采样并行。但要注意 FFT 函数本身不是可重入的,别在两个中断里同时调用。

第三个方向是精度提升。抛物线插值是最划算的改动,十几行代码能把频率精度提高一个数量级。相位差那边可以加一个参考通道做自校准,把通道间的固定延迟扣掉。如果对幅值精度要求高,可以做多点平均,连续测 8 帧取中位数,能有效抑制随机跳动。

改造项改动位置预期收益
点数切换fft_process.c + adc_dma.c适配不同频率范围
抛物线插值峰值搜索函数频率精度提升 5~10 倍
半传输并行DMA 中断 + 主循环实时性提升约 40%
多帧中位数参数输出层抑制随机跳动
查表加窗窗函数初始化单帧耗时降低 60%

最后说一个我自己的习惯:每次改完 FFT 相关参数,我都会先用一个信号发生器灌一个已知频率和幅值的正弦波进去,把测出来的三个参数和设定值对一遍,确认误差在可接受范围再接实际信号。这个步骤看起来笨,但能帮你把“算法问题”和“信号问题”分开,省下大量瞎调的时间。从那以后我每次动采样率或者点数,都强制走一遍这个校准流程。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 14:07:44

低质图像识别鲁棒性提升:数据增强与训练策略实战

我入行做CV&#xff08;计算机视觉&#xff09;这几年&#xff0c;发现一个很扎心的现象&#xff1a;很多在漂亮数据集上刷到高分点的模型&#xff0c;一放到实际场景里就原形毕露。小区门禁的抓拍车牌、工厂流水线上光线忽明忽暗的零件图、户外监控夜里带噪点的行人照片——分…

作者头像 李华
网站建设 2026/10/17 14:07:28

基于Python的人脸识别系统毕设全流程实战:检测、特征提取与调优

每到毕业季&#xff0c;"基于Python的人脸识别系统"都是计算机相关专业里出现频率最高的毕设方向之一。题目看起来直白&#xff0c;网上也有大量现成开源代码&#xff0c;但真正自己动手做一遍&#xff0c;才发现它远不是"装个库、跑几行代码"那么简单&…

作者头像 李华
网站建设 2026/10/17 14:06:48

RAG实战拆解:让大模型学会查资料,解决知识冻结

最近被频繁问到的一个场景&#xff1a;某个系统三个月前升级了新版本&#xff0c;新功能、新参数、新的注意事项都写在内部文档里了&#xff0c;但你去问大模型&#xff0c;它给你答的仍是旧版本的老一套。更尴尬的是&#xff0c;业务方拿着文档找过来&#xff1a;“这上面写得…

作者头像 李华
网站建设 2026/10/17 14:06:40

YOLOv8低帧率追踪优化:光流法运动补偿实践指南

如果只看过YOLOv8的检测demo&#xff0c;你大概率会觉得视频追踪不是难事&#xff1a;每帧检测&#xff0c;做一做框的匹配不就完了&#xff1f;真正跑到实际项目里&#xff0c;尤其是在低帧率场景&#xff0c;你会发现这是完全两码事。我自己就栽过跟头。有个监控类的项目&…

作者头像 李华
网站建设 2026/10/17 14:06:10

上下文工程实战指南:让大模型输出更精准的核心技巧

同一个大模型&#xff0c;为什么有人几句话就让它交出漂亮结果&#xff0c;有人写了一大段还是被它"一本正经地胡说八道"&#xff1f;差别基本不在模型本身&#xff0c;而在上下文工程。简单说&#xff0c;上下文就是你发给大模型的全部信息——你说了什么、给了哪些…

作者头像 李华