☰
STM32F407 ADC采样与DMA搬运实现FFT频谱分析完整方案
2026/10/20 15:04:52 网站建设 项目流程

简介:面向STM32嵌入式开发者的ADC采集与FFT频谱分析工程参考,基于STM32F407内置ADC,通过定时器触发、DMA搬运实现输入电压连续采样,支持512kHz、256kHz、128kHz三档采样频率切换,并将FFT运算结果经串口实时打印输出,适合需要了解ADC高速采集、DMA传输及数字信号处理的中高级开发者对照学习。资源包共187个文件,以h头文件与c源文件为主(分别58个和47个),另有UVProjx工程配置、编译生成的o/d/crf中间文件、hex/axf烧录文件及Keil辅助脚本,整体约4.85MB,可直接打开工程按模块阅读。已有5795人浏览学习。读者可借助该工程掌握定时器与ADC采样频率的联动配置、DMA非中断方式搬运数据的思路、FFT结果的串口格式化输出方法,并可通过修改采样点数与档位,直观体验不同参数下频谱分析的变化,适合作为嵌入式信号处理实验的快速启动模板。

1. 用STM32F407做FFT频谱分析:ADC采样+DMA搬运,一个库解决核心痛点

很多人一提到对模拟信号做频谱分析,第一反应是上专用的DSP芯片或者买一台台式频谱仪。但当你手里只有一块STM32F407开发板、一个麦克风模块或者振动传感器时,你需要的其实是一个可靠的“ADC采集+FFT运算”链路。这套方案的核心思路并不复杂:让ADC连续采样,用DMA把数据搬到内存,再由FFT对采集到的一段数据做频域变换。听起来不新鲜,但真正落地时涉及的采样率配置、DMA缓冲切换、FFT结果标定、频谱泄漏处理,每一个环节都有明显的坑,也是一份可以直接抄作业的源码包价值所在。

这套资源要解决的实际问题是:没有逻辑分析仪、没有示波器时,如何让F407变成一台简易频谱仪。它适用于做音频信号分析、电机振动监测、电源纹波检测等场景的开发者。最直接的反直觉结论是:F407的ADC虽然只有12位,但配合DMA和定时器触发,完全可以在100kHz级别采样率下跑出实用的FFT结果,并不需要一块昂贵的独立高速ADC芯片。

2. 为什么是F407 + ADC + DMA:硬件选型与数据路径设计

2.1 F407的ADC能力边界:别指望它替专业芯片

在做FFT之前,先要把采集端的“天花板”摸清楚。STM32F407内部集成了3个独立ADC,可以工作在12位、10位、8位或6位分辨率模式。12位模式下最高采样率约为2.4Msps(兆次每秒),但这只是芯片理论值,实际受限于GPIO翻转速度、参考电压稳定性和EMI影响,稳定跑的采样率通常在1Msps以内。

我需要给一个更实际的参数:做常规FFT分析时,采样率设定在10kHz到200kHz之间是比较舒服的工作区间。10kHz以下可以用内部RC振荡器驱动,往上建议使用外部晶振或PLL锁相后的时钟。在做FFT时,一个重要的认知是:ADC位数直接决定动态范围。12位ADC的理论动态范围约74dB,F407的实际信噪比通常在64~70dB之间,这意味着你对微弱信号的检测能力受硬件底线约束,软件无法突破这个物理限制。

2.2 DMA为什么要和ADC绑定:中断绕行与缓冲切换

如果只用一个简单的while循环去轮询ADC转换完成标志,CPU会频繁被打断去搬数据,这在高采样率下几乎是灾难。做法是让ADC完成一次转换后直接触发DMA请求,DMA把结果搬进内存数组,整个搬运过程不需要CPU干预。

在配置DMA时有个关键概念叫“循环模式”(Circular Mode)。在这个模式下,DMA搬完一整段数据后,地址会自动回到数组头部重新开始搬,相当于一个环形缓冲区。配合ADC的扫描模式,你可以实现连续采样而无缝闭环。

另外要重视DMA的中断配置:当DMA搬运完半缓冲区时触发一次中断,搬完整个缓冲区时再触发一次中断。这就是“双缓冲”机制。代码上这样写:

void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_InitStructure.DMA_Channel = DMA_Channel_0; // ADC1对应的DMA通道 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 源地址:ADC数据寄存器 DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)adc_buffer; // 目的地址:自定义缓冲区 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; // 外设到内存 DMA_InitStructure.DMA_BufferSize = FFT_POINTS; // 缓冲大小为FFT点数 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环采集模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream0, &DMA_InitStructure); DMA_Cmd(DMA2_Stream0, ENABLE); }

这段代码的核心在DMA_Mode_Circular和DMA_BufferSize = FFT_POINTS这两行。前者让DMA永远不停下来,后者把缓冲区和FFT点数绑定,保证每次取到的数据长度正好等于一次FFT需要的样本数。使用HalfWord(16位)是因为F407的ADC是12位分辨率,数据存在低12位中,16位宽度刚好撑住该范围。若你改用8位采样模式,数据宽度可换成Byte,但一般不推荐,会失去动态范围优势。

2.3 定时器触发采样:让采样率精确可控

ADC也可以软件触发,但那会引入不确定的抖动。在做FFT时采样率不均匀会在频谱上产生额外噪声,所以通常用定时器输出触发信号来接ADC的外部触发引脚。多路ADC时可以先配置一个基本定时器(如TIM2)产生更新事件。以下是参数设置逻辑:

定时器触发频率的计算公式为:F_trigger = TimerClock / (PSC + 1) / (ARR + 1)。比如F407主频168MHz,APB1定时器时钟为84MHz,计划采样率F_s = 40kHz,就可以设PSC=41,ARR=49。当然如果你不在意那一点点抖动,也可以直接采用ADC连续采样模式。

void TIM_Config(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Prescaler = 41; // 84MHz / (41+1) = 2MHz TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_InitStructure.TIM_Period = 49; // 2MHz / (49+1) = 40kHz 采样率 TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &TIM_InitStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); }

要注意的是TIM_SelectOutputTrigger这行,它将定时器更新事件路由到内部触发线路,ADC那边通过ADC_ExternalTrigConvCmd开启外部触发模式。很多新手只配置了定时器却忘了这行,导致ADC用默认的软件触发,采样率完全不受控。调试时先把定时器的触发输出单独接到一个GPIO,用示波器确认频率是否为预期值。

3. FFT那点事:从时域到频域的数学与库选择

3.1 FFT点数与频率分辨率:先定指标再选参数

FFT的核心指标是频率分辨率,这个值等于采样率除以FFT点数。假设用40kHz采样率,做1024点FFT,频率分辨率为40000 / 1024 ≈ 39Hz。这意味着频谱图上每个频点之间相隔39Hz,你分辨不出相距20Hz的两个频率分量。

如果你需要对50Hz市电工频及其谐波做分析,39Hz的分辨率完全够用;但如果你在分析1kHz附近的两个相隔30Hz的音频信号,那就必须提高FFT点数到2048或4096,或降低采样率。选择顺序是:先确定目标信号的频率范围和最小频率间隔,再反推采样率与FFT点数。一组常用组合如下:

采样率FFT点数频率分辨率最大可分析频率(奈奎斯特)
20kHz102419.5Hz10kHz
40kHz102439.1Hz20kHz
40kHz40969.8Hz20kHz
100kHz409624.4Hz50kHz

要注意奈奎斯特定律是硬约束:采样率必须大于信号最高频率的两倍,否则频谱会出现混叠(Fold-back)。工程上通常取采样率是信号最高频率的5到10倍,比如分析20kHz音频信号时,采样率不应低于100kHz,才能让抗混叠滤波器留出过渡带。

3.2 库选择:CMSIS-DSP 与手动实现

在F407上进行FFT计算,最直接的做法是用ARM官方CMSIS-DSP库。这个库提供两种FFT算法入口:基2算法和基4算法。F407带FPU(浮点运算单元),支持硬件浮点计算,推荐使用arm_rfft_fast_f32,输入是浮点序列,输出是复数格式但存在独立数组里,效率比传统定点FFT要高。

有一种情况需要改用定点库:当你对运算速度极端敏感,比如需要在中断里完成FFT,并且不介意重新做浮点转定点缩放。但F407跑120MHz时,做1024点浮点FFT大约需要200微秒量级,这对绝大多数应用场景已经足够。选择CMSIS-DSP的另一个原因在于它做过了ARM内部指令级优化,比自己写循环快得多,尤其是在大点数FFT时明显拉开差距。提供一下头文件和初始化:

#include "arm_math.h" #define FFT_SIZE 1024 #define FFT_HALF (FFT_SIZE / 2) float32_t input_signal[FFT_SIZE]; float32_t fft_output[FFT_SIZE]; float32_t fft_magnitude[FFT_HALF]; static arm_rfft_fast_instance_f32 fft_instance; void FFT_Init(void) { arm_rfft_fast_init_f32(&fft_instance, FFT_SIZE); } void FFT_Run(void) { arm_rfft_fast_f32(&fft_instance, input_signal, fft_output, 0); // 0表示正变换 // 计算幅值,第0个元素是直流分量,其余按实部虚部交错存放 arm_cmplx_mag_f32(fft_output + 2, fft_magnitude + 1, FFT_HALF - 1); fft_magnitude[0] = fabsf(fft_output[0]) / FFT_SIZE; // 直流分量单独处理 }

arm_rfft_fast_f32的输出格式较为特殊:数组前两个元素分别为直流分量的实部和虚部,而从第3个元素开始按“实部、虚部、实部、虚部”交替存储。计算幅值时用arm_cmplx_mag_f32,但需要跳过前两个元素。arm_rfft_fast_init_f32的第二个参数必须是4的幂,而且不得小于32,这一点和基4算法的限制相关。

3.3 窗函数:为什么直接拿原始数据做FFT是错的

采集到的时域信号往往是无限长信号被矩形窗口截断的结果。这个过程在频域等价于信号频谱与矩形窗频谱的卷积,结果是产生了频谱泄漏现象:本来集中于单一频率的能量扩散到两侧频点,对幅值测量和频率定位都会造成误差。

解决方式是在做FFT前把时域数据乘以一个窗函数。汉宁窗(Hamming/Hanning)是最常用的选择,它主瓣宽但旁瓣衰减快,适合大多数通用分析场景。以下是对应代码:

static float32_t window[FFT_SIZE]; void Window_Init(void) { for (uint16_t i = 0; i < FFT_SIZE; i++) { window[i] = 0.5f * (1.0f - cosf(2.0f * PI * i / (FFT_SIZE - 1))); } } void Window_Apply(float32_t *data) { for (uint16_t i = 0; i < FFT_SIZE; i++) { data[i] = data[i] * window[i]; } }

加窗后需要注意配合“幅值修正系数”。因为加窗本身会衰减信号能量,做幅值标定时要把结果除以窗函数的相干增益。汉宁窗的相干增益是0.5,所以在计算真实信号幅值时要将FFT幅值再乘以2。如果不修正,测量的正弦波幅值会比实际偏小约一半,这类问题在波形显示和阈值判断时影响明显。

4. ADC数据到FFT结果的完整链路:配置顺序与代码集成

4.1 初始化顺序:电源、时钟、GPIO、ADC一个都不能乱

新手最容易忽略的是GPIO的模拟模式设置。ADC的输入引脚必须配置为GPIO_Mode_AN,否则引脚还处于数字输入模式,输入电压会被钳位或产生不规则读数。另一个隐藏问题是ADC的采样时间参数,若输入源阻抗较高,你需要更长的采样时间。以下是一个可以落地的初始化版本:

void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_CommonInitTypeDef ADC_CommonInitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式 ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; // PCLK2/4 = 42MHz ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; ADC_CommonInit(&ADC_CommonInitStructure); ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfConversion = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_84Cycles); ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // 启动一次校准,等待完成后再开启触发 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }

这里有两个细节非常值得留意:一是ADC时钟预分频设为Div4,因为F407的APB2时钟为84MHz,ADC最高时钟为42MHz,超过这个值会牺牲精度;二是校准必须在使能ADC之后、等待一个ADC时钟周期后才能启动,这也是数据手册里的硬性要求。外部触发边沿设置为上升沿,配的正是TIM2更新事件的上升沿。

4.2 主循环结构:什么时候采集、什么时候算FFT、什么时候显示

采集和计算不能同时进行,否则DMA正在搬的数据会被FFT读到,产生不一致的混乱结果。双缓冲机制的完全体做法:DMA每搬完半缓冲区就切换一个全局标志位,主循环检查到标志后处理另一半稳定数据。

下面是一个在实战中已验证可用的主逻辑:

volatile uint8_t dma_half_flag = 0; volatile uint8_t dma_complete_flag = 0; void DMA2_Stream0_IRQHandler(void) { if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_HT)) // 半传输中断 { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HT); dma_half_flag = 1; } if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TC)) // 传输完成中断 { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TC); dma_complete_flag = 1; } } int main(void) { uint32_t i; SystemInit(); ADC_Config(); TIM_Config(); DMA_Config(); FFT_Init(); Window_Init(); // 使能DMA双缓冲中断 DMA_ITConfig(DMA2_Stream0, DMA_IT_HT | DMA_IT_TC, ENABLE); NVIC_EnableIRQ(DMA2_Stream0_IRQn); while (1) { if (dma_half_flag) { dma_half_flag = 0; // 处理前半缓冲区 for (i = 0; i < FFT_SIZE / 2; i++) { input_signal[i] = (adc_buffer[i] / 4095.0f) * 3.3f; } // 后半部分继续由DMA填充,这时处理前半个FFT片段 Window_Apply(input_signal); FFT_Run(); Process_FFT_Result(); } if (dma_complete_flag) { dma_complete_flag = 0; // 处理后半缓冲区,步骤同上 } } }

看起来处理了一半就做一次FFT,看似浪费了一半数据。实际上是利用了DMA循环从half到complete的时间间隙来计算——当你的FFT计算时间小于DMA搬满半缓冲区的时间时,这个方案是稳定的。若FFT点数增大到2048以上,要评估时间预算,必要时以降低ADC采样率为代价换取计算窗口。

4.3 结果的工程化处理:去直流、求幅值、做阈值判断

FFT结果是复数数组,单纯拿到数值还不够,通常要转换成幅值谱或功率谱。另外在信号链上需要去掉直流偏置的干扰。ADC采集到的信号如果是单端输入且信号叠加在2.5V偏置上,FFT结果第0个频点(直流分量)会异常大,如果显示时没有对Y轴做缩放,其它频率分量全都看不出腰身。

常见的处理流程是把直流分量直接清零或做带通处理。由于FFT是对整段数据做的全局变换,单独清理第0个频点往往不够,需要配合高通滤波或者在做FFT前先减去这段数据的平均值。下面是实用处理方式:

void Process_FFT_Result(void) { // 将幅值转为实际的电压值 for (uint16_t i = 1; i < FFT_HALF; i++) { // 每个频点的幅值 = 2 * 频谱幅值 / FFT点数 fft_magnitude[i] = (2.0f * fft_magnitude[i]) / FFT_SIZE; } // 寻找最大幅值对应的频率 uint16_t max_index = 1; float32_t max_val = 0.0f; for (uint16_t i = 1; i < FFT_HALF; i++) { if (fft_magnitude[i] > max_val) { max_val = fft_magnitude[i]; max_index = i; } } float32_t freq = (float32_t)max_index * SAMPLE_RATE / FFT_SIZE; // 此时freq是信号的主频率 }

这段代码里乘2的操作已经考虑了汉宁窗幅值修正。max_index与频率分辨率相乘得到主频估计。注意如果峰值出现在靠近零频的位置,即max_index在3到5之间,那大概率是直流偏置未完全滤除产生的残留,建议回看采集信号的平均值做减法。

5. 避坑:ADC采样与FFT实战中的六个常见问题

5.1 频谱图上持续出现大片“墙”式噪声,低频段整体抬高

现象:信号源是干净的1kHz正弦波,但频谱底部有大量噪声基底,且集中在低频段,像是蒙了一层“雾”。

原因:大部分时候是电源去耦不良,ADC参考电压(VDDA)受到板上数字开关噪声污染;另一个高频触发原因是ADC引脚悬空或外部接跟随器,输入阻抗不匹配导致底噪变大。

解决:在VDDA引脚处增加1uF和100nF双电容组合,靠近引脚放置;在ADC输入端加一个RC低通滤波器,截止频率设为采样率的1/2以内,如40kHz采样时设截止频率15kHz。如果做了这些仍有问题,重点检查GND层是否有数字芯片电流回流路径穿过模拟区域。

5.2 主频峰值比实际信号频率偏移几个频点

现象:输入5kHz正弦波,FFT显示峰值在5030Hz附近,而且幅度忽大忽小。

原因:采样率不准确。定时器的PSC和ARR计算看似没问题,但APB1定时器时钟实际频率可能不是你以为的值——F407的APB1预分频为4时,定时器时钟才是84MHz,若预分频为2则只有42MHz。问题多出在RCC_ClocksTypeDef结构体没有取到正确的时钟频率值。

解决:通过RCC_GetClocksFreq函数读取当前时钟树,或在初始化打印定时器实际输出频率。FFT对频率偏差非常敏感,1%的采样率误差就会造成50Hz频偏在5kHz处产生约50Hz偏差。更保险的做法是在调试阶段用已知频率信号(如1kHz方波)对照校准。

5.3 DMA中断比预期触发频繁,导致CPU占用极高

现象:跑1024点FFT采集时,主循环一直被DMA中断抢占,界面上按钮响应卡顿严重。

原因:DMA配置中的DMA_BufferSize设置成与FFT点数相同,导致DMA每搬完512个点就触发一次中断。如果采样率100kHz,你每5毫秒就被打断两次,综合开销很大。

解决:这个BufferSize可以是FFT点数的整数倍,比如设为4倍FFT点数。每次对其中四分之一的数据做FFT时,可以跳过旧的三段,取最新一段,再加窗处理。典型做法是把采集缓冲区做大两倍,而每次只对“最完整”的一段做FFT。用DMA的剩余数据寄存器DMA_GetCurrDataCounter判断当前采集到了哪个位置,以缓解频繁中断压力。

5.4 加窗之后信号幅值变化很明显,比原来的浮点值缩小近一半

现象:原始时域波形幅度显示为3.0V正弦波,做完FFT加窗后,结果中对应频率的幅值为1.5V,幅度偏得离谱。

原因:不是计算错误,而是忘了做窗函数的幅度修正。任何非矩形窗都会衰减信号总功率,所以要除以窗的相干增益。

解决:在使用汉宁窗时,在幅值转电压处乘以2修正系数。更通用的做法是对窗函数做归一化处理:计算窗口系数之和,将FFT结果除以窗口系数均值。这类代码里直接写好归一化逻辑,建议用代码注释标明修正参数。

5.5 输入信号频率接近采样率一半时,幅值明显下降

现象:采样率20kHz,在分析9kHz信号时幅值异常低,但改到5kHz后恢复正常。怀疑ADC坏了或引脚带宽不够。

原因:这是采样定理和ADC前端保持电路共同作用的结果。F407的ADC在12位模式下的采样时间为3~84个ADC时钟周期,保持电容充电时间有限,输入信号频率越高,衰减越严重。另一个因素是定时器触发边沿和ADC采样保持电路的孔径抖动。

解决:把采样率提升到目标信号频率的5到10倍,尽量不要贴着奈奎斯特边界运行。如果确实需要分析接近采样率一半的高频信号,考虑降低ADC分辨率到8位,缩短采样时间周期以提高吞吐量,或更换带内部PGA(可编程增益放大器)的精密ADC芯片。

5.6 第一次上电时FFT结果是乱的,复位后恢复正常

现象:冷启动上电后第一次FFT结果明显错误,但按下Reset之后一切正常。

原因:这是初始化顺序不当的典型症状。ADC在刚上电时模拟电路尚未稳定,DMA却已经开始搬运数据;FFT库在处理器时钟未完全锁定时被调用,有可能触发硬件错误。

解决:将FFT初始化放在ADC校准和DMA使能之后,且在DMA中断使能前加入20~50毫秒延时,等待参考电压和振荡器稳定。另一个可靠做法是先进行10次采样并丢弃,让模拟前端完全进入稳态后再启用DMA中断。从硬件方向也可以考虑:在VDDA上电到ADC使能之间加RC延时电路。

6. 从F407到更进阶的用法:定时采样与双通道同步的实用技巧

当单通道FFT跑通后,你会马上遇到一个需求:两个通道同时采集并做互相关或谱减法。F407有3个ADC,但DMA只有一组映射对应ADC1、ADC2、ADC3。将两个ADC配置为同步采样模式,则可以在同一个触发信号下让ADC1和ADC2同时采样,并把两个结果连续搬运到同一块DMA缓冲区。

配置前,要把ADC_CommonInitStructure.ADC_Mode设为ADC_Mode_Combined_SlowInterl或ADC_Mode_AlterTrig(取决于你要交错还是同步)。DMA缓冲区大小设为2*FFT_SIZE,进入中断后按奇偶索引拆分为两路信号。注意F407的ADC多路同步模式里的采样时间必须一致,而且两路会共用同一个采样周期。如果一路是高阻信号源,另一路是低阻信号源,要优先照顾高阻通道,调大采样时间,以免因压降导致数据失真。

另一个提升测量质量的习惯是:在FFT前把均值减掉。先对整段时域数据计算平均值,然后每个采样点减去该均值。这样可以压制窗口函数对直流分量的拖尾效应,让低频段更干净。以下是改动后的核心片段:

float32_t dc_offset = 0.0f; for (uint16_t i = 0; i < FFT_SIZE; i++) { dc_offset += input_signal[i]; } dc_offset /= FFT_SIZE; for (uint16_t i = 0; i < FFT_SIZE; i++) { input_signal[i] -= dc_offset; }

这段减法看起来简单,却对后续峰值搜索和低幅值信号检测有明显帮助。配合ADC的偏移校准,可让零输入时频谱底噪再降几个dB。若时间预算允许,在每次DMA完成后都重算一次直流分量,而不是只做一次,能进一步防止漂移。每当项目里加入新的定时器或改变主频,我习惯强制走一遍“时钟树确认→定时器输出波形→ADC数值标定→FFT峰值回读”的完整链路,务必用已知信号校准一次。很多后续排查的麻烦都源于最初少做了这一步。希望这套经验对你有帮助,尤其是在你手里那块F407第一次跑出毛刺数据时。

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

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

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

立即咨询