简介:这份资源面向嵌入式开发初学者与电子工程师,围绕STM32F1系列单片机讲解快速傅里叶变换(FFT)的完整实现方案,帮助读者在Cortex-M3平台上完成时域到频域的转换,适用于通信、音频处理与信号分析等场景。压缩包共225个文件,以60个h头文件与56个c源文件为核心,配合30个o目标文件、30个d依赖文件及27个crf等编译中间产物,另有uvprojx工程文件、hex固件、map映射表与sct分散加载文件,整体约4.12MB,工程结构完整可直接编译。内容涉及FFT蝶形运算与位反转、CMSIS-DSP库调用、内存布局规划、定时器中断触发采集、DMA并行加速以及浮点误差分析等关键知识点,并附带Keil调试与性能分析所需的配置。目前已有10174人学习下载,适合希望从理论到工程落地掌握单片机数字信号处理的读者参考实践。
1. 从一块跑不动的 ADC 采样说起:STM32 上做 FFT 到底难在哪
很多人第一次在 STM32 上做 FFT,都是被一个很朴素的需求逼出来的:ADC 采回来一串波形,想在片子上直接看出主频成分,不想把数据传到上位机再算。听起来不难,但真动手就会发现,坑不在 FFT 算法本身,而在采样、定点、内存和实时性这四件事上。STM32 是单片机,不是 DSP,主频、RAM、浮点能力都有限,直接照搬 PC 上的 FFT 代码,大概率跑不动或者结果全是噪声。这个资源包解决的正是这个问题:它把 STM32 上从 ADC 采样到 FFT 输出频谱的完整链路拆开,给出可复现的工程结构和参数配置。适合已经会点 C、用过 STM32 外设、但没在片子上正经做过频域分析的嵌入式从业者。下面按「先搞懂为什么这么选,再动手跑通,最后看坑」的顺序推一遍。
2. 采样与点数怎么定:ADC 配置和 FFT 分辨率的硬约束
2.1 采样率、点数、分辨率三者的关系
FFT 不是孤立算法,它的输入质量由 ADC 采样决定。核心公式只有两个:频率分辨率 Δf = fs / N,最高可分析频率 fmax = fs / 2。fs 是采样率,N 是 FFT 点数。比如你要分析 0~1 kHz 的信号,fs 至少 2 kHz,想分辨到 10 Hz,N 就得 200 以上,实际取 256 或 512。STM32 的 ADC 最快采样率受主频和采样周期限制,F1 系列 12 位模式下通常到 1 MSPS 左右,F4 更高。但别只看手册上限,实际要留出转换时间和 DMA 搬运时间。常见做法是:先定你要看的频段,再反推 fs,最后选 N 为 2 的整数次幂,因为基 2 FFT 最快。这个顺序不能反,反了就会出现「采了半天,分辨率不够,谱线糊成一团」的翻车现场。
2.2 ADC + DMA + 定时器触发:把采样做稳
裸写 ADC 轮询采样,在 FFT 场景下基本不可用,因为采样间隔抖动会直接恶化频谱。正确做法是用定时器触发 ADC,DMA 搬运到缓冲区,CPU 不干预。下面是一段常见的配置骨架,以 HAL 库为例:
// 定时器触发 ADC + DMA 搬运,采样率由 TIM 决定 // TIM 更新频率 = 72MHz / (PSC+1) / (ARR+1) // 例如 PSC=0, ARR=71 -> 1MHz 触发频率 htim3.Init.Prescaler = 0; htim3.Init.Period = 71; // 1MHz 触发 HAL_TIM_Base_Start(&htim3); // ADC 配置为外部触发,DMA 循环模式 hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, FFT_N); // DMA 配置为循环模式,半满和全满中断都可用来做双缓冲 hdma_adc1.Init.Mode = DMA_CIRCULAR;逻辑说明:定时器按固定频率产生 TRGO 事件,ADC 每次收到触发才转换一次,转换结果由 DMA 直接写入adc_buf,全程不占 CPU。参数上,PSC和ARR共同决定触发频率,也就是你的实际采样率 fs。FFT_N是缓冲区长度,必须和 FFT 点数一致。注意 ADC 的采样周期要设够,否则高阻抗信号源还没充完电就转换,数据会偏小。常见坑是 DMA 和 ADC 都开了但没开定时器,结果一个点都采不到,查半天以为是 FFT 的问题。
2.3 点数选 256 还是 1024:RAM 和实时性的权衡
STM32F103 只有 20 KB RAM,1024 点浮点 FFT 光输入输出就要 8 KB,加上中间变量很容易爆。所以小容量片子建议 256 点定点 FFT,用 Q15 格式。F4 系列有 128 KB 以上 RAM 和 FPU,可以上 1024 点浮点。选点数的原则是:先满足分辨率,再看 RAM 余量,最后看一帧计算时间能不能接受。256 点 Q15 FFT 在 72 MHz 的 M3 上大约几百微秒到 1 毫秒,1024 点浮点 F4 大约几十到一百多微秒。如果你要做实时频谱刷新,一帧时间必须小于刷新周期,否则数据会堆积。这个资源包里对两种配置都给了工程,切换时主要改FFT_N和对应的库函数。
3. 定点还是浮点:CMSIS-DSP 库的选型与调用
3.1 为什么优先用 CMSIS-DSP 而不是自己写
自己写基 2 FFT 不是不行,但 CMSIS-DSP 是 ARM 官方针对 Cortex-M 优化的,用了汇编和 SIMD 指令,速度比手写 C 快不少,而且接口稳定。资源包里用的就是 CMSIS-DSP 的arm_rfft_q15和arm_rfft_fast_f32两套。选型逻辑很简单:M3/M0 没有 FPU,用 Q15 定点;M4/M7 有 FPU,用浮点。别在 M3 上硬跑浮点 FFT,软件浮点模拟会慢到怀疑人生。下面是一个 Q15 实数 FFT 的调用示例:
#include "arm_math.h" #define FFT_N 256 q15_t adc_q15[FFT_N]; // ADC 原始数据转成 Q15 q15_t fft_out[FFT_N]; // FFT 输出,实部虚部交替 arm_rfft_instance_q15 rfft; // 初始化:256 点实数 FFT arm_rfft_init_q15(&rfft, FFT_N, 0, 1); // 执行 FFT arm_rfft_q15(&rfft, adc_q15, fft_out); // 计算幅值:每两个点一组,实部在前虚部在后 q15_t mag[FFT_N/2]; arm_cmplx_mag_q15(fft_out, mag, FFT_N/2);逻辑说明:arm_rfft_init_q15第三个参数 0 表示正变换,第四个参数 1 表示位反转,一般保持默认。fft_out里是打包的复数,实部虚部交替存放,所以算幅值要用arm_cmplx_mag_q15,长度是FFT_N/2,因为实数 FFT 输出对称,只看前一半。参数上,adc_q15要把 12 位 ADC 值左移对齐到 Q15 范围,否则幅值会偏小。常见错误是直接把 ADC 原始值当 Q15 用,结果频谱幅度差了几十倍,还以为是算法错了。
3.2 浮点版本与窗函数的配合
如果用的是 F4/F7,浮点版本更直观,不用操心定标。但浮点版本同样要注意窗函数。不加窗的话,非整数周期采样会导致频谱泄漏,谱线旁边出现一堆假峰。资源包里默认加了汉宁窗,代码结构如下:
float32_t adc_f32[FFT_N]; float32_t fft_out[FFT_N]; arm_rfft_fast_instance_f32 rfft; arm_rfft_fast_init_f32(&rfft, FFT_N); // 加汉宁窗:先乘窗系数再 FFT for (int i = 0; i < FFT_N; i++) { adc_f32[i] = (float32_t)adc_buf[i] * hanning_win[i]; } arm_rfft_fast_f32(&rfft, adc_f32, fft_out, 0); arm_cmplx_mag_f32(fft_out, mag_f32, FFT_N/2);逻辑说明:窗系数表可以预先算好存 Flash,不用每次算。arm_rfft_fast_f32最后一个参数 0 表示正变换。加窗会降低幅值精度,如果要做精确幅值测量,需要乘一个窗增益补偿系数,汉宁窗大约是 2.0。参数上,hanning_win长度必须等于FFT_N。坑在于有人加了窗但忘了补偿,测出来的幅值只有实际的一半,还以为是 ADC 量程没设对。
3.3 从 ADC 原始值到 Q15 的定标
这一步是定点 FFT 最容易翻车的地方。12 位 ADC 输出 0~4095,Q15 范围是 -32768~32767。常见做法是减去直流偏置再左移。比如偏置 2048,代码是:
for (int i = 0; i < FFT_N; i++) { int32_t tmp = (int32_t)adc_buf[i] - 2048; // 去直流 tmp = tmp << 4; // 左移 4 位放大到 Q15 if (tmp > 32767) tmp = 32767; if (tmp < -32768) tmp = -32768; adc_q15[i] = (q15_t)tmp; }逻辑说明:去直流是因为 FFT 的直流分量会很大,盖住有用信号。左移 4 位相当于乘 16,把 12 位范围映射到接近 Q15 满量程。参数上,偏置值要按实际电路算,如果是 3.3V 供电、信号偏在 1.65V,那偏置就是 2048。坑在于左移太多会溢出,溢出后波形削顶,频谱出现大量谐波,看起来像信号本身有问题,其实是定标错了。
4. 避坑与排查:FFT 结果不对时先查这五件事
4.1 频谱全是噪声,找不到主峰
现象:FFT 输出幅值杂乱,没有明显峰值。原因通常是采样率设错或定时器触发没生效,实际采样间隔不均匀。解决:先用示波器或 GPIO 翻转测一下 ADC 实际采样率,确认定时器 TRGO 真的在触发。另一个常见原因是 ADC 输入悬空或阻抗太高,采到的就是噪声。检查信号源是否共地、偏置是否在量程中间。
4.2 主峰位置对,但幅值差很多
现象:频率点是对的,但幅值只有预期的一半或几倍。原因:定点版本忘了定标,或者浮点版本加了窗没补偿。解决:定点先确认左移位数和偏置,浮点确认窗增益系数。可以用一个已知幅值的正弦波做标定,反推修正系数。别靠猜,用已知信号测一遍最稳。
4.3 程序跑着跑着就 HardFault
现象:FFT 调用几次后进入 HardFault。原因:缓冲区越界或栈溢出。CMSIS-DSP 的 FFT 实例结构体不小,如果放在局部变量里,M3 默认栈可能不够。解决:把arm_rfft_instance_q15和缓冲区定义成全局或 static,或者加大栈。另外检查FFT_N和缓冲区长度是否一致,DMA 搬运长度是否匹配。
4.4 频谱出现对称的假峰
现象:在 fs/2 附近出现不该有的峰。原因:ADC 采样率不够,高频信号混叠进来了。解决:在 ADC 输入前加抗混叠低通滤波器,截止频率设在 fs/2 以下。软件上没法消除混叠,只能靠硬件。这是血泪经验,很多人调了半天算法,最后发现是前端没滤波。
4.5 实时刷新时数据跳动大
现象:频谱刷新时峰值忽大忽小。原因:没有做多帧平均,或者 DMA 双缓冲没处理好,FFT 算的是半帧旧半帧新的数据。解决:用 DMA 半满和全满中断做双缓冲,确保 FFT 处理的是完整一帧。可以再加简单的滑动平均,牺牲一点刷新率换稳定度。参数上,平均帧数一般取 4 或 8,太多会显得迟钝。
5. 进阶技巧:用查表法加速幅值计算与频谱显示
定点 FFT 算完之后,arm_cmplx_mag_q15内部有开方,虽然已经优化过,但在 M3 上仍然占时间。如果只是做频谱显示,不需要精确幅值,可以用查表法近似。思路是:把实部和虚部绝对值相加再查表,或者直接用max(|re|,|im|) + 0.4*min(|re|,|im|)近似。下面是一个快速幅值近似:
// 快速幅值近似,避免开方 q15_t fast_mag(q15_t re, q15_t im) { q15_t a = (re > 0) ? re : -re; q15_t b = (im > 0) ? im : -im; q15_t max = (a > b) ? a : b; q15_t min = (a > b) ? b : a; return max + (min >> 2) + (min >> 3); // 约等于 max + 0.375*min }逻辑说明:这个近似在工程上足够看峰值位置,误差在 5% 以内,但省掉了开方。参数上,移位系数可以按精度需求调,>>2加>>3是 0.375,接近 0.414 的理论值。如果要做精确测量,还是用库函数。另一个技巧是把频谱结果映射到 OLED 或 TFT 显示时,用对数坐标,否则小信号完全看不见。对数映射可以用查表,把 0~32767 映射到 0~64 像素高度。
从那以后我每次在 STM32 上做 FFT,都强制先跑一遍已知正弦波标定,确认采样率、定标和窗补偿都对,再上真实信号。这个习惯帮我省掉了至少三次通宵排查。希望帮到你。
本文还有配套的精品资源,点击获取