
1. 先把“测频”这件事拆开看STM32单片机做输入捕获、FFT测频表面上是两个独立功能实际到了项目里往往是一套组合拳。输入捕获吃的是定时器硬件适合测方波、PWM、脉冲串FFT吃的是ADC采样数据适合分析正弦、含噪声信号、多频率成分。很多人一上来就问“哪个更准”这个问题本身就不完整——准不准取决于信号类型、频率范围、允许的响应时间以及你手头这颗STM32的算力和外设资源。我最早做测频是在一个电机转速反馈项目里用TIM输入捕获测霍尔脉冲后来做音频频谱显示又切到FFT。两套代码都跑通之后才发现真正麻烦的不是算法而是边界条件信号太小、毛刺太多、频率刚好卡在溢出边缘、FFT峰值落在两个bin之间。下面这些内容就是围绕这些实际场景展开的从原理到配置、从代码到标定尽量把坑一次说透。不管你是刚接触STM32定时器的新手还是已经会用CubeMX点灯、想进一步做测量类项目的开发者都可以直接拿里面的配置和代码去改。1.1 频率、周期、占空比到底谁先算谁频率的本质是单位时间内周期变化的次数公式很简单f 1 / T。输入捕获测频硬件干的事情就是记录两个边沿之间定时器计数器走了多少个数。假设定时器计数频率是1MHz两个上升沿之间差了1000个计数那周期就是1ms频率1kHz。这个链条里计数频率越高测出来的周期分辨率越细但高频信号周期短计数值小相对误差会变大。占空比则是高电平时间除以整个周期。用普通输入捕获只能测周期想同时测占空比要么用PWM输入模式一个通道抓上升沿、另一个通道抓下降沿要么在中断里手动切换触发极性。PWM输入模式是STM32定时器里很实用的一个功能配置稍微麻烦一点但测出来的周期和占空比是硬件同步的比软件切换稳定得多。FFT走的是另一条路。它不关心边沿而是把一段时间的波形采样下来做傅里叶变换看哪个频率点能量最大。频率分辨率等于采样率除以采样点数比如采样率10kHz、1024点分辨率就是9.77Hz。这意味着两个频率相差小于9.77Hz的信号FFT很难分开。想提高分辨率要么降低采样率要么增加点数但降低采样率会压缩可测频率上限增加点数会吃RAM和运算时间。1.2 时域路线和频域路线的选型逻辑输入捕获属于时域测量响应快、资源省、代码短一个定时器加一个中断就能跑起来。它的短板是只能处理边沿清晰的信号如果信号里混了噪声边沿抖动会直接变成频率抖动。而且它一次只能测一个主频率遇到多个频率叠加的信号就无能为力。FFT属于频域测量天生适合分析复杂信号能同时看到基波、谐波、噪声底。代价是需要ADC、DMA、足够的RAM和运算时间。以STM32F103C8T6为例72MHz主频、20KB RAM跑1024点浮点FFT已经比较吃力通常要降到256点或512点。换成STM32F407有FPU和更大RAM1024点甚至2048点都轻松很多。我一般这样选信号是干净的方波、PWM、脉冲频率在几Hz到几百kHz之间优先输入捕获信号是正弦、音频、振动或者需要看频谱成分直接上FFT。如果既要快速响应又要抗噪可以先用输入捕获粗测再用FFT精测后面第4章会讲融合方案。1.3 硬件与开发环境的基本盘测频项目对硬件的要求不算高但有几个地方不能省。第一输入信号电平必须匹配STM32的3.3V逻辑。5V信号直接进GPIO短期可能能用长期会损伤引脚。可以用电阻分压也可以用74LVC245这类电平转换芯片。第二模拟信号进ADC之前最好加一个RC低通滤波截止频率略高于你关心的最高频率既抗混叠又滤毛刺。第三如果信号源阻抗高ADC采样时间要加长否则采样保持电容充不满读数会偏低。开发环境方面Keil MDK加STM32CubeMX是最常见的组合。CubeMX负责生成时钟树、GPIO、TIM、ADC、DMA初始化代码Keil负责写业务逻辑和调试。FFT部分推荐用CMSIS-DSP库ARM官方优化过比手写FFT快很多。F4系列记得在Keil里开启FPU否则浮点运算会软实现速度差好几倍。注意CubeMX生成代码后自己写的代码要放在USER CODE BEGIN和USER CODE END之间否则下次重新生成会被覆盖。这个坑我踩过不止一次。2. 输入捕获测频定时器配置到代码落地输入捕获的核心是定时器的捕获比较单元。STM32的通用定时器TIM2到TIM5都有4个独立通道每个通道可以配置成输入捕获。信号从GPIO进来经过滤波和边沿检测触发捕获事件当前计数器CNT的值会被锁存到捕获比较寄存器CCR里。你只需要在中断里读两次CCR做差再换算成频率。听起来简单但实际配置时有几个关键参数预分频器PSC、自动重装载值ARR、捕获极性、输入滤波。PSC和ARR共同决定计数频率和溢出周期。计数频率 定时器时钟 / (PSC1)。溢出周期 (ARR1) / 计数频率。如果信号周期大于溢出周期计数器会溢出必须额外记录溢出次数否则算出来的频率是错的。2.1 输入捕获的硬件原理与定时器配置思路假设STM32F103TIM3挂在APB1总线上时钟72MHz。配置PSC71计数频率就是1MHz每个计数代表1微秒。ARR设为65535溢出周期约65.5ms。这意味着信号周期超过65.5ms也就是频率低于15.26Hz就会溢出。要测更低的频率要么加大PSC降低计数频率要么在溢出中断里累加溢出次数。对于高频信号比如100kHz周期10微秒计数值只有10。如果计数频率1MHz相对误差可能达到10%。这时候要提高计数频率比如PSC0计数频率72MHz计数值720分辨率就好很多。但ARR不能太大否则溢出周期太短低频段容易溢出。实际项目里我通常先确定频率范围再反推PSC和ARR。配置步骤大致如下使能TIM和GPIO时钟GPIO设为复用推挽或浮空输入TIM时基单元设置PSC和ARR通道配置为输入捕获模式设置触发边沿和输入滤波使能捕获中断和更新中断最后启动定时器和捕获。CubeMX里这些都能点出来但中断回调里的逻辑要自己写。2.2 高频与低频的两种处理策略低频信号比如转速传感器每秒几个脉冲周期可能几十毫秒。这时候计数频率不用太高PSC可以大一点让ARR有足够空间。重点是溢出处理。我的做法是开启更新中断每次溢出给一个全局变量加一捕获中断里把当前CNT和溢出次数组合成一个64位计数值。计算周期时用当前捕获值减去上一次捕获值再加上溢出次数乘以ARR。高频信号周期短溢出基本不会发生但计数值小量化误差大。除了提高计数频率还可以用“测多个周期取平均”的办法。比如连续捕获10个上升沿记录第1个和第11个之间的总计数再除以10得到平均周期。这样能有效降低单次捕获的随机误差。还有一种更稳的方案PWM输入模式。它用两个通道一个上升沿捕获一个下降沿捕获硬件自动把周期和占空比分别存到两个CCR里。配置时把通道1设为主输入通道2设为从输入触发源选TI1FP1。这样不需要手动切换极性也不容易丢边沿。代价是占用两个通道而且配置比普通输入捕获复杂。2.3 代码落地TIM3 CH1 输入捕获加溢出计数下面这段代码基于HAL库STM32F103TIM3 CH1对应PA6。计数频率1MHzARR65535开启更新中断和捕获中断。代码里用了一个64位的溢出计数变量捕获时把CNT和溢出次数拼起来再算周期和频率。#include stm32f1xx_hal.h TIM_HandleTypeDef htim3; volatile uint32_t capture_cnt 0; volatile uint32_t overflow_cnt 0; volatile uint64_t last_capture 0; volatile uint64_t curr_capture 0; volatile uint32_t period_ticks 0; volatile float freq_hz 0.0f; void TIM3_Init(void) { TIM_IC_InitTypeDef sConfigIC {0}; __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLDOWN; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); htim3.Instance TIM3; htim3.Init.Prescaler 71; // 72MHz / 72 1MHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 65535; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_IC_Init(htim3); sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0x0F; // 滤波抑制毛刺 HAL_TIM_IC_ConfigChannel(htim3, sConfigIC, TIM_CHANNEL_1); HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM3_IRQn); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); __HAL_TIM_ENABLE_IT(htim3, TIM_IT_UPDATE); } void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(htim3); } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { overflow_cnt; } } void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { uint32_t ccr HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); curr_capture ((uint64_t)overflow_cnt 16) | ccr; if (last_capture ! 0) { period_ticks (uint32_t)(curr_capture - last_capture); if (period_ticks 0) { freq_hz 1000000.0f / (float)period_ticks; } } last_capture curr_capture; capture_cnt; } }这段代码里overflow_cnt在每次更新中断时加一捕获中断里把它左移16位和CNT组合。因为ARR是65535相当于16位所以左移16位不会丢信息。period_ticks是两个上升沿之间的计数差计数频率1MHz所以频率等于1000000除以计数差。主循环里可以直接把freq_hz打印到串口。注意在中断里不要做浮点打印打印放到主循环中断里只更新变量。2.4 实测标定与误差分析拿信号发生器输出1kHz方波幅度3.3V接PA6。串口打印频率应该在1000Hz附近可能会有几Hz的跳动。误差来源主要有几个晶振本身精度、中断响应延迟、计数量化误差、信号边沿抖动。普通无源晶振精度大概几十ppm1kHz下误差不到0.1Hz中断延迟取决于优先级和当前执行指令通常几个微秒对应1MHz计数就是几个计数1kHz周期1000个计数误差约0.5%。想再准一点可以连续测10个周期取平均。把第1次捕获存下来第11次捕获再算总差值除以10。这样随机误差被平均掉标定曲线会平滑很多。另外输入滤波参数可以适当加大比如ICFilter设为0x0F能滤掉高频毛刺但会引入一点点边沿延迟对低频测量影响可以忽略。2.5 输入捕获的注意事项输入信号一定要干净。如果信号来自长导线、电机、继电器很可能带毛刺。硬件上加RC滤波和施密特触发器软件上加输入滤波双管齐下。电压不能超过3.3V5V信号必须分压或电平转换。中断优先级要合理捕获中断优先级可以高一点但不要在中断里做耗时操作。如果频率很低溢出次数会很大64位变量足够用但要注意读取时关中断防止溢出和捕获同时发生导致数据错位。3. FFT测频从ADC采样到频谱峰值FFT测频的流程比输入捕获长信号调理、ADC采样、DMA搬运、去直流、加窗、FFT运算、求模、找峰值、频率换算。每一步都有细节任何一步没做好最后峰值都可能偏。最大的误区是以为FFT点数越多越准其实频率分辨率由采样率和点数共同决定而且栅栏效应会让峰值落在两个bin之间直接读最大bin只能得到近似值。3.1 采样定理、频率分辨率与栅栏效应采样定理说采样率必须大于信号最高频率的两倍工程上通常取2.5到5倍给抗混叠滤波留余量。比如要测最高5kHz的信号采样率至少12.5kHz我一般用20kHz或25kHz。采样率越高同样点数下频率分辨率越差。比如20kHz采样、1024点分辨率19.53Hz降到10kHz采样分辨率9.77Hz但最高可测频率也降到5kHz以下。栅栏效应是FFT测频的固有现象。假设信号频率1kHz采样率10kHz1024点频率分辨率9.77Hz。1kHz对应bin 102.4不在整数bin上。最大谱线可能落在bin 102或103直接读bin频率是996.1Hz或1005.9Hz误差几Hz。想减小误差可以用抛物线插值用峰值bin和左右两个bin的幅值估算真实峰值位置。插值后误差可以降到分辨率的一小部分。3.2 ADC加DMA加定时器触发采样配置稳定的采样率是FFT测频的前提。用定时器触发ADC是最常用的方法。TIM2设置成PWM模式但不出引脚只用来产生TRGO信号。ADC配置成外部触发触发源选TIM2_TRGODMA循环搬运到缓冲区。TIM2的更新频率就是采样率。比如72MHz时钟PSC0ARR3599更新频率20kHz对应采样率20kHz。代码配置上先初始化TIM2再初始化ADC和DMA最后启动。ADC的采样时间要根据信号源阻抗调整一般设55.5个周期或更长。DMA缓冲区大小等于FFT点数比如1024个uint16_t。半满和全满中断可以用来做双缓冲但初学者先用全满中断读数据也够用。#define FFT_SIZE 1024 uint16_t adc_buf[FFT_SIZE]; float fft_in[FFT_SIZE]; float fft_out[FFT_SIZE]; void TIM2_Init(void) { TIM_HandleTypeDef htim2 {0}; __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 3599; // 72MHz / 3600 20kHz htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim2); TIM_MasterConfigTypeDef sMasterConfig {0}; sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim2, sMasterConfig); HAL_TIM_Base_Start(htim2); } void ADC_DMA_Init(void) { ADC_HandleTypeDef hadc1 {0}; DMA_HandleTypeDef hdma_adc {0}; __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA1_CLK_ENABLE(); hdma_adc.Instance DMA1_Channel1; hdma_adc.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc.Init.MemInc DMA_MINC_ENABLE; hdma_adc.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_adc.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc.Init.Mode DMA_CIRCULAR; hdma_adc.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_adc); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc); hadc1.Instance ADC1; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buf, FFT_SIZE); }这段配置里TIM2的更新事件触发ADCADC每次转换完由DMA搬到adc_buf。1024个点搬完后DMA传输完成中断触发可以在中断里置标志位主循环处理FFT。3.3 预处理去直流、加窗、补零ADC读进来的数据是0到4095对应0到3.3V。如果信号有直流偏置比如1.65V去直流之前频谱里bin 0会很大可能掩盖真实峰值。去直流就是求平均值再减掉。简单做法是累加所有点求平均然后每个点减去平均值。加窗是为了减少频谱泄漏。信号频率不是分辨率整数倍时截断会导致能量向旁边bin扩散。汉宁窗是最常用的主瓣宽一点旁瓣衰减大。加窗的代价是幅值会变需要乘以窗增益修正系数。如果只看频率不做幅值测量加窗后找峰值就行修正系数可以不那么严格。补零是在原始数据后面补零让FFT点数变大。补零能让谱线更密峰值位置看起来更细但不会真正提高分辨率。比如1024点数据补到2048点频率分辨率名义上提高一倍但实际可分辨的最小频率间隔还是由1024点决定。补零适合用来做峰值插值让峰值位置更精确。3.4 代码实现基于CMSIS-DSP的rfftCMSIS-DSP提供了实数FFT比复数FFT省一半内存和运算。使用步骤初始化rfft实例把ADC数据转成浮点并去直流加窗调用arm_rfft_fast_f32然后计算复数模值找最大bin再做抛物线插值。#include arm_math.h arm_rfft_fast_instance_f32 fft_inst; float fft_output[FFT_SIZE]; float mag[FFT_SIZE / 2]; void FFT_Process(void) { float mean 0.0f; for (int i 0; i FFT_SIZE; i) { mean (float)adc_buf[i]; } mean / FFT_SIZE; for (int i 0; i FFT_SIZE; i) { float x ((float)adc_buf[i] - mean) / 4096.0f; float w 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); fft_in[i] x * w; } arm_rfft_fast_init_f32(fft_inst, FFT_SIZE); arm_rfft_fast_f32(fft_inst, fft_in, fft_output, 0); arm_cmplx_mag_f32(fft_output, mag, FFT_SIZE / 2); uint32_t max_idx 0; float max_val 0.0f; for (int i 1; i FFT_SIZE / 2; i) { if (mag[i] max_val) { max_val mag[i]; max_idx i; } } float delta 0.0f; if (max_idx 0 max_idx (FFT_SIZE / 2 - 1)) { float y0 mag[max_idx - 1]; float y1 mag[max_idx]; float y2 mag[max_idx 1]; delta 0.5f * (y0 - y2) / (y0 - 2.0f * y1 y2); } float sample_rate 20000.0f; float freq ((float)max_idx delta) * sample_rate / (float)FFT_SIZE; }这段代码里arm_cos_f32生成汉宁窗arm_rfft_fast_f32做实数FFTarm_cmplx_mag_f32求模。峰值插值用抛物线公式delta是峰值相对bin的偏移量。最后频率等于bin加偏移乘以采样率除以点数。实测1kHz正弦采样率20kHz1024点分辨率19.53Hz插值后频率通常在1000Hz上下1Hz以内。3.5 实测正弦、方波、含谐波信号的频谱表现正弦信号频谱最干净最大峰值就是基波频率。方波信号包含奇次谐波基波幅值最大但三次、五次谐波也很明显。如果直接找最大峰值通常还是基波但如果基波被滤波衰减可能会误判。这时候可以结合输入捕获先测周期确认基波位置。含噪声信号要小心。噪声底抬高后最大峰值可能被随机噪声抢占。解决办法是加窗、多次平均、或者设置一个幅值门限只在门限以上找峰值。另外ADC输入如果没有抗混叠滤波高频噪声会混叠到低频频谱上出现假峰。硬件上加RC低通软件上可以在FFT前做一次简单滑动平均。4. 两种方案的对比、融合与工程取舍输入捕获和FFT不是对立的很多项目里两者配合才能既快又准。理解它们的差异才能根据场景做出合理选择。下面从精度、速度、资源、适用信号几个维度做个对比。4.1 精度、速度、资源占用对比表维度输入捕获FFT测量对象方波、PWM、脉冲正弦、复杂波形、频谱频率范围几Hz到几百kHz取决于采样率通常几Hz到几十kHz响应速度一个周期即可需要N个采样点通常几ms到几十ms频率精度取决于计数频率可很高取决于分辨率和插值通常低于输入捕获资源占用一个定时器加中断ADC、DMA、RAM、FFT运算抗噪能力弱依赖信号质量强可分析频谱多频率分辨不能可以从表里能看出来输入捕获胜在快和准前提是信号干净FFT胜在能看频谱适合复杂信号。如果项目要求实时性比如电机换向输入捕获更合适。如果要做音频频谱显示、振动分析FFT是唯一选择。4.2 融合方案输入捕获粗测FFT细测融合的思路很直接先用输入捕获测一个粗略频率根据这个频率设置ADC采样率和FFT点数让目标频率落在频谱的合适位置。比如输入捕获测到大约1kHz那么采样率设10kHz1024点分辨率9.77Hz目标频率对应bin 102左右峰值清晰。如果输入捕获测到大约10kHz采样率要设50kHz以上点数可能要增加到2048否则分辨率太差。还有一种用法是用输入捕获测周期用FFT确认主频。比如信号里混了谐波输入捕获测的是边沿间隔可能测到谐波频率FFT能看到基波幅值最大用FFT结果校正输入捕获。两者互相验证可靠性更高。实现上可以用一个状态机上电后先启动输入捕获连续测10个周期取平均得到粗频率然后根据粗频率计算采样率启动ADC加DMA加FFT得到精频率最后把两个结果都打印出来人工确认或者自动融合。整个过程在100ms内可以完成对大多数项目足够。4.3 不同场景的推荐路线音频信号、振动信号、含噪声的正弦波直接上FFT。这类信号边沿不清晰输入捕获容易误触发。音频范围20Hz到20kHzSTM32F4跑1024点或2048点FFT完全够用。电源PWM反馈、电机霍尔脉冲、红外遥控信号优先输入捕获。这些信号是方波或脉冲边沿干净频率可能从几十Hz到几十kHz输入捕获响应快资源省。传感器输出频率信号比如某些流量计、转速传感器输入捕获加多周期平均是最稳的方案。如果传感器信号幅度小、带噪声前端加比较器整形再进定时器。需要同时知道频率和幅值的场景比如阻抗测量、锁相环FFT更合适因为FFT能给出幅值和相位信息。输入捕获只能给时间信息。5. 常见问题与排查技巧实录测频项目的问题往往不是算法本身而是配置细节和边界条件。下面这些是我在实际调试中遇到过的典型问题整理成速查表方便对照排查。5.1 输入捕获测不到或频率跳变测不到信号先查GPIO配置。输入捕获引脚要设成复用功能或者浮空输入不能设成普通输出。然后查时钟使能TIM和GPIO时钟都要开。再看中断优先级如果被其他高优先级中断长时间占用捕获中断可能丢失。信号电平也要确认3.3V逻辑太低太高都不行。频率跳变常见原因是溢出处理错误。如果ARR设置得小信号周期又长溢出次数没算对频率会成倍偏差。另一个原因是信号毛刺边沿检测触发多次。解决办法是加大输入滤波硬件上加RC。还有可能是中断里读取CCR的时机不对HAL库的捕获回调里已经清了标志直接读CCR就行。5.2 FFT频谱泄漏、峰值错位、幅值不准频谱泄漏表现为峰值旁边有多个逐渐降低的谱线。原因是信号频率不是分辨率整数倍截断导致能量扩散。加汉宁窗能显著改善。如果泄漏严重到峰值位置都判断不了检查采样率是否准确。定时器触发频率算错采样率就不是设定值频率换算全错。峰值错位可能是栅栏效应直接读最大bin只能得到近似值。用抛物线插值或者补零增加谱线密度。幅值不准通常是窗增益没修正或者ADC参考电压、输入偏置没算对。如果只看频率幅值可以不管如果要测幅值必须做窗增益修正和ADC满量程换算。5.3 中断与DMA冲突、内存与栈溢出ADC加DMA加FFT的配置里DMA传输完成中断和定时器中断可能同时发生。如果FFT运算放在中断里主循环会被长时间阻塞。正确做法是DMA完成中断只置标志位FFT运算放主循环。如果用了双缓冲半满和全满中断里只切换缓冲区指针不运算。内存和栈溢出在F4上不明显在F103上要小心。1024点浮点FFT需要两个1024浮点数组加上CMSIS-DSP内部状态大概10KB RAM。F103C8T6只有20KB还要留栈和堆容易紧张。可以降到512点或者用Q15定点FFT内存减半。5.4 常见问题速查表现象可能原因解决办法输入捕获读数为0GPIO模式错、时钟没开、信号电平不对检查复用配置、使能时钟、调整电平频率成倍偏差溢出次数未处理、ARR设置不合理开启更新中断累加溢出重算PSC和ARR频率跳动大信号毛刺、中断延迟、滤波不足加大输入滤波硬件RC多周期平均FFT峰值旁边多根谱线频谱泄漏加汉宁窗或汉明窗FFT频率差几Hz栅栏效应抛物线插值或补零FFT幅值偏小窗增益未修正、采样时间短乘窗增益系数加长ADC采样时间程序跑飞栈溢出、DMA覆盖、中断冲突减小FFT点数双缓冲运算移出中断6. 我个人踩过的坑和几条实在建议测频这件事工具和方法都不复杂难的是把细节做扎实。我最早用输入捕获测一个风机转速信号频率只有几十HzARR设了65535还是溢出频率算出来总是实际值的几分之一。后来把PSC加大计数频率降到10kHz溢出问题解决了但分辨率又不够转速跳动厉害。最后用PSC719计数频率100kHzARR65535溢出周期约0.65秒兼顾了低频和高频再用10个周期平均数据才稳定下来。FFT这边最开始的坑是采样率算错。我以为TIM2的更新频率等于72MHz除以ARR忘了PSC。结果采样率差了72倍频谱完全对不上。后来养成习惯所有定时器参数先用公式算一遍再用示波器测一下实际触发频率确认无误再往下做。还有一点不要迷信高点数。1024点FFT在F103上跑一次要几十毫秒如果项目要求实时刷新512点甚至256点更合适。频率分辨率不够可以用插值补但运算时间省不下来。F4有FPU1024点轻松但也要注意DMA缓冲区和栈的分配。最后分享一个小技巧调试FFT时先用信号发生器输出一个已知频率的正弦比如1kHz把ADC原始数据和FFT结果都打印出来。看时域波形是不是完整正弦看频谱峰值是不是在预期bin附近。确认链路没问题后再换复杂信号。这样能快速定位是采样问题、算法问题还是信号问题。