STM32+CMSIS-DSP实现FFT频谱分析:从CUBEMX配置到代码落地 简介面向STM32嵌入式开发者的FFT频谱分析工程资料演示如何借助STM32CubeMX配置时钟、DMA及CMSIS-DSP数学库在Cortex-M内核上完成快速傅里叶变换适用于音频采样、振动监测、电力谐波分析等需要频域解析的实时场景。资源包共256个文件约7.8MB其中包含完整的HAL库驱动源码.c/.h、工程配置文件.ioc/.uvprojx、编译生成的固件.hex/.axf以及中间文件.o/.d/.crf目录结构清晰便于直接打开或对照移植。目前已有1155人在CSDN学习下载适合正在学习数字信号处理或计划在STM32项目中集成FFT功能的开发者参考。通过该工程可快速掌握CMSIS-DSP中FFT函数的初始化、输入输出缓冲设置与频域幅值计算流程同时了解硬件FPU对运算效率的优化方式是一份可直接运行验证的实战样例。1. 为什么嵌入式FFT没想象中那么难做电机振动监测、音频频谱显示或者电力谐波分析时FFT几乎是绕不开的算法。但在STM32上跑FFT很多人第一反应是用STM32官方DSP库里的经典算法或者干脆在MATLAB里仿真完了直接移植结果发现板子上要么跑得慢要么波形不对。其实STM32做FFT的难点不在算法本身而在于采样链路的设计和CMSIS-DSP库的正确调用方式。CMSIS-DSP里的arm_cfft_f32函数把FFT核心计算封装好了配合CUBEMX生成的ADCDMA采集链路一套完整的频谱分析系统从零到跑通半天时间足够。这篇博客就把整个流程拆开讲从CUBEMX配置到采样参数计算再到FFT输出结果的实际验证每个环节都给出可直接抄的配置和代码最后附上调试FFT时最常见的几个坑。2. FFT原理与CMSIS-DSP库选型2.1 FFT在嵌入式环境下的计算本质FFT快速傅里叶变换是DFT的高效实现核心思想是把一个N点时域序列分解为频域上的N个复数点。对于嵌入式工程师来说不需要去推导蝶形运算的数学过程但必须理解两个关键参数采样率Fs和FFT点数N。采样率决定频谱分析的最高频率奈奎斯特频率为Fs/2而FFT点数决定频率分辨率即Fs/N。比如采样率8000Hz、做1024点FFT频率分辨率就是7.8125Hz这意味着两个频率差小于7.8Hz的信号在频谱上是无法区分的。在STM32上实现FFT有两条路线HAL库自带的arm_cfft_f32是浮点实现依赖FPU加速arm_cfft_q15是定点实现基于Cortex-M3/M4内核的SIMD指令优化。选择的关键在于芯片型号和实时性要求。对于STM32F103这类不带FPU的Cortex-M3内核跑1024点浮点FFT大约需要1.5ms而STM32F407在168MHz主频下只需不到100us。这就是为什么很多基于F103的项目改用Q15定点FFT虽然精度稍低但速度可以提升一个数量级。2.2 CMSIS-DSP库的两种集成方式CUBEMX已经把CMSIS-DSP库集成到中间件里了不需要手动去ARM官网下载。在CUBEMX的Software Packs里勾选CMSIS-DSP生成的工程里会自动包含DSP库源码和头文件。需要注意的一点是CUBEMX默认生成的是源码形式source而不是静态库形式library这样编译器会直接编译DSP源码好处是兼容性好缺点是编译时间会变长。还有一种手动集成方式适用于不用CUBEMX生成代码的老项目。从ARM官方Github仓库下载CMSIS-DSP源码把Source目录下的TransformFunctions和CommonTables两个子目录拷进工程然后在头文件包含路径里加上Include目录。这里有个关键配置如果芯片有FPU必须在编译选项里加上-mfpufpv4-sp-d16 -mfloat-abihardF4系列否则浮点FFT会被编译器转成软浮点调用性能直接下降十倍以上。确认方法是在Keil的Options - Target里勾选FPU: Single Precision或者用__FPU_USED宏来判断。2.3 实数FFT与复数FFT的选择arm_cfft_f32处理的是复数输入也就是说输入数组的偶数下标存实数部分、奇数下标存虚数部分。而ADC采到的信号是实数的所以最直接的做法是把虚部全部填0然后调用复FFT但这样计算量浪费了一半。CMSIS-DSP库提供了arm_rfft_fast_f32专门针对实数序列做过优化内部先做一次半长复数FFT再加数据重组计算量比直接调arm_cfft_f32少一半左右速度几乎翻倍。实际工程中我更推荐用arm_rfft_fast_f32它的接口比复数FFT更少埋坑。函数原型是arm_rfft_fast_init_f32(S, N)做初始化arm_rfft_fast_f32(S, input, output, 0)执行正变换最后一个参数0表示正变换1表示逆变换。注意这里的N必须是2的幂次方最小16最大4096F1系列受限F4系列可以到8192。而且arm_rfft_fast_f32的输出不是按频率从低到高排列的而是先放直流分量和正频率部分这跟MATLAB直接fft()的输出排列不一样做频谱显示时要额外处理。3. CUBEMX配置ADC采样链路与DMA传输3.1 时钟树配置与采样率计算采样率是整个FFT系统的地基采样率不对后面频域分析全错。STM32的ADC有独立的时钟源F1系列最高14MHzF4系列最高36MHz。CUBEMX的时钟树配置页面里ADC时钟要手动设置分频系数。关键点是PCLK2的配置F1系列ADC挂在APB2上如果APB2是72MHzADC时钟分频设为6就得到12MHz接近14MHz上限F4系列APB2可以到84MHz分频系数设4得到21MHz也是安全的。这里不解释PCLK分频倍频的具体计算但APB1/APB2的最大频率要查芯片的Datasheet比如F103的APB1最高36MHzAPB2最高72MHz。采样率Fs由两条公式决定t_conv 采样周期 12.5个ADC时钟周期。ADC时钟12MHz时1个周期约83.3ns。如果采样周期设为239.5个周期F1系列最大239.5加12.5个固定周期总共252个ADC时钟周期单个通道转换时间是21us。如果直接用定时器触发采样触发频率设为5000Hz那采样率就是5000SPS每秒采集5000个点这个值决定了频率分析上限是2500Hz。实际配置时注意看CUBEMX的ADC Parameter Settings页面Sampling Time下拉菜单里显示的是周期数配合右下角时钟树看实际转换时间。3.2 ADC与DMA环形缓冲区的配合如果用阻塞模式采集N个点MCU会一直卡在HAL_ADC_Start()里采样期间做不了其他事。用DMA加定时器触发可以做到后台连续采样同时CPU可以去跑FFT。CUBEMX里DMA配置页面DMA Settings里添加ADC1通道方向选PeripheralToMemory模式选Circular数据宽度选Word。这些配置里有一个必须改的地方Memory Address Increment必须设为Incremental这样每次转换结果会依次写入缓冲区的下一个位置否则所有数据都写同一个地址。ADC连续转换模式也要注意Continuous Conversion Mode要设为Disabled改用定时器外部触发。如果开连续转换ADC会自己以最快速度轮流采样采样率就不受控了。假设我们配置一个16位定时器TIM2预分频prescaler设为7200-1即7200分频自动重装值period设为1000-1时钟72MHz那么触发频率就是72MHz / 7200 / 1000 10Hz实际上这里可以按需调整10KHz对应的配置是prescaler 72-1period 100-1。这些数字是可以实际算出来的CUBEMX里输入分频值和重装值左侧会有HAL_TIM_Base的时钟频率提示直接看也直观。3.3 CUBEMX外设初始化关系与代码框架CUBEMX生成代码后工程里会多出stm32f1xx_hal_adc.c、stm32f1xx_hal_tim.c、stm32f1xx_hal_uart.c等一系列HAL驱动源文件。ADC驱动占据转换本身的逻辑TIM驱动提供触发源UART用来把FFT结果输出到串口RCC驱动承载整个系统的时钟使能链路。这几个外设之间的依赖关系在MX_ADC1_Init()、MX_TIM2_Init()和MX_USART1_UART_Init()函数里体现。所有外设初始化完成后在main()里按顺序调用这些函数再调用自己的start()函数把定时器、ADC和DMA跑起来。注意生成的代码里没有写具体的逻辑后续的FFT计算代码需要手动在while(1)里写。另外工程文件里的stm32f1xx_hal_adc_ex.c和stm32f1xx_hal_tim_ex.c属于扩展驱动主要处理F1系列特有的注入通道和定时器余项功能。FFT项目用到的外部触发配置和PSC设置其实调用的HAL标准库函数只有用到注入组采样时会涉及_ex文件。如果做电机控制那类需要多通道同步采样的场景_ex里的HAL_ADCEx_InjectedStart才有存在感。单通道FFT分析时没有调用必要。4. 标准库转HAL库的工程迁移与CMSIS-DSP函数核心代码4.1 从SPL标准库到HAL库的初始化差异这块先不说CUBEMX生成过程了——如果你是从旧项目改造原始工程用的还是STM32标准外设库SPL那初始化代码存在几个换算对应关系。标准库里ADC_InitTypeDef结构体里的ADC_Mode、ADC_ScanConvMode映射到HAL库是ADC_InitTypeDef.ScanConvMode标准库的ADC_ExternalTrigConv配置在HAL库里是通过AdcHandle.Init.ExternalTrigConv和ExternalTrigConvEdge完成。当时标准库没有DMA方式配置选项DMA和ADC的配合完全靠手动在DMA_InitTypeDef里指定源地址和目标地址而HAL库的HAL_ADC_Start_DMA()自带源地址绑定省了一次DMA外设配置步骤。还有一个常见的差异藏在时钟使能逻辑上——标准库写的RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE)在HAL库中对应的是__HAL_RCC_DMA1_CLK_ENABLE()。如果你的工程恰好从标准库项目转成CUBEMX生成的HAL框架注意查一下这两处是否重复使能重复使能本身不会报错但会让人误判外设状态。4.2 基于arm_rfft_fast_f32的频谱计算代码CUBEMX生成工程后把以下代码放进main.c的USER CODE区域。先声明一个结构体和两个缓冲区/* USER CODE BEGIN PV */ #define FFT_POINTS 1024 #define Fs 10000 /* 采样率10kHz配合TIM2触发频率 */ arm_rfft_fast_instance_f32 fft_instance; float32_t sample_buffer[FFT_POINTS]; /* ADC填满后做输入 */ float32_t fft_output[FFT_POINTS]; /* FFT输出 */ ADC_HandleTypeDef hadc1; /* CUBEMX已声明 */ /* USER CODE END PV */以下是实际执行FFT并换算幅值的代码/* USER CODE BEGIN 4 */ void FFT_Task(void) { uint16_t i; float32_t magnitude; volatile float32_t freq_res (float32_t)Fs / FFT_POINTS; /* 频率分辨率 */ /* 1. 停DMA更新搬运当前ADC数据到FFT输入注意不能直接原地FFT */ HAL_ADC_Stop_DMA(hadc1); for (i 0; i FFT_POINTS; i) { fft_output[i] (float32_t)adc_values[i]; /* 先填输出数组 */ } /* 2. 执行实数FFT输入输出都用同一个数组 */ arm_rfft_fast_f32(fft_instance, fft_output, fft_output, 0); /* 3. 结果排列index0是DC分量index1开始是0~Fs/2的正频率 */ for (i 0; i FFT_POINTS/2; i) { if (i 0) { magnitude fft_output[0] / FFT_POINTS; /* 直流分量只除一次 */ } else { magnitude sqrtf(fft_output[2*i] * fft_output[2*i] fft_output[2*i1] * fft_output[2*i1]) * 2.0f / FFT_POINTS; } printf(f%.1fHz, amp%.2f\n, i * freq_res, magnitude); } HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_values, FFT_POINTS);/* 恢复采集 */ } /* USER CODE END 4 */这段代码有几个地方值得解释。第1步先把DMA停下来是因为循环缓冲区可能在FFT计算过程中被写入导致数据撕裂。实际项目中缓冲区的值来自之前的HAL_ADC_ConvCpltCallback回调在回调中复制到另一个缓冲区然后用一个标志位告诉主循环“FFT输入已就绪”减少停DMA的窗口时间但逻辑复杂度会高一些。第2步重点说一下fft_output数组在FFT计算前是时域采样值计算后前半部分变成频域复数结果——fft_output[0]存直流分量的实部fft_output[2*i]和fft_output[2*i1]分别是第i个频率点的实部和虚部。第3步幅值换算的意思是FFT结果的能量分布与变换的点数有关直流分量的幅值等于FFT输出除以N交流分量幅值等于FFT输出乘以2再除以N留意这里的经验处理方式。4.3 arm_cfft_f32与arm_rfft_fast_f32的调用差异对比如果你在别人的工程里看到用的是arm_cfft_f32需要知道它的调用方式和arm_rfft_fast_f32有本质差异。arm_cfft_f32要求输入的复数序列以IQ形式交织排列——data[0]实部, data[1]虚部, data[2]实部, data[3]虚部。ADC采到的实数数据要手动把虚部填0再调用arm_cfft_f32(cfft_instance, fft_input, 0, 1)。注意它的第三个参数ifftFlag0表示正变换第四个参数doBitReverse1表示需要位反转正变换必须为1。而arm_rfft_fast_f32不需要填虚部输入数组直接就是实数组相对不容易出错。这两个函数的输出排列也不一样。arm_cfft_f32输出是复数序列实部和虚部仍然是交织存储的DC分量在data[0]和data[1]最大频率分量在data[N-2]和data[N-1]。arm_rfft_fast_f32输出前半部分才是有效频率后半部分存特殊打包格式不是直接可读的频率数据。所以如果你把频谱只画前N/2个点也无须处理后半部分。5. 串口输出与频率分辨率验证5.1 用串口波形工具验证频谱正确性代码跑通后需要拿已知频率的信号来验证整个采样链路的正确性。最简单的办法是用STM32的DAC模块生成一个1kHz正弦波给它自己采样分析没DAC的板子可以外接信号发生器或者干脆用另一个定时器加GPIO翻转配合RC滤波生成波形。将生成的模拟信号接入ADC引脚然后观察串口打印的频谱峰值是否落在1kHz附近。串口输出FFT结果在调试阶段要控制打印频率——1024点FFT计算耗时约1~1.5msF103或80usF407但串口打印几百个频率点需要十几毫秒导致打印期间ADC无法采集完整数据。一般做法是只输出幅度最大的前8个频率点或者降低打印频率到每2秒输出一次。测试时也可以用printf加浮点格式但F103配置补齐重定向。做完数据验证后就该关掉连续printf改发关键峰值的固定频率格式。5.2 采样率不匹配时的频谱失真现象采样率设置错了频谱图上观察到的最典型故障是谐波混叠。比如信号源输出1kHz正弦波如果采样率只有1500Hz硬件上没有低通滤波器1kHz信号在频谱图上出现在500Hz的位置奈奎斯特频率750Hz减实际信号的差值折叠。这就是采样定理说的混叠。另一种是频率分辨率选择不当——FFT点数太少时1kHz和1.03kHz两个频率分量会合并成一个峰肉眼无法分辨。CUBEMX配置里找不到采样率这个选项采样率必须通过定时器触发频率来设定。常见的做法是TIM2触发频率正好等于采样率比如10kHz触发就配置成prescaler7200-1, period10-172MHz时钟。确认触发周期是否正确的快速方法是在TIM2中断服务程序里翻转GPIO用示波器量GPIO的频率。还有一种验证方式直接把ADC的HAL_ADC_Start_DMA()改成单次转换模式用逻辑分析仪看DMA传输完成中断的频率结果更接近实际的采样间隔。5.3 自定义频率片段的截取方法有些场景下并不需要全频段的频谱比如音频信号往往集中在300Hz~3kHz之间低频噪声和高频噪声干扰了频率轴的刻度显示。CMSIS-DSP库没有直接输出指定频段的API但通过调整采样率和FFT点数可以等效实现。比如把采样率降到8000HzFFT点数仍是1024那么频率分辨率就变成了7.8125Hz看到的是4000Hz以下频率段但低频和高频的频谱细节没有额外增加。越多的FFT点数对应越好的频率分辨率但单片机的RAM又限制了点数大小——FFT数组用float32_t的话2048点就要8KB RAM。这里的取舍通常通过加窗函数来权衡。CMSIS-DSP提供了一组窗函数API如arm_hanning_f32按逐个值填充到时间序列上。对直流附近有较大分量、而目标信号又较弱的情况主瓣泄漏可能导致小信号被淹没。正弦波频率与FFT点数的整数倍不对齐时加汉宁窗可以把主瓣展宽、旁瓣压低换来的代价是幅值精度下降约1.5dB。加窗和FFT组合调用时先在时域上把窗函数和ADC采样值相乘再把结果送入arm_rfft_fast_f32顺序不要搞反。6. 进阶技巧ADC采样值到幅值的标定与增益系数修正6.1 标定流程与短路调零幅值标定的最直接做法是输入一个已知峰值的正弦波比如1V峰值信号读取FFT结果对应频率点上的幅值然后存一个比例系数。问题是ADC参考电压通常不是整数比如STM32F103内部参考电压在2.9V到3.1V之间具体看同一个芯片的温度和电压环境。加上ADC本身的偏移误差与增益误差FFT算出来的幅值和实际值可能差好几个百分点。先做零输入校正ADC引脚接地运行FFT观察直流分量附近有多少噪声底。理想情况下直流分量应接近0但实际上ADC的偏移误差会在这里暴露出来记录偏移值并记录。再做满量程校正输入一个干净的VREF/2正弦波比如1.65V峰值运行FFT得到该频率的幅值。经过两步之后实际幅度和FFT结果的比值就是一个线性系数。在代码里可以对magnitude乘以这个系数再输出单点量程校正更适用于直流耦合场景。6.2 频率响应校准与参考源选型ADC的采样保持电路和前端RC滤波都会引入频率相关的增益衰减。所以单点校正只对特定频率点准确。更严谨的做法是配合信号发生器扫频从100Hz到Fs/2逐点记录幅值偏差存成校正表运行时按频率索引线性插值。工程上如果只测一段窄带信号比如中心频率50kHz上下200Hz单点校正就够了而全局扫频那种情况需要在程序里做插值计算。选参考源时注意守则信号发生器输出阻抗要小于1kΩ否则与ADC内部采样电容相互作用折合引入额外误差。STM32的ADC采样周期在1.5个ADC时钟周期时源阻抗要低于几百欧姆用239.5周期的慢速采样就能容忍更高一些的源阻抗坏处是最大采样率被迫降低。测试FFT频谱时把采样周期设为28.5或55.5周期更稳妥避免源头带来的幅值跌落。6.3 浮点FFT与Q15定点FFT的幅值换算差异使用arm_rfft_fast_f32时幅值换算公式是real^2 imag^2开根号后乘以2/N。换到arm_rfft_q15后输入需要缩放成Q15格式即ADC的12位数据左移3位乘8变成16位有符号数或直接调用arm_q15_to_float转换。FFT输出结果也变成Q15格式做幅值换算时要把结果除以32768还原成浮点再按同样公式乘2/N。这个细节很多人踩坑因为Q15的输出是小数表示的定点数除以32768之后值域才是真实的幅值比例。还有一点是arm_rfft_fast_f32内部用到查表法旋转因子对内存有额外要求。F103内存紧张的工程优先考虑1024点以下F407这类M4内核RAM足够大的可以跑4096点。2048点以上的FFT在F103上耗时已经超过几十毫秒实时性差的场景不推荐不如改用硬件方案。而F4系列如果开最高主频和FPU2048点FFT也能压到200微秒左右处理音频流实时分析没问题。选FFT点数之前先确认RAM占用节省一次移植调试的时间。本文还有配套的精品资源点击获取