STM32G431移植CMSIS-DSP库实现FFT频谱分析完整实战指南
2026/9/19 16:19:33 网站建设 项目流程

做嵌入式到一定阶段,总会遇到“时域里看不出来”的问题。我之前给一台小功率电机做电流纹波分析,信号在示波器上就是一团毛刺,但频率成分到底是什么,肉眼看不出。换F4/F7成本高,手边正好有一片STM32G431,Cortex-M4F核、168MHz、还带FMAC和CORDIC,理论上做FFT完全够用。但真上手才发现,网上的DSP库教程绝大多数是F103/F407的,G431这套流程少有人写,零散的问题全靠自己试错。

这篇文章就记录我从零到一在G431上集成CMSIS-DSP库并跑通FFT的完整过程。内容包括DSP库怎么接进工程、采样和FFT参数怎么定、代码怎么写,以及频谱“长毛”、结果不对时我是怎么一步步排查的。适合正在用G4系列做信号分析、或者想把手头G431开发板物尽其用的朋友参考。

1. 为什么是G431:这颗芯片的DSP底子与选型逻辑

1.1 一颗为信号处理准备的Cortex-M4F

STM32G431的核心是Cortex-M4F,带单精度FPU,最高主频168MHz,带DSP指令集。这意味着做FFT这种乘加密集型运算时,硬件浮点单元能帮你省下大量时间。G431的Flash最多128KB,SRAM最多32KB,不算大,但跑256点甚至512点的单精度FFT绰绰有余。

还有一个容易被忽略的点:G431内部集成了FMAC(滤波数学加速器)和CORDIC(坐标旋转数字计算器)。FMAC可以硬件方式做FIR/IIR滤波和复数运算,CORDIC可以硬件算sin/cos、反正切。FFT本身主要靠CMSIS-DSP软件实现,但如果你做的是“FFT前后还要滤波、还要算相位”这种完整链路,这两个外设能帮上大忙。

信号链上G431也很能打:内置运放、比较器、DAC。比如你要采集一个mV级信号,片内PGA先把信号放大,再送ADC,甚至可以不外接运放。我这次测试FFT用的就是片内DAC产生标准正弦波,直接闭环自测,方便到不行。

1.2 和F103、F407比,它的位置在哪里

F103的M3核没有FPU,同样跑float FFT不仅慢,而且代码体积、RAM占用都更大。F407贵、功耗高,用来做简单频谱分析有点浪费。G431处于“便宜够用”的位置:带FPU、带硬件数学加速器,还集成一堆模拟外设,在“想做频域分析的嵌入式产品”这个场景里,性价比是真的好。

从实际工程角度看,G431另一个优势是CubeMX支持得很完整,时钟树、ADC、DMA、TIM触发生成代码都不用手写寄存器。相比F1,G4的ADC支持硬件过采样,细节更丰富。所以如果你在选型阶段,想用一颗M4内核芯片做频谱分析或者振动监测,G431值得认真考虑。

2. DSP库接入:从CubeMX到MDK/IAR的完整集成路径

2.1 方案一:CubeMX自动添加,但别全信默认配置

用CubeMX生成工程时,在“Middleware and Software Packs”里勾选CMSIS-DSP,工具会自动把DSP库目录和lib文件加进工程。这是最省事的方案,但有个坑:不同CubeMX版本生成的默认DSP库可能是针对M0/M3/M4的预编译版本,路径不一定完全匹配G431,有时需要手动指定。

具体来说,如果工程是G431这种带FPU的M4F,库文件一定要选带lf的版本。Keil环境里常见的是arm_cortexM4lf_math.lib,IAR用的是对应的.a文件,STM32CubeIDE(GCC工具链)则要链libarm_cortexM4lf_math.a。选错库、或者选成不带f的普通M4库,运行时会进HardFault,或者FFT结果完全不可信。

提示:M4lf表示 Cortex-M4、小端、带硬浮点。只要你的芯片是G431,就认准这个标签。

2.2 方案二:手动拉源码编译,踩坑最多但最可控

我更推荐有一定基础的人手动集成,因为能看清库的组成。CMSIS-DSP源码在GitHub和Keil安装目录都有,手动集成的关键就三步:

  1. CMSIS/DSP/Include加入头文件路径。
  2. CMSIS/DSP/Source下需要的.c文件加入工程。做FFT至少需要TransformFunctionsComplexMathFunctionsStatisticsFunctionsSupportFunctionsCommonTables这几个目录,如果编译后报缺符号,再把对应的源文件补进去。
  3. 在C/C++预处理器里定义宏。老版本库需要ARM_MATH_CM4,同时确认__FPU_PRESENT=1;新版本库自动识别内核,但定义一下不影响。

这一步最坑的是宏定义缺失或工具链头文件路径不全。第一次手动加库时,我报了一堆unknown type name 'arm_rfft_fast_instance_f32',查了半天才发现是头文件没包含全,core_cm4.h根本找不到。

2.3 链接错误怎么定位:我踩过的L6218E

最典型的链接错误是:

Error: L6218E: Undefined symbol arm_rfft_fast_init_f32

这个错误几乎都是DSP源码没有真正参与编译,而不是函数名拼错。排查顺序:先看工程树里有没有加transform_functions.c,再看有没有编译进目标文件,最后看预处理器宏有没有把声明关掉。强烈建议第一次跑DSP库时,先编译CMSIS-DSP自带的FFT官方示例,确认环境没问题,再往自己工程里搬。

3. 动手之前先定参数:采样率、FFT点数与窗函数的三角关系

3.1 频率分辨率不是想要就能要

有个常见误区:“FFT点数越多,频率分辨率越高。”这句话只对了一半。分辨率公式是:

Δf = fs / N

其中fs是采样率,N是FFT点数。比如采样率48kHz,N=1024,频率分辨率只有46.875Hz。想靠这个区分50Hz和60Hz的工频谐波,基本做不到。

很多人上来就想要“更高分辨率”,于是把N设成8192,结果G431那点SRAM瞬间被吃光。正确的思路是:先算清楚你到底需要多细的分辨率,再反推fs和N。如果带宽很高、又想要高分辨率,那就不是一颗G431能解决的,得考虑降采样或Zoom FFT这类进阶手段。

3.2 真实ADC采样率是由定时器决定的

G431的ADC可以用定时器触发,这是做等间隔采样的标准姿势。定时器周期定了,采样率就定了。常见做法是用TIM1产生PWM输出或TRGO事件,触发ADC转换,再用DMA把结果搬进内存。这样CPU几乎不用管采样过程。

G431的ADC本身是12位,但带硬件过采样功能,可以把分辨率提到16位。代价是过采样会降低等效带宽,因为内部会做抽取滤波。做宽频段频谱分析时通常不用过采样,保持12位、跑高速;只关心某个窄带信号时,过采样反而能压低噪声,效果很好。

3.3 窗函数:不加窗,频谱会“长毛”

FFT默认假设你输入的是整周期截取的信号,但真实信号很难保证这一点。截断会让频谱能量泄漏,明明是单根谱线,结果旁边多出一大堆“毛刺”。解决办法是加窗函数,把数据两端乘一个渐进为零的权重,把截断造成的突变抹平。

窗类型主瓣宽度旁瓣衰减幅值精度适用场景
矩形窗(不加窗)差,约-13dB最好整周期采样、瞬态信号
汉宁窗中等较好,约-31dB中等,需恢复系数2.0通用首选
布莱克曼窗好,约-58dB需恢复系数弱信号检测、多频成分

加窗会影响幅值。比如汉宁窗,单频正弦的峰值会比真实幅值小一半,幅度恢复系数是2.0。如果只是看“哪里有峰”,不修正也能用;但你要报告绝对幅值,必须乘回去。这也是个经典坑。

4. 代码落地:一路从定时器触发ADC到频谱输出

4.1 ADC+DMA采样:先保证数据连续

要让频谱可信,采样必须等间隔、不丢点。用定时器触发ADC,配合DMA搬运到内存,是G431上最稳的方案。先定义一个采样缓冲:

#define FFT_SIZE 512 static uint16_t adc_buf[FFT_SIZE];

采样满FftSize个点后,置一个标志。DMA用循环模式,主循环检测到标志后拷贝数据处理,或者用双缓冲思路避免采集和处理互相覆盖。

volatile uint8_t sample_ready = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { sample_ready = 1; } } while (1) { if (sample_ready) { sample_ready = 0; process_fft((uint16_t *)adc_buf, FFT_SIZE); } }

最简单粗暴的写法就是采满一整块再处理。优点是不用考虑数据覆盖,逻辑清晰;缺点是采样时处理线程不能跑重活。等流程跑通之后,再升级成半传输中断+滑窗,让频谱刷新得更平滑。

4.2 数据预处理:ADC原始值要转成有符号浮点

ADC原始值是0到4095,对应0到3.3V。做FFT前要转成以1.65V为中心的电压,不然直流分量会占掉0Hz附近一大片能量。

static float32_t input[FFT_SIZE]; static float32_t window[FFT_SIZE]; static float32_t fft_out[FFT_SIZE]; // 预处理:转换电压 + 去直流 + 加窗 for (int i = 0; i < FFT_SIZE; i++) { float32_t v = ((float32_t)adc_buf[i] / 4095.0f - 0.5f) * 3.3f; // 软件去直流:减掉这段数据的均值,或者减一个缓慢跟踪的直流偏置 input[i] = (v - dc_offset) * window[i]; }

注意,去直流不是强制要求。如果信号本身是双极性、或者硬件已经做了交流耦合,减均值反而会破坏低频信息。关键是先搞清楚自己的信号到底需要保留哪部分频率。

4.3 FFT核心调用:rfft_fast输出格式是真坑

CMSIS-DSP里最常用的是实数FFT接口,arm_rfft_fast_f32,它针对实数输入做了优化,速度和内存都比全复数FFT好。基本代码:

arm_rfft_fast_instance_f32 fft_inst; arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE); arm_rfft_fast_f32(&fft_inst, input, fft_out, 0); // 0 表示正变换

坑就在fft_out的排布。它为了省内存,把结果压成了一种非常规格式:

fft_out[0] = 直流分量(DC) fft_out[1] = 奈奎斯特频率分量(fs/2) fft_out[2], fft_out[3] = 第1个正频率bin的复数值(实部, 虚部) fft_out[4], fft_out[5] = 第2个正频率bin的复数值 ...

也就是说,从fft_out[0]开始直接arm_cmplx_mag_f32(fft_out, mag_out, FFT_SIZE/2)是错的,会拿错数据算幅值。正确姿势是单独算DC和Nyquist,然后从fft_out + 2开始,计算剩下的幅值:

static float32_t mag_out[FFT_SIZE / 2]; // 手动处理 DC 和 Nyquist mag_out[0] = fabsf(fft_out[0]); mag_out[FFT_SIZE / 2] = fabsf(fft_out[1]); // 其余 bin 用复幅度计算 arm_cmplx_mag_f32(fft_out + 2, mag_out + 1, FFT_SIZE / 2 - 1);

这样mag_out[0]mag_out[FFT_SIZE/2]才是完整的“单边幅值谱”,只包含DC到奈奎斯特频率的N/2 + 1个点。我第一次就是在这儿翻的车。

4.4 找峰值和频率换算

频谱算完,剩下的就是找峰。注意跳过DC分量,也就是bin 0:

float freq_res = SAMPLE_RATE / FFT_SIZE; uint16_t peak_bin = 1; float max_mag = 0; for (uint16_t i = 1; i < FFT_SIZE / 2; i++) { if (mag_out[i] > max_mag) { max_mag = mag_out[i]; peak_bin = i; } } float peak_freq = (float)peak_bin * freq_res;

实际项目里找峰通常还要限定搜索频带,比如只看1kHz到10kHz,避免把电源噪声当成信号。

5. 踩坑实录:频谱不对时我按什么顺序排查

5.1 第一个坑:频率最高处永远有个巨大峰值

最开始我把arm_cmplx_mag_f32直接用在fft_out上,结果最高频段出现一个异常大的值,低频信号反而看不清。原因就是上一节说的输出格式问题:fft_out不是连续复数数组。这个问题只靠肉眼看代码很难发现,但对照CMSIS-DSP官方手册里的“Packed Format”说明就明白了。

建议:在初始化后先用一个已知频率的正弦波做自检,确认每个bin对应位置正确,再开始处理真实信号。

5.2 第二个坑:正弦波测试竟然“长毛”

用片内DAC输出一个1kHz正弦波喂给ADC,FFT结果里1kHz谱线旁边出现了一堆对称旁瓣,像梳子一样。我一开始怀疑是电源干扰,后来用示波器看波形完全正常,排查了大半天才发现是采样率设置得不巧。

我当时的采样率是32kHz,FFT点数是512,分辨率62.5Hz,1kHz正好落在第16个bin,理论上整周期采样,不应该有泄漏。问题是测试信号源有微小频偏,加上DAC本身有量化噪声,导致能量不是完美的单根谱线。之后我改了测试方法:用fs/N的整数倍频率作为测试源,比如采样率32kHz、N=512,就生成1kHz、1.0625kHz这种频率,泄漏立刻小了很多。

这个经验反过来印证了一件事:真实信号不可能正好整周期,所以加窗不能省。汉宁窗是通用选择,频谱会干净很多。

5.3 第三个坑:一上1024点就HardFault,SRAM不够用了

起初我想参考F407的教程直接跑4096点FFT,结果程序一启动就进HardFault。查map文件发现arm_rfft_fast_instance_f32内部有多个缓冲区,单精度FFT点数越大,实例占的RAM越多,4096点光实例就吃掉十几KB,再加上ADC缓冲和显示缓冲,32KB的G431根本扛不住。

G431 CB型号是128KB Flash + 32KB SRAM,做256点或512点非常舒服,1024点需要精打细算。一个省内存的办法是让ADC缓冲和FFT输入缓冲共用一块内存,采集完成后原地处理,不再复制一份。另外,把不需要的双精度库函数排除掉,也能省不少Flash。

5.4 第四个坑:IAR里链接报错,和Keil的库不是一回事

换IAR工程时,我直接把Keil的.lib文件链进去,结果报了一堆conflict。后来查资料才意识到,CMSIS-DSP预编译库是按工具链发布的,Keil用.lib,IAR用.a,GCC用.a但格式不一样。建议不要跨工具链搬运预编译库,要么用源码编译,要么在目标IDE里从软件包管理器安装对应版本。

5.5 排查频谱问题的固定顺序

我现在做FFT调试,已经养成了固定习惯,按这个顺序能解决九成问题:

  1. 先用示波器或串口看ADC原始时域波形,确认没有饱和、没有严重噪声。
  2. 确认输入数组有没有正确去直流、有没有加窗。
  3. 确认FFT输出解析格式正确,尤其是DC和Nyquist的位置。
  4. 算一下目标频率落在哪个bin,和实际峰值的bin做对比。
  5. 检查幅值数量级,确认有没有乘窗函数恢复系数。
  6. 确认采样连续,没有被DMA覆盖或主循环处理太慢导致丢点。

6. 显示与调试:把频谱“看”出来的几种低成本方案

6.1 串口导出到PC,最快看到频谱全貌

算法跑通后,最简单的可视化方式是串口把mag_out数组以CSV格式发出来,在PC端用Python脚本画图:

import serial import matplotlib.pyplot as plt ser = serial.Serial('COM3', 921600, timeout=1) line = ser.readline().decode().strip() data = list(map(float, line.split(','))) plt.plot(data) plt.xlabel('bin') plt.ylabel('magnitude') plt.show()

这个方案的好处是零UI成本,快速确认算法正确性。我所有新算法的第一版验证都是这么干的。

6.2 直接在TFT屏上画柱状图

确认算法没问题后,再接屏幕。G431开发板最常见的搭档是1.8寸或2.8寸TFT。画频谱柱状图的核心是一个循环,把每个bin的幅值按比例映射到屏幕高度:

uint16_t bar_height = (uint16_t)(20.0f * log10f(mag_out[i] / max_mag + 1e-6f)); fr_shadow_fill(x, y, w, h, color);

注意频谱要做对数转换,也就是dB显示,否则弱信号在屏幕上就是一条直线。显示时还可以加峰值保持线,方便观察频谱飘移。

6.3 DSO138那类示波器板,也能变成简易频谱仪

最近看到不少人在折腾“dso138示波器fft固件”,就是用现成的便宜DIY示波器板跑FFT,做出一个简易频谱仪。DSO138这类板子主控性能远不如G431,都能做到显示频谱,说明“采样+FFT+屏幕绘制”这条链路在入门级硬件上完全可行。G431有更大的RAM、更快的FFT,还有片内DAC能做自检,参考这个思路做一台小频谱仪,是很不错的练手项目。

6.4 再补几个实用优化方向

  • 幅值转dB后做峰值排序,只显示最大的前几个频率,适合做语音或振动监测。
  • 加Persistence(余晖)模式,频谱变化轨迹一目了然,方便观察偶发干扰。
  • 用G431的FMAC在FFT之前做一次低通抗混叠滤波,防止高频折叠到低频段。
  • 如果做声学或振动分析,G431有多个ADC通道,可以双通道同步采样,做互功率谱或相位差分析,这比单通道频谱信息量大得多。

一些实际操作中的体会

测试FFT最好用的工具就是G431片内DAC。它本身能产生任意波形,我用它输出一个与fs/N整数倍对齐的正弦波,再灌回ADC,整套FFT链路几秒钟就能验证完。如果没有这个自测手段,每次调参数都要外接信号源,效率低很多。

DSP库这套流程,最难的地方不是调用API,而是理解数据在内存里长什么样。rfft_fast的输出格式、窗函数对幅值的影响、bin和真实频率的换算,任何一个地方没搞明白,结果都会很奇怪。我的建议是:第一次做FFT,不要直接上1024点,先跑256点,配合串口导出画图,把每个bin的含义吃透,再逐步加规模。基础打牢后,再去看FMAC、CORDIC这些硬件加速器,你会对G431这套信号处理生态有更完整的认识。

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

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

立即咨询