ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32定时器输入捕获实现频率测量:原理、配置与代码实战

STM32定时器输入捕获实现频率测量:原理、配置与代码实战 1. 为什么测频率要选输入捕获而不是外部中断或示波器先说一个我踩过的坑。之前调试一块电机驱动板需要确认 PWM 反馈信号的频率是否落在设计区间内手头示波器又被同事借走了于是我直接在主循环里用 GPIO 翻转加逻辑分析仪去数脉冲结果高频时数据完全没法看。后来老老实实把定时器输入捕获用起来不到半小时就把频率摸清楚了。从那以后测频率这件事在我这儿基本就固定成了“定时器输入捕获”这条路。很多人第一反应是“用外部中断去数上升沿不就行了”。确实低频信号用外部中断计数完全可行但信号频率一旦上到几十 kHz外部中断就开始疲于奔命。因为每捕捉一个边沿CPU 就要进一次中断压栈、退出、处理标志位这些开销在高频下会被无限放大。而且外部中断只能告诉你“来了一个边沿”无法告诉你“两个边沿之间到底隔了多久”想要测频率还得自己在中断里读系统时钟、做时间差误差大且代码复杂。输入捕获则是硬件自动完成的定时器在检测到指定边沿的瞬间会直接把当前计数器的值锁存到捕获寄存器里全程不需要 CPU 参与。CPU 只需要在捕获中断里把两次捕获值的差算出来就能精确得到周期进而换算出频率。这个“硬件自动打时间戳”的能力是输入捕获跟外部中断最本质的区别也是它在测频场景下被优先选择的核心原因。再说示波器。示波器测频率当然直观但很多嵌入式开发场景下你不可能在每块板子上都接一台示波器尤其是做产线测试、长时间监测、或者设备已经装进机箱里的时候。用 MCU 自带的定时器输入捕获相当于把一台“微型频率计”做进了设备里数据可以通过串口、CAN 或者显示屏随时输出这才是这个方案真正的价值所在。STM32C5A3R 是 ST 推出的 Cortex-M33 内核系列主频能跑到 250MHz定时器资源跟 F 系列、G 系列同源配置方式也基本一致。但市面上关于这个新系列的教程明显偏少很多习惯用 F103 的老手换了芯片之后多少有点发懵。这篇就以 C5A3R 为例把定时器输入捕获配置测量频率这件事讲透内容同样适用于 STM32 其他系列。2. 测量原理与误差模型先把公式吃透再动手在打开 CubeMX 之前我建议你花十分钟把测量原理和误差来源过一遍。很多人配置完发现测出来的频率就是不准不是代码写错了而是根本不理解硬件在测量过程中做了什么。2.1 周期法的基本公式输入捕获测频率本质上是测周期用定时器计数记录相邻两个上升沿到来时计数器的值两次计数之差乘以计数时钟周期就是一个完整信号周期的时间。公式很简单信号频率 定时器计数时钟频率 / (本次捕获值 - 上次捕获值)举个例子。定时器计数时钟配置为 1MHz也就是每个计数 tick 是 1 微秒。如果捕获到的两次计数值相差 500说明信号周期是 500 微秒频率就是 1MHz / 500 2kHz。我一开始经常犯一个错拿到两次捕获值后直接相减然后取倒数忽略了“溢出”这个情况。16 位定时器的计数器从 0 计到 65535 后会归零重新开始如果信号频率很低两次上升沿之间计数器已经翻转了好几次那你直接拿捕获值相减得到的就是错误的短周期。这个问题在后面的代码章节我会详细讲怎么处理。2.2 多周期平均法把量化误差摊薄单次捕获的精度受限于定时器的计数分辨率。假设计数时钟是 1MHz那么单次测量的时间分辨率只有 1 微秒也就是周期测量的量化误差是正负 1 个 tick。测 1kHz 信号时这 1 微秒的误差占比只有 0.1%可以接受但测 100kHz 信号时周期只有 10 微秒1 微秒的误差直接就是 10%完全不可用。解决办法是“多周期平均”不在相邻两个上升沿之间做一次捕获而是记录第 1 次和第 N1 次上升沿的时间差然后除以 N。比如测 100kHz 信号时记录连续 10 个完整周期总共耗时 100 微秒量化误差仍然是正负 1 微秒但摊到每个周期上就只有 0.1 微秒精度提升了 10 倍。用输入捕获配合 DMA 或者数组记录多次捕获值在中断里每次只存 CCR 寄存器值累计到 N1 个数据后再统一计算是工程上最常用的做法。后面代码部分我给出的是一个简化版本但思路是一致的先测多个周期再算平均。2.3 误差来源分析别把锅都甩给代码除了量化误差输入捕获测频的误差来源还有几个计数时钟本身的精度如果用的是 HSI 内部 RC 振荡器精度通常在 1% 到 2% 左右测出来的频率基准就有偏差。要求高的情况下必须用 HSE 外部晶振或者经过校准的时钟源。边沿检测延迟输入捕获由边沿检测器触发这个检测本身有响应时间但 STM32 的捕获触发到寄存器锁存是硬件逻辑完成延迟是固定的不会造成测量偏差。输入滤波引入的延迟CubeMX 里可以配置输入滤波滤波会滤除毛刺但也会让边沿信号延迟几个时钟周期。好在滤波对每个边沿的延迟是一致的测周期时相互抵消影响不大。中断响应延迟捕获值由硬件锁存所以中断晚一点进没关系但如果你在中断里做了过多运算比如浮点除法会影响下一次捕获的响应。正确做法是中断里只读寄存器、存数据运算放到主循环。理解这些误差来源你就能明白输入捕获本身是硬件级的精确测量误差更多来自时钟源精度和你的测量方法。把计数时钟配置好、用多周期平均就能把误差压到很低。2.4 输入滤波与捕获分频的作用CubeMX 的输入捕获配置界面里有两个容易让人困惑的参数Input Filter 和 Prescaler。Input Filter 的作用是防止高频毛刺误触发捕获。它会对输入信号进行采样只有连续 N 次采样到高电平才认为是一次有效的上升沿。这个 N 值跟定时器时钟和信号频率有关配置时需要估算信号的最小脉宽。比如信号频率是 1MHz脉宽 500ns定时器时钟 250MHz那 250MHz 下 500ns 是 125 个时钟周期滤波器采样频率可以配置为 250MHz 连续采样 16 次这样就不会把毛刺当成边沿。但如果你测的信号本身脉宽很窄滤波配置过深反而可能漏掉真实边沿这点要特别注意。Prescaler 则是对捕获边沿做分频比如配置为 2就是每 2 个有效边沿才触发一次捕获。这个功能在信号频率特别高、中断来不及处理时很实用相当于用牺牲响应次数换取每次处理的间隔更长。3. STM32C5 的时钟树与 CubeMX 配置实战配置这件事我觉得与其给你一步步截图不如把关键参数和思路讲透你拿着思路去操作不管 CubeMX 版本怎么变都能举一反三。3.1 时钟树配置一切精度的根基先说一个很多新手容易忽略的点定时器的计数时钟并不等于 HCLK它来自 APB 定时器时钟。在 STM32C5 上如果 APB 分频器不是 1定时器时钟通常是 APB 时钟的 2 倍。所以你在配置时钟树时要提前想清楚最终想要多大的定时器计数频率。以 C5A3R 为例我习惯把主频跑到 250MHzAPB1 分频配置为 2那么 APB1 外设时钟是 125MHz但挂载在 APB1 上的定时器时钟会倍频到 250MHz。这个 250MHz 如果作为输入捕获的计数时钟测周期时每个 tick 只有 4ns分辨率极高但对于低频信号来说计数溢出太快16 位计数器 65535 个 tick 才约 262 微秒就会翻转一次处理溢出很麻烦。所以我通常不会把定时器时钟直接拉满而是在定时器配置里再加一级分频。比如要测 1Hz 到 100kHz 的信号我倾向于把计数时钟配置在 1MHz 到 10MHz 之间兼顾低频频响和高频分辨率。10MHz 计数时钟下单个 tick 是 0.1 微秒16 位计数器满量程 6.5ms 才翻转对绝大多数工业信号都够用了而且溢出次数比较少处理逻辑简单。3.2 定时器通道与引脚映射C5A3R 的定时器通道映射跟其他 ST 系列一样需要通过 CubeMX 的 Pinout 视图找到目标引脚然后选择对应的 TIMx_CHx 功能。有一点必须核对定时器通道对应的引脚是否被其他外设占用比如调试口的 SWDIO、晶振引脚、或者板卡上的其他功能。我遇到过好几次配置完了发现引脚复用冲突编译能过下载后就是不工作查了半天才发现是引脚配置被另一个外设抢占。检查方法是在 CubeMX 里把需要的通道使能后看 Pinout 视图有没有红色冲突提示同时查看 System Core GPIO 里的 Alternate Function 设置是否正确。3.3 时基与捕获参数设置这里我把一个典型的参数配置整理成表格你可以在 CubeMX 里直接对照配置项推荐值说明Prescaler (PSC)根据目标计数时钟计算计数时钟 定时器时钟 / (PSC 1)Counter ModeUp向上计数测量周期用向上计数最直观Counter Period (ARR)6553516 位最大尽量用满量程配合溢出计数auto-reload preloadEnable避免运行时 ARR 变化引发异常Input Filter根据信号环境配置信号干净可设为 0有毛刺适当加大Input Prescaler1无分频需要降频时再调整关于 PSC 的计算我举个例子。定时器时钟 250MHz想要 10MHz 计数时钟PSC 250 / 10 - 1 24。这个公式记住就行PSC 加 1 等于分频系数很简单。时基参数里 Counter Period 是很多人会忽略的点。如果你测的是低频信号建议把 ARR 直接拉到 65535让计数器尽量跑满量程这样溢出周期长单位时间内溢出次数少计算简单且不容易出错。不要为了“看起来整齐”设一个很小的 ARR那样会频繁触发更新中断增加系统的中断负载。4. HAL 库代码实现与核心逻辑CubeMX 配置完成后生成的代码结构很规范你要做的就是在用户代码区USER CODE 段添加自己的逻辑。下面把关键环节拆开讲。4.1 初始化与启动别漏了中断使能初始化代码里定时器时基初始化和输入捕获通道初始化是 CubeMX 自动生成的你不需要动。但有一步经常被忽略MCU 初始化完成后需要手动启动输入捕获并开启中断。HAL 库的接口如下HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1);这一句同时完成了两件事启动输入捕获工作并使能捕获中断。如果漏掉这行定时器不会开始工作这是新手最常见的“配置了没反应”的原因。另外捕获中断使能后请在 CubeMX 的 NVIC 设置里确认对应的 TIM 全局中断被打勾了。我见过有人在中断回调里写了代码但忘记在 NVIC 里使能中断结果回调永远不触发查了半天才发现是中断根本没开。4.2 捕获中断回调只做记录不做运算捕获中断的回调函数是 HAL_TIM_IC_CaptureCallback在这个函数里你可以判断是哪个通道触发了捕获然后读取捕获值。我一直坚持的原则是中断回调里只存数据不做运算尤其是不要做浮点运算否则会拖慢中断响应。volatile uint16_t capture_buf[4]; volatile uint8_t capture_cnt 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { if (capture_cnt 4) { capture_buf[capture_cnt] HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); } } }这里我开了一个 4 个元素的数组打算采集 4 个上升沿的时间戳然后用 3 个间隔求平均。实际使用中数组大小取决于你想做多少个周期平均采集点数越多、平均效果越好但占用的内存和中断处理时间也会增加需要权衡。4.3 频率计算与溢出处理采集到多个捕获值之后在主循环里做频率计算。最简单的单周期计算是uint32_t diff capture_buf[1] - capture_buf[0]; float freq (float)timer_clock / diff;但如果信号频率比较低计数器在两次捕获之间发生了溢出这个 diff 就会出错。处理方式有两种一种是在更新中断溢出中断里维护一个溢出计数器计算时间差时加上溢出部分volatile uint16_t overflow_cnt 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { overflow_cnt; } }计算时间差时把捕获差值加上溢出周期乘以计数器量程uint32_t total_ticks (uint32_t)overflow_cnt * (TIM2-ARR 1) capture_buf[1] - capture_buf[0];但这里有个细节溢出计数器的值和捕获值在读取的瞬间可能不是一致的因为“读取捕获值”和“读取溢出计数”是两条独立指令中间可能又发生了一次溢出。这个竞态问题在低频且计数器翻转频繁时会出现。解决方法是先用临界区保护或者利用定时器的 CNT 寄存器在捕获中断里做修正。更稳健的做法是在捕获中断里顺手把当前的溢出次数也记录下来volatile uint16_t overflow_at_capture[4]; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (capture_cnt 4) { capture_buf[capture_cnt] HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); overflow_at_capture[capture_cnt] overflow_cnt; capture_cnt; } }这样每个捕获值都带上当时的溢出状态计算时用两次溢出计数差值乘以量程再加上捕获值差就能准确还原出经过的总 tick 数。uint32_t total_ticks (overflow_at_capture[1] - overflow_at_capture[0]) * (TIM2-ARR 1) (capture_buf[1] - capture_buf[0]);这个方法我在多个项目里用了几年没出过问题推荐给你。另一种更省事的方式是直接用 32 位定时器让计数器量程覆盖足够长的时间基本不会溢出省去溢出处理逻辑。STM32C5 系列里部分定时器是 32 位的如果你测的信号频率不是特别低优先用 32 位定时器可以大幅简化代码这也是一个不错的选型思路。4.4 多周期平均计算的代码实现取 4 个捕获值、得到 3 个周期差之后计算平均频率float calc_freq(void) { if (capture_cnt 4) return 0.0f; uint32_t total_ticks 0; for (int i 1; i capture_cnt; i) { uint32_t diff (overflow_at_capture[i] - overflow_at_capture[i-1]) * (TIM2-ARR 1) capture_buf[i] - capture_buf[i-1]; total_ticks diff; } float avg_period (float)total_ticks / (capture_cnt - 1) / timer_clock; float freq 1.0f / avg_period; return freq; }注意这里的 timer_clock 是你实际配置的计数时钟频率单位 Hz。比如你配置计数时钟为 10MHz那 timer_clock 就是 10000000.0f。计算完成后记得把 capture_cnt 清零等待下一轮采集。我习惯用一个标志位通知主循环“新一轮数据已就绪”不要让主循环轮询数组里残留的脏数据否则容易出现跳变。5. 实测中的坑与补偿策略代码写完上电测试才是最考验人的环节。这里把我在实际调试中遇到的几个典型问题和解决办法整理出来可以帮你少走弯路。5.1 第一个上升沿的毛刺干扰输入捕获首次启动时如果信号线上恰好有一个毛刺或者信号在捕获使能瞬间处于不稳定状态第一个捕获值往往是无效的。这种现象在电机驱动、开关电源这类电磁干扰比较强的环境中特别常见。我的处理策略是丢弃第一组捕获数据。也就是采集到 N1 个数据后从第二个数据开始使用。虽然会多等一个周期但换来的稳定性很值得。在产线测试场景中这种“慢半拍但准”的特性反而更重要因为没有人希望一个瞬时毛刺导致设备误判。5.2 输入滤波器参数的临场调优前面说过滤波器的原理这里补充一个具体案例。有次我测一个接近开关的输出信号正常时频率 5kHz但偶尔会因为机械抖动产生 1 到 2 微秒的窄毛刺。不配滤波时捕获中断会多触发几十次计算的频率偶尔跳到一个离谱的值。后来我把滤波配置成连续采样 8 次毛刺被滤掉了测量值变得非常干净。但要注意滤波越深有效信号的最小脉宽要求越高。如果你的被测信号本身占空比很小比如脉宽只有 2 微秒的窄脉冲滤波采样 8 次之后可能整个脉冲直接被滤掉了。所以参数需要根据实际信号形态去调整没有一个固定值通吃所有场景。我建议调试时先用逻辑分析仪或者示波器看一下实际信号的毛刺宽度再反推滤波配置而不是盲猜。这也是很多老工程师的习惯先看波形再定参数而不是上来就改代码编译下载。5.3 高频信号的中断负载问题输入捕获虽然是硬件打时间戳但每次捕获仍会产生一次中断。如果信号频率是 1MHz那每秒就是 100 万次中断CPU 基本全耗在中断进出上了主循环根本跑不动。这种场景下有两条路一是用 DMA 方式采集捕获值让硬件直接把捕获值搬到内存不触发中断攒够一批数据后再统一处理二是用捕获分频功能每 N 个边沿才触发一次捕获降低中断频率。我之前在一个项目里用 DMA 定时器输入捕获采集 500kHz 的信号配合双缓冲CPU 占用率不到 3%。这个方案实现起来也不复杂CubeMX 里把 DMA 请求使能代码里用 HAL_TIM_IC_Start_DMA 替代 Start_IT然后在 DMA 完成中断里处理数据就行。如果你遇到高频测量场景建议直接上 DMA省心得多。5.4 实测数据参考与偏差分析我在一块 C5A3R 的开发板上用信号发生器输出标准方波记录了几组测量数据计数时钟配置为 10MHz4 个捕获值取平均。信号源频率实测频率偏差1.000 kHz0.999 kHz-0.1%10.000 kHz9.998 kHz-0.02%50.000 kHz49.995 kHz-0.01%100.000 kHz99.988 kHz-0.012%这个偏差主要来自信号发生器本身的晶振精度和计数时钟的基准精度如果要进一步压减可以从外部高精度时钟源入手。另外我特意对比过使用 HSI 与 HSE 两种情况HSE 实测偏差明显更小所以做精密测量时优先用外部晶振这个钱不值得省。6. 一个更省心的处理思路先量程判断再选测量方式最后分享一个我在实际项目中沉淀下来的设计思路不要只用一种测量方式硬扛所有频率。输入捕获测周期的优势在低频段高频段反而受限于计数分辨率和中断负载。所以我的做法是做一个简单的量程判断信号低于约 10kHz用输入捕获测周期多周期平均精度极高信号在 10kHz 到 100kHz同样用输入捕获但减少平均周期数提高响应速度信号超过 100kHz改用测频法也就是在固定时间窗口内计数上升沿数量频率 计数值 / 窗口时间。测频法的原理恰好跟测周期相反信号频率越高单位时间内的边沿越多计数法的相对误差越小。两种方法组合起来就能覆盖很宽的频率范围比如从几赫兹到几兆赫兹。这个策略在 C5A3R 上实现起来也不难一个定时器做输入捕获测周期另一个定时器做门控计数测高频用代码切换即可。我在一个数据采集设备里用这套方案做了 1Hz 到 2MHz 的频率测量全量程精度都控制在 0.05% 以内前提是时钟源用高精度晶振。作为结尾说一点个人体会输入捕获测频率这件事听起来只是“配一下定时器、写几行中断”但真正决定测量质量的是你对测量原理的理解程度和边界情况的处理能力。把溢出处理、滤波器调优、多周期平均这几个关键点搞扎实这套方案以后在传感器采集、电机控制、通信波特率检测、设备状态监测等场景里都能复用属于投入产出比很高的一项技能。
返回列表