STM32输入捕获原理与应用:从脉宽测量到PWM输入模式详解

STM32输入捕获原理与应用:从脉宽测量到PWM输入模式详解

1. 从“计时”到“捕获”:为什么我们需要输入捕获?

如果你已经跟着教程玩过STM32的通用定时器,那你应该对它的“计时”和“输出”功能不陌生了。比如,我们可以让定时器每隔1毫秒产生一个中断,或者让它在某个引脚上输出一个精确的PWM波。这些功能的核心,是定时器在“主动”地工作:它按照我们设定的节奏,有条不紊地计数、比较、触发动作。

但现实世界中的信号,往往不是那么听话地按我们的节奏来。比如,你想测量一个外部按键被按下的持续时间,或者一个旋转编码器输出的脉冲宽度,甚至是一个超声波模块返回的高电平时间。这些信号的到来是“被动”的、随机的。定时器如果还像以前那样自顾自地数数,就没办法精确捕捉这些外部事件发生的时刻和持续的长度。

这时候,输入捕获(Input Capture)功能就登场了。它就像是给定时器这个“时钟”装上了一双敏锐的“眼睛”和一只快速的“手”。当指定的外部引脚上发生一个特定事件(比如上升沿或下降沿)时,这双“眼睛”会立刻发现,然后这只“手”会以最快的速度,把当前定时器的计数值(CNT)瞬间“抓拍”下来,保存到一个专用的捕获/比较寄存器(CCR)里。这个过程是硬件自动完成的,速度极快,不受软件中断延迟的影响,从而实现了对信号边沿时间的超高精度测量。

所以,TIM输入捕获要解决的核心问题就是:如何高精度地测量外部信号的脉宽、周期或频率。这是从“主动控制”到“被动感知”的关键一步,在电机测速、红外解码、频率计、脉冲计数等场景中不可或缺。理解了这一点,我们再去配置那些寄存器,就会明白每一个操作背后的意图,而不是机械地抄代码了。

2. 输入捕获的完整工作链路与核心寄存器剖析

输入捕获不是一个孤立的开关,而是一套精密的联动机制。为了彻底搞懂它,我们必须把从引脚到寄存器,再到中断的整个数据流和事件流梳理清楚。以最常见的测量高电平脉宽为例,其硬件工作链路如下图所示(我们会在后续章节用文字详细拆解每一步):

整个链路可以分解为几个关键环节,每个环节都对应着寄存器的特定配置。

2.1 信号输入通道与边沿检测器

首先,外部信号需要通过GPIO引脚进入定时器。STM32的定时器输入通道通常与特定的GPIO引脚复用,需要先将引脚配置为复用功能模式,并映射到对应的定时器通道上。

信号进入定时器后,首先到达的是边沿检测器。这个检测器的工作状态由TIMx_CCER寄存器中的CCxPCCxNP位控制。对于基本输入捕获,我们通常只使用CCxP位:

  • CCxP=0:捕获发生在上升沿。
  • CCxP=1:捕获发生在下降沿。

这个配置决定了“眼睛”在什么时刻会触发“抓拍”。

2.2 捕获的瞬间:预分频器、滤波器与CCR寄存器

当边沿检测器触发后,信号并不会直接去“抓拍”CNT值,而是会先经过两个可选的“处理站”:输入滤波器和预分频器,它们由TIMx_CCMRx寄存器配置。

  1. 输入滤波器(ICxF):这相当于一个数字防抖模块。如果输入信号有毛刺,滤波器可以设置一个采样窗口,只有当信号在连续N个时钟周期内保持稳定,才认为是一个有效的边沿。这对于消除机械开关抖动或噪声干扰非常有用。
  2. 输入预分频器(ICxPSC):这是一个“事件分频器”。比如设置为01(2分频),则意味着每2个有效的边沿事件,才会触发一次捕获。这常用于测量频率,你不需要捕获每个边沿,只需每隔几个周期捕获一次来计算频率,可以降低CPU中断负担。

信号经过(或绕过)这两个模块后,真正的“抓拍”动作发生:硬件将此刻定时器计数器TIMx_CNT的值,一次性锁存到对应的捕获/比较寄存器TIMx_CCRx中。这是输入捕获最核心的一步,CCRx里保存的就是边沿发生时刻的时间戳。

2.3 通知CPU:中断与DMA

“抓拍”完成后,硬件需要通知CPU:“数据已就绪,快来处理!”。这里有两种方式:

  1. 中断通知:这是最常用的方式。当捕获事件发生时,硬件会置位TIMx_SR寄存器中的CCxIF标志位。如果我们在TIMx_DIER寄存器中使能了CCxIE中断,就会触发定时器的捕获中断。在中断服务函数里,我们可以读取CCRx的值,并进行计算。
  2. DMA请求:在超高频率或需要连续捕获的场景下,频繁进入中断可能成为瓶颈。此时可以启用DMA。当CCxIF标志置位时,它也可以作为一个DMA请求源,自动将CCRx寄存器中的值搬运到指定的内存数组中,完全不需要CPU干预。这对于高速数据流采集至关重要。

2.4 定时器时基:测量的尺子

别忘了,CCRx里捕获的只是一个数字,这个数字的意义取决于TIMx_CNT这把“尺子”是怎么走的。定时器的时基单元(由TIMx_PSCTIMx_ARR等寄存器配置)决定了这把尺子的刻度。

  • 时钟源:CNT的时钟从哪里来?内部时钟(CK_INT)?外部时钟?还是其他定时器?
  • 预分频器(PSC):对时钟源进行分频,得到计数时钟CK_CNT。若系统时钟72MHz,PSC=71,则CK_CNT = 72MHz / (71+1) = 1MHz,即CNT每1微秒加1。
  • 计数模式:向上、向下还是中央对齐?这决定了CNT的变化规律。
  • 自动重装载值(ARR):CNT计数的上限。达到ARR后,根据计数模式决定是清零、从ARR向下计数还是产生更新事件。

测量的本质,就是计算两次捕获的CNT值之差,再乘以CNT的时钟周期。因此,时基配置的精度直接决定了测量的精度。

3. 实战:测量PWM波高电平脉宽的两种经典方案

理论很丰满,现在我们来点实际的。假设我们要测量一个PWM信号的高电平宽度,这是一个非常经典的需求。这里我分享两种最常用、也最具代表性的方案,并详细解释为什么这么设计,以及它们各自的优劣。

3.1 方案一:双沿捕获 + 中断处理(适合中低频,思路清晰)

这是最直观的方法:分别在上升沿和下降沿各捕获一次,两次捕获值之差就是高电平期间CNT计数的个数。

步骤拆解与配置详解:

  1. 初始化时基:配置定时器以合适的频率计数。例如,使用72MHz系统时钟,PSC设为71,则计数频率为1MHz(1微秒/计数)。ARR可以设到最大(如0xFFFF),因为我们只关心两次捕获的差值,只要差值不超过ARR,就不会有定时器溢出问题。

    // 时基单元初始化示例 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_Period = 0xFFFF; // ARR,自动重装载值 TIM_TimeBaseInitStructure.TIM_Prescaler = 71; // PSC,预分频器,72M/(71+1)=1M TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数 TIM_TimeBaseInit(TIMx, &TIM_TimeBaseInitStructure);
  2. 配置输入捕获通道:关键在这里,我们需要在运行中动态切换捕获边沿

    • 初始状态:设置为上升沿捕获。当第一个上升沿到来时,捕获CNT值到CCR1,并进入中断。
    // 初始配置为上升沿捕获 TIM_ICInitTypeDef TIM_ICInitStructure; 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(TIMx, &TIM_ICInitStructure);
    • 中断服务函数逻辑:在第一次(上升沿)捕获中断中,我们做三件事: a. 读取CCR1的值,存为Rise_Value。 b.立即将捕获极性改为下降沿(TIM_SetIC1Polarity(TIMx, TIM_ICPolarity_Falling))。 c. 清除中断标志。
    • 当下一个下降沿到来时,硬件会自动捕获CNT值到CCR1,并再次进入中断。在第二次(下降沿)中断中: a. 读取CCR1的值,存为Fall_Value。 b. 计算脉宽:PulseWidth = Fall_Value - Rise_Value(注意处理CNT溢出,见后文)。 c.将捕获极性改回上升沿,为下一次测量做准备。 d. 清除中断标志。

注意:这里有一个非常重要的细节。TIM_ICInit函数在标准库中,会同时配置CCMR和CCER寄存器。而切换极性时,我们使用TIM_SetIC1Polarity函数,它只修改TIMx_CCER寄存器的CC1P位,这是一种更安全、更高效的做法,避免了重新初始化整个通道可能带来的副作用。

方案一优缺点分析:

  • 优点:原理简单,易于理解和调试。只需要一个捕获通道。
  • 缺点:每个PWM周期需要进入两次中断,CPU开销较大。如果PWM频率很高(比如几十kHz),中断频繁,可能影响系统其他任务。另外,在中断中切换极性有极小的软件延迟,对于超窄脉冲可能引入误差。

3.2 方案二:PWM输入模式(硬件全自动,适合高频)

STM32的通用定时器提供了一个强大的“作弊”功能——PWM输入模式。它本质上是将两个输入捕获通道(通常是TI1和TI2)联动起来,硬件自动完成对一个周期内上升沿和下降沿的捕获,并且将两次捕获值存放到不同的CCR寄存器中,通常只产生一个中断。

配置精髓:

  1. 信号连接:将被测PWM信号连接到定时器的TI1引脚。
  2. 寄存器配置魔法
    • TIMx_CCMR1寄存器中的CC1S位设置为01,表示TI1映射到IC1
    • 关键步骤:将CC2S位设置为10,表示TI1也映射到IC2。这样,同一个TI1输入信号,同时进入了IC1和IC2两条捕获通路。
    • 配置IC1捕获上升沿,IC2捕获下降沿。
    • 最后,设置TIMx_SMCR寄存器中的SMS=111,选择“复位模式”,并将TS位选择为TI1FP1。这个配置的意思是:当TI1上出现上升沿(IC1捕获)时,不仅会捕获CNT值到CCR1,还会同时将CNT计数器清零(复位)
  3. 硬件自动流程
    • 第一个上升沿到来:IC1捕获CNT值到CCR1(此时CNT可能是任意值),同时CNT被清零,开始重新向上计数。
    • 接下来的下降沿到来:IC2捕获CNT值到CCR2。这个值就是从上升沿到下降沿的计数,即高电平脉宽
    • 再下一个上升沿到来:IC1再次捕获CNT值到CCR1。注意,此时CNT已经从一个上升沿计数到了下一个上升沿,所以CCR1中的值就是整个PWM信号的周期
    • 同时,这个上升沿再次将CNT清零,循环往复。

如此一来,我们只需要使能CC1IE中断(上升沿中断),在中断里同时读取CCR1(周期值)和CCR2(脉宽值),就能一次性获得周期和占空比!

方案二优缺点分析:

  • 优点:硬件自动完成,每个PWM周期只产生一次中断,效率极高,软件开销极小,精度最高(无软件切换延迟)。
  • 缺点:占用了一个定时器的两个捕获通道(IC1和IC2)。逻辑上稍微复杂一点,需要理解SMCR寄存器的复位模式。

个人心得:在资源允许的情况下,优先考虑PWM输入模式。它不仅是“高效”的体现,更是“把专业的事交给硬件”这一嵌入式设计哲学的典范。除非定时器通道特别紧张,或者需要测量非PWM的单个脉冲宽度,否则双沿切换方案更多是用于理解原理。

4. 精度提升与边界处理:溢出、滤波与时钟校准

即使方案选对了,在实际测量中还是会遇到各种影响精度和稳定性的“坑”。这部分是教程里往往一笔带过,但实际开发中必须严肃对待的。

4.1 计数器溢出处理:让测量范围突破16位

我们的CNT计数器是16位的(对于通用定时器),最大值65535。当时基时钟为1MHz时,最大只能测量65.535ms的脉宽。超过这个时间,CNT就会从0重新开始(溢出)。如果我们简单地用Fall_Value - Rise_Value,当下降沿发生在下一个计数周期时,计算结果会是负数或一个很小的值,完全错误。

解决方案:引入溢出计数器。

  1. 使能更新中断:在TIMx_DIER寄存器中使能UIE(更新中断)。每当CNT从ARR溢出回到0时,就会触发此中断。
  2. 在中断中维护一个全局变量:例如uint16_t TIMx_Overflow_Cnt = 0;。每次进入更新中断,这个变量加1。
  3. 修正捕获计算:在捕获中断中计算脉宽时,公式变为:
    // 假设 Rise_Value 和 Fall_Value 是捕获时刻的CCRx值 // Overflow_Cnt_Rise 和 Overflow_Cnt_Fall 是捕获时记录的溢出计数器快照 uint32_t total_ticks = (uint32_t)Fall_Value - (uint32_t)Rise_Value; total_ticks += (uint32_t)(Overflow_Cnt_Fall - Overflow_Cnt_Rise) * 65536UL;
    这样,测量的时间范围就从16位扩展到了(溢出次数 * 65536 + CCR差值),只要你的溢出计数器变量足够大(比如32位),理论上可以测量任意长的时间。

踩坑记录:务必在捕获中断中立刻读取并保存当前的溢出计数器值,因为更新中断可能随时发生。一个经典的错误是:先读CCR,再读溢出计数,在这两条语句之间恰好发生了一次溢出,导致计数对不上。更稳妥的做法是,在进入捕获中断后,暂时关闭更新中断,读取并保存所有必要值后再开启,但这会增加中断延迟。需要根据实际情况权衡。

4.2 输入滤波器的合理使用:在稳定性和响应速度间权衡

输入滤波器不是摆设,在信号有噪声时是救命稻草。但滤波深度(ICxF位的值)设置不当,会带来新问题。

  • 滤波过深:例如,设置需要连续8个时钟采样稳定才确认边沿。如果信号频率很高,边沿变化快,可能永远无法满足滤波条件,导致信号“消失”,无法触发捕获。
  • 滤波过浅或无滤波:信号上的毛刺会被误认为是边沿,导致捕获值剧烈跳动,测量结果不稳定。

如何设置?这里有个经验法则:滤波器的窗口时间T_filter = (ICxF) * T_ck_int。其中T_ck_int是定时器输入滤波器的时钟周期(通常等于APB总线时钟或2倍频后的时钟,需查数据手册)。设置的原则是:让T_filter略大于你信号上可能出现的最大毛刺宽度,同时远小于你待测信号的有效脉宽或周期。例如,信号有100ns的毛刺,系统时钟72MHz,则T_ck_int约13.9ns。设置ICxF=0x8(8次采样),则T_filter ≈ 111ns,可以滤掉这个毛刺。如果待测脉宽为1ms,则滤波窗口仅占0.011%,影响可忽略。

4.3 时钟精度的影响与校准

一切测量的基石是时钟。如果你用的HSI(内部高速RC振荡器),其典型精度可能只有±1%,这意味着你的1MHz时基实际可能在0.99MHz到1.01MHz之间波动,测量结果自然也有±1%的误差。对于精度要求高的场合(如精密测速),必须使用高精度的外部晶振(HSE)。

更进一步,即使使用了HSE,定时器的时钟路径(从晶振到APB总线,再到定时器)也可能存在微小的分频误差。对于极端苛刻的场合,可以考虑用更高精度的外部时钟源(如GPS的1PPS信号)来校准定时器,或者使用定时器的外部时钟模式。

5. 从输入捕获到高级应用:编码器接口与触发注入

掌握了基本输入捕获,其实你已经摸到了STM32定时器一些高级功能的大门。它们本质上都是输入捕获机制的组合与升级。

5.1 正交编码器接口:本质上是双通道输入捕获的协同

读取旋转编码器(如伺服电机上的)是工业控制中的常见任务。编码器输出两路相位差90度的方波(A相和B相)。STM32的定时器硬件直接提供了“编码器接口模式”,可以自动根据A、B相的边沿和相对相位,判断转向并计数。

你可以这样理解它:它相当于同时配置了两个输入捕获通道(CH1和CH2),都工作在双边沿捕获模式。硬件内部有一个逻辑单元,根据两个通道捕获事件的先后顺序,自动控制CNT计数器向上或向下计数。你只需要读取CNT的值,就知道编码器转了多少个“滴答”,根据计数方向就知道正反转。这比用外部中断手动判断A、B相边沿要高效、可靠得多。

5.2 定时器级联与触发注入:实现同步与精准时序

单个定时器的测量范围有限。有时我们需要测量很长的脉冲,或者需要多个定时器同步动作。这时就需要用到定时器的从模式触发功能。

  • 主从定时器级联:你可以配置一个定时器(TIM2)作为主模式,输出一个更新事件作为触发信号(TRGO)。另一个定时器(TIM3)配置为从模式,选择ITR0(内部触发0,具体连接关系查手册)作为时钟源。这样,TIM2的每次溢出更新,都会驱动TIM3的CNT加1。TIM3的CNT变成了TIM2的溢出计数器。此时,用TIM3做输入捕获,其CNT的高16位(TIM2的溢出次数)和低16位(TIM2的CNT值)共同组成了一个32位甚至更宽的计数器,可以测量超长时间。
  • 触发注入:这是一个更高级的功能。假设你在用定时器输出复杂的PWM序列(如电机驱动SVPWM),同时需要在一个由外部事件(如过流保护信号)触发的特定时刻,立刻捕获当前的时间点,用于故障分析。你可以配置输入捕获通道为“触发注入”模式。当指定的外部触发信号到来时,硬件会暂停当前的输出比较等操作,强制进行一次捕获,并将结果存到专用的寄存器中。这保证了即使在定时器忙于输出时,也能以最高优先级记录下关键事件的发生时刻。

从基础的脉宽测量,到编码器读数,再到复杂的定时器网络同步,其内核思想一脉相承:利用硬件捕获外部事件发生的时间戳。理解了输入捕获这个核心,再去学习这些高级功能,你会发现它们不再是孤立的、复杂的知识点,而是同一棵树干上生长出的不同枝丫。