ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32输入捕获与FFT测频原理及实战详解

2026/9/16 9:38:31 拓冰建站 浏览量
STM32输入捕获与FFT测频原理及实战详解 1. 项目整体设计与方案选型拿到“STM32单片机-输入捕获、FFT测频”这个题目我看第一眼的感觉是这不只是做一个测频器而是在逼你同时吃透两种经典测频方案——时域测频和频域测频。很多人做测频要么只会用定时器输入捕获测周期要么只会套FFT库算频谱能把两条路都打通并且在同一个项目里灵活切换的才算真正理解“测频率”这件事。整个项目的核心需求可以拆成三块一是用STM32的定时器输入捕获功能测量外部方波信号的频率二是用ADC采集信号并进行FFT运算从频谱中提取主频分量三是把两种方案的结果做对比和融合搞清楚各自适合什么场景。实际项目里测电机转速、测PWM输出频率、分析音频信号、分析振动信号都是这类需求的典型场景。我选型的芯片是STM32F103系列因为这是绝大多数人入门STM32的第一块片子定时器资源够用带FPU的F4版本做FFT会更舒服但F103配合官方DSP库做256点乃至1024点的实数FFT也完全能跑。开发环境用Keil MDK配合标准外设库或者HAL库都行我个人习惯标准外设库写底层配置逻辑更直观HAL库封装得太厉害有时候反而不容易看清定时器寄存器到底发生了什么。方案选型上有一个关键考量输入捕获本质上测的是“周期”对单一频率的方波信号能做到很高精度但它测不了“信号里有哪些频率成分”FFT则正好相反它能对一段连续信号做频谱分析把基波、谐波、噪声干扰全部摊开来看但频率分辨率受采样率和采样点数限制实时性差一些。所以这两者不是替代关系而是互补关系。做这个项目时我建议把两种测量方式都实现出来再用一个按键或者串口命令切换显示这样效果最直观。还有一点是资源规划。STM32F103C8T6只有20KB RAM做4096点FFT不现实256点FFT比较稳妥输入捕获中断不能开得太频繁否则CPU全耗在中断里了。动手写代码之前最好先画一张资源分配表哪个定时器做输入捕获哪几个通道做PWM输出ADC用哪个定时器触发FFT用多少点缓冲区放哪块内存。这种前期规划越细后面调试越省心。2. 输入捕获测频的原理与实操细节2.1 输入捕获的工作机制输入捕获测频的核心思路是让定时器自由计数当外部信号的上升沿或下降沿到来时硬件自动把当前计数器的值锁存到捕获寄存器里并触发中断。通过连续两次捕获的计数值之差除以定时器的计数频率就能算出信号周期周期取倒数就是频率。听起来很简单但有几个细节必须想清楚。首先是时基配置。STM32定时器的计数频率由时钟源和预分频器决定例如F103的APB1定时器时钟是72MHz设置预分频PSC为71那么计数频率就是72MHz / (711) 1MHz也就是计数器每1微秒加1。信号周期测量值等于两次捕获计数值之差单位是微秒这样求倒数后频率单位直接是Hz和MHz的换算非常直观。其次要理解输入捕获的“捕捉边沿”是可以配置的。TI1和TI2都可以选择上升沿捕获、下降沿捕获或者双边沿捕获。测普通方波频率用上升沿就够了。但如果信号里有比较强的毛刺干扰只测上升沿可能会把毛刺也当成一个周期导致频率跳动很大。这种情况可以改为测量多个周期的总时间再除以周期数也就是“多周期平均法”能显著抑制抖动。时基配置的另一个容易踩坑的地方是自动重装载值ARR。输入捕获模式下ARR决定了计数器的溢出周期也就是“最多能测多长的信号周期”。如果ARR设得太小低频信号的上升沿还没来计数器就溢出了捕获值就会错乱。以72MHz时钟、PSC71为例如果ARR设为0xFFFF65535那么计数值从0加到65535需要约65.5毫秒对应最低可测频率约15Hz。如果你要测几赫兹的低频信号就得把PSC调大或者使用32位定时器。2.2 定时器捕获配置实战我用的是TIM2的通道1对应引脚PA0。这里给出标准外设库的配置代码HAL库的逻辑也是一样的只是API名字不同。TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 开启定时器2和GPIOA时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置PA0为浮空输入如果有外部上拉可用上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 定时器时基72MHz / (711) 1MHz即1us计数一次 TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 4. 配置输入捕获通道 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 上升沿捕获 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; // 不分频 TIM_ICInitStructure.TIM_ICFilter 0x0F; // 输入滤波抗毛刺 TIM_ICInit(TIM2, TIM_ICInitStructure); // 5. 使能捕获中断并配置NVIC TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE);在这个配置里TIM_ICFilter设为0x0F是很有用的一个细节。输入滤波器会对引脚信号进行采样判断只有连续若干次采样到高电平才算真正的上升沿能滤掉窄脉冲毛刺。但是滤波器也会带来一个问题——它引入了信号延迟对于高频信号几十kHz以上会略微影响捕获精度。所以滤波系数不是越大越好要根据实际信号质量来调信号干净就用较小的值信号噪声大再加大。2.3 中断处理与频率计算捕获中断的处理逻辑如下uint32_t capture_value 0; uint32_t capture_period 0; uint32_t frequency_hz 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { uint32_t current_value TIM_GetCapture1(TIM2); static uint32_t last_value 0; if (current_value last_value) capture_period current_value - last_value; else capture_period current_value 0xFFFF - last_value; // 处理溢出回绕 last_value current_value; // 频率 1MHz / 周期(us) 1000000 / capture_period if (capture_period ! 0) frequency_hz 1000000 / capture_period; TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }这里有一个很关键却容易被忽略的问题计数器溢出回绕。假设上一次捕获的计数值是60000这一次的计数值是5000如果直接用5000 - 60000得到的差值是一个负数。但实际上定时器是从60000继续加到65535再溢出回到0加到5000所以总周期应该是5000 65535 - 60000 1。我写代码时用current_value last_value做判断其实更稳妥的做法是让定时器运行在“循环计数”模式下每次都计算差值如果差值为0说明两次捕获之间的时间小于计数分辨率也不是不可能。频率计算还需要考虑一个小数精度问题。整数除法1000000 / capture_period在低频时没问题但在高频时误差很大。比如输入信号是100kHzcapture_period是10计算结果是100kHz精确但输入是101.3kHz时capture_period是9.87取整后是9频率算成111.1kHz误差接近10%。所以高频测频更推荐“测频法”——在固定时间门限内统计上升沿个数也就是让定时器在1秒内累计捕获多少次。测频法在高频段精度高测周法在低频段精度高这是测频领域的基本规律做项目时要根据目标频率范围选择策略。另外捕获中断里除以capture_period的整数除法速度很快但如果用浮点除法会慢不少。在频率不高、不需要频繁刷新的前提下把计算放到主循环里做中断里只记录原始计数值是更合理的结构。2.4 输入捕获实测记录我实测时用一个信号发生器输出1kHz到50kHz的方波幅度3.3V直接接PA0。1kHz时测量结果是999.8Hz误差0.02%以内非常准。10kHz时测量结果是9.998kHz也基本没偏差。但把频率调到100kHz以上时开始出现跳变原因是定时器中断处理IP核的负载限制了最高测量频率加上每捕获一次就进一次中断CPU开销很大。改进办法有两个方向一是用DMA方式搬运捕获值减少中断次数二是直接用两个定时器的门控级联做单位时间计数主定时器负责整秒门控从定时器负责计数外部脉冲。3. FFT测频的完整实现路径3.1 为什么FFT测频和输入捕获是两条不同路线在讲具体实现之前我想先花点篇幅讲明白FFT测频的优势。输入捕获适合测单一频率的方波它获得的信息只有一个周期值但一个真实世界的信号往往是复合信号。比如一个电机振动信号基频是转频但它上面叠加了齿轮啮合频率、轴承故障特征频率、噪声干扰输入捕获面对这种信号毫无办法因为它只能看到过零或者边沿跳动无法分辨这些频率成分。FFT则能把时域信号变换到频域把各个频率分量的能量都摊开在频谱图上想提取哪个频率一目了然。FFT的本质是离散傅里叶变换DFT的快速算法。DFT的计算量是N的平方级别做1024点DFT需要上百万次复数乘法MCU根本跑不动FFT利用旋转因子的周期性和对称性把计算量降到N log2 N级别1024点FFT只需要约一万次复数运算STM32配合官方DSP库可以轻松完成。3.2 采样率、采样点数与频率分辨率的计算FFT测频的第一个坑就是采样参数设计。根据奈奎斯特采样定理采样率必须大于信号最高频率的两倍否则采样后的频谱会出现混叠。实际工程中留出充足余量一般让采样率是最高分析频率的4到8倍。频率分辨率是FFT频谱中两个相邻频点之间的间隔计算公式为频率分辨率 采样率 / 采样点数例如采样率8000Hz采样点数1024点频率分辨率约7.8Hz。也就是说如果你要分辨两个相隔5Hz的频率成分这个参数设计就达不到要求。要提高分辨率要么提高采样点数但RAM占用直接翻倍要么降低采样率但可分析的最高频率随之下降。我实际项目里的做法是用ADC连续采样256点采样率为20kHz频率分辨率约78Hz可以看清0到10kHz频谱的主要成分。如果后续要细化某个频段就降低采样率重采。STM32F103的RAM做256点FFT很轻松做1024点也够用但要小心不能同时开太多其他缓冲。3.3 ADC采样触发与DMA传输配置FFT需要均匀间隔的采样数据不能像普通ADC那样想采就采。这里推荐用定时器触发ADC采样每个更新事件触发一次转换DMA把结果自动搬进数组全程不需要CPU干预保证采样间隔精准均匀。我用的配置思路是TIM3作为触发源输出频率就是采样率ADC1的通道1接信号输入开启扫描模式和DMA循环模式。采样率由TIM3的更新频率决定配置方法如下// TIM3更新频率 72MHz / (预分频1) / (自动重载1) // 目标采样率20kHz预分频72-1自动重载50-1 TIM_TimeBaseStructure.TIM_Period 49; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);PSC71意味着72MHz除以72等于1MHz即定时器计数频率1MHzARR49意味着计数50次溢出一次更新频率就是1MHz / 50 20kHz。DMA配置时要注意DMA的传输方向是外设到内存外设地址固定为ADC的数据寄存器地址内存地址是ADC缓冲数组传输长度等于采样点数开启循环模式。当DMA传输完一整轮后可以产生传输完成中断在中断里置一个标志位主循环检测到标志位再做FFT。3.4 FFT运算与频谱分析代码STM32的官方DSP库提供了arm_rfft_fast_f32函数可以直接做实数序列的FFT比先填成复数再调arm_cfft_f32效率更高。代码大致流程如下#include arm_math.h #define FFT_SIZE 256 float32_t input_buffer[FFT_SIZE]; // ADC原始数据直接由DMA搬入并转float float32_t fft_output[FFT_SIZE]; // FFT输出缓冲区 float32_t magnitude[FFT_SIZE/2]; // 幅值谱 arm_rfft_fast_instance_f32 fft_instance; // 初始化FFT实例 arm_rfft_fast_init_f32(fft_instance, FFT_SIZE); // 对采样数据执行实数FFT arm_rfft_fast_f32(fft_instance, input_buffer, fft_output, 0); // 计算幅值谱复数结果前N/2个点对应频率0到采样率/2 arm_cmplx_mag_f32(fft_output, magnitude, FFT_SIZE/2);注意arm_rfft_fast_f32的输出格式是交错排列的实部和虚部前FFT_SIZE/2个复数点对应正频率分量第0个点对应直流分量。计算幅值谱后遍历magnitude数组找到最大值对应的索引乘以频率分辨率就得到主频率。uint32_t max_index 0; float32_t max_value 0; for (uint32_t i 1; i FFT_SIZE/2; i) // 跳过直流 { if (magnitude[i] max_value) { max_value magnitude[i]; max_index i; } } float32_t freq_resolution 20000.0f / FFT_SIZE; // 约78.125Hz float32_t dominant_freq max_index * freq_resolution;实测下来这种直接找最大幅值的方法对纯正弦信号效果很好误差在分辨率范围内比如输入1kHz正弦测出来约1015Hz左右有一定量化误差。如果想要更精确可以用“频谱插值”方法利用峰值点相邻两个频点的幅值关系估算真实频率能把误差缩小到分辨率以下。3.5 加窗与突变信号的注意事项FFT有一个隐含假设被分析的信号是无限周期延拓的。但实际上我们只能截取一段有限长度这相当于给信号乘了一个矩形窗频谱会发生泄漏能量会从主瓣洩漏到旁瓣。对于纯正弦信号矩形窗的泄漏效应还算能接受如果信号频率不是采样率的整数倍频谱图上会出现明显的“频谱拖尾”现象峰值幅度变低相邻频点出现鼓包。解决这个问题的方法是给采样数据加窗函数比如汉宁窗、海明窗、布莱克曼窗。加窗之后再执行FFT主瓣变宽但旁瓣大幅降低峰值更干净代价是频率分辨率略微下降。我在实测1kHz正弦时发现加汉宁窗后最大峰值所在的频点更稳定不会因为数据起始相位不同而跳变适合做频率估计。加窗代码很简单在FFT之前对input_buffer逐点乘窗系数static float32_t window[FFT_SIZE]; for (uint32_t i 0; i FFT_SIZE; i) { window[i] 0.5f - 0.5f * arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1)); } // 采样完成后执行加窗 for (uint32_t i 0; i FFT_SIZE; i) { input_buffer[i] adc_buffer[i] * window[i]; }还有一个容易被忽略的点就是去直流。如果你测的信号叠加了直流偏置频谱第0个频点会有一个很大的直流分量虽然不影响找主峰我们跳过了i0但直流分量会占据幅值计算时的动态范围可能让小信号的频谱被淹没。建议在加窗之前先减去信号的直流平均值float32_t mean 0; for (uint32_t i 0; i FFT_SIZE; i) { mean adc_buffer[i]; } mean / FFT_SIZE; for (uint32_t i 0; i FFT_SIZE; i) { input_buffer[i] (adc_buffer[i] - mean) * window[i]; }我遇到过一种情况测一个混合信号基波100Hz幅度1V谐波500Hz幅度只有5mV没去直流时500Hz那个峰直接找不到去掉直流后谐波峰才明显出来。所以在做FFT频谱分析前“去直流加窗”是标准预处理流程。4. 输入捕获与FFT到底怎么选怎么配合4.1 两种测频方式的核心差异很多初学者会有个问题既然FFT那么强大为什么还要用输入捕获这个问题的答案其实隐藏在“实时性”和“精度”两个词里。输入捕获测频是“即时”的信号来一个上升沿中断里很快就能得出一个周期值时延只有微秒到毫秒级别适合对实时性要求高的场合比如电机转速环的测速反馈。FFT要凑够一整帧采样数据才能做一次分析以20kHz采样率、256点为例采一帧数据要12.8毫秒加上FFT运算时间整体延迟在几十毫秒量级用来做高速闭环控制不现实。但从频域分析角度看FFT提供了输入捕获完全无法提供的信息——频率成分组成、各分量幅值、谐波失真度、信噪比。这些信息在音频分析、振动诊断、电力质量分析里都是刚需。所以两者定位很清楚输入捕获适合“知道大概频率并需要快速响应”的场景FFT适合“需要仔细了解信号成分”的场景。对比项输入捕获测频FFT测频实时性高逐周期运算低需整帧数据频率精度低频高精度测周法高频受限受频率分辨率限制可插值提升频谱信息无有完整幅频特性对信号波形要求必须是边沿清晰的方波或脉冲任意波形均可资源开销极低需要RAM做FFT缓冲和运算典型场景电机转速、PWM频率、计数音频分析、振动诊断、谐波分析4.2 组合方案的工程价值做完单一功能后我强烈建议做一个“自动切换”的组合模式系统上电先用FFT对整个频段做一次粗扫得到主频的大致位置然后自动配置输入捕获的定时器量程对主频信号进行高精度定测。这就像先用望远镜扫视一片区域找到目标再换显微镜精细观察两个工具配合效率和精度都拉满。这个组合方案在真实项目里非常常见。比如我做过一个电机测试设备启动时不知道电机的转速范围用FFT粗测转频再用输入捕获锁定转速做精确测速同时FFT持续监测频谱如果发现振动频谱里出现了异常的频率成分就说明轴承或齿轮出现了故障隐患。这种“FFT宏观监视 输入捕获微观锁定”的思路比单一方案实用得多。组合模式还有一个好处是相互校验。输入捕获的结果可以用FFT结果交叉验证两个读数偏差在允许范围内才认为系统工作正常。这在工业设备里是很重要的一层安全逻辑。比如PLC读到的转速和FFT分析出来的转频误差超过2%大概率是传感器信号有问题或者信号处理链路有故障需要报警提醒。4.3 资源分配与性能优化建议STM32F103做FFT时性能确实紧张我用标准库实测256点实数FFT大约需要1.2毫秒1024点大约需要8毫秒左右。如果采样率20kHz、采集256点需要12.8毫秒那FFT运算时间已经接近采样帧间隔的10%整个系统还能正常工作但如果你想把采样率和采样点数同时提高CPU负载会直线上升。优化方向有几个我看网上很多人写文章都提过但实操时容易忽略第一尽量用单精度浮点DSP库而不是自己写FFT官方库针对Cortex-M3/M4做了指令级优化比自己写的循环快好几倍第二如果芯片是M4内核带FPU直接使用arm_rfft_fast_f32浮点版本但如果芯片是M3内核没有FPU浮点运算会很慢建议改用arm_rfft_q15或者arm_rfft_q31定点版本速度能快很多只是精度需要权衡第三如果只是测主频而不需要完整频谱可以只用前几个频点的FFT结果不必把整个幅值谱都算出来能省下不少时间。RAM占用也要算清楚。256点FFT需要ADC缓冲256字节如果用u16存储或512字节如果用u32FFT蝶形运算需要额外的浮点数组我习惯直接定义float32_t input_buffer[256]1KBfloat32_t fft_output[256]1KBfloat32_t magnitude[128]512B加起来约2.5KBF103C8T6的20KB RAM完全顶得住。如果换到F407或者F411资源就更宽裕了可以做4096点甚至更大规模的FFT。5. 常见问题与排查技巧实录5.1 输入捕获不工作时的排查方向捕获中断进不去首先看GPIO复用功能配置对不对标准外设库要调用GPIO_PinRemapConfig做引脚重映射如果用TIM2_CH1的默认PA0则不需要其次看定时器时钟是否使能APB1总线上的TIM2时钟默认是36MHz而不是72MHz如果PSC按72MHz计算会差一倍最后看NVIC优先级分组配置如果HAL库默认优先级分组和中断优先级设置不匹配中断可能不会被响应。捕获值一直是0检查TIM_ICInitStructure.TIM_ICPolarity配置如果设成下降沿捕获而输入信号默认是高电平那确实要等一个下降沿才会触发。把示波器或者逻辑分析仪接上确认信号实际上有边沿变化而不是引脚根本没接对。频率值跳变严重这个我踩过很多次。先看输入信号是不是干净的方波如果信号是正弦波直接接进PA0在过零点附近会有抖动捕获到的边沿时间不稳定频率读数自然跳。解决办法是加一个比较器或施密特触发器整形或者利用STM32内部施密特触发器输入模式还可以在代码里多采几次求平均。最稳妥的做法是采连续8个周期去掉最大最小后平均这样即使有个别毛刺也不至于把结果带偏。高频信号测不准前面说过测周法在高频段的精度会下降而且中断频率太高会拖垮CPU。如果目标频率超过100kHz建议改用DMA捕获模式。TIM的捕获事件可以触发DMA请求把每次的捕获值自动搬运到内存数组CPU只在DMA传完一整批数据后处理一次能大幅降低中断负载。5.2 FFT结果异常时怎么查频谱全是乱码先确认采样率是否真如预期。用示波器测量DAC输出的正弦波接到ADC输入如果FFT结果里最大峰值对应的频率和你设置的信号频率对不上大概率是定时器触发采样率配置错了。我遇到过TIM_Prescaler和TIM_Period写反导致采样率不是预期值整个频谱横轴全偏差的情况排查时直接用已知频率的信号源做校准最有效。峰值频率偏了半个分辨率这是FFT固有的栅栏效应你可以先做一个简单的验证——信号源设成采样率除以FFT点数的整数倍频率比如20kHz/256≈78.125Hz设成78.125Hz、156.25Hz测出来正好在正确的频点上设成100Hz就会落在邻近频点上幅值被摊开。解决办法是采用频谱插值算法如三点内插法利用峰值点及其左右两个频点的幅值做抛物线拟合精度可以提升到0.1个频点以内。FFT结果幅值明显偏小最常见的原因是采样不满一个整周期导致频谱能量泄漏。加窗之后幅值会进一步偏小因为窗函数本身会损耗一部分能量。工程上通常对幅值做修正比如汉宁窗的幅值恢复系数约等于1.6也就是计算出来的幅值乘以1.6才是真实幅值。如果只是测频不测幅这个修正可以不做主峰对应的频点位置不受加窗影响。程序跑死了或者堆栈溢出FFT运算需要比较大的局部变量如果你在中断服务函数里直接调用FFT函数很容易爆栈。建议把FFT运算放到主循环里执行中断里只置标志位。另外有些DSP库函数内部会申请临时缓冲区如果用标准库的malloc动态分配在STM32上要格外小心堆大小配置我一般直接用静态数组不动态分配。5.3 实测避坑清单把我在调试过程中踩过的坑整理成一张清单供后来人参考输入捕获的“数字滤波器”系数不是越大越好高频信号下会恶化精度。ADC采样触发的定时器更新中断可以不开但只要开了中断服务函数里千万别做耗时操作。FFT的input_buffer和adc_buffer不能共用同一块内存否则DMA还在不停搬运数据时会破坏FFT正在读取的数据。STM32F103的ADC输入阻抗有限信号源输出阻抗太高时采样值会不准确需要加运放跟随器或者把采样时间调长。Keil MDK编译时默认优化级别可能影响FFT精度我建议把FFT相关文件设为-O2其他文件用-O0便于调试。如果串口打印频率值频率计算和打印最好分离别在中断里直接调用耗时很长的printf函数。这些经验看着琐碎但在现场调试时每一条都能帮你省下至少半小时查问题的时间。我在第一次做完整项目时光是因为ADC缓冲和FFT缓冲共用内存导致频谱偶尔诡异就折腾了整整一个下午后来才意识到是DMA覆盖了正在计算的数据。6. 从测频项目延伸出去的思考做完这个项目回头再看STM32输入捕获和FFT测频并不是两个孤立的知识点它们共同构成了嵌入式信号处理的两个基本视角时域和频域。输入捕获代表的是时域测量手段关注信号边沿、周期、脉宽FFT代表的是频域分析手段关注频率构成、幅度谱、相位谱。一个完整的嵌入式测控系统通常两种手段都需要根据应用场景灵活选择。这个项目如果再往下扩展可以加串口屏实时显示波形和频谱、加SD卡存储数据用于离线分析、加按键切换测量模式、加RS485通信把数据上报到上位机。也可以往算法方向深入做自适应采样率控制、多音检测、语谱图分析等。对我来说这个项目最大的收获是建立了一套“根据需求选测量方案”的思维方式而不是只知道调用库函数。最后分享一个小技巧调试输入捕获时用PWM输出一个已知频率的信号接到PA0上对着改代码测试比用手持信号发生器方便太多——因为你可以随时改PWM频率代码重新编译下载不用手动去扭信号发生器的旋钮。把PWM频率设成RTT打印的校准值就能快速验证捕获逻辑正确与否。这个小技巧帮我少走了不少弯路。