☰
STM32F407 ADC+DMA+FFT实战:从采样到频谱的完整配置与避坑指南
2026/10/16 4:06:31 网站建设 项目流程

简介:这份资源面向学习STM32嵌入式开发与数字信号处理的工程师及学生,围绕STM32F407内置ADC的采样与FFT频谱分析展开。工程通过定时器触发ADC并配合DMA持续搬运采样数据,可灵活设置512kHz、256kHz、128kHz三档采样频率,采样点数与频率均可修改,最终将FFT运算结果经串口打印输出,便于直观观察输入电压的频谱分布,是理解ADC采集链路与FFT实际应用的完整参考。压缩包共187个文件,约4.85MB,以58个h头文件与47个c源文件为主体,涵盖ADC、定时器、DMA等外设驱动及FFT算法实现,另含Keil工程文件、编译中间产物与hex烧录文件,结构完整可直接编译运行。目前已有5795人学习下载,适合希望动手验证采样率与频谱关系、掌握定时器+DMA+ADC组合用法的开发者参考借鉴。

1. 从一次串口波形异常说起:STM32F407 上 ADC+DMA+FFT 到底能干什么

去年帮一个做电机状态监测的朋友看代码,他用的是一块 STM32F407 开发板,串口打印出来的频谱图总是糊成一团,低频段全是毛刺,高频段又像被削平了。他以为是 FFT 算法写错了,折腾了两周,最后发现是 ADC 采样和 DMA 搬运的节奏没对上——采样率实际只有设定值的一半,频谱自然全乱。这个坑在 STM32 上做信号采集的人几乎都踩过,而STM32F407_ADC_DMA_FFT这个资源包,就是围绕“ADC 连续采样 + DMA 自动搬运 + 定时器触发 + FFT 频谱分析”这条链路,把配置、代码和参数说明整理成了一套可以直接复现的工程。

它解决的不是“FFT 怎么算”这种教科书问题,而是“怎么让 FFT 拿到干净、时序正确的数据”。适合两类人:一类是刚接触 STM32 信号采集、想跑通第一个频谱 Demo 的新手;另一类是在做振动分析、音频处理、电源谐波检测,需要一套稳定采集框架的从业者。资源里包含的是完整的工程文件、外设初始化代码和 FFT 调用示例,不是零散片段。下面按“资源是什么 → 怎么配 → 坑在哪 → 怎么验证”的顺序拆开讲。

2. 拆开资源包:ADC+DMA+FFT 三件套的工程结构与选型逻辑

2.1 为什么是 F407 而不是 F103,为什么用 DMA 而不是中断

STM32F407 和 F103 最大的差别在 ADC 和内核。F407 的 ADC 支持最高 2.4 MSPS 的采样率,配合 FPU 和 DSP 指令集,做 1024 点浮点 FFT 的时间在百微秒级别;F103 没有 FPU,同样点数要慢一个数量级,而且 ADC 最快只有 1 MSPS。如果你要做音频(20 kHz 带宽)或者电机振动(几 kHz 到几十 kHz),F407 是更稳妥的起点。资源包选 F407 不是随便挑的,是因为它的 ADC + DMA + 定时器触发这套组合能跑出比较干净的连续采样流。

再说 DMA。很多人第一版代码用 ADC 中断读数据,每采一个点进一次中断,CPU 被频繁打断,采样率一高就丢点。DMA 的作用是把 ADC 数据寄存器里的值自动搬到内存数组,CPU 只在半满和全满时各处理一次。这样采样节奏由定时器硬件控制,不受中断响应抖动影响。资源包里 DMA 配置成循环模式,缓冲区大小是 FFT 点数的两倍,半满时处理前半段,全满时处理后半段,实现“采集不停、处理不断”。

2.2 工程文件清单与关键文件职责

资源包解压后是一个标准的外设库工程,核心文件如下:

文件职责需要关注的点
main.c主循环、FFT 调用、串口输出半满/全满回调里的处理顺序
adc.cADC1 初始化、通道配置采样时间、连续转换模式
dma.cDMA2 流配置循环模式、数据宽度、优先级
tim.cTIM2 触发配置触发频率决定实际采样率
fft.c浮点 FFT 与求模窗函数、点数、归一化
arm_math.hCMSIS-DSP 头文件需要工程里包含 DSP 库

这里要提醒一句:资源包里的 FFT 用的是 CMSIS-DSP 的arm_cfft_f32,不是自己手写的蝶形运算。自己写不是不行,但定点化和旋转因子精度容易出问题,CMSIS-DSP 在 F407 上已经过优化,直接调用更省事。如果你用的是 Keil,需要在工程选项里勾选 DSP 库;用 STM32CubeIDE 的话,在main.c里包含arm_math.h并链接libarm_cortexM4lf_math.a。

2.3 采样率、点数与频率分辨率的关系

这是整个链路里最容易被忽略的参数链。资源包里默认配置是:TIM2 触发频率 20 kHz,ADC 采样 1024 点,FFT 输出 512 个频点。频率分辨率 = 采样率 / 点数 = 20000 / 1024 ≈ 19.53 Hz。也就是说,频谱上相邻两个点之间相差约 19.5 Hz,如果你要分辨两个相差 10 Hz 的信号,这个配置不够,得增加点数或者降低采样率。

但点数不是越大越好。1024 点浮点 FFT 在 F407 上大约 200 微秒,2048 点约 450 微秒,4096 点接近 1 毫秒。如果你用 20 kHz 采样,1024 点对应 51.2 毫秒的数据,处理耗时 0.2 毫秒,完全跟得上;但如果点数加到 8192,数据时长 410 毫秒,处理耗时约 2 毫秒,虽然也跟得上,但内存占用和实时性会变差。资源包里给了 1024 和 2048 两套配置,切换时记得同步改 DMA 缓冲区和 FFT 实例的初始化参数。

提示:采样率必须严格等于定时器触发频率,而不是 ADC 时钟分频后的值。很多人在这里翻车,以为设了 20 kHz 就是 20 kHz,实际用示波器测 GPIO 翻转才发现差了一倍。

3. 从零配通采集链路:定时器触发、DMA 搬运与 FFT 调用

3.1 定时器触发 ADC 的配置步骤与参数计算

先看 TIM2 的配置。资源包里 TIM2 挂在 APB1 总线上,时钟 84 MHz。要得到 20 kHz 的触发频率,预分频器和自动重装载值这样算:

// tim.c 关键配置 TIM_HandleTypeDef htim2; htim2.Instance = TIM2; htim2.Init.Prescaler = 84 - 1; // 84MHz / 84 = 1MHz 计数频率 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 50 - 1; // 1MHz / 50 = 20kHz 触发频率 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim2); // 配置触发输出,TRGO 更新事件 TIM_MasterConfigTypeDef sMasterConfig = {0}; sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);

逻辑说明:预分频器把 84 MHz 降到 1 MHz,自动重装载值设为 49,计数器每计满 50 次产生一次更新事件,更新事件通过 TRGO 输出给 ADC 作为外部触发。这样 ADC 的采样启动完全由硬件定时器控制,不受软件循环影响。参数上,Prescaler和Period必须根据你的系统时钟重新算,不能直接抄。如果你用的是 168 MHz 主频,APB1 定时器时钟可能是 84 MHz 或 168 MHz,要看时钟树配置。

3.2 ADC 连续转换模式与 DMA 循环缓冲的配合

ADC1 的配置要点是:外部触发选择 TIM2 TRGO,连续转换模式使能,DMA 请求使能。资源包里 ADC 通道用的是 PA0,采样时间设为 84 周期,在 12 位分辨率下转换时间约 1.5 微秒,远小于 50 微秒的触发间隔,不会丢点。

// adc.c 关键配置 ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.DMAContinuousRequests = ENABLE; hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig);

这里ContinuousConvMode设为 ENABLE 后,ADC 在每次触发后会自动开始下一次转换,配合 DMA 循环模式,数据会源源不断填入缓冲区。DMAContinuousRequests也必须使能,否则 DMA 传完一轮就停了。DMA 配置里,Mode设为DMA_CIRCULAR,PeriphDataAlignment和MemDataAlignment都设为半字(16 位),因为 ADC 数据寄存器是 12 位右对齐,用uint16_t数组接收。

3.3 FFT 调用与窗函数选择:矩形窗还是汉宁窗

数据进缓冲区后,FFT 处理在 DMA 半满/全满回调里做。资源包里默认用汉宁窗,因为电机振动和音频信号通常不是整周期采样,不加窗会有频谱泄漏,低频段尤其明显。

// fft.c 关键处理 #define FFT_SIZE 1024 float32_t adc_buf[FFT_SIZE * 2]; // DMA 目标缓冲区 float32_t fft_input[FFT_SIZE]; // FFT 输入 float32_t fft_output[FFT_SIZE]; // FFT 输出(复数) arm_cfft_instance_f32 fft_inst; void FFT_Process(float32_t *src) { // 1. 去直流偏置并加汉宁窗 float32_t mean = 0; arm_mean_f32(src, FFT_SIZE, &mean); for (int i = 0; i < FFT_SIZE; i++) { float32_t window = 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); fft_input[i] = (src[i] - mean) * window; } // 2. 执行 FFT arm_cfft_f32(&fft_inst, fft_input, 0, 1); // 3. 求模 arm_cmplx_mag_f32(fft_input, fft_output, FFT_SIZE / 2); }

逻辑说明:先求均值去掉直流分量,否则 0 Hz 处会有一个很大的峰,掩盖低频信号。汉宁窗的公式是0.5 * (1 - cos(2πn/(N-1))),加窗后信号两端被压到接近零,减少截断效应。arm_cfft_f32的第三个参数 0 表示不进行位反转(因为输入是自然顺序),第四个参数 1 表示正向变换。求模后只取前一半,因为实信号 FFT 是对称的。参数上,FFT_SIZE必须和 DMA 缓冲区的一半大小一致,否则数组越界。

注意:如果你测的是整周期信号(比如用信号发生器输出的正弦波),矩形窗反而更准,因为汉宁窗会降低幅值。资源包里留了宏定义,切换窗函数时记得同步改幅值补偿系数。

4. 避坑与排查:采样率对不上、频谱泄漏、DMA 溢出这三件事

4.1 现象:频谱峰值位置和理论值差一倍

原因:定时器触发频率算错。常见情况是 APB1 定时器时钟不是你以为的 84 MHz,而是 42 MHz 或 168 MHz,导致实际触发频率翻倍或减半。另一个原因是 ADC 的ExternalTrigConv选错了触发源,比如选了T2_TRGO但 TIM2 的 TRGO 输出配置成了其他事件。

解决:用示波器测一个 GPIO 翻转,在 DMA 半满回调里翻转引脚,测出的频率就是实际采样率。如果和设定值不符,回到时钟树确认 APB1 定时器时钟,重新算预分频器和重装载值。资源包里有一个debug_pin.c文件,专门用来做这个验证。

4.2 现象:低频段出现很多不该有的峰,或者峰旁边有裙边

原因:频谱泄漏。要么是没加窗,要么是加窗后没做幅值补偿,要么是信号本身包含非整周期分量。还有一种容易被忽略的情况:ADC 采样时间太短,采样保持电容没充够,导致采样值偏低,等效于对信号做了非线性衰减。

解决:先确认窗函数已启用,汉宁窗的幅值补偿系数约为 2.0(因为窗函数平均值为 0.5)。然后检查 ADC 采样时间,在 12 位分辨率下建议不低于 84 周期,如果信号源内阻较大,还要加长。资源包里fft.c的FFT_Process函数已经包含了补偿,但如果你改了窗函数类型,补偿系数要跟着改。

4.3 现象:串口输出偶尔卡死,或者 FFT 结果突然全为零

原因:DMA 溢出。当 FFT 处理时间超过半缓冲区的采集时间时,DMA 会覆盖还没处理的数据,或者处理函数还没返回,下一次半满中断又来了。在 1024 点、20 kHz 采样下,半缓冲区对应 25.6 毫秒,FFT 处理约 0.2 毫秒,理论上不会溢出。但如果你的 FFT 里加了串口打印,printf是阻塞的,一打印就超时。

解决:把串口输出改成 DMA 发送,或者只在全满回调里打印一次,不要在半满和全满里都打印。资源包里默认用arm_cfft_f32后直接算模,不打印中间数据。如果你要调试,建议把结果存到数组,在主循环里慢慢发。

4.4 现象:FFT 结果里 0 Hz 处有一个巨大峰值,其他频率被淹没

原因:直流偏置没去掉。ADC 采集到的原始值在 0 到 4095 之间,均值大约 2048,这个直流分量在 FFT 里对应 0 Hz 的一个大峰,幅值可能是交流信号的几十倍。

解决:在加窗之前先减均值。资源包里用arm_mean_f32求均值,然后逐点相减。注意均值要在加窗之前求,因为加窗会改变信号的平均值。如果你用的是定点 FFT,减均值这一步同样不能省,否则定点溢出会导致结果完全错误。

4.5 现象:换用 2048 点后程序跑飞或结果错乱

原因:DMA 缓冲区大小、FFT 实例初始化、arm_cfft_f32的调用参数没有同步修改。arm_cfft_instance_f32需要根据点数选择对应的初始化函数,1024 点用arm_cfft_init_f32(&fft_inst, 1024),2048 点要改成 2048。另外 DMA 的BufferSize也要从 2048 改成 4096(因为每个点占两个字节,且缓冲区是点数的两倍)。

解决:资源包里把点数做成了宏FFT_SIZE,所有相关数组和初始化都用这个宏。切换点数时只改宏定义,然后重新编译。但要注意,arm_cfft_init_f32的第二个参数必须是 16 的倍数,且不能超过 4096。如果你要跑 8192 点,得确认芯片 RAM 够不够,F407 的 192 KB RAM 跑 8192 点浮点 FFT 会比较紧张。

5. 进阶验证:用已知信号校准幅值精度与频率分辨率

跑通采集链路之后,下一步是验证 FFT 输出的幅值和频率是否可信。我一般会用一个信号发生器输出 1 kHz、1 Vpp 的正弦波,接到 ADC 输入端,然后看频谱上 1 kHz 处的幅值是否接近理论值。这里有一个容易忽略的点:ADC 输入范围是 0 到 3.3 V,如果你直接输入双极性信号,负半周会被削掉,频谱上会出现大量谐波。正确做法是加一个偏置电路,把信号抬到 1.65 V 左右,或者用运放做电平搬移。

验证频率分辨率时,用信号发生器输出两个相差 50 Hz 的正弦波,比如 1 kHz 和 1.05 kHz。在 1024 点、20 kHz 采样下,分辨率约 19.5 Hz,两个峰应该能分开。如果你看到的是一个宽峰,说明分辨率不够,需要增加点数或降低采样率。资源包里有一个calib.c文件,里面用查表法生成了一个已知频率和幅值的测试信号,可以直接灌入 FFT 函数验证算法本身是否正确,排除硬件干扰。

幅值校准的公式是:幅值 = 模值 / (FFT_SIZE / 2) * 窗函数补偿系数。汉宁窗的补偿系数取 2.0,矩形窗取 1.0。如果你用的是 CMSIS-DSP 的arm_cmplx_mag_f32,输出的是复数模值,需要除以FFT_SIZE / 2再乘补偿系数才是实际电压幅值。注意 ADC 的参考电压是 3.3 V,12 位分辨率下每个 LSb 对应 3.3/4096 ≈ 0.8 mV,所以最终电压还要乘这个系数。

提示:校准的时候先把信号源关掉,看底噪水平。如果底噪在 -60 dB 以下,说明采集链路比较干净;如果底噪很高,检查电源纹波和地线布局,ADC 的参考电压引脚要加去耦电容。

从那以后我每次配 ADC+DMA+FFT,都强制先跑一遍 GPIO 翻转测实际采样率,再用已知信号验证幅值,最后才接真实传感器。这套流程帮我省掉了至少三次“以为算法错了、其实是硬件配置错了”的返工。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询