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

资讯详情

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

STM32实战:输入捕获+FFT实现高精度频率测量

STM32实战:输入捕获+FFT实现高精度频率测量 直接分享一个我最近在STM32上折腾完的测量方案输入捕获 FFT测频。这俩词儿单看都不陌生定时器输入捕获是单片机入门经典功能FFT是数字信号处理里的常客但是把这两者放一起、在同一个项目里做频率测量并让它们分工协作这里面的门道比想象中多。我最初的需求很简单一个频率范围和信号形状都不固定的传感器输出信号需要稳定读出基频。试过纯输入捕获也试过纯FFT都各自碰了一鼻子灰最后把两者结合才彻底解决。这篇就把完整的实现思路、代码细节、参数计算和踩过的坑都写出来。1. 输入捕获那套逻辑测方波没问题测非方波就抓瞎先说输入捕获是怎么回事。STM32的定时器以通用定时器TIM2-TIM5为例内部有一个输入捕获通道信号从通道引脚进来后可以被边沿检测电路捕捉硬件自动把当前计数器的值锁存到捕获寄存器里然后触发中断。这样我们就拿到了一个边沿出现的时间戳相邻两次上升沿或下降沿的时间差就是信号的周期频率自然就是周期的倒数。这套机制在测方波时非常顺手。比如一个输出固定频率方波的传感器或者PWM信号占空比和形状都是规整的上升沿和下降沿都是陡峭的跳变捕获到的边沿时间差非常准确测频精度可以做到很高。但一旦信号变成正弦波、三角波或者带有谐波失真的波形输入捕获的短板就暴露出来了正弦波在过零点附近的斜率比较缓如果信号经过比较器整形后送入捕获引脚边沿会产生明显的抖动导致每次捕获的周期值来回跳。如果信号本身含有多个频率分量比如电机振动信号基频之外有大量谐波分量输入捕获只能告诉你上升沿间隔对应的周期这个值可能代表基频也可能代表某个幅度较大的谐波甚至可能出现计数乱跳的情况根本拿不到稳定的主频。输入捕获本质上测的是信号的时域周期它永远回答不了这个信号里都有哪些频率成分这个问题。这时候就需要FFT登场了。FFT快速傅里叶变换把时域波形变换到频域能明确看出哪个频率分量能量最大也就是信号的主频。哪怕波形里有复杂的谐波、噪声只要信噪比不是太离谱基频那个峰通常都是最突出的。我遇到的实际场景就有意思了一个振动传感器输出是带有明显畸变的正弦波理想状态是干净的单频但实际带了很多高频毛刺信号幅度还会波动。用输入捕获直接测频率读数在400Hz和800Hz之间来回跳因为谐波被整形出来后触发了两次捕获。用FFT一分析频谱里400Hz的峰值高出一大截主频确定无误。所以最终方案的逻辑很清晰低频/方波用输入捕获高频/复杂波形用FFT两者配合覆盖全场景。2. 输入捕获的硬件细节与代码实现比你想的要多注意几个寄存器输入捕获的实现方式很直接但有几个关键点不注意到的话测出来的数据会让人怀疑人生。2.1 定时器时钟源与预分频的选择定时器挂在APB1或APB2总线上STM32F103的定时器时钟通常是72MHz如果APB1预分频系数不为1定时器时钟会是系统时钟的2倍。这里的预分频器PSC决定了计数器的计数时钟。我测的信号范围是1Hz-100kHz如果直接用72MHz计数1Hz信号对应72000000个计数早就溢出16位计数器了0-65535。所以必须配合溢出中断来扩展计数范围或者干脆把时基配成上升沿触发、下降沿触发交替的模式。我这里列出捕获模式的核心配置思路// 以STM32F103的TIM3为例PA6作为CH1输入 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 1. 时基设置72MHz计数时钟预分频71得到1MHz计数频率 TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 2. 输入捕获配置上升沿捕获直接映射到TI1 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 0x0; // 数字滤波抖动太大会用 TIM_ICInit(TIM3, TIM_ICInitStructure);这段配置里有三个参数是重点预分频、捕获极性和滤波系数。预分频决定了每个计数脉冲对应的物理时间我配成1MHz计数频率也就是每个计数值代表1微秒1Hz信号的计数值就是1000000这个值放在32位变量里两个16位寄存器拼接完全够用。滤波系数后面单说抖动的信号必须加。2.2 边沿时间差计算溢出不处理永远是垃圾数据核心代码是捕获中断处理函数uint32_t cap_value 0; // 当前捕获值 uint32_t prev_cap 0; // 上一次捕获值 uint32_t period_ticks 0; // 周期计数个数 uint16_t overflow_cnt 0; // 计数器溢出次数 void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); cap_value TIM_GetCapture1(TIM3); if (cap_value prev_cap) { period_ticks cap_value - prev_cap overflow_cnt * 65536; } else { // 计数器回绕说明中间发生了溢出 period_ticks cap_value - prev_cap 65536 overflow_cnt * 65536; } prev_cap cap_value; overflow_cnt 0; // 每次有效捕获后清溢出计数 } if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); overflow_cnt; // 溢出中断累加 } }这里最容易被忽略的就是溢出处理。如果计数器溢出中断没开启或者开了但不在中断里累加那么当信号周期大于65535微秒预分频71、1MHz计数的情况时计数器会在两次捕获之间发生回绕period_ticks会变成一个小得离谱的错值频率瞬间跳成高频。我早期调试时频率读数从低频跳到几十万Hz就是这个问题。说白了溢出计数就是给计数器扩展了位数从16位撑到了32位。频率计算公式float frequency_hz 1000000.0f / (float)period_ticks; // 因为计数频率是1MHz2.3 输入捕获的适用边界从原理上推出来输入捕获能测多高的频率理论上只要两个相邻上升沿之间的计数值大于1就能计算频率。但实际上中断响应本身有延迟如果频率太高比如1MHz以上定时器捕获中断可能还没来得及读完CCR寄存器下一轮捕获已经把值覆盖了或者中断频繁进入导致主程序卡死。一般建议输入捕获适合10Hz到几十kHz的中低频信号再高就得上方法二了。实际项目里我还发现一个细节FFT方法不需要关心信号是什么波形。正弦波、三角波、锯齿波、方波只要有周期性特征FFT都能在频谱上看到一个清晰的峰。输入捕获对波形边缘形状敏感FFT才是从根源上绕开了整形电路带来的误差。这也是我把FFT作为兜底方案的根本原因。3. FFT测频链路拆解从ADC采样到频谱峰提取的完整过程FFT测频的整体链路是模拟信号进入ADC以固定采样率连续采样得到时域序列对这序列做FFT变换得到频域幅度谱再在幅度谱里寻找峰值峰对应的频率就是信号主频。3.1 ADC采样率和采样点数的配对关系先讲最关键的参数配对。FFT的频率分辨率由公式决定频率分辨率 采样率 / FFT点数这意味着采样率固定后点数越多频谱上能看到的最小频率间隔越小频率读数的精度越高。我用的配置是采样率Fs 20kHzFFT点数N 1024那么分辨率就是20kHz / 1024 ≈ 19.5Hz也就是说在频谱上19.5Hz以内的频率差是区分不开的。对我实际场景来说400Hz的基频测出来可能是390还是410误差在±10Hz左右对于初步诊断够用但如果你要精确测某个频率这个分辨率就得提高。两个方向可选提高采样点数到2048或4096但MCU计算时间会成倍增加。降低采样率到10kHz但要保证采样率满足采样定理采样率必须大于信号最高频率的两倍。如果信号里有超过10kHz的高频分量ADC采样会引入混叠反而污染低频段的频谱。所以正确做法是先大致确认信号的最高频率再定采样率然后根据需要的频率精度定点数。这三个参数互相牵制没法单独拍脑袋。我定的20kHz采样率就是对传感器输出信号摸底之后的结果——信号能量主要集中在2kHz以内留10倍裕量。3.2 ADC连续采样定时器触发别用连续模式裸采STM32的ADC可以工作在扫描连续转换模式也可以由定时器触发单次转换。这里强烈建议用定时器触发采样拿一个定时器的更新事件触发ADC转换这样采样间隔是硬件精确控制的不像在主循环里启动转换那样有不可控的抖动。ADC采样的时间均匀性是FFT准确性的前提采样间隔抖动会让频谱毛刺变多峰值不集中。配置逻辑是这样的// 定时器2作为ADC触发源 TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz/72 1MHz TIM_TimeBaseStructure.TIM_Period 50; // 1MHz/50 20kHz 采样率 TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // ADC配置 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_CC2; // 定时器2事件触发 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换由触发控制这样每个20kHz触发脉冲到来时ADC就采样一次得到确定的采样时间点。DMA设置为循环模式把ADC转换结果自动搬到内存数组里攒够1024个点后通知CPU做FFT中途不占CPU时间。3.3 窗函数的选择别小看这个细节直接对截断后的时域数据做FFT会出现频谱泄漏——一个干净的单一频率信号会在频谱上抹出一坨能量旁边的频点都有幅度这样找峰值时偏差会比较大。解决方式就是加窗函数。我实测对比过三种窗矩形窗相当于不加窗主瓣窄但旁瓣高适合频率相隔较远的强信号频率精度高但容易泄漏污染。汉宁窗主瓣稍宽但旁瓣衰减明显适合大多数周期信号是工程里最常用的折中选项。布莱克曼窗旁瓣衰减更大但主瓣也更宽频率分辨率变差适合需要区分弱信号和强信号不追求精确频率的场景。我最终用了汉宁窗因为它对主频估计偏差足够小代码实现也简单查正弦表乘一次即可。窗函数系数是预先算好存在数组里的每次FFT前把ADC原始数据乘以对应系数得到加窗后的序列再送进FFT。3.4 从频谱里找主频直流分量和幅值谱的取舍FFT输出是复数数组每个点包含实部和虚部。幅度谱计算mag[ k ] sqrt( real[k]^2 imag[k]^2 )但实际做的时候没必要每个点都开方可以用近似公式|x| ≈ |real| |imag|或者直接比较平方和省掉开方运算在MCU上提速明显。找主频时最容易被坑的地方是直流分量。信号带个直流偏置的话第0个频点的幅度巨大直接找峰值会锁定在0Hz。处理办法是从第1个频点开始找跳过第0点或者采样时做直流偏置消除先累加1024个采样值求平均再用每个采样值减掉平均值把信号整体拉到0V附近。主频计算// peak_index 是幅度谱中幅度最大的频点索引 float freq (float)peak_index * Fs / N; // Fs20000, N10243.5 FFT的ST官方库与自写的坑实现FFT有两种途径要么用ST官方DSP库里的arm_cfft_f32需要配合CMSIS-DSP库要么用网上流传的各种精简FFT算法比如基2时间抽取法。我一开始图省事用了STM32F1标准外设库里自带的cr4_fft_1024_stm32这个函数只输出模方值不需要额外算幅度非常省事。但它的输入格式是固定的Q15格式代码上需要对ADC采样值做左移处理不然精度不够。后来换了STM32F4之后直接用arm_cfft_f32浮点运算速度提升明显代码也更简单。坑在换库时踩了一个不同库对FFT输入输出的排列顺序定义不一样有的库FFT后的频谱是自然顺序的第k个元素对应k×Fs/N的频率有的是位反转顺序的直接套用数据下标算频率会算出完全错误的结果。解决方式是在调试阶段先输入一个已知频率的标准正弦波检查峰值对应的索引和理论值是否吻合不吻合就翻库的文档确认输出排列方式而不是急着分析实际信号。4. 测频方法融合策略怎么让两种模式各干各的活还能无缝切换前面两套方案都讲完了但真正的问题来了两个方案不是独立的要么切换要么融合。我最终实现的是按信号频率范围自动切换这里把切换逻辑和背后的考量说清楚。4.1 为什么不用单一方案硬扛所有场景有人会问既然FFT什么波形都能测为啥不干脆只用FFT原因有二低频信号测不准。20kHz采样率采样1024点只能覆盖约0.05秒的时域窗口如果信号是10Hz这个窗口里只有半个周期做FFT后基频峰根本分辨不出来。要么把采样率降低到几百Hz但那样高频场景又没法测FFT天然受限于时间窗长度与频率分辨率成反比这条铁律。实时性差。FFT计算一次1024点在F103上是几毫秒的耗时加上采样等待时间整体刷新率有限而输入捕获只要两次中断就能算一次频率响应快得多。所以正确的分工是输入捕获扛低频段、扛实时性FFT扛复杂波形、扛高频段。4.2 自动切换的核心逻辑我在代码里设了一个频率阈值设为1kHz。每次循环先看输入捕获模式有没有输出有效频率有且数值在100Hz-1kHz之间就采用输入捕获结果如果捕获值一直为零或者捕获结果反复横跳超过阈值范围就自动切到FFT模式。切换逻辑伪代码float freq_result; float cap_freq get_input_capture_freq(); uint32_t adc_buf[1024]; if (cap_freq 100.0f cap_freq 1000.0f) { // 输入捕获结果可信直接采用 freq_result cap_freq; } else { // 输入捕获异常或超过范围启动FFT测量 adc_sample_and_fft(adc_buf); freq_result fft_get_main_freq(adc_buf); }这个判定逻辑里有个细节为什么不是捕获到频率就采用因为这正是我前面说的捕获到谐波场景——信号有强烈的失真时捕获逻辑也能稳定触发比如一个400Hz基波叠加800Hz谐波整形后可能每个基波周期触发两次边沿读到的频率是800Hz。这种情况单纯看捕获值的稳定性是判断不出来的必须辅助FFT。所以我的方案里只要捕获值落在可疑区间通常是基频的两倍左右就直接交给FFT去仲裁而不是试图在捕获模式里做数字滤波。4.3 切换时机与状态机设计为了避免频繁切换导致测量值抖来抖去我加了一个简单的状态机三个状态输入捕获模式、FFT准备模式、FFT测量模式每次切换设定一个滞留时间——在FFT模式下至少测8次才会切回输入捕获在输入捕获模式下如果连续3次捕获异常才切到FFT准备模式。这个滞回设计就是为了防止信号瞬态波动导致模式频繁切换这是实测中特别容易遇到的问题。5. 实测效果与校准方法没有这一步测出来的都是数字不是数据光把代码跑通还不能算完成必须用标准信号源校准记录误差。这里提三个关键的校准动作每一步我都在项目里做过。5.1 标准信号源扫频确认两个模式的边界我用信号发生器输出精确的方波和正弦波从1Hz扫到100kHz记录两种模式在不同频率下的误差信号类型频率范围测量模式误差表现方波1Hz - 1kHz输入捕获±0.1%非常稳定方波1kHz - 20kHz输入捕获±0.5%读数随干扰轻微抖动正弦波100Hz - 1kHz输入捕获±3%边沿抖动导致读数不稳正弦波100Hz - 5kHzFFT±1Hz以内但需要用插值算法提升精度失真锯齿波100Hz - 2kHzFFT主频峰清晰±1Hz左右这张表是我实测的典型数据不同板子、不同晶振精度会有差异。测试的意义在于知道哪个模式下哪个频段靠谱这是后面定切换阈值的最客观依据。5.2 频率读数的插值修正FFT峰值偏一半分辨率的问题FFT频谱里有一个天然的问题如果信号频率正好落在两个频点之间峰值会分布在两个相邻频点上直接取峰值索引会得到整数倍的频率误差。最明显的例子是信号频率是395Hz采样率20kHz点数1024分辨率19.5HzFFT峰值会落在390Hz或410Hz的频点上读出来就偏差了。工程上最简单的修正方法是抛物线插值也叫二次插值法取峰值频点p以及它左右各一个频点p-1和p1的幅度用二次曲线拟合出真实峰值的偏移量float delta (mag[p-1] - mag[p1]) / (2.0f * (mag[p-1] - 2.0f*mag[p] mag[p1])); float freq (p delta) * Fs / N;这个公式能把频率误差从±19.5Hz压到±1Hz以内是FFT测频实战里性价比极高的一个步骤强烈建议别省。但要注意这套公式只在主峰比较尖锐、没有明显旁瓣干扰时有效如果信号本身谐波丰富插值偏移量会被旁边的峰拉扯反而要慎重。5.3 晶振误差与温度漂移测的是一致而非绝对准确最后提醒一下MCU的HSE晶振本身有误差常见的8MHz晶振精度是±20ppm对应到测频上就是万分之二的误差。如果要更高的绝对精度就得用外部高精度时钟源或者做温漂补偿。但如果你的场景是监测信号频率变化趋势或者判断是否偏离正常范围内部晶振完全够用不需要额外折腾。我实际运行中还有一个习惯每次开机先测一个已知频率的基准信号比如把PWM输出配置成精确的10kHz用它自己测自己把得到的偏差系数记录在校准值里然后所有实测频率都乘以这个系数做修正。这样就算晶振有偏差测出来的相对值也是准的——对大多数传感器监测场景来说相对稳定性远比绝对精度重要。6. 实测中的几个意外问题和排查思路这部分挑三个我在项目里真实踩过、并且坑得比较深的点每个都给出完整的排查链路而不是直接给答案。6.1 问题一频率读数周期性跳变到极高值现象输入捕获模式测一个稳定的2kHz方波正常情况下读数是2.000kHz但每隔一两秒会跳一个50kHz的异常值然后恢复正常。排查链路第一步怀疑是捕获中断丢失。用逻辑分析仪抓定时器的捕获输出引脚确认信号本身是稳定的方波排除信号源问题。第二步在中断处理函数里加一个计数器每次进入捕获中断就累加主循环里周期读取。结果发现中断触发次数明显少于方波边沿数说明中断被其它更高级别的中断阻塞了。第三步检查中断优先级配置发现ADC的DMA传输完成中断和捕获中断配成了同一优先级而DMA中断服务函数里做了一些耗时的浮点运算导致定时器捕获中断被长时间挂起。第四步把DMA中断优先级调低、捕获中断调高并把DMA中断里的浮点运算移到主循环处理问题解决。根因中断优先级配置不合理高频率的DMA中断占用了大量CPU时间导致捕获中断响应延迟过大。捕获中断本身不丢但ARR计数值已经走了好几个周期算出来的频率自然就偏高。6.2 问题二FFT主频峰偏移到谐波位置现象输入一个标称1kHz、波形有明显削顶失真的正弦波FFT读出来的主频是2kHz或者3kHz。排查链路第一步用示波器看实际波形确认它确实不是单纯的正弦波削顶后的波形过零检测等效于一个方波谐波能量非常大。第二步在采样时把波形同时送给示波器确认ADC采样到的波形和示波器看到的一致排除采样混叠导致的高频假象。第三步把FFT点数从1024放到2048重新测发现峰值还是落在2kHz而且2kHz峰的幅度确实比1kHz高说明真实的信号频谱里2kHz成分确实强于基频。第四步意识到问题不是FFT算错了而是失真信号的基频在频谱上的幅度未必最大。有严重谐波失真时谐波的幅度可以超过基频。这时候就得用找第一个显著峰而不是找最高峰的逻辑从低频向高频扫找到第一个幅度超过阈值的峰作为基频再用基频位置校准后续峰为谐波。根因这是FFT测频最经典的方向错误——把幅度最高当成了基频。实际上处理带有失真的信号时基频的判定必须带上前面的第一个显著峰逻辑否则失真带来的高次谐波会直接带走主峰。6.3 问题三输入捕获模式频率读数稳定但明显偏低现象信号发生器输出精确的100Hz方波输入捕获给的是98.5Hz偏了1.5%。排查链路第一步先怀疑预分频的配置把计数频率从1MHz改成另外的值对比读数。发现改预分频会整体改变偏差方向说明问题在时基本身。第二步用定时器的PWM输出同时输出一路已知频率方波再喂给输入捕获通道发现自测结果完全准确。第三步对比信号发生器和单片机时基发现信号发生器标称100Hz但用频率计测实际输出是99.8Hz最终判定是信号源自身偏了而不是捕获电路偏了。第四步换成自带校准输出的信号源实测读数与标称值误差小于0.05%问题消失。根因测量系统只能告诉你被测频率相对于参考时基是多少参考时基本身有误差测出来自然就有偏差。这个案例的价值在于提醒遇到频率偏差先别急着改代码先用一个独立且可信的频率基准比如用另一块板子的晶振做参考来确认误差来源是被测对象还是测量端。硬件排查和软件排查是两套思维很多时候问题不在代码里。7. 代码结构建议与扩展思路到这里整个方案基本完整了。最后分享一点代码组织的经验。我把整个测频模块拆成了三个文件freq_input_capture.c输入捕获驱动、freq_fft.cADC采样和FFT处理、freq_manager.c模式切换和结果仲裁。这样做的好处是将来想换更高级的DSP库或者把输入捕获换成编码器测速只需要替换对应文件模式切换逻辑完全不用动。如果你后续要做的事不止测频还可以在这个基础上扩展几个方向基于FFT结果的波形质量评估既然频谱已经在手上了顺手算个THD总谐波失真也不难把基频幅度和谐波幅度做个比例即可。多通道频率监测如果有多个传感器信号要测可以在ADC上扩展多通道用DMA的乒乓缓冲交替采样轮流做FFT代价是每个通道的刷新率下降。与PWM输出联动测出主频后用定时器自动调整PWM输出频率做成一个简单的自适应激励系统比如根据共振频率调整驱动频率。测频这件事很多人以为拿个定时器数边沿就完事但真正上项目就会发现信号形态的复杂性决定了单一方法都有死角。输入捕获在时域上有直觉、有速度FFT在频域上有全景、有精度两者结合的方案才是嵌入式工程里既要又要的解法。希望这篇对正在鼓捣测频的朋友有帮助。
返回列表