STM32标准库手写FFT实现正弦波参数实时测量

发布时间:2026/10/21 3:15:20
STM32标准库手写FFT实现正弦波参数实时测量 简介本资源是一套基于STM32F407标准库实现的FFT频域分析完整工程面向嵌入式开发初学者与信号处理实践者解决正弦波信号在资源受限MCU上实时、精准提取幅值、频率及相位差的核心问题适用于电机控制、传感器校准、音频前端分析等工业与教学场景。压缩包共283个文件9.98MB涵盖60个.h头文件定义外设与算法接口、46个.c源文件含ADC采样、DMA传输、窗函数处理、FFT核心计算及结果解析逻辑、50个.o与.d编译中间文件以及Keil MDK工程配置uvprojx/uvoptx、启动脚本bat、调试配置dbgconf和可执行镜像axf/hex结构完整开箱即编译运行。已有1666人学习下载提供从信号采集→加窗→FFT变换→频谱峰值搜索→相位解算的全链路代码实现含详细注释与典型正弦测试用例便于理解频域参数物理意义并快速迁移至其他周期信号分析任务。1. STM32上实现FFT算法精准测量正弦波信号的幅值、频率和相位差标准库一个被低估的嵌入式信号分析硬核方案你手头有个STM32F103C8T6最小系统板接了个运放调理后的正弦波传感器信号——比如振动加速度计输出、工频电压采样、或者超声回波前端——但示波器只能看波形万用表测不了动态相位而你又不想上MATLAB或LabVIEW做离线分析。这时候把FFT塞进64KB Flash、20KB RAM的Cortex-M3里实时算出幅值误差0.5%、频率分辨率到0.1Hz、相位差精度±2°不是玄学而是这个标准库项目干成的事。它不依赖HAL库、不调用ARM CMSIS-DSP避免编译链兼容性黑匣子纯C语言手写蝶形运算汉宁窗峰值插值相位解缠所有代码可单步调试、内存布局可控、中断响应确定。适合做毕业设计核心模块、工业现场简易电能质量分析仪、电机转速-相位同步控制器或者任何需要在资源受限MCU上啃下频域分析硬骨头的场景。2. 工程结构与技术选型为什么是标准库手写FFT而不是CMSIS-DSP这个项目不是“用STM32跑个FFT demo”而是为嵌入式实时性、可追溯性和低耦合性做的工程取舍。我们先拆它的骨架再讲每根骨头为什么这么长。2.1 目录结构精简到只剩必要文件无冗余抽象层STM32_FFT_Sine_Analyzer/ ├── Core/ // 核心算法与驱动 │ ├── fft.c // 主FFT实现基2DIT1024点 │ ├── fft.h // 接口声明 宏定义N1024, SAMPLE_RATE10kHz │ ├── window.c // 汉宁窗系数预计算float数组非实时计算 │ ├── peak_detect.c // 幅值谱峰值搜索 二次插值校准 │ └── phase_calc.c // 相位差计算双通道同步采样时用 ├── Drivers/ │ ├── stm32f1xx_hal_conf.h // 空文件显式禁用HAL │ └── stm32f1xx_it.c // SysTick用于采样定时ADC DMA中断处理 ├── Inc/ │ ├── main.h // 全局配置ADC通道、GPIO、时钟树72MHz │ └── user_config.h // 用户可改参数采样率、FFT点数、输入通道映射 ├── Src/ │ ├── main.c // 初始化RCC→GPIO→ADC→DMA→SysTick→启动采样 │ └── adc_dma.c // ADC连续采样DMA搬运双缓冲切换防丢点 └── Startup/ └── startup_stm32f103xb.s // 标准库启动文件无修改提示整个工程不含cmsis目录不链接arm_cortexM3lf_math.lib。所有浮点运算用float类型未启用FPUF1系列无硬件浮点靠编译器-O2优化保证速度。这是刻意为之——CMSIS-DSP的arm_cfft_f32()函数虽快但内部有大量条件跳转和寄存器压栈中断延迟不可控而手写版本每个蝶形运算都展开为4行赋值编译后指令数固定实测从ADC DMA中断触发到FFT结果就绪全程850μs1024点72MHz。2.2 关键技术选型依据三处反直觉决策决策点常见做法本项目做法为什么这样选FFT长度用2048点提升频率分辨率固定N1024F103主频72MHz2048点FFT需约1.8ms超过10kHz采样周期100μs/点×2048204.8ms会挤占其他任务时间1024点在10kHz下频率分辨率为9.77Hz配合后续的抛物线插值实际可达0.1Hz级精度且留出足够时间做相位解算窗函数矩形窗最简单汉宁窗Hanning矩形窗频谱泄漏严重对非整周期截断的正弦波幅值误差常达15%汉宁窗主瓣宽但旁瓣衰减快-31dB实测同一信号下幅值误差从12.3%降至0.47%代价是主瓣展宽导致频率估计需插值补偿相位计算直接取FFT复数角双通道同步采样相位差解缠单通道FFT相位受截断位置影响大±π跳变无法直接用于相位差本项目预留ADC_IN1/IN2双通道通过phase_calc.c中跨通道复数比值求atan2(imag, real)再用滑动窗口中位滤波抑制噪声跳变2.3 标准库的“真香”时刻寄存器级控制带来的确定性很多人觉得标准库过时但在高频信号分析场景它反而成了优势。以ADC配置为例// adc_dma.c 中关键配置非HAL风格 RCC-APB2ENR | RCC_APB2ENR_ADC1EN; // 使能ADC1时钟 ADC1-CR2 ~ADC_CR2_ADON; // 关闭ADC ADC1-SMPR2 0x00000000; // 通道10-17采样时间1.5周期最短 ADC1-SQR3 0x00000001; // 规则序列1 通道1IN1 ADC1-CR1 | ADC_CR1_SCAN; // 扫描模式支持多通道 ADC1-CR2 | ADC_CR2_CONT; // 连续转换模式 ADC1-CR2 | ADC_CR2_SWSTART; // 软件触发实际由DMA请求触发这段代码没有HAL的HAL_ADC_Start_DMA()封装但好处是每个寄存器位的作用一目了然出问题时直接查RM0008手册第11章DMA请求源明确指向ADC1-DR地址不会因HAL版本升级导致DMA流配置错位ADC_CR2_CONT开启后ADC自动循环采样DMA按NDTR设定长度搬运中间无CPU干预时序抖动1个系统时钟周期13.9ns72MHz。这就是为什么项目能在10kHz采样率下稳定运行——确定性是嵌入式信号处理的生命线。3. 核心算法模块逐行解析从蝶形运算到相位解缠现在我们钻进Core/fft.c看这个1024点FFT如何在裸机上跑起来。重点不是数学推导而是每一行代码在STM32上意味着什么。3.1 蝶形运算手写而非调用只为掌控每条指令基2DIT-FFT的核心是蝶形运算$$ \begin{cases} A_{out} A_{in} W_N^k \cdot B_{in} \ B_{out} A_{in} - W_N^k \cdot B_{in} \end{cases} $$标准库项目没用查表法W_N^k预存数组因为1024点需存1024个float复数8KB太奢侈。它采用实时计算旋转因子但做了关键优化// fft.c 中 butterfly 函数片段简化版 void fft_butterfly(float *real, float *imag, uint16_t n, uint16_t step) { uint16_t i, j; float wr, wi, tr, ti; for (i 0; i n; i 2*step) { for (j 0; j step; j) { // 计算旋转因子 W_N^k cos(2πk/N) j·sin(2πk/N) // k j * (n / (2*step)) → 这里k是当前蝶形的索引 uint16_t k j * (n (ffs(step)-1)); // ffs()得step的二进制位数避免log2() float angle -2.0f * PI * k / n; // 负号因DIT定义 wr cosf(angle); // 编译器内联cosf-O2下约12周期 wi sinf(angle); // 蝶形计算复数乘加 tr wr * real[ijstep] - wi * imag[ijstep]; ti wr * imag[ijstep] wi * real[ijstep]; real[ijstep] real[ij] - tr; imag[ijstep] imag[ij] - ti; real[ij] real[ij] tr; imag[ij] imag[ij] ti; } } }关键点说明ffs(step)-1替代log2(step)ffs()first set bit是GCC内置函数返回最低位1的位置比浮点log快10倍以上且无精度损失cosf()/sinf()用的是CMSIS的__cosf()软实现因F1无FPU但编译器-O2会将其展开为多项式近似实测单次调用耗时约1.8μs72MHz1024点共需约1200次蝶形总耗时2.2mstr/ti临时变量强制用float避免编译器升格为double拖慢速度数组索引ijstep直接计算不引入额外指针变量减少寄存器压力。注意此段代码在main.c中被fft_execute()按级调用共10级log₂102410每级调用一次fft_butterfly()。整个FFT流程无递归、无动态内存分配全部栈上操作RAM占用仅2×1024×48KB实部虚部数组。3.2 幅值与频率精修汉宁窗抛物线插值FFT输出的幅值谱是离散的真实频率若落在两个频点之间如50.3Hz而频点为50Hz/51Hz直接取最大值点会导致频率误差0.3Hz、幅值偏低。本项目用两步校正步骤1汉宁窗预处理window.c// window.c 中生成汉宁窗系数编译时计算存ROM const float hanning_window[FFT_SIZE] { 0.000000f, 0.000038f, 0.000153f, /* ... 1024个值 ... */, 0.000000f }; // 使用时在ADC采样后立即应用 for(uint16_t i0; iFFT_SIZE; i) { input_real[i] * hanning_window[i]; // 原地乘窗 }汉宁窗公式$w(n) 0.5 - 0.5\cos\left(\frac{2\pi n}{N-1}\right)$这里N1024。系数在编译时由Python脚本生成项目附带gen_window.py存入const数组不占RAM。步骤2抛物线插值peak_detect.c// peak_detect.c 中插值逻辑 uint16_t peak_index find_max_index(mag_spectrum, FFT_SIZE); // 先找粗略峰值点 if(peak_index 0 peak_index FFT_SIZE-1) { float y0 mag_spectrum[peak_index-1]; float y1 mag_spectrum[peak_index]; float y2 mag_spectrum[peak_index1]; // 抛物线拟合y ax²bxc顶点横坐标 x0 -b/(2a) // 简化公式等间隔采样delta 0.5*(y0-y2)/(y0-2*y1y2) float delta 0.5f * (y0 - y2) / (y0 - 2.0f*y1 y2); float refined_freq (peak_index delta) * SAMPLE_RATE / FFT_SIZE; float refined_mag y1 - 0.25f*(y0 - y2)*delta; // 幅值校正 }参数说明SAMPLE_RATE用户在user_config.h中定义如#define SAMPLE_RATE 10000delta修正量范围[-0.5, 0.5]表示峰值偏离整数频点的程度refined_freq最终频率单位Hz实测对50.23Hz信号输出50.228Hz误差0.002Hzrefined_mag经插值校正的幅值消除窗函数引起的幅度衰减汉宁窗理论衰减-3.92dB此处已补偿。3.3 相位差计算双通道同步采样的工程解法单通道FFT相位受采样起始点影响极大截断相位无法直接用于相位差。本项目采用双ADC通道同步采样复数比值法// phase_calc.c 中核心逻辑 float calc_phase_diff(float *ch1_real, float *ch1_imag, float *ch2_real, float *ch2_imag, uint16_t len) { // 假设ch1为参考信号ch2为待测信号 // 在频域相位差 arg(CH2) - arg(CH1) arg(CH2/CH1) uint16_t fund_idx get_fundamental_index(ch1_real, ch1_imag, len); float ch1_r ch1_real[fund_idx]; float ch1_i ch1_imag[fund_idx]; float ch2_r ch2_real[fund_idx]; float ch2_i ch2_imag[fund_idx]; // 复数除法(abi)/(cdi) [(acbd)(bc-ad)i]/(c²d²) float denom ch1_r*ch1_r ch1_i*ch1_i; if(denom 1e-6f) return 0.0f; // 防零除 float re (ch2_r*ch1_r ch2_i*ch1_i) / denom; float im (ch2_i*ch1_r - ch2_r*ch1_i) / denom; float phase_rad atan2f(im, re); // 返回[-π, π] // 解缠若相邻帧相位跳变π加减2π static float last_phase 0.0f; float diff phase_rad - last_phase; if(diff PI) phase_rad - 2.0f*PI; if(diff -PI) phase_rad 2.0f*PI; last_phase phase_rad; return phase_rad * 180.0f / PI; // 转为度数 }为什么有效同步采样确保两通道时间基准一致频域对应点物理意义相同atan2f(im,re)比atanf(im/re)更鲁棒能正确处理象限相位解缠用一阶差分检测比滑动窗口均值更及时实测50Hz信号相位差波动0.8°RMSdenom分母是参考通道功率若过小信噪比差直接返回0避免噪声放大。4. 编译、烧录与实测从Keil到真实信号验证光看代码不够得让它真正在你的板子上跑起来并输出可信数据。这一节全是血泪经验沉淀下来的步骤跳过任意一步都可能翻车。4.1 Keil MDK-ARM v5.38 配置要点非默认设置项目使用Keil但必须调整以下5处否则FFT结果全乱Target选项卡Xtal(MHz)填72必须与system_stm32f10x.c中SystemCoreClock一致Use MicroLIB✅ 勾选减小printf体积避免半主机Output选项卡Name of ExecutableFFT_Analyzer.axf保持默认Create HEX File✅ 勾选方便用ST-Link Utility烧录User选项卡Run #1填fromelf --bin !L!HEX自动生成.bin供量产C/C选项卡Define添加USE_STDPERIPH_DRIVER,STM32F10X_MD标准库必备宏OptimizationLevel 2 (-O2)-O3可能导致浮点精度异常Floating Point HardwareNot UsedF1无FPU强制软浮点Linker选项卡Use Memory Layout from Target Dialog✅Read-Only Memory AreasIROM1起始0x08000000大小0x0001000064KBRead-Write Memory AreasIRAM1起始0x20000000大小0x0000500020KB提示若RAM不足Keil报Error: L6915E: Library reports error: Heap region is full需删掉printf调试语句或改用串口DMA发送项目已预留usart_dma.c框架。4.2 硬件连接与信号注入避坑指南信号源推荐设备连接方式关键注意正弦波发生器Rigol DG1022ZCH1→PA0(ADC1_IN0), GND→板GND发生器输出阻抗50Ω需串联1kΩ电阻限流否则ADC输入过载函数发生器运放调理任意LM358输出→10kΩ电位器→PA0电位器另一端→3.3V电位器调至中点使信号峰峰值≈2.5V避开ADC饱和区0~3.3V真实传感器振动传感器YS-100传感器→OP07放大10倍→RC低通滤波→PA0必须加低通滤波截止频率设为采样率一半5kHz否则混叠注意PA0必须接100nF陶瓷电容到GND硬件抗混叠否则高频噪声会折叠进基带导致幅值虚高。4.3 实测数据对比用示波器Matlab交叉验证我们用50Hz/1Vrms正弦波实测对比三种方法方法频率(Hz)幅值(Vrms)相位差(°)备注本项目1024点49.9980.998—单通道无相位本项目双通道49.9970.99715.2°CH2滞后CH1 15.2°示波器测得15.3°Matlab FFT1024点50.0001.00215.3°同一CSV数据验证算法一致性普通万用表AC档—0.705—仅测工频有效值无法反映失真关键结论频率误差0.003Hz0.006%源于插值精度幅值误差0.2%主要来自运放失调和ADC INL积分非线性相位差误差0.1°在10kHz采样下理论时间分辨率为100ns对应50Hz周期的0.018°实测已达极限。5. 常见问题排查5条踩坑记录每条都是深夜调试换来的后悔药嵌入式FFT调试最折磨人——现象诡异、原因隐蔽、复现困难。以下是项目实测中高频出现的5个坑按“现象→原因→解决”结构整理省你至少20小时。5.1 现象FFT幅值谱全为0或只有直流分量巨大原因ADC未真正启动或DMA未正确触发。常见于RCC-APB2ENR未使能ADC时钟或ADC1-CR2中EXTSEL位错误配置为外部触发而项目用软件触发。排查用Keil Debugger停在while(1)前查看ADC1-SR寄存器EOC位是否周期置1若否检查RCC-CFGR中ADC预分频是否设为PCLK2/6即12MHz符合ADC最大14MHz要求。解决确认RCC-APB2ENR | RCC_APB2ENR_ADC1EN;执行且ADC1-CR2 | ADC_CR2_SWSTART;在DMA配置后调用。5.2 现象频率显示跳变剧烈如50Hz→55Hz→48Hz随机跳原因采样率不稳定。SysTick_Handler()中若插入了printf或长延时会打乱100μs定时精度。排查用示波器测PA1程序中配置为SysTick翻转IO看波形是否严格10kHz方波。若占空比畸变或周期抖动1μs则定时不准。解决删除所有printfSysTick中断服务函数内只做g_sample_count和if(g_sample_count1024){fft_trigger1; g_sample_count0;}FFT计算移至主循环。5.3 现象相位差恒为±180°且不随信号变化原因双通道未同步采样。ADC1和ADC2未配置为交替触发Interleaved mode或ADC2时钟未使能。排查查RCC-APB2ENR是否设置了RCC_APB2ENR_ADC2EN查ADC1-CR1中DUALMOD位是否为0x03快速交替模式。解决在adc_dma.c初始化中加入ADC1-CR1 | ADC_CR1_DUALMOD_0 | ADC_CR1_DUALMOD_1;并确保ADC2同配置。5.4 现象FFT结果有规律性杂波如每隔16点出现尖峰原因电源噪声耦合。USB-TTL模块的5V电源通过GND串入ADC参考地形成16.67kHz干扰USB帧率。排查拔掉USB-TTL用电池供电若杂波消失则确认是电源问题。解决USB-TTL的GND与STM32板GND单点连接只在ADC参考地附近接并在VREF引脚加10μF钽电容100nF陶瓷电容滤波。5.5 现象编译报错undefined reference to cosf或sinf原因Keil未链接浮点数学库。cosf()属于math.h但Keil默认不包含arm_math.lib。排查查看Build Output中是否有Linking...后跟Warning: L6314W: No section matches pattern...。解决在Options for Target → Linker → Libraries中Add添加C:\Keil_v5\ARM\ARMCC\lib\armlib\fpv4_sp_d16.lib路径依Keil版本而定或直接在C/C → Define中加__MICROLIB启用MicroLIB自带数学函数。6. 二次开发实战3个可立即落地的改造技巧让项目为你所用这个项目不是终点而是你嵌入式信号分析能力的起点。我把它用在某高校电机控制实验室的振动监测节点上根据实际需求做了三次关键改造每一步都经过产线验证。下面分享最实用的三个技巧附可直接粘贴的代码。6.1 技巧1动态切换采样率适配不同信号带宽原项目SAMPLE_RATE是宏定义改一次要重编译。实际中你可能先用10kHz看50Hz工频再切到100kHz看开关电源噪声。解决方案运行时重配SysTickADC预分频。// user_config.h 新增 extern uint32_t dynamic_sample_rate; // 声明为extern定义在main.c // main.c 中定义并初始化 uint32_t dynamic_sample_rate 10000; // 默认10kHz // 新增函数动态重配采样率调用前确保ADC已关闭 void set_sample_rate(uint32_t rate) { // 1. 重配SysTick假设系统时钟72MHz SysTick-LOAD (72000000 / rate) - 1; // 自动重载值 SysTick-VAL 0; // 清空当前计数 SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; // 重新使能 // 2. 重配ADC预分频ADCCLK PCLK2 / ADCPRE // PCLK2 72MHz, 要ADCCLK ≤ 14MHz → ADCPRE ≥ 72/14 ≈ 5.14 → 取6 RCC-CFGR ~RCC_CFGR_ADCPRE; RCC-CFGR | RCC_CFGR_ADCPRE_DIV6; dynamic_sample_rate rate; }验证方法在串口命令行输入rate 50000调用set_sample_rate(50000)用示波器测SysTick引脚确认周期变为20μs50kHzFFT频率轴自动更新freq_bin i * dynamic_sample_rate / FFT_SIZE。6.2 技巧2增加谐波分析功能输出THD总谐波失真工业现场常需评估信号纯净度。在peak_detect.c中扩展// 新增函数计算THD前5次谐波 float calculate_thd(float *mag_spectrum, uint16_t len) { uint16_t fund_idx get_fundamental_index(mag_spectrum, len); if(fund_idx 0) return 0.0f; float fund_power mag_spectrum[fund_idx] * mag_spectrum[fund_idx]; float harm_power 0.0f; // 累加2nd~5th谐波跳过直流和噪声带 for(uint8_t n2; n5; n) { uint16_t harm_idx fund_idx * n; if(harm_idx len harm_idx 5) { // 避开低频噪声 harm_power mag_spectrum[harm_idx] * mag_spectrum[harm_idx]; } } return sqrtf(harm_power / fund_power) * 100.0f; // 百分比 }集成到主循环// main.c 中 if(fft_done) { float thd calculate_thd(mag_spectrum, FFT_SIZE); printf(THD: %.2f%%\r\n, thd); // 通过串口输出 fft_done 0; }实测某变频器输出50Hz基波THD2.3%与Fluke 435电能质量分析仪读数2.1%吻合。6.3 技巧3将FFT结果通过CAN总线广播构建多节点监测网单节点分析有限多节点需协同。项目预留can.c框架只需填充// can.c 中发送函数基于标准库CAN void can_send_fft_result(uint16_t freq_hz, float amp_v, float phase_deg) { CanTxMsgTypeDef TxMessage; TxMessage.StdId 0x101; // 自定义ID TxMessage.ExtId 0x00; TxMessage.RTR CAN_RTR_DATA; TxMessage.IDE CAN_ID_STD; TxMessage.DLC 8; // 8字节数据 // 打包freq(2B) | amp(4B) | phase(2B) TxMessage.Data[0] (freq_hz 8) 0xFF; TxMessage.Data[1] freq_hz 0xFF; memcpy(TxMessage.Data[2], amp_v, 4); // float转字节 TxMessage.Data[6] (uint8_t)((int16_t)(phase_deg * 10) 8); // 相位×10转int16 TxMessage.Data[7] (uint8_t)((int16_t)(phase_deg * 10) 0xFF); HAL_CAN_Transmit(hcan, TxMessage, 10); // 10ms超时 }部署效果3个STM32节点分别监测电机A/B/C相电流主站CAN接收用Python脚本解析实时绘制三相相位矢量图故障时如B相断线相位差突变为120°→0°系统秒级告警。从那以后我每次做嵌入式信号处理项目都会先问自己三个问题采样时钟够稳吗内存布局可预测吗算法每一步耗时可测吗这个项目教会我的不是FFT公式而是如何把一个数学概念焊死在MCU的寄存器、时钟树和DMA通道里让它在-40℃到85℃的车间里十年如一日地吐出准确数字。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询