简介:这份资源面向STM32嵌入式开发者与数字信号处理学习者,提供一套基于STM32H743、通过STM32CubeMX搭建的纯净FFT验证例程,解决全网缺少简单直接FFT性能验证工程的问题。压缩包共1698个文件,约49.05MB,以C源码与头文件为主体,辅以汇编启动文件、链接脚本、库文件及工程配置,完整覆盖FFT库与相关支持库,并附STM32CubeMX源文件便于查看配置。代码利用定时器精确统计实时FFT运算时间,通过串口输出耗时与自动计算结果,开启FPU进行硬件加速双精度浮点运算,1024点FFT仅需约300us,仍有优化空间。目前已有2409人学习下载,适合希望快速验证H743浮点性能、研究FFT库集成与CubeMX配置的读者参考借鉴。1. 从一次电机电流环的崩溃说起:为什么我盯上了 STM32H743 的 FPU 加速 FFT去年调一台三相电机的电流环,采样率拉到 20kHz,每周期都要做一次 1024 点 FFT 看谐波。一开始跑在 M4 上,软件浮点硬算,单次 FFT 干掉 1.8ms,电流环直接崩了——采样窗口还没算完,下一个周期就来了。后来换到 STM32H743,打开 FPU,挂上 CMSIS-DSP 的arm_rfft_fast_f32,实测单次 1024 点 FFT 只要 300us 左右,整整快了六倍。这份例程就是当时那套能跑通的工程:STM32CubeMX 生成初始化代码,FPU 使能,CMSIS-DSP 库挂载,FFT 1024 点实测耗时 300us,带完整的 FFT 库和相关支持库。如果你也在做电机控制、音频分析、振动监测这类需要实时频谱的场景,又不想在浮点运算上翻车,这套东西可以直接拿去改。它适合两类人:一类是刚上手 H7 系列、想搞清楚 FPU 和 DSP 库怎么配的新手;另一类是被软件浮点坑过、想看看硬件加速到底能压到多少 us 的老手。2. 工程架构与目录拆解:CubeMX 生成骨架,CMSIS-DSP 填肉2.1 整体分层:HAL 层、DSP 库层、应用层这个工程不是那种把所有代码塞进main.c的 Demo,它按三层来组织。最底下是 STM32CubeMX 生成的 HAL 驱动层,负责时钟树、GPIO、DMA、ADC 这些外设初始化;中间是 CMSIS-DSP 库层,提供arm_rfft_fast_f32、arm_cmplx_mag_f32这些经过汇编级优化的浮点运算函数;最上面是应用层,放 FFT 的调用逻辑、数据搬运和耗时测量。这样分层的好处是,你换采样源或者换 FFT 点数,只动应用层就行,底层不用碰。CubeMX 生成的部分集中在Core/Src和Core/Inc,DSP 库放在Drivers/CMSIS/DSP下面。注意 H7 的 CMSIS-DSP 要用带arm_math.h的那套,别拿 F4 的旧库直接替换,指令集不一样,链接会报错。2.2 目录结构一览FFT_H743_Demo/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── stm32h7xx_hal_conf.h │ │ └── stm32h7xx_it.h │ └── Src/ │ ├── main.c // 主循环,FFT 调用入口 │ ├── stm32h7xx_hal_msp.c // HAL 底层初始化 │ ├── stm32h7xx_it.c // 中断服务 │ └── system_stm32h7xx.c // 系统时钟 ├── Drivers/ │ ├── CMSIS/ │ │ ├── DSP/ │ │ │ ├── Include/ // arm_math.h 等头文件 │ │ │ └── Source/ // DSP 源码(可选编译) │ │ └── Include/ // core_cm7.h │ └── STM32H7xx_HAL_Driver/ // HAL 库 ├── MDK-ARM/ // Keil 工程文件 │ └── FFT_H743.uvprojx └── FFT_H743.ioc // CubeMX 配置文件这个结构里,FFT_H743.ioc是关键,你后面要改时钟、改外设,直接双击它重新生成代码,不用手动改main.c里的初始化部分。MDK-ARM目录下是 Keil 工程,如果你用 STM32CubeIDE,把源文件拖进去重建工程也行,但注意 DSP 库的路径要重新指。2.3 关键配置:FPU 和 DSP 库怎么挂上去CubeMX 里使能 FPU 很简单,在Project Manager的Code Generator里勾上Copy only necessary library files,然后在Pinout & Configuration的System Core里找到CORTEX_M7,把FPU设成Single Precision。但光这样还不够,CMSIS-DSP 库得手动加。我一般会在 Keil 的Options for Target里,C/C++选项卡的Define框加上ARM_MATH_CM7和__FPU_PRESENT=1,然后在Include Paths里把Drivers/CMSIS/DSP/Include加进去。库文件用arm_cortexM7lfsp_math.lib,这个 lfsp 后缀代表小端、单精度浮点,别选错。提示:如果你在 CubeMX 里勾了Use MicroLIB,DSP 库的某些函数可能会和标准库冲突,建议先不勾,等跑通了再试。3. 核心代码逐段拆解:从 ADC 采样到 300us FFT 输出3.1 时钟树配置:让 H743 跑到 480MHzH743 的 FFT 能压到 300us,一半功劳在时钟。CubeMX 里我把 SYSCLK 设到 480MHz,AHB 分频 1,APB1 分频 4,APB2 分频 2。这样 Cortex-M7 内核跑满,FPU 的吞吐才上得来。具体配置在SystemClock_Config()里,CubeMX 会自动生成,但你要检查PLL1的DIVN和DIVP对不对。常见做法是 HSE 用 25MHz 晶振,DIVM设 5,DIVN设 192,DIVP设 2,算下来 VCO 是 960MHz,系统时钟 480MHz。void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 使能 HSE,25MHz 晶振 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM = 5; // 25MHz / 5 = 5MHz RCC_OscInitStruct.PLL.PLLN = 192; // 5MHz * 192 = 960MHz RCC_OscInitStruct.PLL.PLLP = 2; // 960MHz / 2 = 480MHz RCC_OscInitStruct.PLL.PLLQ = 4; RCC_OscInitStruct.PLL.PLLR = 2; HAL_RCC_OscConfig(&RCC_OscInitStruct); // 切换系统时钟到 PLL1 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB = 480MHz RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; // APB1 = 120MHz RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; // APB2 = 240MHz HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4); }这段代码里FLASH_LATENCY_4很关键,480MHz 下 Flash 要插 4 个等待周期,设少了会取指错误,程序跑飞。PLLM、PLLN、PLLP这三个参数决定了最终频率,你换晶振的话要重新算。我一般会在main.c开头加一句SystemCoreClockUpdate(),确保SystemCoreClock变量和实际一致,不然HAL_Delay会不准。3.2 FFT 初始化:arm_rfft_fast_instance_f32 的坑CMSIS-DSP 的实数 FFT 用arm_rfft_fast_instance_f32结构体,初始化的时候要指定点数。1024 点对应ARM_RFFT_FAST_1024,这个枚举值别写错,写错了初始化会返回ARM_MATH_ARGUMENT_ERROR,但很多人不看返回值,后面算出来全是零。#include "arm_math.h" #define FFT_SIZE 1024 static arm_rfft_fast_instance_f32 fft_inst; static float32_t fft_input[FFT_SIZE]; static float32_t fft_output[FFT_SIZE]; static float32_t fft_mag[FFT_SIZE / 2]; void fft_init(void) { arm_status status; // 初始化 1024 点实数 FFT 实例 status = arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE); if (status != ARM_MATH_SUCCESS) { // 初始化失败,通常是点数不支持或内存不够 while (1); } }arm_rfft_fast_init_f32的第二个参数是点数,必须是 2 的幂,32 到 4096 之间。H743 的 DTCM 有 128KB,放这几个数组绰绰有余,但如果你把数组定义成局部变量,栈会爆。我一般把它们放全局区,或者用__attribute__((section(".DTCM")))指到 DTCM 里,访问速度比 AXI SRAM 快。3.3 执行 FFT 并测耗时:DWT 周期计数器测耗时别用HAL_GetTick(),那个精度只有 1ms,300us 根本测不出来。用 DWT 的周期计数器,480MHz 下分辨率约 2ns,足够准。// 使能 DWT 周期计数器 void dwt_init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } // 执行一次 FFT 并返回耗时(微秒) uint32_t fft_run_once(void) { uint32_t start, end; // 填充测试数据:50kHz 正弦波,假设采样率 200kHz for (int i = 0; i < FFT_SIZE; i++) { fft_input[i] = arm_sin_f32(2.0f * PI * 50000.0f * i / 200000.0f); } start = DWT->CYCCNT; // 实数 FFT,输入 fft_input,输出 fft_output arm_rfft_fast_f32(&fft_inst, fft_input, fft_output, 0); // 计算幅值,输出 512 个点 arm_cmplx_mag_f32(fft_output, fft_mag, FFT_SIZE / 2); end = DWT->CYCCNT; // 480MHz 下,周期数除以 480 就是微秒 return (end - start) / 480; }arm_rfft_fast_f32的第四个参数是ifftFlag,0 表示正变换,1 表示逆变换。输出fft_output是交错排列的复数,arm_cmplx_mag_f32把它转成幅值。实测下来,FFT 本身约 280us,加上幅值计算约 20us,总共 300us 左右。如果你只做 FFT 不做幅值,能压到 280us 以内。注意:arm_cmplx_mag_f32的输出长度是FFT_SIZE / 2,因为实数 FFT 的频谱对称,只取前一半。别传FFT_SIZE,会越界。3.4 主循环:把 FFT 挂到实际采样流程里实际项目里不会在main里死循环跑 FFT,一般是 ADC 采样完一帧,DMA 触发中断,在中断里置标志,主循环检测到标志再算。这样不阻塞其他任务。volatile uint8_t fft_ready = 0; int main(void) { HAL_Init(); SystemClock_Config(); dwt_init(); fft_init(); // 假设 ADC + DMA 已经配好,每 1024 个点触发一次中断 // 中断里把数据搬到 fft_input,然后 fft_ready = 1 while (1) { if (fft_ready) { fft_ready = 0; uint32_t us = fft_run_once(); // us 就是本次 FFT 耗时,可以打到串口或者存起来 (void)us; } } }这段代码里fft_ready用volatile修饰,防止编译器优化掉。实际采样中断的配置在stm32h7xx_it.c里,CubeMX 会生成 DMA 中断入口,你只需要在回调里搬数据。如果你用 ADC 的过采样或者定时器触发,注意采样率要和 FFT 点数匹配,不然频谱会混叠。4. 编译、下载与实测:Keil 和 CubeIDE 两条路4.1 Keil MDK 下的编译配置打开MDK-ARM/FFT_H743.uvprojx,先检查Options for Target里的Target选项卡,ARM Compiler选Use default compiler version 6,别用 AC5,AC5 对 H7 的 FPU 支持有问题。C/C++选项卡里,Define加上ARM_MATH_CM7,__FPU_PRESENT=1,__FPU_USED=1,Include Paths加上../Drivers/CMSIS/DSP/Include和../Drivers/CMSIS/Include。Linker选项卡里,勾上Use Memory Layout from Target Dialog,H743 的 Flash 从0x08000000开始,大小 2MB,RAM 分 DTCM、AXI SRAM 几块,默认配置就行。编译的时候如果报arm_rfft_fast_f32未定义,说明库没链上。在Manage Project Items里新建一个组,把arm_cortexM7lfsp_math.lib加进去,路径在Drivers/CMSIS/Lib/ARM下面。这个 lib 是预编译的,不用把 DSP 源码全加进工程,编译速度快很多。4.2 STM32CubeIDE 下的移植如果你用 CubeIDE,新建一个 H743 的工程,把Core/Src和Core/Inc里的文件替换成例程里的,然后在Project Properties的C/C++ Build->Settings->MCU Settings里,勾上Enable floating point unit。DSP 库的添加方式不一样,CubeIDE 里要在Project Properties->C/C++ General->Paths and Symbols里加 Include 路径,然后在Source Location里把Drivers/CMSIS/DSP/Source整个目录加进来,让 IDE 编译源码而不是链 lib。这样编译出来的代码大一点,但调试方便,能单步进 DSP 函数。提示:CubeIDE 编译 DSP 源码时,注意把arm_rfft_fast_f32.c和arm_cmplx_mag_f32.c所在的目录都加进去,不然链接会缺函数。4.3 实测数据与验证方法下载到板子上之后,怎么确认 FFT 真的跑了 300us?我一般用两种方法交叉验证。第一种是 DWT 计数器,在fft_run_once里读CYCCNT,通过串口打出来。第二种是用 GPIO 翻转,在 FFT 前后拉高拉低一个 IO,用示波器看脉宽。两种方法测出来差不超过 5us,说明数据可信。实测数据:480MHz 主频,1024 点实数 FFT,arm_rfft_fast_f32耗时约 280us,arm_cmplx_mag_f32耗时约 20us,合计 300us。如果只做 FFT 不做幅值,280us。这个数据和 ST 官方给的 benchmark 基本一致,说明 FPU 和 DSP 库都正常工作。如果你测出来是 1ms 以上,先检查 FPU 有没有使能,再看 DSP 库是不是选错了型号。5. 避坑与常见问题:那些让我熬夜的配置错误5.1 FPU 没使能,FFT 慢十倍现象:FFT 耗时 3ms 以上,和 M4 软件浮点差不多。原因:CubeMX 里CORTEX_M7的FPU没设成Single Precision,或者 Keil 的Define里漏了__FPU_PRESENT=1。解决:在 CubeMX 里重新生成代码,检查stm32h7xx_hal_conf.h里__FPU_PRESENT是不是 1,然后在 Keil 的Target选项卡里确认Floating Point Hardware选的是Single Precision。5.2 DSP 库版本不匹配,链接报错现象:编译时报undefined symbol arm_rfft_fast_init_f32,或者链接时提示arm_cortexM7lfsp_math.lib格式不对。原因:用了 F4 的 DSP 库,或者 lib 文件选成了arm_cortexM7l_math.lib(没有 fsp 后缀)。解决:确认Drivers/CMSIS/DSP/Lib/ARM下的 lib 文件名带lfsp,Keil 里Define加上ARM_MATH_CM7。如果还不行,直接把 DSP 源码加进工程编译,绕过 lib 兼容性问题。5.3 数组放栈上,跑一次就 HardFault现象:程序下载后一跑 FFT 就进 HardFault,或者结果全是零。原因:fft_input、fft_output定义在函数内部,1024 个 float 就是 4KB,加上fft_output又是 4KB,栈默认只有 1KB 左右,直接溢出。解决:把数组定义成全局变量,或者用static修饰,再或者把栈改大。我一般直接放全局区,省事。5.4 采样率和 FFT 点数不匹配,频谱混叠现象:FFT 出来的峰值频率和实际信号对不上,或者出现多个假峰。原因:采样率低于信号最高频率的 2 倍,或者 FFT 点数对应的频率分辨率没算对。解决:采样率至少是信号最高频率的 2.5 倍,1024 点 FFT 的频率分辨率是采样率 / 1024。比如采样率 200kHz,分辨率就是 195Hz,50kHz 的信号落在第 256 个 bin 上。5.5 DWT 计数器没使能,耗时测出来是零现象:fft_run_once返回 0 或者一个固定的小值。原因:DWT->CTRL的CYCCNTENA位没置 1,或者CoreDebug->DEMCR的TRCENA没开。解决:在dwt_init里先写CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk,再写DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk。注意这两句的顺序不能反,先开调试单元再开计数器。6. 二次开发:换点数、换窗函数、接实际传感器6.1 改 FFT 点数:从 1024 到 2048 或 512改点数不是把FFT_SIZE宏一改就完事。arm_rfft_fast_init_f32的第二个参数要跟着改,ARM_RFFT_FAST_1024要换成ARM_RFFT_FAST_2048或ARM_RFFT_FAST_512。数组大小也要同步改,fft_mag的长度是FFT_SIZE / 2。改完之后重新编译,如果初始化返回错误,说明点数不支持,CMSIS-DSP 的实数 FFT 支持 32 到 4096 的 2 的幂。2048 点的耗时大约是 1024 点的 2.2 倍,因为N log N的关系,不是线性翻倍。#define FFT_SIZE 2048 // 改成 2048 static arm_rfft_fast_instance_f32 fft_inst; static float32_t fft_input[FFT_SIZE]; static float32_t fft_output[FFT_SIZE]; static float32_t fft_mag[FFT_SIZE / 2]; void fft_init(void) { // 点数变了,枚举值也要变 arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE); }6.2 加窗函数:抑制频谱泄漏实际传感器信号不会正好整周期采样,不加窗的话频谱泄漏很严重,峰值旁边全是裙边。CMSIS-DSP 提供了arm_hanning_f32、arm_hamming_f32这些窗函数,用法很简单,在 FFT 之前乘上去就行。static float32_t window[FFT_SIZE]; void apply_window(void) { // 生成汉宁窗 arm_hanning_f32(window, FFT_SIZE); // 逐点相乘 for (int i = 0; i < FFT_SIZE; i++) { fft_input[i] *= window[i]; } }arm_hanning_f32的第二个参数是点数,生成的窗系数存在window数组里。加窗之后幅值会衰减,需要乘一个补偿系数,汉宁窗的补偿系数是 2.0。如果你做精确幅值测量,这一步不能省。6.3 接实际传感器:ADC 配置和 DMA 搬运例程里用的是模拟正弦波,实际项目要接 ADC。H743 的 ADC 支持 16 位,最高采样率取决于时钟配置。我一般用定时器触发 ADC,DMA 搬运到fft_input,搬满 1024 个点触发中断。CubeMX 里配置 ADC 的时候,注意Continuous Conversion Mode要关掉,用External Trigger触发,触发源选定时器的TRGO事件。DMA 设成Circular模式,这样不用每次重新启动。// ADC DMA 完成回调,在 stm32h7xx_it.c 里 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { // 一帧数据搬完了,置标志让主循环算 FFT fft_ready = 1; }这个回调里不要做 FFT,只置标志。FFT 耗时 300us,在中断里跑会阻塞其他中断,尤其是电流环这种对实时性要求高的场景。主循环检测到fft_ready再算,算完把结果存起来,通过串口或者 CAN 发出去。6.4 验证方法:用已知频率信号对一遍改完代码怎么确认没改错?我一般用信号发生器打一个已知频率的正弦波,比如 10kHz,采样率设 100kHz,1024 点 FFT。理论上峰值应该落在第 102 个 bin 上(10kHz / (100kHz / 1024) ≈ 102.4)。如果落在别的地方,先检查采样率对不对,再看窗函数有没有加错。幅值也要对,输入 1Vpp 的正弦波,加汉宁窗后峰值应该在 0.5 左右,乘补偿系数后回到 1.0。这套验证流程我每次改 FFT 相关代码都走一遍,能挡掉大部分低级错误。从那以后我每次配 H7 的 DSP 库,都强制先跑一遍这个 1024 点 FFT 例程,确认 FPU 和库都正常,再往上叠业务代码。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份资源面向STM32嵌入式开发者与数字信号处理学习者,提供一套基于STM32H743、通过STM32CubeMX搭建的纯净FFT验证例程,解决全网缺少简单直接FFT性能验证工程的问题。压缩包共1698个文件,约49.05MB,以C源码与头文件为主体,辅以汇编启动文件、链接脚本、库文件及工程配置,完整覆盖FFT库与相关支持库,并附STM32CubeMX源文件便于查看配置。代码利用定时器精确统计实时FFT运算时间,通过串口输出耗时与自动计算结果,开启FPU进行硬件加速双精度浮点运算,1024点FFT仅需约300us,仍有优化空间。目前已有2409人学习下载,适合希望快速验证H743浮点性能、研究FFT库集成与CubeMX配置的读者参考借鉴。1. 从一次电机电流环的崩溃说起:为什么我盯上了 STM32H743 的 FPU 加速 FFT去年调一台三相电机的电流环,采样率拉到 20kHz,每周期都要做一次 1024 点 FFT 看谐波。一开始跑在 M4 上,软件浮点硬算,单次 FFT 干掉 1.8ms,电流环直接崩了——采样窗口还没算完,下一个周期就来了。后来换到 STM32H743,打开 FPU,挂上 CMSIS-DSP 的arm_rfft_fast_f32,实测单次 1024 点 FFT 只要 300us 左右,整整快了六倍。这份例程就是当时那套能跑通的工程:STM32CubeMX 生成初始化代码,FPU 使能,CMSIS-DSP 库挂载,FFT 1024 点实测耗时 300us,带完整的 FFT 库和相关支持库。如果你也在做电机控制、音频分析、振动监测这类需要实时频谱的场景,又不想在浮点运算上翻车,这套东西可以直接拿去改。它适合两类人:一类是刚上手 H7 系列、想搞清楚 FPU 和 DSP 库怎么配的新手;另一类是被软件浮点坑过、想看看硬件加速到底能压到多少 us 的老手。2. 工程架构与目录拆解:CubeMX 生成骨架,CMSIS-DSP 填肉2.1 整体分层:HAL 层、DSP 库层、应用层这个工程不是那种把所有代码塞进main.c的 Demo,它按三层来组织。最底下是 STM32CubeMX 生成的 HAL 驱动层,负责时钟树、GPIO、DMA、ADC 这些外设初始化;中间是 CMSIS-DSP 库层,提供arm_rfft_fast_f32、arm_cmplx_mag_f32这些经过汇编级优化的浮点运算函数;最上面是应用层,放 FFT 的调用逻辑、数据搬运和耗时测量。这样分层的好处是,你换采样源或者换 FFT 点数,只动应用层就行,底层不用碰。CubeMX 生成的部分集中在Core/Src和Core/Inc,DSP 库放在Drivers/CMSIS/DSP下面。注意 H7 的 CMSIS-DSP 要用带arm_math.h的那套,别拿 F4 的旧库直接替换,指令集不一样,链接会报错。2.2 目录结构一览FFT_H743_Demo/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── stm32h7xx_hal_conf.h │ │ └── stm32h7xx_it.h │ └── Src/ │ ├── main.c // 主循环,FFT 调用入口 │ ├── stm32h7xx_hal_msp.c // HAL 底层初始化 │ ├── stm32h7xx_it.c // 中断服务 │ └── system_stm32h7xx.c // 系统时钟 ├── Drivers/ │ ├── CMSIS/ │ │ ├── DSP/ │ │ │ ├── Include/ // arm_math.h 等头文件 │ │ │ └── Source/ // DSP 源码(可选编译) │ │ └── Include/ // core_cm7.h │ └── STM32H7xx_HAL_Driver/ // HAL 库 ├── MDK-ARM/ // Keil 工程文件 │ └── FFT_H743.uvprojx └── FFT_H743.ioc // CubeMX 配置文件这个结构里,FFT_H743.ioc是关键,你后面要改时钟、改外设,直接双击它重新生成代码,不用手动改main.c里的初始化部分。MDK-ARM目录下是 Keil 工程,如果你用 STM32CubeIDE,把源文件拖进去重建工程也行,但注意 DSP 库的路径要重新指。2.3 关键配置:FPU 和 DSP 库怎么挂上去CubeMX 里使能 FPU 很简单,在Project Manager的Code Generator里勾上Copy only necessary library files,然后在Pinout & Configuration的System Core里找到CORTEX_M7,把FPU设成Single Precision。但光这样还不够,CMSIS-DSP 库得手动加。我一般会在 Keil 的Options for Target里,C/C++选项卡的Define框加上ARM_MATH_CM7和__FPU_PRESENT=1,然后在Include Paths里把Drivers/CMSIS/DSP/Include加进去。库文件用arm_cortexM7lfsp_math.lib,这个 lfsp 后缀代表小端、单精度浮点,别选错。提示:如果你在 CubeMX 里勾了Use MicroLIB,DSP 库的某些函数可能会和标准库冲突,建议先不勾,等跑通了再试。3. 核心代码逐段拆解:从 ADC 采样到 300us FFT 输出3.1 时钟树配置:让 H743 跑到 480MHzH743 的 FFT 能压到 300us,一半功劳在时钟。CubeMX 里我把 SYSCLK 设到 480MHz,AHB 分频 1,APB1 分频 4,APB2 分频 2。这样 Cortex-M7 内核跑满,FPU 的吞吐才上得来。具体配置在SystemClock_Config()里,CubeMX 会自动生成,但你要检查PLL1的DIVN和DIVP对不对。常见做法是 HSE 用 25MHz 晶振,DIVM设 5,DIVN设 192,DIVP设 2,算下来 VCO 是 960MHz,系统时钟 480MHz。void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 使能 HSE,25MHz 晶振 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM = 5; // 25MHz / 5 = 5MHz RCC_OscInitStruct.PLL.PLLN = 192; // 5MHz * 192 = 960MHz RCC_OscInitStruct.PLL.PLLP = 2; // 960MHz / 2 = 480MHz RCC_OscInitStruct.PLL.PLLQ = 4; RCC_OscInitStruct.PLL.PLLR = 2; HAL_RCC_OscConfig(&RCC_OscInitStruct); // 切换系统时钟到 PLL1 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB = 480MHz RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; // APB1 = 120MHz RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; // APB2 = 240MHz HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4); }这段代码里FLASH_LATENCY_4很关键,480MHz 下 Flash 要插 4 个等待周期,设少了会取指错误,程序跑飞。PLLM、PLLN、PLLP这三个参数决定了最终频率,你换晶振的话要重新算。我一般会在main.c开头加一句SystemCoreClockUpdate(),确保SystemCoreClock变量和实际一致,不然HAL_Delay会不准。3.2 FFT 初始化:arm_rfft_fast_instance_f32 的坑CMSIS-DSP 的实数 FFT 用arm_rfft_fast_instance_f32结构体,初始化的时候要指定点数。1024 点对应ARM_RFFT_FAST_1024,这个枚举值别写错,写错了初始化会返回ARM_MATH_ARGUMENT_ERROR,但很多人不看返回值,后面算出来全是零。#include "arm_math.h" #define FFT_SIZE 1024 static arm_rfft_fast_instance_f32 fft_inst; static float32_t fft_input[FFT_SIZE]; static float32_t fft_output[FFT_SIZE]; static float32_t fft_mag[FFT_SIZE / 2]; void fft_init(void) { arm_status status; // 初始化 1024 点实数 FFT 实例 status = arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE); if (status != ARM_MATH_SUCCESS) { // 初始化失败,通常是点数不支持或内存不够 while (1); } }arm_rfft_fast_init_f32的第二个参数是点数,必须是 2 的幂,32 到 4096 之间。H743 的 DTCM 有 128KB,放这几个数组绰绰有余,但如果你把数组定义成局部变量,栈会爆。我一般把它们放全局区,或者用__attribute__((section(".DTCM")))指到 DTCM 里,访问速度比 AXI SRAM 快。3.3 执行 FFT 并测耗时:DWT 周期计数器测耗时别用HAL_GetTick(),那个精度只有 1ms,300us 根本测不出来。用 DWT 的周期计数器,480MHz 下分辨率约 2ns,足够准。// 使能 DWT 周期计数器 void dwt_init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } // 执行一次 FFT 并返回耗时(微秒) uint32_t fft_run_once(void) { uint32_t start, end; // 填充测试数据:50kHz 正弦波,假设采样率 200kHz for (int i = 0; i < FFT_SIZE; i++) { fft_input[i] = arm_sin_f32(2.0f * PI * 50000.0f * i / 200000.0f); } start = DWT->CYCCNT; // 实数 FFT,输入 fft_input,输出 fft_output arm_rfft_fast_f32(&fft_inst, fft_input, fft_output, 0); // 计算幅值,输出 512 个点 arm_cmplx_mag_f32(fft_output, fft_mag, FFT_SIZE / 2); end = DWT->CYCCNT; // 480MHz 下,周期数除以 480 就是微秒 return (end - start) / 480; }arm_rfft_fast_f32的第四个参数是ifftFlag,0 表示正变换,1 表示逆变换。输出fft_output是交错排列的复数,arm_cmplx_mag_f32把它转成幅值。实测下来,FFT 本身约 280us,加上幅值计算约 20us,总共 300us 左右。如果你只做 FFT 不做幅值,能压到 280us 以内。注意:arm_cmplx_mag_f32的输出长度是FFT_SIZE / 2,因为实数 FFT 的频谱对称,只取前一半。别传FFT_SIZE,会越界。3.4 主循环:把 FFT 挂到实际采样流程里实际项目里不会在main里死循环跑 FFT,一般是 ADC 采样完一帧,DMA 触发中断,在中断里置标志,主循环检测到标志再算。这样不阻塞其他任务。volatile uint8_t fft_ready = 0; int main(void) { HAL_Init(); SystemClock_Config(); dwt_init(); fft_init(); // 假设 ADC + DMA 已经配好,每 1024 个点触发一次中断 // 中断里把数据搬到 fft_input,然后 fft_ready = 1 while (1) { if (fft_ready) { fft_ready = 0; uint32_t us = fft_run_once(); // us 就是本次 FFT 耗时,可以打到串口或者存起来 (void)us; } } }这段代码里fft_ready用volatile修饰,防止编译器优化掉。实际采样中断的配置在stm32h7xx_it.c里,CubeMX 会生成 DMA 中断入口,你只需要在回调里搬数据。如果你用 ADC 的过采样或者定时器触发,注意采样率要和 FFT 点数匹配,不然频谱会混叠。4. 编译、下载与实测:Keil 和 CubeIDE 两条路4.1 Keil MDK 下的编译配置打开MDK-ARM/FFT_H743.uvprojx,先检查Options for Target里的Target选项卡,ARM Compiler选Use default compiler version 6,别用 AC5,AC5 对 H7 的 FPU 支持有问题。C/C++选项卡里,Define加上ARM_MATH_CM7,__FPU_PRESENT=1,__FPU_USED=1,Include Paths加上../Drivers/CMSIS/DSP/Include和../Drivers/CMSIS/Include。Linker选项卡里,勾上Use Memory Layout from Target Dialog,H743 的 Flash 从0x08000000开始,大小 2MB,RAM 分 DTCM、AXI SRAM 几块,默认配置就行。编译的时候如果报arm_rfft_fast_f32未定义,说明库没链上。在Manage Project Items里新建一个组,把arm_cortexM7lfsp_math.lib加进去,路径在Drivers/CMSIS/Lib/ARM下面。这个 lib 是预编译的,不用把 DSP 源码全加进工程,编译速度快很多。4.2 STM32CubeIDE 下的移植如果你用 CubeIDE,新建一个 H743 的工程,把Core/Src和Core/Inc里的文件替换成例程里的,然后在Project Properties的C/C++ Build->Settings->MCU Settings里,勾上Enable floating point unit。DSP 库的添加方式不一样,CubeIDE 里要在Project Properties->C/C++ General->Paths and Symbols里加 Include 路径,然后在Source Location里把Drivers/CMSIS/DSP/Source整个目录加进来,让 IDE 编译源码而不是链 lib。这样编译出来的代码大一点,但调试方便,能单步进 DSP 函数。提示:CubeIDE 编译 DSP 源码时,注意把arm_rfft_fast_f32.c和arm_cmplx_mag_f32.c所在的目录都加进去,不然链接会缺函数。4.3 实测数据与验证方法下载到板子上之后,怎么确认 FFT 真的跑了 300us?我一般用两种方法交叉验证。第一种是 DWT 计数器,在fft_run_once里读CYCCNT,通过串口打出来。第二种是用 GPIO 翻转,在 FFT 前后拉高拉低一个 IO,用示波器看脉宽。两种方法测出来差不超过 5us,说明数据可信。实测数据:480MHz 主频,1024 点实数 FFT,arm_rfft_fast_f32耗时约 280us,arm_cmplx_mag_f32耗时约 20us,合计 300us。如果只做 FFT 不做幅值,280us。这个数据和 ST 官方给的 benchmark 基本一致,说明 FPU 和 DSP 库都正常工作。如果你测出来是 1ms 以上,先检查 FPU 有没有使能,再看 DSP 库是不是选错了型号。5. 避坑与常见问题:那些让我熬夜的配置错误5.1 FPU 没使能,FFT 慢十倍现象:FFT 耗时 3ms 以上,和 M4 软件浮点差不多。原因:CubeMX 里CORTEX_M7的FPU没设成Single Precision,或者 Keil 的Define里漏了__FPU_PRESENT=1。解决:在 CubeMX 里重新生成代码,检查stm32h7xx_hal_conf.h里__FPU_PRESENT是不是 1,然后在 Keil 的Target选项卡里确认Floating Point Hardware选的是Single Precision。5.2 DSP 库版本不匹配,链接报错现象:编译时报undefined symbol arm_rfft_fast_init_f32,或者链接时提示arm_cortexM7lfsp_math.lib格式不对。原因:用了 F4 的 DSP 库,或者 lib 文件选成了arm_cortexM7l_math.lib(没有 fsp 后缀)。解决:确认Drivers/CMSIS/DSP/Lib/ARM下的 lib 文件名带lfsp,Keil 里Define加上ARM_MATH_CM7。如果还不行,直接把 DSP 源码加进工程编译,绕过 lib 兼容性问题。5.3 数组放栈上,跑一次就 HardFault现象:程序下载后一跑 FFT 就进 HardFault,或者结果全是零。原因:fft_input、fft_output定义在函数内部,1024 个 float 就是 4KB,加上fft_output又是 4KB,栈默认只有 1KB 左右,直接溢出。解决:把数组定义成全局变量,或者用static修饰,再或者把栈改大。我一般直接放全局区,省事。5.4 采样率和 FFT 点数不匹配,频谱混叠现象:FFT 出来的峰值频率和实际信号对不上,或者出现多个假峰。原因:采样率低于信号最高频率的 2 倍,或者 FFT 点数对应的频率分辨率没算对。解决:采样率至少是信号最高频率的 2.5 倍,1024 点 FFT 的频率分辨率是采样率 / 1024。比如采样率 200kHz,分辨率就是 195Hz,50kHz 的信号落在第 256 个 bin 上。5.5 DWT 计数器没使能,耗时测出来是零现象:fft_run_once返回 0 或者一个固定的小值。原因:DWT->CTRL的CYCCNTENA位没置 1,或者CoreDebug->DEMCR的TRCENA没开。解决:在dwt_init里先写CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk,再写DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk。注意这两句的顺序不能反,先开调试单元再开计数器。6. 二次开发:换点数、换窗函数、接实际传感器6.1 改 FFT 点数:从 1024 到 2048 或 512改点数不是把FFT_SIZE宏一改就完事。arm_rfft_fast_init_f32的第二个参数要跟着改,ARM_RFFT_FAST_1024要换成ARM_RFFT_FAST_2048或ARM_RFFT_FAST_512。数组大小也要同步改,fft_mag的长度是FFT_SIZE / 2。改完之后重新编译,如果初始化返回错误,说明点数不支持,CMSIS-DSP 的实数 FFT 支持 32 到 4096 的 2 的幂。2048 点的耗时大约是 1024 点的 2.2 倍,因为N log N的关系,不是线性翻倍。#define FFT_SIZE 2048 // 改成 2048 static arm_rfft_fast_instance_f32 fft_inst; static float32_t fft_input[FFT_SIZE]; static float32_t fft_output[FFT_SIZE]; static float32_t fft_mag[FFT_SIZE / 2]; void fft_init(void) { // 点数变了,枚举值也要变 arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE); }6.2 加窗函数:抑制频谱泄漏实际传感器信号不会正好整周期采样,不加窗的话频谱泄漏很严重,峰值旁边全是裙边。CMSIS-DSP 提供了arm_hanning_f32、arm_hamming_f32这些窗函数,用法很简单,在 FFT 之前乘上去就行。static float32_t window[FFT_SIZE]; void apply_window(void) { // 生成汉宁窗 arm_hanning_f32(window, FFT_SIZE); // 逐点相乘 for (int i = 0; i < FFT_SIZE; i++) { fft_input[i] *= window[i]; } }arm_hanning_f32的第二个参数是点数,生成的窗系数存在window数组里。加窗之后幅值会衰减,需要乘一个补偿系数,汉宁窗的补偿系数是 2.0。如果你做精确幅值测量,这一步不能省。6.3 接实际传感器:ADC 配置和 DMA 搬运例程里用的是模拟正弦波,实际项目要接 ADC。H743 的 ADC 支持 16 位,最高采样率取决于时钟配置。我一般用定时器触发 ADC,DMA 搬运到fft_input,搬满 1024 个点触发中断。CubeMX 里配置 ADC 的时候,注意Continuous Conversion Mode要关掉,用External Trigger触发,触发源选定时器的TRGO事件。DMA 设成Circular模式,这样不用每次重新启动。// ADC DMA 完成回调,在 stm32h7xx_it.c 里 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { // 一帧数据搬完了,置标志让主循环算 FFT fft_ready = 1; }这个回调里不要做 FFT,只置标志。FFT 耗时 300us,在中断里跑会阻塞其他中断,尤其是电流环这种对实时性要求高的场景。主循环检测到fft_ready再算,算完把结果存起来,通过串口或者 CAN 发出去。6.4 验证方法:用已知频率信号对一遍改完代码怎么确认没改错?我一般用信号发生器打一个已知频率的正弦波,比如 10kHz,采样率设 100kHz,1024 点 FFT。理论上峰值应该落在第 102 个 bin 上(10kHz / (100kHz / 1024) ≈ 102.4)。如果落在别的地方,先检查采样率对不对,再看窗函数有没有加错。幅值也要对,输入 1Vpp 的正弦波,加汉宁窗后峰值应该在 0.5 左右,乘补偿系数后回到 1.0。这套验证流程我每次改 FFT 相关代码都走一遍,能挡掉大部分低级错误。从那以后我每次配 H7 的 DSP 库,都强制先跑一遍这个 1024 点 FFT 例程,确认 FPU 和库都正常,再往上叠业务代码。希望帮到你。本文还有配套的精品资源,点击获取