尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32测频方案详解:输入捕获与FFT频谱分析的原理、实现与选型

STM32测频方案详解:输入捕获与FFT频谱分析的原理、实现与选型 1. 项目概述输入捕获与FFT测频到底在解决什么问题接触STM32的朋友应该都有过这种经历想测一个方波或者正弦波的频率第一反应是拿定时器做输入捕获但一旦信号频率稍微高一点、占空比又不规则捕获中断就开始疯狂触发CPU忙得喘不过气换成FFT做频谱分析吧又不知道点数怎么选、窗函数要不要加、结果怎么换算成频率。这个项目标题“STM32单片机-输入捕获、FFT测频”把两大主流测频方案放在一起正好是工程里最常用的两种互补手段。先说说这个内容能解决什么问题。输入捕获适合测低频到中频的周期信号精度高、实时性强一个定时器就能搞定而FFT测频则适合处理高频信号、非周期信号或者你压根不知道信号长什么样想看看频谱里有哪些频率分量。把两者结合起来既能应对常规方波测速、测脉宽又能处理音频、振动、电力谐波这类复杂信号。适合谁看刚做完定时器实验想进阶的初学者准备做毕业设计需要测速测频的同学以及在项目里被干扰信号折磨的嵌入式工程师。不管你是哪种这篇文章都会把原理、实现、踩坑一次讲透。我先把话说在前面输入捕获看着简单其实坑很多——滤波、溢出、DMA搬运时机、中断优先级每个细节都能让你调一天FFT看着高深其实STM32的DSP库已经把核心计算全封装好了关键反而在于采样率怎么定、缓冲长度怎么选、加不加窗。下面我从原理到代码再到排查思路一步步拆开讲。2. 输入捕获测频原理、配置与实战2.1 输入捕获的核心原理边沿、计数器与时间戳输入捕获本质上就是利用定时器的硬件能力在信号边沿到达时自动把当前计数器的值“拍一张快照”存到捕获寄存器里同时触发中断或DMA。这样我们不需要软件去轮询引脚电平硬件会替我们记录下每个边沿发生的精确时间点。举一个日常生活的类比你在操场上跑步每隔一圈经过一个打卡点会有人在表上记录时刻。你只需要看两次打卡时刻的差值就能算出一圈的耗时。输入捕获就是这个打卡记录员计数器就是操场上的时钟信号的一个上升沿就是一次经过打卡点。两次捕获值之差就是信号的周期乘以定时器时钟周期再取倒数就是频率。这里有个关键点捕获的是“计数器的值”而计数器到底是怎么走的取决于你的定时器配置。如果计数器是向上计数那捕获值就是递增的如果发生了溢出捕获值可能比上一次小这时候必须处理溢出补偿否则算出来的周期就是错的。很多新手第一次测频发现数值乱跳十有八九是没处理计数器溢出。脉冲宽度测量也是同样的思路只不过需要把捕获通道配置成同时捕获上升沿和下降沿。比如测PWM占空比可以用上升沿捕获作为周期起点下降沿捕获作为高电平持续时间两次捕获相减得到高电平时间再除以周期就得到占空比。2.2 定时器通道选择与时钟树配置从CubeMX开始的完整步骤我建议直接用STM32CubeMX生成初始化代码手写寄存器虽然能加深理解但工程效率太低。以常见的STM32F103为例核心配置就这几步。第一步选择定时器。输入捕获功能几乎每个定时器都有但要注意不同定时器的时钟源不同。比如TIM1和TIM8挂在APB2总线上时钟频率可能和APB1上的TIM2~TIM7不一样。在CubeMX的Clock Configuration页面先确认定时器时钟频率比如APB1定时器时钟如果是72MHz那TIM2的计数频率最快就是72MHz。一般测频场景不需要这么高的计数频率反而要适当分频让计数器对信号边沿有更好的分辨率。第二步配置定时器的时钟源为内部时钟选择捕获通道。比如用TIM2的Channel 1作为输入捕获引脚在Pinout视图里把PA0设置为TIM2_CH1模式CubeMX会自动完成GPIO复用配置。第三步设置预分频器PSC和自动重装值ARR。这里需要根据你预期的信号频率范围来算。比如要测1kHz到100kHz的信号定时器时钟72MHz预分频设成72那计数频率就是1MHz计数器每个计数代表1微秒。对1kHz信号来说一个周期是1000微秒计数器的值会累计到1000左右完全够用对100kHz信号来说一个周期是10微秒计数器值只有10分辨率会显得不足但至少能测出来。如果你的信号频率更高建议加大预分频或者直接转投FFT方案。第四步使能捕获中断或者用DMA方式把每次捕获值搬进内存数组。中断方式适合低频信号每秒几百次中断可以接受如果是高频信号比如几十kHz建议用DMA循环模式每次捕获自动写入内存CPU完全不参与只在需要时批量读取数据。第五步设置捕获边沿。要测频率的话只捕获上升沿即可要测脉宽需要设置同时捕获上升沿和下降沿或者用两条通道分别捕获不同的边沿。CubeMX里支持在捕获/比较通道配置中选择上升沿捕获、下降沿捕获或双边沿捕获。2.3 频率计算的完整公式与溢出补偿配置好之后频率计算就是纯粹的数学问题。设定时器计数频率为 f_timer 定时器时钟 / (PSC 1)两次连续上升沿捕获值分别为 val1 和 val2那么信号的周期 T_signal (val2 - val1) / f_timer频率 f_signal 1 / T_signal。这里有个必须处理的情况如果定时器是16位的比如STM32F103的TIM2虽然可以级联成32位但默认是16位ARR最大只能写到65535。如果信号频率太低周期超过了计数器最大值就会发生溢出。此时捕获值 val2 可能小于 val1直接相减会出现负数。解决办法是在捕获中断里判断计数器的溢出标志Update Event记录溢出次数然后周期 T_signal (溢出次数 × (ARR1) val2 - val1) / f_timer。也可以把ARR设置得足够大或者启用定时器级联形成32位计数器这个在F4和H7系列上更灵活。我还推荐一种更健壮的做法用一个环形缓冲数组存储每次捕获的边沿时间戳然后在空闲时一次处理这批数据。这样即使在捕获过程中CPU被其他任务占用了也不容易丢失边沿事件。前提是缓冲区要开得足够大避免高频信号把数组写爆。2.4 输入捕获实测方波频率测量完整示例下面给一个可以直接跑通的示例代码框架基于STM32F103 HAL库使用TIM2通道1。这里假设CubeMX已经配置好了定时器和中断只需要写逻辑部分。// capture.h #define CAPTURE_BUF_SIZE 16 extern volatile uint32_t capture_buffer[CAPTURE_BUF_SIZE]; extern volatile uint8_t capture_index; extern volatile uint8_t capture_ready; void capture_process_frequency(void);// capture.c #include capture.h #include tim.h volatile uint32_t capture_buffer[CAPTURE_BUF_SIZE]; volatile uint8_t capture_index 0; volatile uint8_t capture_ready 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { capture_buffer[capture_index] HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); if (capture_index CAPTURE_BUF_SIZE) { capture_index 0; capture_ready 1; } } } void capture_process_frequency(void) { if (!capture_ready) return; // 计算相邻两个上升沿之间的差值 uint32_t diff capture_buffer[1] - capture_buffer[0]; // 如果出现了计数器溢出需要加上ARR1这里简化处理实际要查溢出标志 // 假设定时器计数频率为1MHz即每个计数代表1us float period_us (float)diff; float frequency_hz 1000000.0f / period_us; printf(Frequency: %.2f Hz\n, frequency_hz); capture_ready 0; }这段代码很简单但已经能跑通最基本的测频流程。注意我用了数组缓存多组数据而不是每次中断都算一次频率这样可以平均多次测量结果减小抖动。实际工程里我会对连续多个周期取平均值比如把缓冲区里的所有相邻差值求平均然后再算频率这样读数会稳定很多。3. FFT测频的原理与STM32 DSP库使用3.1 FFT在测频中的价值时域看不懂频域一目了然输入捕获虽然精度高但有三个硬限制第一它要求信号是周期性且边沿清晰的最好是方波第二当信号混入噪声或者有多个频率分量时输入捕获只能测出“主边沿”的间隔无法告诉你频谱里有什么第三当信号频率超过定时器计数能力时捕获就失效了。FFT快速傅里叶变换测频的思路完全不同。它把一段时域信号变换到频域然后找出频谱中幅值最大的频率分量这个频点就是信号的主频。它的优势是对信号波形没有特殊要求正弦波、三角波、叠加了噪声的信号都能处理能同时分析多个频率分量处理高频信号只取决于ADC采样率不依赖定时器计数频率。代价是需要做ADC采样和数学运算对实时性和内存有要求。STM32做FFT传统上是在Cortex-M3/M4/M7系列上利用ARM官方CMSIS-DSP库里的浮点FFT函数或者基于Q15/Q31的定点FFT函数。F4系列因为带FPU浮点FFT速度非常快F1系列没有FPU浮点运算慢但用定点版本也能应付。H7更强几百点的FFT几微秒就算完了。3.2 采样基础采样率、采样点数与频率分辨率的关系FFT测频能不能测准核心不在于FFT算法本身而在于ADC采样参数的设置。有三个关键参数采样率 fs、采样点数 N、频率分辨率 Δf。频率分辨率 Δf fs / N。意思是你做一次N点FFT频谱里相邻两根谱线间隔是Δf。想要分辨间隔相近的两个频率成分Δf必须足够小也就是N要够大或者fs要够小。但根据奈奎斯特采样定理fs至少要大于信号最高频率的两倍实际工程中一般取最高频率的5到10倍否则会混叠。举一个具体例子假设你要测一个范围在500Hz到5kHz的信号最高频率5kHz那最低采样率至少10kHz保险起见取50kHz采样率。如果用1024点FFT频率分辨率是50000/1024 ≈ 48.8Hz这个精度对测速够用但对精确测量一个4888Hz的信号就有接近50Hz的误差。想提高分辨率要么把采样率降下来但会限制最高频率要么提高FFT点数到2048或4096代价是内存和计算时间翻倍。STM32的ADC采样方式有多种定时器触发ADC、DMA搬运、双重ADC交替采样等。定时器触发是比较推荐的方案因为可以用定时器的更新事件或者比较事件来精确控制采样间隔保证采样的等间隔性。如果直接用ADC连续采样再软件延时采样间隔的抖动会影响FFT结果的准确性尤其是在高频段。3.3 加窗函数的原因与选择泄漏问题必须要懂如果直接对一段截断的时域信号做FFT结果会出现“频谱泄漏”——原本应该集中在一个频点上的能量扩散到了相邻的很多频点上导致主峰旁边出现一堆假的频率分量。原因是截断相当于把无限长的信号乘了一个矩形窗矩形窗在频域有旁瓣所以频谱就糊了。解决办法就是对采样数据先乘以一个窗函数抑制时域截断带来的频谱旁瓣。常用的窗函数有汉宁窗Hanning、汉明窗Hamming、布莱克曼窗Blackman。汉宁窗是测量领域的默认选择主瓣稍宽但旁瓣衰减大适合大多数周期信号测频布莱克曼窗旁瓣衰减更大但主瓣更宽适合需要强分离度的场景矩形窗不加窗只在信号严格周期截断时才有意义工程上一般不推荐。引入窗函数之后有一个代价信号的主瓣变宽了频率分辨率会略变差而且幅值会有衰减。所以在计算幅值时要注意是否需要做幅值恢复。对于测频来说我们只关心峰值频点位置幅值是不是精确关系不大。如果你还要测幅值那就要用加窗恢复系数来校正。3.4 STM32 FFT测频完整示例从ADC采样到频率输出这里我用STM32F407为例因为自带FPU浮点FFT速度很快。使用ADC1采样一个模拟信号采样率用定时器触发控制为40kHz采样点数为1024使用CMSIS-DSP库的arm_cfft_f32函数。CubeMX里需要配置ADC1启动DMA循环模式数据宽度为half word16位定时器TIM3产生PWM输出频率设为40kHz占空比50%用来触发ADC采样DMA接收缓冲区大小为1024使能DMA传输完成中断。核心代码逻辑如下。#include arm_math.h #define FFT_SIZE 1024 #define SAMPLING_RATE 40000 static volatile uint16_t adc_buffer[FFT_SIZE]; static float32_t fft_input[FFT_SIZE * 2]; // 实部虚部交替存放 static float32_t fft_output[FFT_SIZE]; // DMA传输完成回调说明已经采满1024个点 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { // 在这里处理FFT注意不要在中断里做太长时间运算 // 简单做法是设置标志位在主循环里执行 } void fft_measure_frequency(void) { // 将ADC原始值转换为浮点并填入实部虚部清零 for (int i 0; i FFT_SIZE; i) { fft_input[2 * i] (float32_t)adc_buffer[i]; fft_input[2 * i 1] 0.0f; } // 可选加汉宁窗 for (int i 0; i FFT_SIZE; i) { fft_input[2 * i] * 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); } // 执行FFT arm_cfft_f32(arm_cfft_sR_f32_len1024, fft_input, 0, 0); // 计算幅值 arm_cmplx_mag_f32(fft_input, fft_output, FFT_SIZE); // 寻找峰值频率忽略DC分量 uint32_t peak_index 1; float32_t max_mag 0.0f; for (int i 1; i FFT_SIZE / 2; i) { if (fft_output[i] max_mag) { max_mag fft_output[i]; peak_index i; } } // 换算频率 float frequency (float)peak_index * SAMPLING_RATE / FFT_SIZE; printf(FFT Freq: %.2f Hz\n, frequency); }这里有几个细节值得注意。第一个细节是复数FFT的输入格式。CMSIS-DSP库要求输入数组是实部和虚部交替排列长度为2×N所以数组类型是float32_t长度是FFT_SIZE的2倍。做FFT之前要把虚部全部置零。第二个细节是幅值计算的函数arm_cmplx_mag_f32它输出的是每个频点的幅值不是功率。虽然测频不需要精确幅值但找峰值时用幅值排序就够了。第三个细节是峰值搜索时从索引1开始因为索引0是直流分量通常信号里会有直流偏置不参与频率峰值判断。第四个细节是加窗函数。我在上面代码里加了汉宁窗这是必要的。如果你不加窗当信号频率不是FFT分辨率整数倍时峰值周围会有很大的旁瓣干扰找出来的峰值频率会明显偏离真实值。4. 输入捕获和FFT的对比与选型什么时候用哪个4.1 各项性能指标对比很多读者肯定纠结既然FFT这么强大为什么还要学输入捕获因为两者各有所长。我整理了一个对比表方便你根据场景快速决策。对比项输入捕获测频FFT测频原理测量相邻边沿时间间隔时域信号变换到频域找峰值适用信号方波、PWM、脉冲信号正弦波、复杂波形、带噪信号实时性高中断/DMA即时响应中需要采集一段数据后批量处理测量速度快一个周期就能出结果慢至少一个采样周期FFT计算时间多频率分量不支持支持可以同时识别多个峰抗噪能力弱噪声边沿容易误触发中窗函数能抑制部分旁瓣硬件资源定时器中断ADCDMA大量RAM浮点运算内存占用极低高1024点浮点FFT需要10KB以上精度高取决于计数器分辨率受采样率和点数限制最高频率取决于计数器频率和中断处理能力取决于ADC采样率适合场景电机测速、转速计、脉宽测量音频分析、振动检测、谐波识别简单来说如果信号是干净的方波或者你只关心一个主频率输入捕获是首选代码简单、响应快、CPU开销低。但如果信号带了噪声或者你怀疑信号里有多频率成分那就必须上FFT。4.2 混合方案输入捕获粗测 FFT精测在实际项目中我经常采用混合方案。比如做一个可测1Hz到100kHz频率范围的仪器1Hz到10kHz用输入捕获精度高、响应快10kHz以上用FFT因为此时信号可能畸变且定时器捕获在高频下容易抖动。还有一种场景是信号频率未知且范围跨度大先用FFT做一次全频谱扫描大约知道主频在哪然后把该频点附近的频率用输入捕获做精确测量。这种方法既能覆盖宽范围又能发挥两种方案的各自优势。不过要注意时序配合切换方案时让系统停止采样重新配置外设避免过渡状态产生错误数据。4.3 一个典型的电机测速应用分析拿电机测速来说码盘输出的通常是方波信号频率和转速成正比。如果电机转速在几百转到几千转码盘每转输出100个脉冲那输出信号频率大约在几百Hz到几十kHz。用输入捕获测一个脉冲周期再乘以码盘线数就能得到转速平滑一下读数效果非常好。这也是我做过最多的应用类型。但如果是直流无刷电机霍尔信号或者反电动势信号往往不是干净的方波里面包含换相噪声。这时候单纯用输入捕获测频率测出来的值会忽高忽低。我的处理办法是先用一个比较器或者施密特触发器整形把信号变成干净方波再送入定时器捕获引脚。如果信号本身仍有脉宽调制成分那就用FFT去看频谱找到基频成分因为基频对应电机转速。5. 常见问题与排查技巧实录5.1 捕获频率数值不稳定、乱跳这是最常遇到的问题。我先列出几个典型原因然后对应给出排查方向。第一信号本身带噪声边沿抖动导致捕获值忽大忽小。这时可以在信号输入端并联一个小电容比如10nF到100nF做低通滤波也可以用软件滤波比如连续测量多次取中值。第二定时器预分频和自动重装值配置不当。如果计数频率太低捕获的分辨率太低相邻两次捕获值之差受量化误差影响很大如果计数频率太高计数器容易溢出溢出处理没做对。建议根据信号频率选择一个合适的计数频率使一个周期的计数值在100到10000之间。第三中断处理不及时。在中断里做了太多事情比如打印、浮点运算导致捕获事件丢失。解决方法是中断里只读取捕获值并存入数组打印和计算放到主循环。第四没有正确清零捕获标志。有些芯片在读取捕获值和清除中断标志之间有顺序要求如果不正确会产生重复中断。HAL库一般不用手动清但寄存器操作要注意。5.2 FFT峰值频率偏移或不准FFT测出的频率比实际值偏了十几个赫兹甚至更多我见过的大部分原因是采样率设置不准确。定时器触发的ADC采样频率取决于定时器的时钟频率和分频值如果定时器配置错了实际采样率和理论值不符FFT结果自然就不对。排查方法是用一个已知频率的信号源输入比如用定时器PWM输出一个1kHz的方波送到ADC然后看FFT峰值是不是恰好落在1kHz附近的谱线上。第二个常见原因是信号频率不是频率分辨率的整数倍导致峰值落在了两根谱线之间你取的是离散的峰值索引所以误差最多可达Δf/2。改善方法有增加FFT点数提高分辨率使用频率校正算法比如抛物线插值或者Goertzel算法精确算单频或者把采样率调成信号频率的整数倍正弦波测量时常用这招。第三个原因是加窗函数带来的频谱主瓣展宽让峰值位置偏了一点。这在谱线分辨率较粗时明显。解决办法是使用更合适的窗函数比如汉宁窗的峰值偏差通常比矩形窗小。5.3 浮点FFT在F1上跑得太慢怎么办STM32F103没有FPU做1024点浮点FFT需要几十毫秒这在很多实时场景不可接受。如果必须用F1建议改用定点FFT。CMSIS-DSP库提供了arm_cfft_q15和arm_cfft_q31它们是定点运算速度比浮点快很多但需要处理Q15或Q31格式的缩放问题。另外如果采样点数是128或256FFT运算时间会大幅下降。用256点FFT和40kHz采样率频率分辨率是156Hz对很多粗测应用完全够用。我做过一个转速计用128点FFT 峰值搜索整段处理时间不到1ms在F103上也能跑得飞快。如果连定点FFT也效率不够那就把FFT点数降到64只关注信号主频的大致范围然后用输入捕获在时域精确测频。这又回到我们前面说的混合方案了组合拳永远比单一方案好使。5.4 DMA缓冲区溢出导致数据错乱在ADC DMA采样过程中如果CPU来不及处理DMA传输完成中断DMA缓冲区会被持续覆盖导致数据错乱。解决方法是使用DMA双缓冲模式比如在STM32F4上可以配置两个缓冲区DMA在第一个缓冲区填满时自动切换至第二个缓冲区同时触发中断让CPU处理第一个缓冲区。这样CPU有一整个采样周期的时间去处理上一次的数据不容易丢数据。还有一种情况是DMA没有设置为循环模式采满一次就停止第二次采样无法开始。务必确认DMA和ADC都配置成循环模式除非你有特殊的一次性测量需求。6. 写在最后我的一些实践心得说了这么多最后分享几个我自己重复踩坑后总结出来的经验。第一无论是输入捕获还是FFT把采样参数写在程序开头用宏定义方便后续调整。我第一次做项目时测频范围需求临时变了结果硬编码的采样率和定时器分频值改了三次差点把需求文档和代码对不上。用宏定义集中管理一改全改省心很多。第二永远不要相信示波器上看到的“干净信号”。实际电路里感性负载、开关电源、电机换相都会产生毛刺。输入捕获引脚上串联一个几百欧姆的电阻、并联一个几十pF的电容能滤掉很多高频毛刺。FFT采样前端加一个低通抗混叠滤波器比如简单的RC一阶低通也能显著改善结果。这比你写多少行软件滤波都更直接。第三测量结果建议做一下平滑。无论时域插值还是频域峰值搜索单次读数都会受噪声影响。我的习惯是对频率结果做滑动平均比如保存最近10次测量结果每次都输出10次的平均值。这样数值稳定很多而且不会明显牺牲响应速度。第四多利用STM32的DMA能力。输入捕获可以用DMA把捕获值搬走ADC采样更是离不开DMA。CPU被解放出来以后你才能在同一个项目里同时做显示、通信、控制而不是被中断风暴拖死。这个项目做到最后其实你会发现在单片机领域测频只是冰山一角。理解了输入捕获的边沿时间戳思想你就能理解PWM捕获、正交编码器、脉冲计数理解了FFT的频谱分析思路以后做音频均衡器、振动分析、电力谐波检测都是顺手的事。工具本来就不复杂复杂的是你面对的信号和需求。希望这篇文章能帮你少走几步弯路把时间省下来去做真正有价值的部分。
返回列表