news 2026/9/18 2:55:42

STM32输入捕获与FFT测频:从定时器到频谱分析实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32输入捕获与FFT测频:从定时器到频谱分析实战

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和更大RAM,1024点甚至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共同决定计数频率和溢出周期。计数频率 = 定时器时钟 / (PSC+1)。溢出周期 = (ARR+1) / 计数频率。如果信号周期大于溢出周期,计数器会溢出,必须额外记录溢出次数,否则算出来的频率是错的。

2.1 输入捕获的硬件原理与定时器配置思路

假设STM32F103,TIM3挂在APB1总线上,时钟72MHz。配置PSC=71,计数频率就是1MHz,每个计数代表1微秒。ARR设为65535,溢出周期约65.5ms。这意味着信号周期超过65.5ms,也就是频率低于15.26Hz,就会溢出。要测更低的频率,要么加大PSC降低计数频率,要么在溢出中断里累加溢出次数。

对于高频信号,比如100kHz,周期10微秒,计数值只有10。如果计数频率1MHz,相对误差可能达到10%。这时候要提高计数频率,比如PSC=0,计数频率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库,STM32F103,TIM3 CH1对应PA6。计数频率1MHz,ARR=65535,开启更新中断和捕获中断。代码里用了一个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的跳动。误差来源主要有几个:晶振本身精度、中断响应延迟、计数量化误差、信号边沿抖动。普通无源晶振精度大概几十ppm,1kHz下误差不到0.1Hz;中断延迟取决于优先级和当前执行指令,通常几个微秒,对应1MHz计数就是几个计数,1kHz周期1000个计数,误差约0.5%。

想再准一点,可以连续测10个周期取平均。把第1次捕获存下来,第11次捕获再算总差值,除以10。这样随机误差被平均掉,标定曲线会平滑很多。另外,输入滤波参数可以适当加大,比如ICFilter设为0x0F,能滤掉高频毛刺,但会引入一点点边沿延迟,对低频测量影响可以忽略。

2.5 输入捕获的注意事项

输入信号一定要干净。如果信号来自长导线、电机、继电器,很可能带毛刺。硬件上加RC滤波和施密特触发器,软件上加输入滤波,双管齐下。电压不能超过3.3V,5V信号必须分压或电平转换。中断优先级要合理,捕获中断优先级可以高一点,但不要在中断里做耗时操作。如果频率很低,溢出次数会很大,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,采样率10kHz,1024点,频率分辨率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_TRGO,DMA循环搬运到缓冲区。TIM2的更新频率就是采样率。比如72MHz时钟,PSC=0,ARR=3599,更新频率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的更新事件触发ADC,ADC每次转换完由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的rfft

CMSIS-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做实数FFT,arm_cmplx_mag_f32求模。峰值插值用抛物线公式,delta是峰值相对bin的偏移量。最后频率等于(bin加偏移)乘以采样率除以点数。实测1kHz正弦,采样率20kHz,1024点,分辨率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,那么采样率设10kHz,1024点,分辨率9.77Hz,目标频率对应bin 102左右,峰值清晰。如果输入捕获测到大约10kHz,采样率要设50kHz以上,点数可能要增加到2048,否则分辨率太差。

还有一种用法是用输入捕获测周期,用FFT确认主频。比如信号里混了谐波,输入捕获测的是边沿间隔,可能测到谐波频率;FFT能看到基波幅值最大,用FFT结果校正输入捕获。两者互相验证,可靠性更高。

实现上可以用一个状态机:上电后先启动输入捕获,连续测10个周期,取平均,得到粗频率;然后根据粗频率计算采样率,启动ADC加DMA加FFT,得到精频率;最后把两个结果都打印出来,人工确认或者自动融合。整个过程在100ms内可以完成,对大多数项目足够。

4.3 不同场景的推荐路线

音频信号、振动信号、含噪声的正弦波,直接上FFT。这类信号边沿不清晰,输入捕获容易误触发。音频范围20Hz到20kHz,STM32F4跑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. 我个人踩过的坑和几条实在建议

测频这件事,工具和方法都不复杂,难的是把细节做扎实。我最早用输入捕获测一个风机转速信号,频率只有几十Hz,ARR设了65535还是溢出,频率算出来总是实际值的几分之一。后来把PSC加大,计数频率降到10kHz,溢出问题解决了,但分辨率又不够,转速跳动厉害。最后用PSC=719,计数频率100kHz,ARR=65535,溢出周期约0.65秒,兼顾了低频和高频,再用10个周期平均,数据才稳定下来。

FFT这边,最开始的坑是采样率算错。我以为TIM2的更新频率等于72MHz除以ARR,忘了PSC。结果采样率差了72倍,频谱完全对不上。后来养成习惯,所有定时器参数先用公式算一遍,再用示波器测一下实际触发频率,确认无误再往下做。

还有一点,不要迷信高点数。1024点FFT在F103上跑一次要几十毫秒,如果项目要求实时刷新,512点甚至256点更合适。频率分辨率不够可以用插值补,但运算时间省不下来。F4有FPU,1024点轻松,但也要注意DMA缓冲区和栈的分配。

最后分享一个小技巧:调试FFT时,先用信号发生器输出一个已知频率的正弦,比如1kHz,把ADC原始数据和FFT结果都打印出来。看时域波形是不是完整正弦,看频谱峰值是不是在预期bin附近。确认链路没问题后,再换复杂信号。这样能快速定位是采样问题、算法问题还是信号问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 2:53:40

看 Agentic RL 烧 Token:mimo-v2.6-pro 接 TaoToken 端点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 2:51:59

薛定谔的追问:生命如何靠负熵对抗熵增和热寂?

你有多久没有认真想过“生命是什么”这个问题了&#xff1f;不是查百度词条&#xff0c;不是背课本定义&#xff0c;而是在某个深夜突然被这个问题击中。我最早接触这个命题&#xff0c;是因为一本科普小册子&#xff0c;薛定谔写的《生命是什么》。一个物理学家跨界跑来回答生…

作者头像 李华
网站建设 2026/9/18 2:51:54

Spring Boot+Vue容器化部署:Docker镜像构建与Compose一键编排

前后端项目部署这件事&#xff0c;说起来就是一套流程&#xff0c;但真自己动手时能把人折腾到怀疑人生。本地启动Spring Boot和后端联调没问题&#xff0c;前端Vue跑在开发服务器里也顺畅&#xff0c;可一旦要把这两货放到一台服务器上&#xff0c;Port占用、路径混乱、环境不…

作者头像 李华
网站建设 2026/9/18 2:51:50

激光雷达点云数据处理全流程:Terrasolid从分类到成果交付

简介&#xff1a;一份面向遥感测绘、激光雷达点云处理学习者的专业参考文献&#xff0c;以PDF论文形式讲解基于Terrasolid软件的点云数据处理全流程。内容系统阐述机载LiDAR对地定位原理&#xff0c;重点分析点云滤波与分类方法&#xff0c;包括移动窗口法、数学形态滤波、基于…

作者头像 李华
网站建设 2026/9/18 2:51:29

Spring Boot购物推荐系统实战:协同过滤算法与电商业务整合全解析

做购物推荐网站这个题目&#xff0c;在Spring Boot毕设里属于常青树了——既有标准化的CRUD、权限、购物车、订单这套“保底”功能&#xff0c;又能通过推荐引擎把项目的技术含量拉起来&#xff0c;不管是课程设计还是毕业论文答辩&#xff0c;讲出来都比纯增删改查有看点。我之…

作者头像 李华
网站建设 2026/9/18 2:49:30

oh-my-zsh rand-quote 插件:一条命令随机获取英文名人名言

oh-my-zsh rand-quote 插件&#xff1a;一条命令随机获取英文名人名言 【免费下载链接】ohmyzsh &#x1f643; A delightful community-driven (with 2,500 contributors) framework for managing your zsh configuration. Includes 300 optional plugins (rails, git, macOS,…

作者头像 李华