STM32定时器输入捕获:从原理到实战,精准测量脉冲宽度与频率

发布时间:2026/7/30 8:22:05
STM32定时器输入捕获:从原理到实战,精准测量脉冲宽度与频率 1. 项目概述为什么我们需要捕获边沿信号在嵌入式开发尤其是基于STM32这类MCU的项目里我们经常需要测量外部信号的频率、脉宽或者精确地记录某个事件发生的时刻。比如你想知道一个旋转编码器转得多快一个超声波传感器返回的回波脉冲有多宽或者一个按键被按下的确切时长。这时候仅仅靠GPIO读取电平是远远不够的因为它无法提供精确的时间信息。定时器的输入捕获功能就是为了解决这类“计时”问题而生的核心外设。简单来说输入捕获就像给MCU配了一块高精度的秒表和一个反应极快的记录员。当指定的引脚捕获通道上发生你预设的事件比如电平从低变高即上升沿时这个“记录员”会瞬间按下秒表的“暂停键”并把当前的计时器计数值也就是时间戳锁存到一个专用的寄存器里。这样你就能精确地知道这个边沿事件是在计时器启动后的哪个时刻发生的。通过连续捕获一个脉冲的上升沿和下降沿的时间戳两者相减就能得到这个脉冲的精确宽度。这就是标题中“使用定时器捕获上升/下降沿”所要实现的核心功能。对于STM32开发者而言STM32CubeMX/HAL库极大地简化了外设的初始化流程但同时也隐藏了不少底层细节。很多新手在配置和使用输入捕获时常常卡在如何正确配置定时器模式、如何理解捕获/比较通道、以及如何编写稳定可靠的中断服务程序这些问题上。本文将基于STM32Cube生态深入拆解定时器输入捕获的配置要点、工作原理和实战代码并分享我在多个工控和测量项目中积累的调试经验和避坑指南。2. 定时器输入捕获的核心原理与模式选择要玩转输入捕获首先得理解STM32定时器的基本架构。一个通用定时器如TIM2, TIM3, TIM4通常包含一个16位或32位的自动重载计数器CNT、一个预分频器PSC和一个自动重载寄存器ARR。输入捕获功能依赖于定时器的捕获/比较通道每个通道都关联着一个捕获/比较寄存器CCR和输入滤波、边沿检测等数字电路。2.1 输入捕获的工作流程其工作流程可以概括为以下几步信号输入外部信号通过GPIO引脚接入到定时器的特定通道如TIMx_CH1。边沿检测通道配置为输入模式并设置要捕获的边沿类型上升沿、下降沿或双边沿。事件触发当检测到设定的边沿事件时硬件会立即产生一个捕获事件。值锁存在捕获事件发生的瞬间当前计数器CNT的值被硬件自动复制到对应的捕获/比较寄存器CCRx中。这个过程是原子操作不受软件中断延迟影响保证了时间戳的精确性。中断/DMA请求可以配置在捕获事件发生时产生中断或DMA请求通知CPU来读取CCRx中的值并进行处理。2.2 关键模式解析PWM输入模式与普通输入捕获模式在CubeMX中配置输入捕获时你会遇到两个容易混淆的模式PWM Input Mode和Input Capture direct mode。理解它们的区别至关重要。普通输入捕获模式 (Input Capture direct mode)这是最基础、最灵活的模式。你需要手动为每个通道分别配置边沿极性。例如你可以将通道1配置为上升沿捕获用于测量周期再将通道2配置为下降沿捕获与通道1配合测量脉宽。或者你也可以将同一个通道配置为双边沿捕获在HAL库中通常通过切换极性或使用特定函数实现来测量单个脉冲的宽度。这种模式需要更多的软件干预但控制粒度更细。PWM输入模式 (PWM Input Mode)这是一种“一键配置”的高级模式专门用于测量PWM信号的频率和占空比。它硬件上固定使用两个通道例如TI1同时映射到CH1和CH2。其中一个通道如CH1被自动配置为上升沿捕获用于测量周期另一个通道如CH2被自动配置为下降沿捕获用于测量高电平脉宽。硬件会自动将两次捕获的值存入不同的CCR寄存器并可以仅产生一个中断通常来自通道1或通道2。它的优点是全硬件自动处理两次捕获的关联性软件逻辑简单缺点是占用了两个捕获通道且模式固定不够灵活。选择建议如果你只需要测量频率或周期或者明确要测量一个PWM信号的频率和占空比PWM输入模式是最方便的选择。如果你需要更灵活的控制例如捕获非周期性的单次脉冲、测量任意两个边沿之间的时间间隔或者资源紧张只能用一个通道那么务必选择普通的输入捕获模式并通过软件逻辑来处理双边沿捕获。2.3 定时器时基计算精度与量程的权衡捕获到的时间戳是一个计数值要转换成真实时间微秒、毫秒必须清楚定时器的计数频率。公式如下定时器计数频率 定时器时钟源频率 / (PSC 1)计数周期 1 / 定时器计数频率最大可测量时间 (ARR 1) * 计数周期例如APB1总线时钟为72MHzTIM2挂载在其上。设置PSC71ARR65535。计数频率 72MHz / (711) 1MHz即每个计数代表1微秒。计数周期 1微秒。最大可测量时长 (655351) * 1us 65.536 ms。如果你要测量一个50Hz的PWM信号周期20ms这个配置是足够的。但如果要测量1Hz的信号周期1秒ARR就需要设置得更大或者降低计数频率增大PSC。这里有一个经典陷阱为了追求高精度计数频率高设置了很小的PSC导致ARR很快溢出在测量长周期信号时如果软件没有处理计数器溢出计算结果会完全错误。我的经验是先确定你要测量的最大信号周期然后根据此时基公式反推PSC和ARR在精度和量程之间取得平衡。通常让ARR溢出周期略大于待测最大周期并开启定时器更新中断来处理溢出计数是测量长周期信号的可靠方法。3. 基于STM32CubeMX的输入捕获配置详解我们以STM32F103C8T6的TIM3通道1PA6引脚为例配置为上升沿和下降沿捕获测量脉冲宽度。3.1 CubeMX图形化配置步骤引脚与定时器使能在Pinout Configuration标签页找到TIM3。将Channel1设置为Input Capture direct mode。对应的GPIO引脚PA6会自动配置为复用模式。参数配置切换到Configuration标签页点击TIM3进行参数设置。时基单元 (Parameter Settings):Prescaler (PSC - 16 bits value): 根据计算设置。例如填71得到1MHz计数频率。Counter Mode:Up向上计数。Counter Period (AutoReload Register - 16 bits value): 设置为最大值65535或根据量程计算的值。auto-reload preload: 建议Enable确保ARR的更新在下次更新事件时生效避免中间状态。输入捕获通道配置 (Channel1):Polarity Selection: 初始设置为Rising Edge上升沿。注意在HAL库中我们可以在运行时动态改变极性来实现双边沿捕获。IC Selection:Direct直接模式。对于PWM输入模式这里会是Indirect等选项。IC Filter: 滤波值。如果输入信号有毛刺可以设置一个滤波值如0x8来稳定信号但会引入微小延迟。对于干净的数字信号设为0。IC Prescaler: 分频器。选择No division每个有效边沿都捕获。中断/DMA使能在NVIC Settings或DMA Settings中使能TIM3 global interrupt和TIM3 capture compare interrupt。3.2 生成代码与关键函数解析生成代码后在tim.c中可以看到HAL库已经帮我们完成了定时器的初始化。我们需要在用户代码中关注以下几个核心HAL函数HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1): 启动指定定时器通道的输入捕获并使能捕获/比较中断。这是启动捕获的入口。HAL_TIM_IC_Stop_IT(htim3, TIM_CHANNEL_1): 停止捕获和中断。__HAL_TIM_GET_COUNTER(htim3): 获取当前计数器的值。用于在中断外查询。__HAL_TIM_SET_CAPTUREPOLARITY(htim3, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING):动态设置捕获极性。这是实现单通道双边沿捕获的关键。可以在中断服务函数中在捕获到上升沿后立即将极性切换为下降沿等待下一次捕获。3.3 单通道双边沿捕获的软件实现逻辑这是本文的重点和难点。很多教程只讲PWM输入模式但单通道双边沿捕获在实际中更常用资源占用少。其软件状态机逻辑如下初始化配置定时器为上升沿捕获启动捕获中断。定义两个变量capture_val_rising和capture_val_falling以及一个状态标志capture_state 0。第一次中断上升沿在中断回调函数HAL_TIM_IC_CaptureCallback中首先判断是哪个通道的中断。如果capture_state 0表示等待第一个上升沿。此时读取CCR1的值存入capture_val_rising。关键操作立即调用__HAL_TIM_SET_CAPTUREPOLARITY将通道极性改为下降沿TIM_INPUTCHANNELPOLARITY_FALLING。将capture_state置为1表示正在等待下降沿。第二次中断下降沿此时capture_state 1。读取CCR1的值存入capture_val_falling。计算脉宽pulse_width capture_val_falling - capture_val_rising。注意处理计数器溢出的情况见下文。关键操作再次调用__HAL_TIM_SET_CAPTUREPOLARITY将通道极性改回上升沿。将capture_state置为0为下一个脉冲的测量做好准备。此时一个完整脉冲的宽度已经计算出来可以用于后续处理如打印、控制等。注意在中断回调函数中切换极性一定要迅速且最好在读取CCR值之后立即进行。因为从边沿触发到中断响应有延迟如果切换太慢可能会错过紧接着的相反边沿特别是对于很窄的脉冲。4. 中断服务程序编写与脉宽计算实战下面给出一个完整的、带有溢出处理的单通道双边沿捕获示例代码。我们假设定时器时钟为1MHz1us/计数ARR65535。// 在用户变量区定义 uint32_t capture_rising 0; uint32_t capture_falling 0; uint32_t pulse_width_us 0; volatile uint8_t capture_state 0; // 0:等上升沿1:等下升沿 volatile uint32_t overflow_count 0; // 定时器溢出次数 // 在main.c的合适位置如定时器初始化后启动捕获 HAL_TIM_Base_Start_IT(htim3); // 启动定时器并开启更新溢出中断用于计数溢出 HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); // 启动输入捕获中断 // 定时器更新溢出中断回调函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { overflow_count; // 每次溢出溢出计数器加1 } } // 输入捕获中断回调函数 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { switch(capture_state) { case 0: // 捕获到上升沿 capture_rising HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 记录溢出次数并清空本地溢出计数或作为基准 // 这里简化处理实际需结合overflow_count计算绝对时间戳 __HAL_TIM_SET_CAPTUREPOLARITY(htim3, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); capture_state 1; break; case 1: // 捕获到下降沿 capture_falling HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 计算脉宽考虑溢出 // 假设overflow_count在上升沿和下降沿之间没有变化短脉冲 // 长脉冲需要更复杂的溢出追踪逻辑 if (capture_falling capture_rising) { pulse_width_us capture_falling - capture_rising; } else { // 下降沿计数值小于上升沿说明中间发生了计数器溢出从ARR回到0 // 此时脉宽 (ARR - capture_rising 1) capture_falling pulse_width_us (__HAL_TIM_GET_AUTORELOAD(htim3) - capture_rising 1) capture_falling; // 如果开启了溢出中断并计数还需要加上 overflow_count * (ARR1) } // 计算得到的是计数值乘以计数周期才是时间。这里1MHz时钟1计数值1us。 // pulse_width_time pulse_width_us * 1us; // 切换回上升沿捕获准备下一次测量 __HAL_TIM_SET_CAPTUREPOLARITY(htim3, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); capture_state 0; // 在这里可以使用pulse_width_us例如通过串口打印 // printf(Pulse Width: %lu us\n, pulse_width_us); break; } } }这段代码的精髓在于对计数器溢出的处理。当脉冲宽度可能超过定时器ARR设置的最大计数值时计数器会从0重新开始。如果只简单用下降沿值减上升沿值在发生溢出后结果会是负数实际是很大的正数。上面的代码通过判断capture_falling和capture_rising的大小关系处理了一次溢出的情况。对于更长的、可能发生多次溢出的脉冲就需要借助overflow_count这个全局变量来记录溢出的次数计算绝对时间戳绝对时间戳 overflow_count * (ARR 1) CCR值。在上升沿和下降沿分别记录下当时的绝对时间戳两者相减即得脉宽。5. 高频信号捕获、滤波与抗干扰实践当被测信号频率很高或环境噪声较大时简单的配置可能无法稳定工作。5.1 输入滤波器的使用在CubeMX的通道配置中IC Filter选项就是数字滤波器。它是一个事件计数器只有当连续N个采样点都满足边沿条件时才认为是一个有效的边沿事件。N的值由IC Filter的数值决定对应特定的采样时钟数和事件数。增大滤波值可以有效抑制高频毛刺但会带来两个副作用一是增加了输入延迟二是可能滤掉真正的高频窄脉冲。我的经验法则是滤波时间常数应设为最小需要识别的脉冲宽度的1/3到1/5以下。例如要测量1us的脉冲滤波时间最好小于200ns。对于72MHz定时器时钟8分频后为9MHz一个采样周期约111ns滤波值设为2即连续2个采样带来的延迟约222ns尚可接受。5.2 捕获分频器 (IC Prescaler)这个参数容易被忽略。它可以设置为每N个事件才捕获一次。例如设置为“每2个事件”那么每两个上升沿才会触发一次捕获。这主要用于测量频率可以降低CPU中断频率。但在测量脉宽时绝对不能使用分频否则会丢失边沿事件导致测量完全错误。务必保持为No division。5.3 高频信号测量的限制定时器输入捕获的最高频率受限于多个因素定时器时钟频率这是理论上的最高采样频率。输入滤波器滤波器会限制最高有效信号频率。中断响应与处理时间如果每个边沿都进中断且中断服务程序执行时间过长在高频下会导致CPU负载过高甚至丢失中断。对于高频周期信号更推荐使用PWM输入模式或编码器模式让硬件自动测量或者使用DMA将CCR值循环传输到内存再由主程序批量处理彻底解放CPU。5.4 硬件连接注意事项信号完整性对于边沿敏感的测量确保信号线远离噪声源如电机、电源线。可以使用双绞线或屏蔽线。电平匹配确保输入信号的电压电平在MCU GPIO的容限范围内通常是0-3.3V。对于5V信号必须使用电平转换电路或分压电阻。上拉/下拉电阻如果信号源是开漏输出或者需要确定空闲状态应在GPIO引脚配置内部或外部上拉/下拉电阻。在CubeMX的GPIO配置中即可设置。6. 调试技巧与常见问题排查实录即使配置正确调试阶段也可能遇到各种奇怪的问题。下面是我总结的常见问题清单和排查手段。现象可能原因排查方法捕获不到任何中断1. 定时器时钟未使能。2. GPIO引脚模式错误应为Alternate Function。3. 中断未使能或在NVIC中优先级配置有冲突。4. 捕获极性设置与信号实际边沿相反。1. 检查__HAL_RCC_TIMx_CLK_ENABLE()是否被调用CubeMX通常已生成。2. 在CubeMX中确认引脚功能用万用表或逻辑分析仪测量引脚是否有信号。3. 在CubeMX的NVIC配置中确认中断已勾选。在main.c中搜索HAL_TIM_IRQHandler是否被正确调用。4. 用示波器观察信号并核对Polarity Selection。测量值跳动大不稳定1. 信号本身有噪声或振铃。2. 没有使用滤波或滤波值设置不当。3. 中断处理时间过长错过了后续边沿。4. 计数器溢出未处理。1. 用示波器观察信号质量增加硬件滤波RC电路。2. 适当增加CubeMX中的IC Filter值。3. 优化中断服务函数只做最必要的操作读值、改极性。将复杂计算移到主循环。4. 确保开启了更新中断并正确处理溢出计数。脉宽测量值恒为0或极小1. 上升沿和下降沿捕获的是同一个值极性切换太慢或太快。2. 用于计算脉宽的两个变量是同一个或计算逻辑错误。3. 信号脉宽本身极窄接近或小于系统响应时间。1. 在中断回调开头和结尾加调试引脚电平翻转用示波器测量中断响应和极性切换时间。2. 仔细检查代码确保capture_rising和capture_falling是两个不同的变量并在正确的位置赋值。3. 对于纳秒级窄脉冲考虑使用定时器的外部触发或高级控制功能或者换用更高主频的MCU。测量长脉宽时结果错误1. 未考虑计数器溢出。2. ARR设置过小溢出频繁软件溢出计数变量溢出。1. 实现如第4节所述的溢出处理逻辑。2. 增大ARR值或降低定时器计数频率增大PSC使溢出周期大于被测脉宽。对于超长测量可以使用32位定时器如LPTIM或软件扩展计数。PWM输入模式测得频率正确占空比不对1. 两个通道的滤波设置不一致。2. 信号占空比极端接近0%或100%下降沿捕获可能不稳定。3. 理解错误PWM输入模式下占空比 (CCR2 / CCR1) * 100%前提是CH1捕获周期CH2捕获脉宽。1. 检查两个通道的IC Filter和IC Prescaler是否完全相同。2. 检查信号质量极端占空比可能使下降沿幅度不足。3. 确认数据手册读取正确的CCR寄存器进行计算。一个高级调试技巧使用GPIO模拟“软件逻辑分析仪”。在中断服务函数的开始和结束位置分别控制一个空闲的GPIO引脚置高和置低。// 在中断回调开始 HAL_GPIO_WritePin(DEBUG_PIN_GPIO_Port, DEBUG_PIN_Pin, GPIO_PIN_SET); // ... 中断处理逻辑 ... HAL_GPIO_WritePin(DEBUG_PIN_GPIO_Port, DEBUG_PIN_Pin, GPIO_PIN_RESET);然后用示波器同时观察这个调试引脚和你的输入信号。你可以清晰地看到从边沿触发到中断服务结束的延迟时间这对于优化代码和判断是否错过边沿至关重要。最后关于资源选择对于简单的边沿计数或低频测量基本定时器加外部中断也能做但输入捕获的优势在于其硬件级的精确计时。对于电机控制、高频脉冲计数等复杂场景高级定时器TIM1, TIM8的互补通道和刹车功能可能会派上用场。掌握输入捕获是玩转STM32定时器家族的重要一步它为你打开了一扇精确测量和控制时间事件的大门。