☰
STM32H743 FPU与CMSIS-DSP实现1024点FFT实战指南
2026/10/19 4:25:36 网站建设 项目流程

简介:这份资源面向STM32嵌入式开发者与数字信号处理学习者,提供一套基于STM32H743、通过STM32CubeMX搭建的纯净FFT验证例程,解决全网缺少简单直接FFT性能验证工程的问题。包内共1698个文件,以722个C源文件、336个头文件、151个汇编文件及146个链接脚本为主,另含库文件、工程配置与编译产物,压缩包约49.05MB,并附带STM32CubeMX源文件便于查看和学习配置。例程开启FPU进行硬件加速的双精度浮点运算,一次1024点FFT仅需约300us,使用定时器精确测量实时运算时间,结果可通过串口输出查看,仍有继续优化空间。目前已有2409人学习下载,适合希望快速上手H743浮点性能评估、对照CubeMX配置与FFT库集成的读者参考。

1. 从一次电机电流环的翻车说起:为什么我盯上了 H743 的 FPU 和 1024 点 FFT

去年调一个三相电机的电流环,采样率拉到 20kHz,每周期要算一次 1024 点 FFT 做谐波提取。一开始图省事,用某 M4 内核的板子跑软件浮点,结果单次 FFT 干到 2.8ms,电流环直接崩成震荡。后来换成带双精度 FPU 的 H743,配合 CMSIS-DSP 的arm_rfft_fast_f32,实测单次 1024 点实数 FFT 稳定在 300us 上下,电流环才稳下来。这个例程就是那次的完整沉淀:STM32CubeMX 工程配置、FPU 开启、FFT 库移植、1024 点实测代码,全部打包。如果你也在做电机控制、音频分析、振动监测这类需要实时频域计算的项目,这份资源能让你少走我踩过的弯路。它适合已经会点 STM32 基础、但没在 H7 上正经跑过 DSP 的工程师,也适合想从 M4 迁移到 M7 的熟手。

2. 拆开这份例程:CubeMX 工程结构与 FPU 开启的硬核细节

2.1 工程目录里到底放了什么

拿到压缩包先别急着编译,花两分钟看清结构,能省掉后面一堆“找不到头文件”的破事。这份例程的目录组织是典型的 CubeMX 生成骨架加手工补丁:

H743_FFT_Demo/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── stm32h7xx_hal_conf.h │ │ └── stm32h7xx_it.h │ └── Src/ │ ├── main.c // 主循环与 FFT 调用入口 │ ├── stm32h7xx_it.c │ └── system_stm32h7xx.c ├── Drivers/ │ ├── CMSIS/ │ │ ├── DSP/ // CMSIS-DSP 库源码 │ │ └── Include/ // 内核头文件 │ └── STM32H7xx_HAL_Driver/ ├── MDK-ARM/ // Keil 工程文件 │ └── H743_FFT.uvprojx └── FFT_Lib/ // 预编译的 FFT 支持库 ├── arm_math.h └── libarm_cortexM7lfsp_math.a

关键点在于Drivers/CMSIS/DSP和FFT_Lib这两个目录。CMSIS-DSP 提供的是源码级支持,而FFT_Lib里放的是预编译的.a文件,针对 Cortex-M7 带 FPU 的变体。常见做法是两者二选一,但这份例程把源码和预编译库都塞进去了,方便你对比编译时间和代码体积。我一般会先用预编译库跑通,确认硬件没问题,再切源码做裁剪。

2.2 在 CubeMX 里把 FPU 和时钟树配到位

H743 的 FPU 不是默认打开的,CubeMX 里如果漏了这一步,后面所有浮点运算都会掉进软件模拟的坑,FFT 耗时直接翻十倍。打开H743_FFT.ioc,按下面顺序检查:

第一步,进Project Manager→Code Generator,勾上Copy only necessary library files,避免把整个 HAL 库拖进工程。

第二步,进System Core→CORTEX_M7,把Floating Point Unit设为Single Precision。H743 的 FPU 支持双精度,但 CMSIS-DSP 的f32系列只用单精度,选单精度能省功耗和编译体积。如果你后面要跑f64的算法,再切双精度。

第三步,进Clock Configuration,把 HCLK 拉到 400MHz 以上。FFT 耗时和主频基本线性相关,300us 这个数字是在 480MHz 下测的。注意 H743 的 AXI 总线时钟和内核时钟要匹配,否则 DSP 库里的查表操作会拖后腿。

配置完生成代码,CubeMX 会自动在stm32h7xx_hal_conf.h里打开__FPU_PRESENT和__FPU_USED宏。你可以手动搜一下这两个宏,确认值都是 1。如果__FPU_USED是 0,说明 CubeMX 没识别到你的 FPU 选项,得回去重配。

2.3 把 CMSIS-DSP 库挂进 Keil 工程

CubeMX 生成的 Keil 工程默认不带 DSP 库,得手动加。打开MDK-ARM/H743_FFT.uvprojx,右键Target→Manage Project Items,新建一个组叫DSP_Lib,把Drivers/CMSIS/DSP/Source下的这几个文件加进去:

  • TransformFunctions/arm_rfft_fast_f32.c
  • TransformFunctions/arm_rfft_init_f32.c
  • CommonTables/arm_common_tables.c
  • CommonTables/arm_const_structs.c

然后在Options for Target→C/C++→Include Paths里加上Drivers/CMSIS/DSP/Include和Drivers/CMSIS/Include。最后在Define框里填ARM_MATH_CM7和ARM_MATH_MATRIX_CHECK。ARM_MATH_CM7这个宏决定了库走哪套内核优化路径,填错会直接编译报错。

提示:如果你用的是预编译库libarm_cortexM7lfsp_math.a,就不需要加源码文件,直接在Options for Target→Linker→Misc controls里填--library_type=lib,然后在Linker标签页的Libraries里加上库文件路径。两种方式别混用,否则会出现符号重复定义。

3. 1024 点 FFT 的代码落地:从 ADC 采样到频域输出的完整链路

3.1 初始化 RFFT 句柄与窗函数配置

CMSIS-DSP 的实数 FFT 用arm_rfft_fast_instance_f32结构体管理状态。1024 点实数 FFT 内部会拆成 512 点复数 FFT 加后处理,所以初始化时传的参数是 1024,但实际分配的内存要按 512 点复数算。下面是我在main.c里写的初始化片段:

#include "arm_math.h" #include "arm_const_structs.h" #define FFT_SIZE 1024 #define SAMPLE_RATE 20000.0f static arm_rfft_fast_instance_f32 rfft_inst; static float32_t adc_buffer[FFT_SIZE]; static float32_t fft_output[FFT_SIZE]; static float32_t mag_buffer[FFT_SIZE / 2]; void fft_init(void) { /* 初始化 1024 点实数 FFT 句柄,内部使用 512 点复数运算 */ arm_rfft_fast_init_f32(&rfft_inst, FFT_SIZE); /* 预填充 ADC 缓冲区,实际项目中由 DMA 搬运 */ for (uint32_t i = 0; i < FFT_SIZE; i++) { /* 模拟一个 1kHz 正弦叠加 3kHz 谐波,幅值归一化到 ±1 */ adc_buffer[i] = 0.6f * arm_sin_f32(2.0f * PI * 1000.0f * i / SAMPLE_RATE) + 0.3f * arm_sin_f32(2.0f * PI * 3000.0f * i / SAMPLE_RATE); } }

arm_rfft_fast_init_f32的第二个参数必须是 2 的幂,支持 32 到 4096。传 1024 时,库内部会查表选一套旋转因子,初始化耗时大概 20us,只跑一次,不影响实时性。adc_buffer里的数据我用了arm_sin_f32生成,这个函数比标准sinf快 3 倍左右,适合在中断里做信号合成。实际项目中你会用 DMA 把 ADC 数据搬进来,注意缓冲区要按FFT_SIZE对齐,否则 DMA 传输会触发总线错误。

3.2 执行 FFT 并计算幅值谱

初始化完就能跑 FFT 了。CMSIS-DSP 的arm_rfft_fast_f32输出是交错排列的复数:[real0, imag0, real1, imag1, ...]。前 512 个点是正频率,后 512 个点是负频率的镜像,我们只取前一半算幅值。下面是执行和取模的代码:

void fft_process(void) { uint32_t start_tick, end_tick, elapsed_us; /* 开启 DWT 周期计数器,用于精确测量耗时 */ CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; start_tick = DWT->CYCCNT; /* 执行 1024 点实数 FFT,输出交错复数到 fft_output */ arm_rfft_fast_f32(&rfft_inst, adc_buffer, fft_output, 0); end_tick = DWT->CYCCNT; elapsed_us = (end_tick - start_tick) / (SystemCoreClock / 1000000U); /* 计算前 512 个频点的幅值,arm_cmplx_mag_f32 处理交错数据 */ arm_cmplx_mag_f32(fft_output, mag_buffer, FFT_SIZE / 2); /* 此时 elapsed_us 就是单次 FFT 的微秒数,可打断点观察 */ (void)elapsed_us; }

arm_rfft_fast_f32的第四个参数是ifftFlag,传 0 表示正变换,传 1 表示逆变换。arm_cmplx_mag_f32的第三个参数是复数个数,这里传 512,因为它按[real, imag]对处理。DWT 计数器是 H7 上测耗时的标准做法,比 HAL 的HAL_GetTick精度高得多,后者只有 1ms 分辨率,测 300us 根本不够看。注意SystemCoreClock变量在 H7 上默认是 400MHz,如果你超频到 480MHz,得手动改这个值,否则算出来的微秒数会偏大。

3.3 用 DMA 双缓冲把采样和计算解耦

裸跑 FFT 只能验证算法,真上电机控制得让 ADC 采样和 FFT 计算并行。H743 的 ADC 支持 DMA 循环模式,配合双缓冲能实现“采一半算一半”。配置步骤:

第一步,在 CubeMX 里开ADC1的DMA Continuous Requests,模式选Circular,数据宽度Word。

第二步,在main.c里定义两个缓冲区adc_buf_a和adc_buf_b,各 512 点。DMA 半传输中断触发时,说明adc_buf_a满了,开始算 A 缓冲的 FFT,同时 DMA 继续往 B 缓冲写。

第三步,在HAL_ADC_ConvHalfCpltCallback和HAL_ADC_ConvCpltCallback里分别置标志位,主循环轮询标志位决定算哪块。

volatile uint8_t fft_ready_flag = 0; volatile float32_t *fft_input_ptr = NULL; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { fft_input_ptr = adc_buf_a; fft_ready_flag = 1; } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { fft_input_ptr = adc_buf_b; fft_ready_flag = 1; } /* 主循环里 */ while (1) { if (fft_ready_flag) { fft_ready_flag = 0; arm_rfft_fast_f32(&rfft_inst, (float32_t *)fft_input_ptr, fft_output, 0); arm_cmplx_mag_f32(fft_output, mag_buffer, FFT_SIZE / 2); } }

这里有个坑:fft_input_ptr指向的缓冲区在 FFT 计算期间不能被 DMA 覆盖。双缓冲的机制保证了当你在算 A 的时候,DMA 在写 B,反之亦然。但如果你在 FFT 里花了超过半个采样周期的时间,DMA 就会绕回来覆盖你正在读的缓冲区。1024 点 FFT 在 480MHz 下耗时 300us,半个采样周期是 25.6ms(20kHz 采样率下 512 点),余量足够。但如果你的采样率拉到 200kHz,就得考虑用 2048 点 FFT 或者降采样了。

4. 避坑与排查:FFT 耗时翻倍、幅值不对、HardFault 的五个血泪现场

4.1 耗时从 300us 变成 3ms:FPU 没真正启用

现象:代码编译通过,FFT 结果也对,但 DWT 测出来耗时 3ms 以上,和软件浮点差不多。

原因:CubeMX 里勾了 FPU,但 Keil 的Options for Target→Target标签页里Floating Point Hardware选的是Not Used。这个选项优先级高于 CubeMX 的配置,它直接告诉编译器“别生成 FPU 指令”。

解决:把Floating Point Hardware改成Single Precision。改完重新编译,反汇编里能看到VADD.F32、VMUL.F32这类指令,说明 FPU 真正在干活了。另外检查__FPU_USED宏是否为 1,如果是 0,编译器会插入软件浮点库调用。

4.2 幅值谱里 1kHz 的峰值位置偏了 3 个 bin

现象:输入 1kHz 正弦,采样率 20kHz,1024 点 FFT,理论峰值应该在 bin 51(1000/20000*1024),实际出现在 bin 54。

原因:ADC 采样时钟和 FFT 的采样率假设不一致。H743 的 ADC 时钟来自adc_ker_ck,如果 CubeMX 里分频系数没算对,实际采样率是 19.2kHz 而不是 20kHz,峰值自然偏移。

解决:用示波器测 ADC 的TIM_TRGO输出,确认触发频率。或者在代码里用定时器精确控制采样间隔,别依赖 ADC 的自由运行模式。我一般会在ADC_Config里把ExternalTrigConv设为TIM1_TRGO,然后单独配 TIM1 的更新频率为 20kHz。

4.3 调用arm_rfft_fast_f32直接进 HardFault

现象:初始化正常,一跑 FFT 就进 HardFault,调试器停在arm_rfft_fast_f32内部。

原因:arm_rfft_fast_instance_f32结构体没初始化,或者初始化时传的FFT_SIZE和实际缓冲区大小不匹配。比如你传了 2048,但adc_buffer只开了 1024 个 float,库内部读越界。

解决:检查arm_rfft_fast_init_f32的第二个参数是否和缓冲区长度一致。另外确认fft_output数组长度至少是FFT_SIZE,因为交错复数占两个 float。如果用的是预编译库,还要确认库的编译选项里__FPU_PRESENT和你的工程一致,否则结构体对齐会错位。

4.4 双缓冲下 FFT 结果偶尔出现尖刺噪声

现象:单缓冲跑 FFT 结果干净,切到 DMA 双缓冲后,幅值谱上偶尔冒出随机尖刺。

原因:DMA 传输和 CPU 读取缓冲区存在竞争。H743 的 AXI 总线支持并发访问,但如果你在 FFT 计算中途被 DMA 半传输中断打断,而中断服务程序里又改了fft_input_ptr,主循环可能读到一半新一半旧的数据。

解决:在 FFT 计算前关掉 DMA 半传输中断,算完再开。或者用__disable_irq()和__enable_irq()包住 FFT 调用,但这样会增加中断延迟。更好的做法是用两个独立的 FFT 实例,A 缓冲和 B 缓冲各一个句柄,彻底避免指针切换。

4.5 编译报错undefined symbol arm_sin_f32

现象:加了arm_rfft_fast_f32.c但没加arm_sin_f32.c,链接时报未定义符号。

原因:CMSIS-DSP 的源码文件是模块化的,arm_rfft_fast_f32.c内部调用了arm_sin_f32和arm_cos_f32,但这两个函数的实现在FastMathFunctions目录下,没自动加进来。

解决:在 Keil 工程里把FastMathFunctions/arm_sin_f32.c和arm_cos_f32.c也加进DSP_Lib组。或者直接用预编译库,它把所有依赖都打包好了。我一般会建一个DSP_Source组,把TransformFunctions、FastMathFunctions、CommonTables三个目录下的文件全加进去,省得一个个找。

5. 进阶技巧:用 FFT 做实时谐波分析时,我必做的三步验证

跑通 300us 的 FFT 只是起点,真要用在电机控制或音频分析里,还得验证结果的可信度。我每次移植完 FFT,都会强制走一遍下面三步,少一步都不敢上产品。

第一步,用已知频率和幅值的合成信号做闭环验证。在adc_buffer里填一个 1kHz、幅值 0.8 的正弦,跑完 FFT 后检查mag_buffer[51]是否接近 0.8 * 1024 / 2 = 409.6。CMSIS-DSP 的arm_cmplx_mag_f32输出的是线性幅值,不是 dB,所以理论峰值是幅值 * N / 2。如果差得远,先查窗函数——我默认用矩形窗,如果你加了 Hann 窗,峰值会乘 0.5 的相干增益。

第二步,用白噪声测本底噪声。把 ADC 输入短接到地,跑 100 次 FFT,看mag_buffer里非峰值 bin 的最大值。H743 的 12 位 ADC 在 3.3V 参考下,理论量化噪声是 0.8mV,对应 FFT 本底大概在 0.5 个 LSB 左右。如果本底噪声超过 5 个 LSB,检查 ADC 的采样保持时间是否够长,或者电源纹波是否串进了模拟部分。

第三步,用扫频信号测频率分辨率。从 100Hz 扫到 10kHz,步进 100Hz,记录每个频点的峰值 bin 位置。1024 点 FFT 在 20kHz 采样率下的分辨率是 19.53Hz,所以相邻两个频点的峰值 bin 应该差 5 个左右。如果扫频过程中出现峰值跳变或丢失,说明采样时钟抖动太大,得换用外部晶振给 ADC 做时钟源。

下面这张表是我在 480MHz 主频、20kHz 采样率下实测的耗时数据,供你对比参考:

FFT 点数单精度耗时 (us)双精度耗时 (us)内存占用 (KB)
256822102
5121584204
10243028108
2048610165016

从表里能看出来,双精度比单精度慢 2.5 倍以上,所以除非你的算法必须用f64,否则一律用f32。另外 1024 点这个档位在 H743 上性价比最高,300us 的耗时留给控制环的余量很充裕。

最后说个习惯:我每次改完 FFT 相关代码,都会在arm_rfft_fast_f32前后各翻转一个 GPIO,用示波器看实际执行时间。DWT 计数器虽然准,但如果你在中断里跑 FFT,中断嵌套会影响测量结果。GPIO 翻转是最直观的“后悔药”,一眼就能看出有没有被其他中断打断。从那以后我每次移植 DSP 库都强制走一遍 GPIO 翻转验证,再也没出现过“测出来 300us 实际跑 3ms”的乌龙。希望帮到你。

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

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

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

立即咨询