
1. 从按键到脉冲为什么我们需要输入捕获如果你玩过正点原子的STM32开发板大概率是从点亮一个LED或者读取一个按键状态开始的。这些操作直观、简单能快速建立成就感。但当你开始接触电机控制、编码器测速、红外遥控解码甚至是测量一个简单方波的频率和占空比时你会发现GPIO的简单“读高低电平”变得力不从心。这时候STM32内部一个强大的外设——通用定时器General-purpose timer的输入捕获Input Capture功能就该登场了。简单来说输入捕获就是让定时器这个“高精度秒表”去帮你“抓拍”某个引脚上电平跳变发生的精确时刻。比如一个脉冲的上升沿来了定时器立刻把当前计数值“咔嚓”一下存到专门的寄存器里这个值就代表了从定时器启动到上升沿发生所经过的时间。如果再捕获一次下降沿两个时间一减脉冲的高电平宽度占空比就出来了连续捕获两个上升沿脉冲的周期频率也就知道了。我最初学这个功能时总觉得它比PWM输出要抽象。PWM是我命令定时器输出波形一切尽在掌握而输入捕获是定时器被动响应外部世界的“突发事件”需要理解中断、影子寄存器、滤波等概念。但正是这种“被动响应”的能力让STM32能从简单的控制器升级为能与复杂外部信号“对话”的交互核心。正点原子的Mini板以其简洁的外设和丰富的例程是啃下这块硬骨头的绝佳平台。接下来我就以Mini板常用的TIM2定时器为例带你从原理到代码彻底搞懂输入捕获并分享几个调试中容易踩的坑。2. TIM2输入捕获的硬件原理与核心寄存器拆解正点原子STM32 Mini板通常基于STM32F103系列其TIM2是一个高级控制定时器功能全面。理解输入捕获不能只停留在库函数调用必须扒开寄存器看看它到底是怎么工作的。这能让你在调试异常时心里有底。2.1 信号通路从引脚到捕获寄存器想象一下信号进入STM32的旅程引脚首先你需要将一个GPIO引脚例如PA0配置为复用功能并将其映射到TIM2的通道1TIM2_CH1。边沿检测器信号进入定时器后首先经过边沿检测器。你可以通过寄存器配置它是检测上升沿、下降沿还是双边沿。这是捕获的“触发器”。输入滤波器这是一个非常实用但常被忽略的环节。如果输入信号有毛刺比如机械按键抖动滤波器可以设置一个采样频率和数字滤波深度只有当连续N个采样点都是有效电平时才认为边沿有效。这对于抗干扰至关重要。预分频器捕获到的边沿信号可以经过一个预分频器。比如设置为“每2个事件触发一次捕获”这可以用来测量频率较低的信号减少中断开销。捕获单元这是核心。当有效的边沿事件最终到来时硬件会立即把当前定时器计数器CNT的值复制到对应的捕获/比较寄存器CCR1中。注意这里有一个“影子寄存器”的概念你程序里读到的CCR1寄存器可能并不是直接抓拍的那个值而是它的影子寄存器。这种设计保证了在捕获瞬间即使程序正在读取CCR1也不会读到正在被硬件更新的、不完整的值确保了数据的原子性。中断/DMA在值被捕获到CCR1的同时如果使能了捕获中断就会产生一个中断。你可以在中断服务函数里读取CCR1的值进行后续计算。对于高频信号还可以使用DMA将CCR值直接搬运到内存数组极大减轻CPU负担。2.2 关键寄存器精讲我们挑几个最核心的寄存器用“人话”解释TIMx_CCMR1捕获/比较模式寄存器1这个寄存器负责通道1和2的模式设置。CC1S[1:0]这两位决定通道1是输入还是输出。对于输入捕获必须设置为01输入映射到TI1或10输入映射到TI2。简单应用通常用01即信号直接来自本通道的引脚。IC1F[3:0]输入捕获滤波器。设置越大滤波效果越强但延迟也略微增加。对于无抖动的方波可以设为0对于有抖动的信号如红外接收头输出可以设为较高的值如0xF8个时钟滤波。IC1PSC[1:0]输入捕获预分频器。00表示每个边沿都捕获01表示每2个边沿捕获一次以此类推。TIMx_CCER捕获/比较使能寄存器CC1E通道1捕获使能位必须置1。CC1P捕获极性选择。0表示捕获上升沿1表示捕获下降沿。这里有个大坑很多教程只讲到这里。实际上在STM32F1中还有一个CC1NP位用于互补通道通常为0。但更关键的是CC1P和CC1NP共同决定了边沿检测类型。标准库函数TIM_ICInit()内部会帮你正确设置这对组合。如果你直接操作寄存器只设CC1P1可能无法捕获下降沿必须同时设置CC1NP0。这是新手极易出错的地方。TIMx_DIERDMA/中断使能寄存器CC1IE通道1捕获中断使能。置1后捕获发生时会产生中断。TIMx_SR状态寄存器CC1IF通道1捕获中断标志。硬件在捕获发生时置1软件读CCR1寄存器或向该位写0可以清除它。调试时一定要看这个标志位有没有被置起来这是判断硬件是否正常触发的第一线索。TIMx_CCR1捕获/比较寄存器1这就是最终存放“抓拍”时间戳的地方。在输入捕获模式下它是只读的。理解了这些再看库函数配置就不会觉得是一堆“黑魔法”了。库函数只是用更友好的方式帮你把这些寄存器位组合起来。3. 基于标准库的输入捕获测频实战代码逐行解析理论说再多不如一行代码。我们以测量PA0引脚TIM2_CH1上方波频率为例使用STM32标准外设库。正点原子的例程通常采用此库结构清晰。3.1 初始化步骤与“为什么”// 1. 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // TIM2挂在APB1总线 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA挂在APB2总线 // 2. 初始化GPIO GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入防止悬空 // GPIO_Mode_IN_FLOATING; // 浮空输入如果外部信号有确定驱动能力可用 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 输入模式下此参数影响不大 GPIO_Init(GPIOA, GPIO_InitStructure); // 注意这里还没有配置复用功能对于标准库的输入捕获GPIO模式设置为输入即可 // 复用功能的重映射如果有需要通过GPIO_PinRemapConfig配置但TIM2_CH1默认在PA0通常无需重映射。 // 3. 初始化定时器基本时基 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 0xFFFF; // 自动重装载值ARR16位定时器最大值 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 预分频器PSC72分频 // 为什么是72-1因为系统时钟72MHz72分频后得到1MHz的计数时钟即每个计数1微秒。 // 这样ARR65535最大可测量65.535ms的周期对应约15Hz频率。对于更高频率需减小PSC。 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分频与输入滤波无关通常DIV1 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 4. 初始化输入捕获参数 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel TIM_Channel_1; // 通道1 TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 捕获上升沿 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; // 直接映射到TI1输入 TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; // 每个边沿都捕获 TIM_ICInitStructure.TIM_ICFilter 0x0; // 不滤波适用于干净信号 TIM_ICInit(TIM2, TIM_ICInitStructure); // 这个函数内部会智能配置CCMR1、CCER等寄存器包括正确设置边沿极性组合。 // 5. 使能中断 TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); // 使能通道1捕获中断 // 配置NVIC嵌套向量中断控制器 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 6. 启动定时器 TIM_Cmd(TIM2, ENABLE);3.2 中断服务函数与频率计算逻辑中断函数是处理捕获数据的核心。我们的目标是测频率思路是捕获连续两个上升沿的时间戳其差值就是一个周期的计数值。// 定义全局变量 volatile uint32_t g_capture_count 0; // 捕获值 volatile uint32_t g_last_capture 0; // 上一次捕获值 volatile uint32_t g_period_ticks 0; // 周期计数值 volatile uint8_t g_capture_flag 0; // 捕获完成标志 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) // 检查是否是通道1中断 { // 读取本次捕获值 g_capture_count TIM_GetCapture1(TIM2); // 计算周期注意处理计数器溢出 if (g_last_capture ! 0) { // 正常情况下本次值应大于上次值向上计数 if (g_capture_count g_last_capture) { g_period_ticks g_capture_count - g_last_capture; } else { // 如果本次值更小说明定时器在两次捕获间发生了溢出ARR从65535翻到0 // 周期 (ARR - 上次值) 本次值 1。因为ARR0xFFFF所以简化如下 g_period_ticks (0xFFFF - g_last_capture) g_capture_count 1; } g_capture_flag 1; // 设置标志通知主循环周期已更新 } // 更新上一次捕获值为下次计算做准备 g_last_capture g_capture_count; // 清除中断标志位非常重要 TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }在主循环中你可以检查g_capture_flag然后计算频率if (g_capture_flag) { g_capture_flag 0; // 已知计数时钟为1MHz (1 tick 1us)周期单位是微秒 float period_us (float)g_period_ticks * 1.0f; // 1 tick/μs float frequency_hz 1000000.0f / period_us; // 频率 1 / 周期 printf(Period: %.2f us, Frequency: %.2f Hz\r\n, period_us, frequency_hz); }这里有几个关键点溢出处理代码中else分支处理了计数器溢出。这是测量长周期信号时必须考虑的。如果定时器在两次捕获之间溢出了多次上述简单处理就不够了需要引入一个溢出计数变量。标志位通信中断函数应尽可能短平快只做最必要的读写和计算通过设置标志位让主循环处理显示、传输等耗时操作。清除中断标志TIM_ClearITPendingBit必须调用否则中断会连续触发导致程序卡死。4. 进阶应用双边沿捕获与高精度占空比测量只测频率还不够很多场景如PWM调速反馈、舵机信号解析需要测量占空比。这就需要捕获一个周期内的上升沿和下降沿。4.1 配置与思路一种方法是使用同一个通道在中断中动态切换捕获极性。但更高效、更可靠的方法是使用定时器的PWM输入模式这其实是输入捕获的一个特殊应用。不过我们先用标准双通道法来理解原理。我们可以配置通道1捕获上升沿通道2捕获下降沿并且将两个通道都映射到同一个输入引脚TI1上。这样硬件会自动将同一个信号路由给两个捕获单元。// 初始化部分在基本时基配置后 TIM_ICInitTypeDef TIM_ICInitStructure; // 通道1配置为上升沿捕获直接映射到TI1 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; // 直接TI1 TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0; TIM_ICInit(TIM2, TIM_ICInitStructure); // 通道2配置为下降沿捕获间接映射到TI1 TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Falling; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_IndirectTI; // 间接TI1 TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0; TIM_ICInit(TIM2, TIM_ICInitStructure); // 使能两个通道的中断 TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_CC2, ENABLE);4.2 中断处理与计算在中断函数中我们需要区分是哪个通道触发的中断。volatile uint32_t g_rise_capture 0, g_fall_capture 0; volatile uint32_t g_high_ticks 0, g_period_ticks 0; volatile uint8_t g_duty_flag 0; void TIM2_IRQHandler(void) { // 处理通道1上升沿中断 if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { g_rise_capture TIM_GetCapture1(TIM2); // 如果已经有一个下降沿捕获值则可以计算高电平时间 if (g_fall_capture ! 0) { // 注意时序一个完整的周期可能是 上升沿 - 下降沿 - 上升沿 // 这里计算的是上一个下降沿到本次上升沿的时间不对。 // 更稳健的做法是记录连续两个上升沿的时间差作为周期 // 记录上升沿和紧随其后的下降沿时间差作为高电平时间。 // 这需要更复杂的状态机来管理是常见的难点。 // 简化处理假设信号稳定我们总是用最新的上升沿和下降沿计算。 // 但这样需要确保下降沿发生在上升沿之后。 if (g_fall_capture g_rise_capture) { g_high_ticks g_fall_capture - g_rise_capture; } else { // 处理计数器溢出 g_high_ticks (0xFFFF - g_rise_capture) g_fall_capture 1; } } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } // 处理通道2下降沿中断 if (TIM_GetITStatus(TIM2, TIM_IT_CC2) ! RESET) { g_fall_capture TIM_GetCapture2(TIM2); // 如果已经有一个上升沿捕获值则可以计算高电平时间同上 if (g_rise_capture ! 0) { if (g_fall_capture g_rise_capture) { g_high_ticks g_fall_capture - g_rise_capture; } else { g_high_ticks (0xFFFF - g_rise_capture) g_fall_capture 1; } g_duty_flag 1; // 标志高电平时间已更新 } TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); } // 周期计算仍需依赖两次上升沿可以复用之前的逻辑单独维护一个周期计算状态。 }这里暴露了一个关键问题简单的双通道捕获在计算占空比和周期时需要精心设计状态机来配对正确的上升沿和下降沿否则在信号占空比变化或中断响应延迟时容易出错。这也是为什么对于精确的PWM测量推荐使用下面要讲的“PWM输入模式”它硬件自动配对更为可靠。5. 调试避坑指南从现象到根源的完整排查链路输入捕获功能不出数据或者数据跳变严重是新手常遇到的问题。别慌按照以下链路排查能解决90%的问题。5.1 现象完全捕获不到任何中断第一步检查硬件连接用万用表或示波器确认信号是否真的到达了MCU引脚。信号电压是否在GPIO可识别的范围对于3.3V系统高电平2.0V低电平0.8V检查GPIO配置模式。输入捕获必须配置为输入模式GPIO_Mode_IN_FLOATING或GPIO_Mode_IPU/IPD而不是复用推挽输出。这是我见过最多的配置错误。第二步检查定时器时钟与使能确认RCC_APB1PeriphClockCmd(TIM2, ENABLE)已调用。用调试器查看RCC-APB1ENR寄存器对应位是否为1。确认TIM_Cmd(TIM2, ENABLE)已调用。查看TIM2-CR1寄存器的CEN位是否为1。第三步检查中断配置确认TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE)已调用。确认NVIC配置正确且全局中断已开启在启动文件startup_stm32f10x_xx.s中默认开启。在中断服务函数入口设置断点。如果断点永远打不上说明中断没触发。进入中断函数后首先检查TIM_GetITStatus读取的状态标志是否真的为1。有时是清除标志位的代码位置不对导致中断持续触发程序跑飞。第四步检查捕获通道配置确认TIM_ICInit函数被正确调用参数无误。特别是TIM_Channel和TIM_ICSelection。使用寄存器视图直接观察。在调试模式下查看TIM2-CCER寄存器CC1E位应为1捕获使能CC1P/CC1NP位应正确设置。查看TIM2-CCMR1CC1S位应为01或10输入模式。5.2 现象捕获值不变或变化无规律第一步检查信号质量用示波器看波形。是否有严重毛刺边沿是否陡峭如果边沿缓慢如RC充放电可能无法触发边沿检测。考虑使用施密特触发器整形或调整输入滤波器(IC1F)。第二步检查计数器是否在跑在调试模式下实时观察TIM2-CNT寄存器的值。它应该随着时间递增。如果不增检查定时器预分频器PSC是否设置过大比如65535导致计数极慢或者ARR是否设为0。第三步检查中断标志清除确保在中断函数末尾清除了对应的中断标志(TIM_ClearITPendingBit)。如果不清除中断会连续发生你可能来不及处理第一次捕获的数据就被第二次中断覆盖看起来像是数据没变。第四步处理计数器溢出如果你的信号周期较长超过(ARR1) * 计数周期那么计数器会在一个信号周期内多次溢出。你需要一个溢出计数变量TIM_Update中断在计算周期时将其考虑进去。例如总周期 溢出次数 * (ARR1) 本次捕获值 - 上次捕获值。5.3 现象测量值精度差、跳动大根源分析量化误差与中断延迟量化误差这是由定时器分辨率决定的。如果你的计数时钟是1MHz1us那么测量的最小时间单位就是1us。对于10kHz的信号周期100us理论上有±1us的误差即±1%的误差。提高基准时钟频率减小PSC可以降低量化误差但会缩短最大可测周期。中断延迟从捕获事件发生到CPU进入中断服务函数读取CCR值存在延迟。虽然这个延迟是微秒级的且对于周期测量两次捕获在中断中相减可以抵消大部分但在高频率或需要绝对时间戳的场合仍有影响。对于超高精度要求需使用DMA方式。信号抖动信号源本身就不稳定。使用示波器的统计功能测量频率看其标准差与MCU测量结果对比。软件滤波对于跳动的数值可以在主循环中进行软件滤波如滑动平均滤波、中值滤波等。#define FILTER_LEN 10 uint32_t freq_buffer[FILTER_LEN] {0}; uint8_t buf_index 0; uint32_t filtered_freq 0; // 在计算得到frequency_hz后 freq_buffer[buf_index] (uint32_t)frequency_hz; buf_index (buf_index 1) % FILTER_LEN; // 计算平均值 uint32_t sum 0; for(int i0; iFILTER_LEN; i) sum freq_buffer[i]; filtered_freq sum / FILTER_LEN;6. 从标准库到HAL库与CubeMX的配置迁移现在很多新项目使用STM32CubeMX和HAL库。虽然底层逻辑不变但配置方式差异很大。了解如何迁移能让你更灵活。6.1 使用CubeMX图形化配置Pinout Configuration在Pinout视图找到PA0将其功能设置为TIM2_CH1。Configuration - TIM2Clock Source选择Internal Clock。Parameter SettingsPrescaler (PSC - 16 bits value)设为71因为HAL库的预分频值是实际分频数-1与标准库一致。Counter ModeUp。Counter Period (AutoReload Register)65535。Internal Clock Division (CKD)No Division。auto-reload preloadDisable。下方切换到“Input Capture Channel 1”Polarity SelectionRising Edge。IC SelectionDirect。IC PrescalerNo Prescaler。IC Filter0。NVIC Settings勾选TIM2 global interrupt并设置优先级。生成代码。6.2 HAL库代码要点CubeMX生成的代码会初始化好外设。你需要补充中断回调逻辑。// 在main.c的合适位置如用户代码区2定义变量 uint32_t last_capture 0, period_ticks 0; uint8_t capture_flag 0; // 启动输入捕获 HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); // 重写输入捕获回调函数 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { uint32_t current_capture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); if (last_capture ! 0) { if (current_capture last_capture) { period_ticks current_capture - last_capture; } else { period_ticks (0xFFFF - last_capture) current_capture 1; } capture_flag 1; } last_capture current_capture; } } // 主循环中检查capture_flag并计算频率HAL库与标准库的一个显著区别HAL库在中断中自动清除了标志位你不需要手动调用__HAL_TIM_CLEAR_IT。这使得代码更简洁但也意味着如果你在回调函数里做了太耗时的操作可能会影响下一次中断的响应。7. 项目实战扩展将输入捕获应用到真实场景掌握了基础我们可以看看它能做什么。结合网络热词里的几个方向编码器测速正交编码器输出两路相位差90度的脉冲。可以使用定时器的编码器接口模式这本质上是输入捕获的高级应用硬件自动根据两路信号边沿计数和判断方向直接读取CNT值就能得到位置和速度。这比软件捕获两路信号高效准确得多。红外遥控解码NEC等红外协议用脉冲宽度表示0和1。可以用输入捕获双边沿测量每个高低电平的宽度再根据协议规则解码出地址和命令。这里输入滤波器(ICFilter)非常关键能有效抑制红外接收头的噪声。PWM输入反馈在闭环控制中需要读取反馈的PWM占空比。使用PWM输入模式是最佳选择。该模式将一路信号同时映射到两个捕获通道如TI1映射到IC1和IC2并自动将其中一个通道设置为反极性。只需读取两个CCR值一个就是周期另一个就是高电平时间硬件自动配对无需复杂状态机。频率计/占空比仪结合LCD或串口将上述测量结果实时显示出来就是一个简易的仪器。注意处理量程通过调整PSC和显示刷新率。输入捕获是STM32定时器家族里一颗璀璨的明珠它连接了MCU的精准计时能力和外部异步事件。从理解寄存器位开始到写出稳健的中断服务函数再到处理各种边界条件和噪声这个过程是对嵌入式开发者硬件抽象能力和细致调试能力的绝佳锻炼。在Mini板上跑通它你收获的不仅仅是一个功能更是一套应对外部信号交互的方法论。下次当你遇到需要测量时间间隔、解析数字脉冲的任务时你会自信地知道TIMx的输入捕获功能正在那里待命。