简介:这份资源面向STM32嵌入式开发者与数字信号处理学习者,提供一套基于STM32H743、通过STM32CubeMX搭建的纯净FFT验证例程,解决全网缺少简单直接FFT实测工程的问题。压缩包共1698个文件,约49.05MB,以722个C源文件、336个头文件、151个汇编文件及146个链接脚本为主,另含库文件、工程配置与少量文档,完整覆盖FFT库及相关支持库。例程开启FPU进行硬件加速双精度浮点运算,1024点FFT仅耗时约300us,并用定时器精确测量实时运算时间,结果可通过串口输出查看,同时附有STM32CubeMX源文件便于学习配置。目前已有2409人学习下载,适合希望快速验证FFT性能、研究FPU加速与DSP库集成的开发者参考,并留有进一步优化空间。
1. 从一次 3 毫秒的 FFT 说起:为什么你的 STM32H743 跑不进 300us
前阵子帮一个做音频分析的朋友看代码,他用 STM32H743 跑 1024 点 FFT,实测耗时 3ms 出头,CPU 占用率飙到 70%,后面还得接一堆滤波和通信任务,整个系统卡得没法看。我让他把工程发过来,打开 STM32CubeMX 一看,FPU 压根没开,编译器浮点 ABI 还是软件模式,DSP 库也没挂上——等于拿一颗 480MHz 的 M7 当 F103 在用。重新配了一遍,同样的 1024 点 FFT,耗时直接掉到 300us 左右,CPU 占用降到个位数。
这个场景太常见了。STM32H743 这颗片子本身带双精度 FPU、带 DSP 指令集、带大容量 DTCM,硬件底子完全够用,但 CubeMX 里几个勾选项没配对,性能就天差地别。这篇笔记就是把我自己搭这套环境的完整过程记下来:从 CubeMX 的版本选择、时钟树配置、FPU 和 DSP 库的挂载,到 CMSIS-DSP 的 FFT 例程怎么跑通、怎么验证耗时,最后再说几个我踩过的坑。适合正在用 H743 做信号处理、又不想被软件浮点拖死的开发者。
2. 环境准备:CubeMX、HAL 库和 CMSIS-DSP 的版本搭配
2.1 工具链版本选择
这套环境涉及三个东西:STM32CubeMX、STM32CubeH7 固件包、CMSIS-DSP 库。版本不匹配是新手最容易翻车的地方,我一般按下面这个组合来:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| STM32CubeMX | 6.10 及以上 | 6.9 之前对 H7 的 DTCM 分配有 bug |
| STM32CubeH7 固件包 | 1.11.0 及以上 | 1.10 之前 CMSIS-DSP 版本偏旧 |
| CMSIS-DSP | 随固件包自带 | 不要单独去下,版本容易冲突 |
| IDE | Keil MDK 5.38 / STM32CubeIDE 1.14 | 两者都行,看习惯 |
CubeMX 6.10 之后的版本在 Project Manager 里对 FPU 的选项描述更清楚,不会出现「勾了 FPU 但生成的代码没生效」这种玄学问题。固件包建议直接用 CubeMX 里的 Manage embedded software packages 在线装,装完在本地仓库路径下能找到Middlewares/ST/ARM/DSP目录,这就是后面要挂的 CMSIS-DSP。
2.2 新建工程与芯片选型
打开 CubeMX,File → New Project,在搜索框输入 STM32H743,选具体型号。我手上用的是 STM32H743VIT6,LQFP100 封装,Flash 2MB、RAM 1MB,够跑 FFT 加缓存。如果你用的是 H743ZI 或者 H743II,配置逻辑一样,只是引脚分布不同。
选完芯片进主界面,先别急着点引脚,按顺序来:RCC → SYS → 时钟树 → 然后才是外设。这个顺序能避免后面改时钟时把已配好的外设时序搞乱。
2.3 RCC 与调试接口配置
在 Pinout & Configuration 里找到 System Core → RCC:
- High Speed Clock (HSE):选 Crystal/Ceramic Resonator
- Low Speed Clock (LSE):按需,跑 FFT 用不到可以不管
再到 SYS:
- Debug:选 Serial Wire
- Timebase Source:选 TIM1 或 SysTick 都行,我习惯用 TIM1,把 SysTick 留给 RTOS
这两步看着简单,但 SYS 的 Debug 如果不选 Serial Wire,生成代码后 SWD 口会被复用成普通 GPIO,下次就烧不进去了,只能靠 BOOT0 拉高救回来。这个坑我踩过一次,后面会细说。
2.4 时钟树配置:把 H743 拉到 480MHz
H743 的最高主频是 480MHz,但 CubeMX 默认给你配的是 64MHz 的 HSI。要跑到 480MHz,得手动改时钟树。
在 Clock Configuration 标签页里,按这个路径配:
- PLL Source:选 HSE
- 输入 HSE 频率:按你板子上的晶振填,一般是 8MHz 或 25MHz
- PLL1 的 DIVM、DIVN、DIVP 三个分频/倍频系数要凑出 480MHz
以 25MHz 晶振为例,常见做法是:
# 时钟树参数(25MHz HSE 输入) PLL1 DIVM = 5 # 25MHz / 5 = 5MHz 参考频率 PLL1 DIVN = 192 # 5MHz * 192 = 960MHz VCO PLL1 DIVP = 2 # 960MHz / 2 = 480MHz 系统时钟 AHB Prescaler = 1 # HCLK = 480MHz APB1 Prescaler = 2 # APB1 = 120MHz APB2 Prescaler = 2 # APB2 = 120MHz配完在 Clock Configuration 界面能看到 SYSCLK 显示 480MHz,如果显示红色说明某一级超频了,回去检查分频系数。这里有个细节:DIVN 的取值范围是 4~512,DIVP 只能是 2、4、6、8,凑不出整数就换个 DIVM 再试。
提示:时钟树配好后先别急着生成代码,把 Voltage Scale 调到 Scale 0,否则 480MHz 下会不稳定。这个选项在 Power and Thermal 配置里。
3. 开启 FPU 与挂载 CMSIS-DSP:让硬件浮点真正生效
3.1 FPU 配置的三个关键点
FPU 不是勾一个选项就完事的,CubeMX 里涉及三个地方,少一个都白搭。
第一处在 Project Manager → Code Generator 里,找到 Floating Point Unit/Extension 选项,勾上。但光勾这个还不够,它只是告诉 CubeMX 生成对应的编译选项。
第二处在 Target 标签页,确认 Floating-point ABI 选的是 Hard float(ABI)。如果这里选成 Soft float,前面勾了 FPU 也是软件模拟,性能差十倍不止。
第三处是编译器本身的选项。CubeMX 生成的工程会带-mfpu=fpv5-d16 -mfloat-abi=hard,但如果你手动改过 Makefile 或者 Keil 的 Target 选项,可能被覆盖掉。生成代码后我一般会去检查一下:
# 检查 GCC 编译选项(Makefile 工程) grep -r "mfloat-abi" ./Makefile # 期望输出:-mfloat-abi=hard -mfpu=fpv5-d16 # Keil 工程在 Options for Target → Target 标签页 # Floating Point Hardware 选 "Use FPU (Double Precision)"H743 的 FPU 是双精度的(fpv5-d16),支持单精度和双精度浮点运算。FFT 如果用 float32 类型,走的是单精度路径,速度更快;用 float64 会走双精度,精度高但慢一些。CMSIS-DSP 的 FFT 例程默认用 float32,这也是 300us 耗时的前提。
3.2 挂载 CMSIS-DSP 库
CMSIS-DSP 不在 HAL 库里,得单独挂。有两种方式:
方式一,在 CubeMX 的 Middleware 里勾选。Software Packs → Select Components → ARM → CMSIS → DSP,勾上。这样 CubeMX 会自动把 DSP 库的路径加到工程里,生成代码时也会带上初始化。
方式二,手动添加。如果 CubeMX 里找不到 DSP 组件(有些固件包版本没带),就去固件包目录下手动拷:
# 固件包路径示例(CubeMX 本地仓库) STM32Cube/Repository/Packs/STMicroelectronics/X-CUBE-AI/... # CMSIS-DSP 实际路径 STM32Cube/Repository/Packs/ARM/CMSIS/<version>/CMSIS/DSP # 把 DSP 目录拷到工程 Middlewares 下 cp -r CMSIS/DSP ./Middlewares/Third_Party/CMSIS/DSP然后在 IDE 里把Include路径加上,把Source下的.c文件加入编译。DSP 库文件不少,全加进去编译会慢,我一般只加用到的:TransformFunctions下的arm_cfft_f32.c、arm_rfft_fast_f32.c,还有CommonTables里的arm_common_tables.c。
3.3 预编译宏与头文件包含
CMSIS-DSP 依赖几个宏定义,不配会报一堆未定义符号。在工程的预编译宏里加上:
// 预编译宏(Keil 在 C/C++ → Define,GCC 在 Makefile 的 CFLAGS) ARM_MATH_CM7 // 指定 Cortex-M7 内核 ARM_MATH_DSP // 启用 DSP 指令集优化 __FPU_PRESENT=1 // 告诉库 FPU 存在 __FPU_USED=1 // 告诉库 FPU 已启用ARM_MATH_CM7这个宏特别关键,它决定了 CMSIS-DSP 用哪套汇编优化。H743 是 M7 内核,如果错写成ARM_MATH_CM4,编译能过,但跑的是 M4 的优化路径,性能会打折。
头文件包含就一行:
#include "arm_math.h"如果编译报找不到arm_math.h,检查 Include 路径里有没有CMSIS/DSP/Include。这个路径在 CubeMX 生成工程时如果勾了 DSP 组件会自动加,手动挂的话得自己补。
4. FFT 例程落地:1024 点实数 FFT 的完整代码与参数
4.1 选择 FFT 类型:实数还是复数
CMSIS-DSP 提供好几种 FFT 函数,选错了要么结果不对,要么性能浪费。常见的有:
arm_cfft_f32:复数 FFT,输入输出都是复数arm_rfft_f32:实数 FFT,输入实数,输出复数arm_rfft_fast_f32:实数快速 FFT,专为实数优化,速度最快
音频、振动这类信号处理,输入都是实数序列,用arm_rfft_fast_f32最合适。它内部会把实数序列打包成 N/2 点复数做运算,再还原,比直接上arm_cfft_f32快将近一倍。1024 点实数 FFT 用这个函数,在 480MHz H743 上跑 300us 是正常水平。
4.2 初始化结构体与缓冲区
arm_rfft_fast_f32需要一个实例结构体和输入输出缓冲区。代码骨架如下:
#include "arm_math.h" #define FFT_SIZE 1024 // FFT 实例结构体 static arm_rfft_fast_instance_f32 fft_instance; // 输入缓冲区(实数,长度 FFT_SIZE) static float32_t fft_input[FFT_SIZE]; // 输出缓冲区(复数,长度 FFT_SIZE,实部虚部交替存放) static float32_t fft_output[FFT_SIZE]; void fft_init(void) { // 初始化实数 FFT 实例,参数为 FFT 点数 arm_rfft_fast_init_f32(&fft_instance, FFT_SIZE); } void fft_process(void) { // 执行 FFT,第三个参数 0 表示正变换,1 表示逆变换 arm_rfft_fast_f32(&fft_instance, fft_input, fft_output, 0); // 计算幅值(可选) float32_t mag[FFT_SIZE / 2]; arm_cmplx_mag_f32(fft_output, mag, FFT_SIZE / 2); }arm_rfft_fast_init_f32只需要调一次,放在系统初始化里。它内部会根据点数预计算旋转因子表,1024 点的表大概占几 KB 的 RAM,H743 的 DTCM 完全放得下。
arm_rfft_fast_f32的第三个参数是变换方向,0 是正变换(时域到频域),1 是逆变换。注意输出缓冲区长度是 FFT_SIZE,但有效数据只有前 FFT_SIZE/2 个复数,因为实数 FFT 的频谱是对称的,后一半是镜像。
4.3 数据放置:DTCM 还是 AXI SRAM
H743 的内存分好几块:DTCM、ITCM、AXI SRAM、SRAM1~4。FFT 的输入输出缓冲区放哪里,对性能有影响。
DTCM 是紧耦合内存,CPU 访问零等待,但 DMA 访问不了。AXI SRAM 容量大,DMA 能访问,但 CPU 访问有延迟。FFT 是纯 CPU 运算,不涉及 DMA,所以缓冲区放 DTCM 最快。
在链接脚本里把缓冲区定位到 DTCM:
// GCC 链接脚本片段(.ld 文件) .dtcm_data (NOLOAD) : { . = ALIGN(4); *(.dtcm_data) . = ALIGN(4); } > DTCMRAM // C 代码里指定段 static float32_t fft_input[FFT_SIZE] __attribute__((section(".dtcm_data"))); static float32_t fft_output[FFT_SIZE] __attribute__((section(".dtcm_data")));Keil 的话用__attribute__((section(".ARM.__at_0x20000000")))或者直接在 scatter file 里配。DTCM 的起始地址是 0x20000000,大小 128KB,放两个 1024 点的 float 数组(各 4KB)绰绰有余。
注意:如果开了 Cache,DTCM 不需要考虑 Cache 一致性,但 AXI SRAM 上的数据在 DMA 和 CPU 之间传递时要做 Cache 维护。FFT 不涉及 DMA,放 DTCM 省心。
4.4 耗时测量:用 DWT 周期计数器
想知道 FFT 到底跑了多少微秒,最准的办法是用 DWT 的周期计数器。H743 的 DWT 在 480MHz 下,一个周期约 2.08ns,数周期再换算成时间。
// 启用 DWT 周期计数器 void dwt_init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } // 测量 FFT 耗时 uint32_t t0, t1, cycles; float us; dwt_init(); t0 = DWT->CYCCNT; arm_rfft_fast_f32(&fft_instance, fft_input, fft_output, 0); t1 = DWT->CYCCNT; cycles = t1 - t0; us = (float)cycles / 480.0f; // 480MHz 下,周期数除以 480 得微秒480MHz 下,300us 对应 144000 个周期。如果实测出来是 140000~150000 之间,说明配置到位了。如果跑到 100 万周期以上,回去检查 FPU 和 DSP 宏。
5. 避坑与排查:FPU 开了但没生效的几种情况
5.1 现象:FFT 耗时 3ms,和没开 FPU 一样
原因:CubeMX 里勾了 FPU,但编译器 ABI 还是 soft float。CubeMX 的 FPU 选项只影响它生成的代码,如果你用的是自己建的工程或者改过 Makefile,编译选项可能没同步。
解决:检查编译命令里有没有-mfloat-abi=hard。GCC 工程看 Makefile 的CFLAGS,Keil 看 Options for Target → Target → Floating Point Hardware。改完重新全编译,不能只增量编译,否则旧的软件浮点目标文件还在。
5.2 现象:编译报 undefined reference toarm_rfft_fast_init_f32
原因:CMSIS-DSP 的源文件没加进工程,或者加了但没编译。CubeMX 勾选 DSP 组件时,有时只加了 Include 路径,没把 Source 加进编译列表。
解决:手动把arm_rfft_fast_f32.c、arm_common_tables.c、arm_cfft_f32.c这几个文件加到工程的源文件列表里。注意arm_common_tables.c必须加,FFT 的旋转因子表在里面,不加会报一堆表未定义。
5.3 现象:FFT 结果全是 0 或者乱码
原因:输入缓冲区没填数据,或者填了但被编译器优化掉了。还有一种可能是arm_rfft_fast_init_f32没调用,实例结构体里全是 0。
解决:先确认fft_init在main里被调用了。然后在 FFT 前给输入填一个正弦波测试:
// 填 1kHz 正弦波,采样率 48kHz for (int i = 0; i < FFT_SIZE; i++) { fft_input[i] = arm_sin_f32(2.0f * PI * 1000.0f * i / 48000.0f); }如果这样跑出来频谱在 1kHz 处有峰值,说明 FFT 本身没问题,是实际信号采集环节的事。
5.4 现象:SWD 烧不进去,提示 no target connected
原因:SYS 的 Debug 没选 Serial Wire,生成代码后 PA13/PA14 被配成普通 GPIO,SWD 口失效。
解决:把 BOOT0 拉高,复位进系统存储器启动模式,用 STM32CubeProgrammer 擦除 Flash,然后重新生成代码时记得选 Serial Wire。这个坑我踩过一次,板子焊死在设备里,拆下来接 BOOT0 跳线折腾了半天。
5.5 现象:480MHz 下跑一会儿就 HardFault
原因:Voltage Scale 没调到 Scale 0,或者 Flash 等待周期没配对。H743 在 480MHz 下需要 Scale 0 电压和至少 4 个 Flash 等待周期。
解决:CubeMX 的 Power and Thermal 里把 Voltage Scale 设为 Scale 0,Flash 的 Latency 在 RCC 配置里会自动算,但如果你手动改过时钟树,回去确认一下。另外,如果用了外部 SDRAM 或者 QSPI,这些外设的时序也要跟着调。
6. 装完怎么验证:从周期数到频谱峰值的完整检查链
环境搭完,别急着上真实信号,按下面这条链走一遍,每一步都能定位问题出在哪。
第一步,确认 FPU 真的在跑。写一段纯浮点乘加循环,用 DWT 测周期:
volatile float a = 1.234f, b = 5.678f, c; uint32_t t0 = DWT->CYCCNT; for (int i = 0; i < 1000; i++) { c = a * b + c; } uint32_t cycles = DWT->CYCCNT - t0; // 硬件 FPU 下,1000 次乘加约 1000~2000 周期 // 软件浮点下,至少 50000 周期以上如果这段代码跑出来几万周期,说明 FPU 没生效,回去查 ABI 和编译选项。正常应该在 2000 周期以内。
第二步,验证 CMSIS-DSP 的 FFT 初始化。调用arm_rfft_fast_init_f32后,检查实例结构体里的fftLenRFFT是不是等于 1024,pTwiddleRFFT指针是不是非空。这两个字段不对,后面 FFT 肯定出错。
第三步,跑已知频率的正弦波,看频谱峰值位置。填 1kHz 正弦,采样率 48kHz,1024 点 FFT 的频率分辨率是 48000/1024 ≈ 46.875Hz。1kHz 对应第 1000/46.875 ≈ 21.3 个 bin,峰值应该出现在第 21 或 22 个 bin 附近。如果峰值位置差很远,检查采样率参数和 FFT 点数是否匹配。
第四步,测实际耗时。用 DWT 包住arm_rfft_fast_f32调用,跑 100 次取平均,排除首次运行的 Cache 冷启动影响。480MHz、DTCM、FPU 全开的情况下,1024 点实数 FFT 应该在 280~320us 之间。如果明显偏高,检查缓冲区是不是放在了 AXI SRAM 而不是 DTCM。
第五步,看 CPU 占用。如果 FFT 是放在主循环里跑,用 GPIO 翻转加示波器看高电平占比。300us 的 FFT 在 1ms 周期里占 30%,如果占比超过 50%,要么降 FFT 点数,要么把 FFT 放到中断里用 DMA 触发。
我自己的习惯是,每次新建 H743 工程,先把这五步走一遍再写业务代码。有一次偷懒跳过了第一步,结果调了三天才发现是 CubeMX 生成工程时 ABI 选项被重置了。从那以后,DWT 测周期这段代码我都是直接做成模板,新建工程第一件事就是把它跑通。希望这套流程能帮你少走点弯路。
本文还有配套的精品资源,点击获取