STM32定时器输入捕获实现频率测量:原理、配置与代码实战
2026/9/11 17:36:12 网站建设 项目流程

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 次和第 N+1 次上升沿的时间差,然后除以 N。比如测 100kHz 信号时,记录连续 10 个完整周期总共耗时 100 微秒,量化误差仍然是正负 1 微秒,但摊到每个周期上就只有 0.1 微秒,精度提升了 10 倍。

用输入捕获配合 DMA 或者数组记录多次捕获值,在中断里每次只存 CCR 寄存器值,累计到 N+1 个数据后再统一计算,是工程上最常用的做法。后面代码部分我给出的是一个简化版本,但思路是一致的:先测多个周期,再算平均。

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 为例,我习惯把主频跑到 250MHz,APB1 分频配置为 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)65535(16 位最大)尽量用满量程,配合溢出计数
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 第一个上升沿的毛刺干扰

输入捕获首次启动时,如果信号线上恰好有一个毛刺,或者信号在捕获使能瞬间处于不稳定状态,第一个捕获值往往是无效的。这种现象在电机驱动、开关电源这类电磁干扰比较强的环境中特别常见。

我的处理策略是:丢弃第一组捕获数据。也就是采集到 N+1 个数据后,从第二个数据开始使用。虽然会多等一个周期,但换来的稳定性很值得。在产线测试场景中,这种“慢半拍但准”的特性反而更重要,因为没有人希望一个瞬时毛刺导致设备误判。

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 的开发板上用信号发生器输出标准方波,记录了几组测量数据,计数时钟配置为 10MHz,4 个捕获值取平均。

信号源频率实测频率偏差
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% 以内,前提是时钟源用高精度晶振。

作为结尾,说一点个人体会:输入捕获测频率这件事,听起来只是“配一下定时器、写几行中断”,但真正决定测量质量的,是你对测量原理的理解程度和边界情况的处理能力。把溢出处理、滤波器调优、多周期平均这几个关键点搞扎实,这套方案以后在传感器采集、电机控制、通信波特率检测、设备状态监测等场景里都能复用,属于投入产出比很高的一项技能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询