STM32输入捕获原理与实战:从定时器到精准脉冲测量
2026/8/27 4:24:31 网站建设 项目流程

1. 从“计时”到“捕获”:为什么输入捕获是嵌入式竞赛的必争之地

在蓝桥杯嵌入式这类竞赛中,定时器(Timer)是选手们最熟悉的老朋友。我们用它来产生精确的延时,驱动PWM波控制电机或LED亮度,这些都是定时器作为“输出”角色的典型应用。但今天,我们要聊的是它的另一面——作为“侦探”的输入捕获(Input Capture)功能。这个功能,往往是区分“会写代码”和“能解决实际问题”选手的关键分水岭。

想象一个场景:你需要测量一个旋转编码器的转速,或者一个超声波模块返回的高电平脉冲宽度,又或者是一个遥控器发送的PPM信号。这些信号的共同特点是,它们都是随时间变化的脉冲,而你需要精确地知道这个脉冲的“高电平持续了多久”或者“两个上升沿之间隔了多长时间”。如果你试图用主循环不断轮询GPIO引脚的电平变化来计算时间,精度会惨不忍睹,并且会严重占用CPU资源。这时,输入捕获功能就登场了。它就像给定时器配了一个敏锐的“耳朵”和一块精准的“秒表”,当指定的引脚上发生特定事件(比如上升沿或下降沿)时,它会瞬间“咔嚓”一声,把当前定时器的计数值锁存到一个专门的寄存器里。你只需要在中断服务程序里读取这个“快照”值,就能精确计算出事件发生的时间点或脉冲宽度。

在蓝桥杯嵌入式竞赛中,尤其是使用STM32G4系列或类似MCU的平台,输入捕获是高频考点。它不仅是客观题的理论重点,更是综合应用题中实现人机交互(如旋转编码器菜单)、传感器数据采集(如超声波测距)的核心技术。掌握它,意味着你拥有了精确测量外部事件时间的能力,这是从基础控制迈向精确感知的关键一步。接下来,我将以一个STM32G431RB(蓝桥杯常用芯片)为例,手把手带你从原理到代码,彻底吃透定时器的输入捕获。

2. 输入捕获的核心原理:硬件如何为你按下“快门”

要玩转输入捕获,必须理解其背后的硬件工作原理。我们以STM32的通用定时器(如TIM2, TIM3, TIM4)为例,其输入捕获单元的核心部件可以概括为三部分:输入通道与边沿检测器、捕获/比较寄存器(CCR)、捕获预分频器

2.1 信号路径与边沿检测

首先,一个外部信号通过GPIO引脚接入。这个引脚必须被配置为复用功能(Alternate Function),并映射到具体的定时器通道上(例如,PA0可以映射为TIM2_CH1)。信号进入定时器内部后,首先会经过一个输入滤波器边沿检测器

  • 输入滤波器:这是一个数字滤波器,用于消除信号上的毛刺。你可以通过配置一个采样频率和数字滤波深度(例如,连续N次采样为同一电平才确认有效)来确保捕获事件的稳定性。在电机调速等有噪声的环境中,这个功能至关重要。
  • 边沿检测器:这是决定“何时按下快门”的关键。你可以独立配置它为在上升沿、下降沿或双边沿触发捕获事件。例如,要测量一个高电平脉冲的宽度,通常的配置是:上升沿捕获一次(记录脉冲开始时间),下降沿再捕获一次(记录脉冲结束时间)。

2.2 捕获寄存器与计数器“快照”

定时器内部有一个核心的计数器寄存器(CNT),它根据内部时钟源(经过预分频后)不断递增或递减。当边沿检测器检测到预设的边沿事件时,它会立刻产生一个硬件触发信号。这个信号会做一件最重要的事:将当前计数器CNT的值,一次性、原子性地复制到对应的捕获/比较寄存器(CCRx, x=1,2,3,4)中。这个过程是硬件自动完成的,速度极快,不受软件延迟影响,从而保证了极高的时间戳精度。

注意:这个“快照”值是一个绝对时间点,是相对于定时器本次启动(或溢出)后的计数值。我们通常需要计算两次捕获值之间的差值,才能得到我们想要的脉冲宽度或周期。

2.3 捕获预分频器与中断管理

在信号到达边沿检测器之前,还可以经过一个捕获预分频器。它的作用不是改变定时器主时钟,而是对输入信号本身进行分频。例如,设置为“每2个事件捕获一次”或“每4个事件捕获一次”。这在测量高频信号时非常有用,可以降低中断频率,减轻CPU负担。

当捕获事件发生时,除了更新CCRx寄存器,硬件还会置位相应的捕获标志位(如CCxIF),如果使能了对应的捕获中断,就会跳转到中断服务函数。在中断里,我们的核心任务就是:读取CCRx的值,进行必要的计算,并清除标志位

理解了这个硬件流程,我们就能明白,输入捕获的本质是利用定时器的高精度时钟作为时间标尺,用硬件自动记录外部事件发生的时刻。软件的角色从“疲于奔命的测量员”变成了“从容不迫的记录员”。

3. 实战配置:以STM32CubeMX配置TIM2通道1输入捕获为例

理论清晰后,我们进入实战。使用STM32CubeMX工具可以极大简化配置过程,但我们必须清楚每一步配置的意义。假设我们要测量PA0引脚上输入脉冲的高电平宽度(脉宽)。

3.1 时钟与定时器基础配置

  1. 系统时钟:首先确保系统时钟(如HSI或HSE)正确配置,并分配至APB1总线(TIM2挂载在APB1上)。APB1的时钟频率决定了定时器计数时钟的上限。
  2. 定时器模式:在CubeMX中选中TIM2。
    • Clock Source:选择“Internal Clock”。这样定时器的计数时钟就来自APB1。
    • Prescaler (PSC):预分频器。这是决定定时器“滴答”一次实际时间的关键。计算公式:定时器时钟 = APB1时钟 / (PSC + 1)。例如,APB1时钟为80MHz,若PSC设为79,则定时器计数时钟为 80MHz / 80 = 1MHz,即计数器每1微秒加1。
    • Counter Mode:选择“Up”(向上计数)。
    • Counter Period (ARR):自动重装载值。计数器从0计数到ARR后溢出归零,并产生更新中断。对于输入捕获,ARR通常设置为最大值(如0xFFFF),以获得最大的测量范围,避免在测量一个脉冲期间计数器溢出。如果需要测量很长的周期,也可以利用溢出中断来扩展计数。
    • Auto-reload preload:使能。确保ARR的值在更新事件时才被载入,避免计数过程中产生毛刺。

3.2 输入捕获通道详细参数设定

在TIM2的配置界面,找到“Channel 1”,选择“Input Capture direct mode”。

  • ICPolarity:输入捕获极性。这是边沿检测器的设置。
    • 要测量高电平脉宽,我们通常设置为“Rising Edge”和“Falling Edge”分别捕获。但CubeMX一个通道一次只能配置一种极性。因此,一种常见做法是:先配置为上升沿捕获,在上升沿中断里,将极性切换为下降沿,并清除计数器;在下降沿中断里,读取捕获值(即为脉宽),再将极性切换回上升沿,准备下一次测量。另一种更高效的方式是使用一个通道的双边沿捕获,这需要更精细的寄存器操作。
    • 我们先配置为“Rising Edge”。
  • ICSelection:选择“Direct TI”。表示输入信号直接连接到TI1FP1(定时器输入1),这是最常用的模式。
  • ICPrescaler:捕获预分频器。设为“DIV1”,即每个边沿都捕获。
  • ICFilter:输入滤波器。根据信号质量设置。如果信号干净,可以设为0(无滤波)。如果信号有抖动,可以适当增加滤波值,例如设为0x4或0x8,表示需要连续几个时钟周期采样稳定才确认边沿。

3.3 GPIO与NVIC配置

  • GPIO:找到PA0引脚,其模式会自动变为“Alternate Function”,并映射到TIM2_CH1。无需额外配置上拉/下拉,除非外部电路需要。
  • NVIC:在NVIC配置中,找到TIM2全局中断或TIM2_CC_IRQn(捕获比较中断),并勾选使能。设置合适的抢占优先级和子优先级。

生成代码后,CubeMX会帮我们完成GPIO、定时器时钟、时基单元和输入捕获通道的基础初始化。但核心的测量逻辑和中断处理,需要我们手动在代码中完善。

4. 核心代码实现与脉宽测量逻辑

生成了基于HAL库的代码后,我们在main.c或单独的模块文件中添加以下逻辑。

4.1 变量定义与状态机

我们使用一个简单的状态机来管理测量过程。

// 定义测量状态 typedef enum { CAPTURE_IDLE = 0, // 空闲,等待上升沿 CAPTURE_RISING_EDGE, // 已捕获上升沿,等待下降沿 } CaptureState_t; volatile CaptureState_t g_capture_state = CAPTURE_IDLE; volatile uint32_t g_capture_start = 0; // 上升沿捕获值 volatile uint32_t g_pulse_width_ticks = 0; // 最终计算的脉宽(计数值) volatile uint32_t g_pulse_width_us = 0; // 最终计算的脉宽(微秒)

4.2 定时器启动与中断回调函数

main函数的初始化部分,启动定时器和中断。

// 启动定时器基础计数 HAL_TIM_Base_Start(&htim2); // 启动输入捕获中断(针对通道1) HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);

最关键的是重写输入捕获中断的回调函数。这个函数会在每次捕获事件发生时被调用。

// 在 tim.c 文件末尾或用户文件中重写此弱函数 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) // 判断是TIM2触发的中断 { if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // 判断是通道1 { switch(g_capture_state) { case CAPTURE_IDLE: // 第一次捕获,必然是上升沿(因为我们初始配置为上升沿) g_capture_start = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 重要:清除计数器,从0开始计数脉宽,简化计算 __HAL_TIM_SET_COUNTER(htim, 0); // 切换为下降沿捕获 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); g_capture_state = CAPTURE_RISING_EDGE; break; case CAPTURE_RISING_EDGE: // 第二次捕获,是下降沿 g_pulse_width_ticks = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 注意:此时计数器值就是下降沿的时刻,由于上升沿后计数器清零,所以这个值就是高电平期间的计数值 // 转换为微秒 (假设定时器时钟为1MHz,即1 tick = 1us) g_pulse_width_us = g_pulse_width_ticks; // 如果PSC配置使1tick=1us,则数值相等 // 切换回上升沿捕获,准备下一次测量 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); g_capture_state = CAPTURE_IDLE; // 这里可以设置一个标志位,通知主循环脉宽已更新 break; default: break; } } } }

4.3 主循环处理与溢出考虑

在主循环中,我们可以检查脉宽是否更新,并进行显示或后续处理。

while (1) { // 示例:将测量到的脉宽(微秒)通过串口打印出来 static uint32_t last_pulse_width = 0; if(g_pulse_width_us != last_pulse_width) { printf("Pulse Width: %lu us\r\n", g_pulse_width_us); last_pulse_width = g_pulse_width_us; } // ... 其他任务 }

关于计数器溢出:上面的代码在上升沿将计数器清零,因此只要高电平脉宽小于定时器从0计数到溢出(ARR值)的时间,就不会有问题。如果脉宽可能很长,你需要处理溢出中断。在溢出中断(更新中断)中,用一个软件变量(如overflow_count)记录溢出次数。在计算脉宽时,公式变为:脉宽 = overflow_count * (ARR+1) + current_capture_value。务必在CubeMX中使能定时器的更新中断。

5. 高频考点与实战避坑指南

在竞赛和实际项目中,仅仅让代码跑通是不够的,效率和稳定性才是拿高分的关键。以下是我总结的几个核心要点和常见“坑点”。

5.1 测量精度与时钟配置的权衡

精度是输入捕获的首要目标。它直接取决于定时器的计数时钟频率

  • 时钟源:优先使用外部高速晶振(HSE),它比内部RC振荡器(HSI)更稳定、更精确。
  • 预分频器PSC定时器时钟 = APB1时钟 / (PSC + 1)。PSC越小,定时器时钟频率越高,计时分辨率越高(例如1MHz对应1us,80MHz对应0.0125us)。但频率越高,计数器溢出越快,测量范围越小。你需要根据待测信号的最大周期来权衡。一个技巧:将PSC配置为(APB1时钟 / 期望的定时器时钟) - 1,并确保结果为整数。
  • ARR自动重载值:决定了不处理溢出时的最大测量范围。最大时间 = (ARR + 1) * 定时器时钟周期。对于16位定时器,ARR最大65535。

5.2 输入滤波与抗干扰配置

这是最容易忽略但至关重要的一环。如果被测信号有噪声(如机械按键抖动、电机换相干扰),会导致边沿检测器频繁误触发。

  • ICFilter参数:这个值不是随便设的。它定义了数字滤波的采样窗口。公式大致是:滤波时间 = ICFilter值 * 定时器时钟周期。例如,定时器时钟1MHz(1us),ICFilter设为8,则信号需要稳定8us以上才会被确认。对于消除按键抖动(通常>5ms)来说,这个值太小;但对于滤除高频毛刺非常有效。对于按键,更适合在软件中做消抖。
  • 硬件滤波:对于极其恶劣的环境,应考虑在GPIO引脚外部增加RC低通滤波电路。

5.3 中断服务函数的设计优化

中断函数要快进快出,这是铁律。

  • 避免浮点运算:中断中不要做floatdouble计算。所有时间计算先用整数(计数值)进行,在主循环中再根据需要转换。例如,在主循环中计算:脉宽_us = g_pulse_width_ticks * (1000000.0f / timer_clock_hz)
  • 使用标志位通信:中断函数只负责采集原始数据、更新状态、设置标志位。所有复杂的计算、显示、逻辑判断都放到主循环中根据标志位来处理。
  • 及时清除标志位:HAL库通常在中断处理末尾会自动清除硬件标志位,但如果你直接操作寄存器,务必手动清除TIMx->SR寄存器中的CCxIF位和UIF位(如果使能了更新中断)。

5.4 双边沿捕获与单通道脉宽测量的进阶技巧

前面我们通过切换极性来测量脉宽,这会在中断中引入额外的配置操作。对于STM32,有一个更高效的方法:使用一个定时器的两个输入通道(如CH1和CH2),分别设置为上升沿和下降沿捕获,并将它们映射到同一个输入引脚(TI1)。这需要配置定时器的“从模式”和“输入交叉”功能。

  1. 将CH1配置为上升沿捕获,直接映射到TI1。
  2. 将CH2配置为下降沿捕获,也映射到TI1(通过配置TIMx->CCMR1寄存器的CC2S位和IC2PSC等位)。
  3. 这样,同一个引脚上的上升沿和下降沿会分别触发CH1和CH2的捕获事件,并锁存到CCR1和CCR2寄存器。高电平脉宽 = CCR2 - CCR1(考虑溢出)。这种方法只需要一次配置,中断中直接读取两个寄存器即可,效率更高,是竞赛中体现技术深度的亮点。

5.5 调试技巧:利用示波器与逻辑分析仪

当测量结果不准或异常时,不要只盯着代码。

  • 示波器:直接观察被测信号的波形,确认其上升/下降沿是否干净,频率、幅值是否符合预期。这是判断问题是出在信号源还是代码端的金标准。
  • 逻辑分析仪:同时抓取被测信号和MCU的某个GPIO(在中断入口置位、出口清零)的波形。你可以清晰地看到从信号边沿到进入中断的延迟,以及中断服务函数的执行时间。这对于优化代码和评估系统实时性至关重要。
  • 串口打印辅助:在中断中谨慎地使用printf输出调试信息(注意可能影响时序),或者通过设置GPIO引脚电平,用示波器观察关键节点的时序。

输入捕获是一个将硬件特性与软件逻辑紧密结合的经典功能。吃透它,不仅能让你在蓝桥杯等竞赛中游刃有余,更能让你在面对真实的嵌入式传感、控制问题时,拥有一个强大而精准的工具。记住,所有的配置都是为了服务于“精确记录事件发生时刻”这一核心目标,从时钟源到滤波器,每一个参数的选择背后都有其物理意义和权衡考量。多动手,多测量,多思考“为什么这样配置”,你就能真正掌握这门技术。

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

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

立即咨询