尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32F103 FFT频谱分析实战:定时器触发ADC与CMSIS-DSP实现
简介基于ARM官方库的STM32F103 FFT例程是一套面向嵌入式开发者的实战工程覆盖快速傅里叶变换在单片机上的完整落地流程从定时器采样、ADC/DMA数据搬运到FFT运算与结果幅频转换适合信号处理、频谱分析和实时监测场景兼顾快速入门与二次开发。压缩包共179个文件其中45个头文件、18个C源码文件构成核心实现另有32个o与31个crf等编译中间文件附带hex/axf可执行镜像可直接烧录与调试工程还包含CubeMX配置、map内存映射及链接脚本便于外设初始化迁移与内存占用分析。整套约7.34MB已有2130人学习。除算法代码外例程还演示了定点数运算、中断与DMA结合等性能优化技巧可帮助中高级开发者理解STM32与ARM数学库协同工作的细节并用于快速开发音频分析、故障诊断等嵌入式应用。1. 为什么在 F103 上做 FFT一个被低估的频谱分析场景把频谱分析搬进单片机很多人觉得是玄学FFT 不是上位机的事吗一颗 ARM 内核的单片机算得动这块基于 Cortex-M3 内核的 STM32F103 核心板用实测告诉我1024 点实数 FFT 只要几毫秒采样由定时器触发、DMA 搬运CPU 只在一次变换和找峰时忙一下串口传的是最终结果不是几千字节的原始波形。这个 stm32f103 fft 例程就是一套从 ADC 采样到幅值谱输出的完整流程适合振动监测、音频频谱显示这类嵌入式硬件场景也适合手里只有一块开发板、没有上位机条件的单片机入门者。跟着例程把采样率、点数和内存账算清楚你会发现频谱分析没那么高不可攀。2. FFT 跑在 72MHz 的 MCU 上从采样定理到频率分辨率很多人拿到 FFT 例程的第一反应是把采样率拉满、点数加到最大结果性能不够还找不到原因。核心问题在于没有先算清楚三件事采样率、采样点数、频率分辨率之间的数学关系以及单片机的内存和 CPU 能不能承受。2.1 采样率、采样点数和频率分辨率怎么定FFT 输出的每个频点间隔由公式决定频率分辨率 采样率 / 采样点数。注意这里没有单片机本身任何事它只取决于你喂给 FFT 的数据有多长、采得有多密。采样率首先受采样定理约束目标信号的最高频率不能超过采样率的一半否则高频分量会折叠到低频区也就是混叠。比如要分析 4kHz 以内的振动信号采样率至少要 8kHz实际工程上我会留出 1.5 到 2 倍余量取 10kHz 或更高。但采样率不是越高越好——在点数固定的前提下采样率越高频率分辨率越差一帧数据覆盖的时间也越短。下面是同一个 10kHz 采样率下不同点数的实际表现这张表在做任何参数选择前都值得先过一遍采样点数频率分辨率 (Hz)单帧时长 (ms)需 RAM 约 (KB)25639.0625.62.551219.5351.24.510249.77102.48.520484.88204.816.5从这个表能看出如果你的目标是分辨 50Hz 和 100Hz 两个频率成分39Hz 的分辨率也够用256 点就行但如果你想看 200.0Hz 和 202.5Hz 这种靠得很近的谱线就得上 2048 点甚至更多。F103 的 RAM 非常有限C 系列只有 20KBZ 系列是 48KB选 1024 点是一般都能接受的平衡点下面的计算都按 1024 点展开。2.2 单片机上 FFT 的两种实现CMSIS-DSP 还是手写蝶形网上有很多手写基 2 FFT 的教程蝶形运算、位反转、旋转因子表代码能写几百行。但我不建议在 F103 上这么做除非你的目的是教学否则重复造轮子的成本和风险都不划算。CMSIS-DSP 库提供了 arm_rfft_fast_f32 这类专门为实数输入优化的 FFT 接口直接在 Cortex-M3 内核上用硬件乘法器加速支持 f32 浮点和 q15 定点两种类型。它内部对实数输入的对称性做了优化1024 点实数 FFT 实际计算量只有复数 FFT 的一半左右而且不用自己维护旋转因子表。选型逻辑很简单有浮点需求的场景优先用 f32 版本代码直观、动态范围大、不易踩坑只有 RAM 实在紧张、或对计算时间极其敏感时才考虑 q15 定点版本。q15 的输入要压缩到 -1.0 到 1.0 之间输出结果还要按比例还原小信号容易被截断新手很容易在这上面翻车。常见做法是先用 f32 跑通流程确认功能和参数都对了再根据需求决定要不要优化到定点。2.3 内存与时间的账N1024 要花多少这里给 F103C8T6 这种 20KB RAM 的芯片算笔细账。1024 点 float 数组占 4KBarm_rfft_fast_f32 要求输出数组也是 float 形式原地操作时输入输出可以共用一块内存但大多数情况需要单独开输出缓存又是 4KB。加上 DMA 缓存用 uint16_t 存 ADC 原始值双缓冲 2x1024 点要 4KB。光这三个结构就去了 12KB剩下 8KB 要放栈、全局变量和库的实例结构体勉强够但要非常克制不能再开大数组。时间方面的账相对乐观。1024 点实数 FFT 在 72MHz 主频的 Cortex-M3 上实测是毫秒级10kHz 采样率下采满一帧要 102.4ms计算只占其中约十分之一不到。这意味着可以把 FFT 放到主循环里跑帧与帧之间即使有轻微延迟也不会丢数据。如果是 256 点单帧时间 25.6ms计算耗时更短实时性会更好适合频点比较稀疏的场景。3. 把 ADC 输出变成 FFT 输入定时器触发采样与 DMA 搬运FFT 的质量七成取决于采样环节。很多例程用 ADC 连续采样模式采完一批算一次这种做法在低频段勉强能看但谱线旁边会拖出一圈噪声。真正工程上可靠的做法是定时器产生精确采样时钟ADC 每次定时溢出时转换一次DMA 把结果搬进内存全程不打扰 CPU。3.1 定时器触发 ADC 的配置流程先用定时器产生均匀的更新事件ADC 配置成外部触发模式由定时器更新事件启动转换。下面这段是我常用的定时器配置假设定时器时钟是 72MHz目标是 10kHz 采样率static void TIM2_Config(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_InitStruct.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz TIM_InitStruct.TIM_Period 100 - 1; // 1MHz / 100 10kHz TIM_InitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStruct); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); }实际采样率等于定时器时钟除以预分频值1再除以自动重载值1也就是 72MHz / 72 / 100 10kHz。注意这里的预分频和重载值都有减一是寄存器从零开始计数的特征新手最容易在这里把采样率算错一倍。ADC 侧需要把触发模式改成外部定时器触发。对应代码大致是开启 ADC 时钟、配置 ADC1 的采样时间为 55.5 周期、选择定时器触发源然后使能 DMA 请求。采样时间可以适当拉长ADC 输入阻抗高的话采样时间不够会导致结果偏小这是硬件电路上常被忽略的点。3.2 DMA 搬运与双缓冲思路DMA 在这里扮演两个角色一是把 ADC 数据寄存器里的转换结果及时搬走避免数据覆盖二是用双缓冲让采集和计算并行。#define FFT_N 1024 volatile uint16_t adc_buf[2][FFT_N]; volatile uint8_t buf_index 0; static void ADC1_DMA_Config(void) { DMA_InitTypeDef dma; dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)adc_buf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize FFT_N * 2; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, dma); }数据宽度配成 HalfWord因为 F103 的 ADC 是 12 位用 16 位搬正好。DMA 工作在循环模式BufferSize 直接配成 2 倍 N这样缓冲区的前半段和后半段分别对应一次完整采集通过 DMA 的半传输中断和传输完成中断来区分当前数据落在哪个半区。void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HT1)) { DMA_ClearITPendingBit(DMA1_IT_HT1); buf_index 0; // 前半段可处理 } if (DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); buf_index 1; // 后半段可处理 } }中断里只翻转标志位把 FFT 计算放主循环这是关键。FFT 要几毫秒如果在中断服务函数里直接跑下次 DMA 中断会丢失采集就会断断续续。我一般在主循环里检测 buf_index 是否改变了改变就说明对应半区的数据已经采满可以拿去计算。3.3 为什么不能用 ADC 连续采样模式直接跑 FFTADC 连续采样模式确实省事配置好启动DMA 就一直接收数据看起来和定时器触发没什么区别。但连续模式的采样间隔由 ADC 时钟和转换时间决定不受精确控制每个样本之间的间隔有微小抖动。这种抖动在时域上看着不严重放大到频域会把单根谱线的能量打散到整个噪声底幅度小的信号直接被淹没。另一个问题是连续模式的启动时机不可控采出来的前几个点是转换稳定期还是有效数据没法保证。而定时器触发模式下每次启动转换的间隔都是确定不变的数据边界清晰。频率分辨率、幅值一致性这些指标才有意义。4. 调用 CMSIS-DSP 的 FFT 函数从复数映射到幅值谱ADC 和 DMA 拿到的是 16 位整型数组不能直接丢给 FFT 库。CMSIS-DSP 的实数 FFT 接口期望输入是 float 数组还要做直流去除和类型转换这章把从原始数据到幅值谱的完整链路拆开讲。4.1 实例结构与内存预算使用 CMSIS-DSP 前要先初始化一个实例结构体 arm_rfft_fast_instance_f32它内部保存了旋转因子和位反转表。初始化时告诉库一次处理多少点后面每次计算都复用这个实例不用重复生成表。#include arm_math.h #define FFT_N 1024 static float32_t fft_input[FFT_N]; static float32_t fft_output[FFT_N]; static arm_rfft_fast_instance_f32 fft_inst; void fft_init(void) { arm_rfft_fast_init_f32(fft_inst, FFT_N); }注意一个细节arm_rfft_fast_f32 的输入输出虽然都是 float32_t 数组但输出排列是按复数形式交替存放的即每个频点的实部和虚部相邻。因此从功能上看输出数组至少需要 N 个 float 的空间f32 的 N 点需要 4KB规划 RAM 时按双份算比较稳妥。4.2 把 ADC 数据映射成浮点输入并完成变换下面这段是完整的处理函数涵盖去直流、实数 FFT、幅值计算三部分void fft_run(uint16_t *adc_raw) { uint32_t i; for (i 0; i FFT_N; i) { fft_input[i] (float32_t)adc_raw[i] - 2048.0f; } arm_rfft_fast_f32(fft_inst, fft_input, fft_output, 0); for (i 0; i FFT_N / 2; i) { float32_t re fft_output[2 * i]; float32_t im fft_output[2 * i 1]; fft_output[i] sqrtf(re * re im * im) / (float32_t)FFT_N; } }第一步减去 2048 是有原因的F103 的 12 位 ADC 输出范围是 0 到 4095中间值约 2048信号叠加在约 1.65V 的直流偏置上。如果不减去这个直流偏置FFT 结果第 0 个频点直流分量会出现一个巨大的尖峰其他频率成分相对值小小信号完全压扁。减掉偏置后输入变成以零为中心的近似双极性浮点信号。arm_rfft_fast_f32 的第四个参数是 0 代表正变换输出不缩放。所以计算幅值时要除以 N把 FFT 累加结果的能量还原到原始幅值量级。输出数组的排列不是顺序频点而是每两个相邻 float 表示一个频点的实部和虚部代码里用 2i 和 2i1 去取就是对的位置。4.3 做完 FFT 之后幅值、频率坐标与常见误用幅值算完后第 i 个桶对应的实际频率是 i 乘以采样率除以 N。比如 10kHz 采样率 1024 点第 10 个桶就是 10 * 10000 / 1024 97.66Hz。工程上最常见的误用有两个。第一个是把 FFT 输出直接当幅值不去求模。复数输出里实部和虚部不能单独看必须做 sqrt(rere imim)否则看到的值是乱的。第二个是忘了直流分量和对称分量的处理。第 0 个桶是直流它只对应一个分量除数 N 后幅值等于平均值而 i 大于 0 的桶同时对应正负频率单边谱里真正的信号幅值要乘 2。这就是为什么用袖珍示波器看同一个正弦波例程打印出来的基波幅值会比实际输入信号小一半补上乘 2 的系数才对得上。加窗也会改变幅值估算。用汉宁窗这类非矩形窗时信号能量被窗函数削减了一半幅值需要往上修正。常见做法是加窗后对非直流桶乘一个固定系数补偿具体系数和窗函数类型有关。测试时先不加窗矩形窗下幅值最容易核对确认整条链路没问题后再考虑加窗抑制频谱泄漏。5. 常见问题排查FFT 结果翻车的三处根源我把主要调试 FFT 例程时遇过的坑整理成检查清单。顺着现象找原因比重新看一遍代码效率高得多。问题一频谱像噪声一样铺开完全看不到尖峰现象输入是干净的 1kHz 正弦波FFT 结果却是一条接近水平的噪声底偶尔有几个小鼓包。原因采样间隔抖动是最大嫌疑。最常见的情况是用了 ADC 连续采样模式或者定时器没有真正触发 ADC而是靠软件延时循环去读数据。软件延时的抖动容易到几十微秒对 10kHz 采样来说误差能到 50%FFT 自然全糊。解决回到第 3 章的方案强制用定时器更新事件触发 ADCDMA 搬数据。先确认定时器输出触发信号确实存在可以用示波器挂 TIM2 的 TRGO 引脚看脉冲周期再检查 ADC 外部触发使能位有没有打开。问题二峰值出现但频率整体偏移100Hz 显示成 95Hz 左右现象信号发生器给的是准确的 100Hz 正弦程序打印出 94.73Hz而且偏差比例固定。原因采样率算错了。定时器时钟不是想象的 72MHzF103 的 APB1 预分频如果设成了 2 分频定时器时钟实际是 36MHz你按 72 分频去算 10kHz实际只有 5kHz。采样率变一半每个桶对应的频率也跟着变一半。解决RCC 配置里把 APB1 开成 2 分频后定时器时钟会自动倍频回 72MHz但前提是外设时钟使能顺序正确。我一般会写一个自检函数用定时器输出一个已知频率的方波再用另一块板子测实际频率对不上就直接重查 RCC 配置。问题三波形幅值正常FFT 谱线旁边拖出一串旁瓣还衰减得很慢现象主峰旁边每隔几个桶就有一串次峰幅值还不小看起来像是主频被“复制”了。原因这是典型的频谱泄漏不是故障。1024 点 FFT 采 102.4ms如果信号频率不是频率分辨率的整数倍——比如输入 100Hz 到 9.77Hz 分辨率的系统时100 / 9.77 10.24 不是整数——能量就会从主峰漏到相邻频点。解决加窗函数是最直接的缓解手段。在 fft_input 乘上汉宁窗或布莱克曼窗主峰旁边旁瓣就会快速衰减。代价是主峰会变宽幅值还需要修正。如果系统里频率已知且固定另一个办法是调整采样率让目标频率正好落在某个频点正中但这在实际场景里很难实现所以工程上还是加窗更通用。问题四DMA 中断里跑 FFT 后数据帧错乱现象程序跑一会儿幅值开始抖动甚至串口打印出明显的断帧整个频谱像被人切了一刀。原因把 FFT 计算直接写在中断服务函数里了。FFT 需要几毫秒DMA 的中断一旦错过采集链路就不连续。中断嵌套和缓冲区切换互相干扰数据前后错位。解决中断里只置标志主循环里检测到标志后再做 FFT。缓冲区切到哪个半区也由标志位记录不靠中断里做计算。这是嵌入式里中断快进快出的基本纪律FFT 这种耗时操作必须挪到主循环。问题五用 q15 定点版本小信号直接消失现象输入信号调到 100mV 左右FFT 结果里找不到对应峰值或者峰值明显失真。原因q15 格式的动态范围有限输入在 -1.0 到 1.0 之外会被饱和而小信号在定点下只有很小的量化步数转换和计算舍入误差就把信号淹没在噪声里。解决先用 f32 浮点版本跑通整个链路确认原理没问题再考虑定点优化。如果一定要用 q15输入前先整体做缩放让信号幅度尽量贴近满量程输出侧再反向缩放回真实幅值。这个缩放系数必须和采集增益一起调不能拍脑袋定。6. 用测正弦波两步验证 FFT 例程输出频率与幅值同时对上才算跑通拿到例程第一件事不是改代码而是验证数据链路。我最常用的方法是信号发生器输出 1kHz 正弦波1V 峰峰值叠加 1.65V 直流偏置保证信号始终落在 ADC 输入范围内。接好线后先只打印原始 ADC 最大值和最小值确认波形没有削顶再进行下一步。第一步验证频率。跑一次 FFT找到幅值最大的索引用频率公式换算uint32_t peak_idx 1; float32_t peak_mag 0.0f; for (i 1; i FFT_N / 2; i) { if (fft_output[i] peak_mag) { peak_mag fft_output[i]; peak_idx i; } } float32_t bin_hz SAMPLE_RATE / FFT_N; float32_t freq (float32_t)peak_idx * bin_hz;理想情况下打印结果应该在 999Hz 到 1001Hz 之间。偏差超过 1% 就先查采样率配置不要急着怀疑 FFT 函数。第二步验证幅值把 fft_output[peak_idx] 乘 2 后和信号发生器设的峰峰值比较。这里最容易出偏差的地方是我在 4.3 里说的矩形窗不加窗的倍数问题核对时先确认代码里有没有乘 2 的补偿。幅值误差在 10% 以内就可以认为整条链路正确。验证通过后我会再做一次加大采样率到 40kHz 的对比测试输入同一个 1kHz 信号确认峰值频率不随采样率改变而改变。这一步对排查混叠特别有效如果高频段突然多出一些莫名其妙的峰值多半是前置滤波没做好或者输入信号本身不干净。从那以后我每次拿到一块新板子或者改一次采样率都会强制先跑一遍“已知频率正弦波验证流程”频率和幅值两个数都对上了才继续调代码。这个习惯帮我避开了无数次“FFT 结果不对但不知道是采集还是计算问题”的尴尬。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

[Kotlin]入门学习记录

[Kotlin]入门学习记录

本文为kotlin学习记录 文章目录本文内容一、语法1. companion object2. 正则表达式2.1 去除特定字符二、使用步骤1.引入库2.读入数据总结本文内容 本文用以记录笔者kotlin学习过程:包括语法等 一、语法 1. companion object 2. 正则表达式 2.1 去除特定字符 保…

📅 2026/10/24 1:40:44
基于VS和Qt的FFmpeg播放器开发:从解码到音视频同步实战

基于VS和Qt的FFmpeg播放器开发:从解码到音视频同步实战

简介:这是一份基于VS与Qt开发的视频播放器练习工程,面向刚开始接触Qt界面编程或VS项目配置的读者,可用于课程设计或个人练手。工程使用Qt的UI设计器搭建播放界面,实现了播放、暂停、文件选取与更换视频等常见交互,适合…

📅 2026/10/24 1:40:44
Oracle - 两表关联更新

Oracle - 两表关联更新

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/24 1:40:44
MORE NEWS

更多资讯

📰

DeepInfer 产品分析:DeepSeek 把自家“推理引擎“开源了,到底强在哪?

DeepInfer 产品分析:DeepSeek 把自家"推理引擎"开源了,到底强在哪?一句话概括:DeepInfer 是 DeepSeek V4.1 Flash 背后那台发动机。它开源的不是一个玩具 demo,而是每天跑 155 万亿 token的生产级推理框架—…

📰

【元宝搜索开发者征文大赛】基于机器视觉的矿山油井多模态安全预警风险监测评估研判系统

矿山/油井多模态安全风险研判系统 系统功能 模型链路 优缺点 创新点 基于 Django 5.2 YOLO26-m YOLOE PatchCore 开源承诺:系统训练集均来自ultralytics官方提供数据,系统不会用做任何商业活动Annotate, Train, and Deploy Vision AI on Ultral…

📰

Python turtle 绘制科赫雪花,实现 0~4 阶分形图形

前言分形几何是一个非常有意思的数学分支,和我们平时学的欧式几何不一样,分形图形拥有自相似性:把图形的局部放大之后,局部的形状和整体高度相似。科赫雪花,也叫科赫岛,是入门分形最经典的例子。本次课程作…

📰

亚马逊陪跑和录播课怎么选?分清 2 种培训模式,别选错耽误起店节奏

不少卖家在挑选亚马逊课程的时候,最先纠结的问题就是:我到底该买录播课,还是直接报陪跑?市面上很多机构会刻意渲染“录播课没用,只有陪跑才能出单”,也有说法是“陪跑就是智商税,看录播自学足够…

📰

全开源!浙大高飞团队发布CrossFly:面向无人机跨室内外交互导航的全新基准

「能飞到目标≠能做物理交互」 目录 01 CrossFly 瞄准的两个缺口 02 CrossFly‑Agent:分层式 VLA 基线智能体 2.1 高层 VLM 规划器 2.2 低层 VLA 动作生成策略 2.3 训练方式与推理衔接 03 数据怎么造:重建场景 航路点驱动 子任务拆解 …

📰

【论文阅读】并行 MRI 深度学习综述:从 SENSE、GRAPPA 到学习式重建

文章目录01 基础信息02 背景03 方法04 结果05 思考06 新掌握的表达01 基础信息 标题:Deep-Learning Methods for Parallel Magnetic Resonance Image Reconstruction: A Survey of the Current Approaches, Trends, and Issues。作者:Florian Knoll、Ke…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬