引言
前面学习了通用定时器的输出比较功能,实现了输出希望的方波或PWM波形,用于电机控制或LED亮度调节等。那么接下来,我们继续学习另一个功能------输入捕获功能,简单说,前面学的那是我们往外输出,那么本次学的就是往内输入产生的不一样的碰撞。话不多说,首先我们介绍输入捕获功能的作用及工作原理。
一、输入捕获功能概述
输入捕获可以用来测量PWM波形的周期和频率,听到这个可能会有些蒙,毕竟前面学习的定时器主要都是用来计数或者输出0或1产生方波的,这里咋还能测周期了呢?其实主要还是通用定时器有了这个捕获的功能,简单说他就可以检测输入波形的上升沿或者下降沿,然后此时开始计数,然后再次出现上升沿或者下降沿的时候就将计数数值存起来,此时这个数值就是输入波形的一个周期了,这样就测出来了,所以这就是输入捕获功能的基本原理。
接下来,我们借助手册框图再详细介绍一下输入捕获的作用以及工作原理。
二、输入捕获的作用
根据前面对通用定时器的功能框图介绍可知其中下半部分为四个信号输入输出的通道,对应了前面介绍的输出比较以及本次介绍的输入捕获功能。显然捕获的应该是外部经过通道输入的东西,这个东西不能是完整的信号,最可能捕获的应该是某些更具标志性的变化瞬间 ,换句话说输入捕获的作用即捕获从外部输入到通道上的信号的上升沿或者下降沿。
由于能够捕获边沿信号,因此只需测出连续两个上升沿 或者下降沿 产生的时间间隔即可测量PWM波形的周期,通过周期与频率关系(f=1/T)也可得到PWM波的频率。
同时,同理也可测量出PWM波形的占空比,只需测量连续的一个上升沿和一个下降沿的时间间隔 ,然后除以PWM波形周期即可。换句话说,可以测出一段方波中一个周期的高电平或低电平的占比大小(高低电平宽度)。

因此,总结一下:
输入捕获能够捕获边沿信号 (上升沿或者下降沿),常常被用于测量PWM波形的周期、频率以及占空比等行为。
三、输入捕获的工作原理
接下来,我们对照通用定时器的框图介绍输入捕获的工作原理,打开参考手册找到框图如下图所示,只截取了相关部分。

3.1 输入捕获功能结构概述
首先,输入捕获功能对应在框图上的内容如下图所示。

其中,这部分可分为输入部分 、时基单元(计数部分 )以及捕获部分(捕获/比较寄存器)三个部分,也就是下图所框内容。

首先介绍输入部分 。图中可以看出,输入部分包含4路输入,外接引脚对应4个不同的复用通用定时器通道输入输出的GPIO端口。如果还记得前面输出比较所述,可以发现输入通道与输出通道是一致的,即对应的引脚也是一致的,这意味着我们同一时间只能进行输出比较或者输入捕获的功能,而不能同时进行,因此对于同一路引脚,只能处于输入捕获或者输出比较。
除了引脚,可以看见通道1的构成略显不同。其中输入的信号还会和自身或者通道2或3发生异或后的输入信号进行两路选择,再变成TIx(x=1、2、3、4)信号 。而且输入部分还有输入滤波器、边沿检测器以及预分频器等器件,它们主要是用于优化输入信号的。
其次是计数器部分。这部分大家应该非常熟悉,CNT计数器通过我们选择的技术模式进行计数,比如以向上计数为例,计数到自动重装载寄存器的值ARR时的下一个时钟上升沿会产生溢出事件,随后从0开始计数,循环往复。
最后是捕获部分 ,即捕获/比较寄存器。图中可见对应4个通道,一个通道有一个捕获/比较寄存器,且各个捕获/比较寄存器均与CNT计数器有总线关联。该寄存器与输出比较寄存器共用,因此同一通道的寄存器只能用于记录输入捕获或者输出比较的相关数值。
3.2 对应框图介绍原理
输入捕获在框图上的体现大家应该比较清楚,接下来就对照着详细介绍其工作原理,我们以通道1为例进行介绍,如下图所示部分,且其他通道基本类似。

假设本次计数器的计数方式为向上计数,自动重装载寄存器的值应该给到最大即65535,尽可能避免还未捕获到就出现计数器溢出了。
那么按照外部信号进入的走向来看,首先,信号会经过TIMx_CH1引脚输入进到通道1,然后若通道2或3也有信号输入,则输入的信号可能还会与通道1或通道2或3输入的异或后的信号进行多路选择变成TI1信号,如下图所示;

其次,TI1信号进入输入滤波器和边沿检测器 。看名字也容易理解,输入滤波器 用来对输入的信号进行滤波,比如输入信号如果不稳定,存在毛刺信号的话经过滤波器就可以尽可能去除,当然信号质量好则可以不滤波;边沿检测器 就是用来检测输入信号的边沿信号的,如上升沿或者下降沿,这用于确定我们希望捕获的边沿是上升沿还是下降沿,该设置对应前面输出比较的通道极性选择。
可以看见经过这俩器件后会产生两组信号,分别是TI1FP1和TI2FP2 ,也就是TI1经过FP滤波边沿检测后得到的信号,它们有三种走向,一是TI1FP1信号直接往右走,二是是前面介绍时钟来源时该信号还可以作为外部时钟源的一种,三是TI1FP2还可以给通道2去使用,同时通道2的TI2FP1和TRC(触发控制产生的信号)也可以在通道1进去,因此此时就是TI1FP1、TI2FP1以及TRC信号经过多路选择器继续往右走变成IC1信号,也就是正式成为通道1输入捕获的信号(Input Capture 1),如下图所示。

接着,IC1信号进入预分频器 ,这里的预分频器与时基单元那的预分频器实际上是一样的功能也就是对信号频率进行分频降低,在这里若捕获信号频率较高,可以对捕获的信号进行分频以此降低信号频率,这一般用来处理输入的高频信号,如下图所示。

然后信号从预分频器出去变成IC1PS,也就是Input Capture PSC输入捕获分频信号,很好理解就是分频后的输入捕获信号,该信号产生后就会出现以下几种行为:
1、产生一个捕获比较事件U;
2、若开了中断,也会产生捕获比较中断CC1I(Capture Compare 1 Interrupt);
3、立即把计数器寄存器的值存入到捕获寄存器中,且下次捕获事件产生之前,捕获寄存器的值都不会发生变化。

到这里,定时器就算捕获到当前输入信号的边沿了,然后无非是多次捕获,进而得出连续的边沿的时间间隔。
3.2 原理举例说明
借助框图介绍了原理,可能还是略显抽象,因此接下来,我们举例来说明输入捕获功能对PWM信号的周期进行测量的过程。
为了方便处理,我们假设输入信号质量比较好 ,且频率适中,因此在捕获前不进行滤波和分频,同时我们以捕获上升沿为例。

假设使用的内部时钟源72MHz,为了方便计算,且使计数器频率尽可能高(避免输入信号频率太高而出现计数一次出现多次上升沿,导致不方便测量),所以对计数器时钟进行72分频得到10MHz的计数频率,也就是1us计数一次。(当前,若输入信号的频率高于10MHz,还未计数1次就出现上升沿的话,此时捕获用于测量也会比较麻烦)
然后计数方式设置为向上计数,接着设置自动重装载值寄存器的值ARR,为了避免输入信号频率太小导致产生一个上升沿需要计数器计数很多次而发生溢出,所以尽可能将ARR设置大一些,即最大值65535,这样我们方便测量的输入信号的周期最小可以支持16Hz。(因为1/(65535*1us*10^-6) 约为15.26Hz,即计数65535才产生一次上升沿的输入信号频率即最小能支持的信号频率为15.26Hz,所以能支持至少16Hz的输入信号进行方便测量 )
因此,该假设下,我们方便测量的输入信号频率范围大概是16Hz~10MHz之间的信号。所以先说方便测量时的捕获测量过程:
首先,信号会经过通道输入进来被捕获到第一个上升沿信号,此时计数器寄存器的值会被重置,从0开始计数;
其次,当再次捕获,也就是捕获到第二次上升沿时,计数器寄存器的值会被自动复制一份给到捕获寄存器 ,这个时候我读取的捕获寄存器的值即为该输入信号的周期,单位us。如下图所示

以上就是测量的信号频率在16Hz到10Mhz之间的周期的基本过程。
当然,如果信号频率太高,超过了10MHz ,此时第一个上升沿到了都还没完成一次累加计数,那么等到第二次上升沿到了也是没有完成一次累加,这个时候测量起来就会比较麻烦,当然也能测。测量方法就是可以考虑测量第1个上升沿到第n个上升沿的时间间隔,然后除以n即可得到信号的周期了,如下图所示。

这种方法一般用于测量高频信号,即频率超过计数器时钟频率。
而频率要是太低,低到了16Hz,或者说15.26Hz,那首先这个自动重装载值是最大了改不了了,那理论上应该是降低计数频率,这样才能使能测的最小频率下降,但是这样我们能测的最大频率也会下降。
因此在实际测量时,我们没有适合所有情况的设置,而是需要根据实际情况设置合适的预分频数值和自动重装载值。
好了,以上就是关于输入捕获原理的介绍。
四、相关寄存器介绍
接下来,就进行实现输入捕获功能需要涉及到的寄存器作一下介绍,我们按照功能框图中的信号流向顺序依次介绍 吧,还是通道1为例。

从图中可以看出,我们可能要配置的应该是信号输入,所以通道方向肯定要配置为输入模式 ,且输入选择通道1的输入信号还是其他通道的输入信号也需要配置;
然后经过滤波器 和边沿检测器,所以滤波器肯定会要配置,边沿检测实际就是通道极性,这也要配置;
然后是经过多路选择,因此输入的信号是对应哪一个我们其实也要设置,这实际上和配置通道方向是一起设置的;
接着就是设置预分频器,确定具体要怎么分频;
最后要开启捕获中断的话,也要配置输入捕获中断相关的。
对于捕获寄存器内容,前面介绍过他与计数器间的复制行为是自动进行的,所以无需软件设置。
4.1 TIMx_CR2寄存器
首先是要设置输入的信号选择,因为框图中表示形成TI1前有多路选择,可以是通道1来的,也可以是通道2或3来的,所以需要对输入信号选择一下,其在控制寄存器2中进行配置,如下图所示

如图中红框的TI1S位,即用于选择产生TI1信号的输入信号,介绍如下图所示

可见,若TI1信号由通道1引脚输入产生,则将TI1S位置0即可,参考代码如下
cpp
// TI1的选择: CH1引脚直接连接到TI1 TI1S-0
TIM4->CR2 &= ~TIM_CR2_TI1S;
4.2 TIMx_CCMR1寄存器
后面几个寄存器大家应该比较熟悉了,不过我们只是配置其中的某些位,所以重点还是在于知道什么功能该配置什么位。

如上图所示,这是CCMR1寄存器,我们本次是输入捕获,所以该寄存器会涉及到的位如上图红框所示,分别是CC1S,设置通道方向;IC1PSC,设置预分频器;IC1F设置输入滤波器的。接下来,我们逐一介绍这3位的配置方式。
关于这个CC1S ,可以发现输入有三种情况:

首先是01 ,IC1映射到TI1上,也就是通道1捕获的信号又TI1传递过来的,对应框图多路选择的第一种,如下图所示。
可以理解为,这是一种直接映射关系,表示后面的捕获信号直接由通道1输入后处理形成。
其次是10 ,IC1映射到TI2上,这对应框图多路选择的第二种,如下图所示。
可以理解为,这是一种交叉映射关系,表示通道1的捕获信号由通道2输入后处理然后给通道1形成的。
最后是11 ,IC1映射在TRC上,这是对应框图多路选择的第3种,是一种触发输入的信号,后面介绍到再说,这里暂时不做说明。
而本次我们以通道1输入为例,因此其他通道不会有信号输入,因此对于CC1S的配置设置为01即可,参考代码如下。
cpp
// 仅通道1输入时,输入模式 IC1映射TI1 CC1S-01
TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;
然后是IC1PSC位,显然这是输入捕获的预分频器配置位,介绍如下图所示。

可以看出对输入的信号进行预分频的方法是出现n次进行一次捕获实现的n分频,而能够设置的分频系数只有4种,即不分频、2分频、4分频以及8分频。
若不进行分频,则IC1PSC配置为00即可,参考代码如下
cpp
TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC;
最后是IC1F位,也就是输入滤波器配置位,介绍如下图所示。

可以看出这实际上是芯片内部自带有一个数字滤波器,然后通过设置不同的采样频率实现的滤波效果。
若信号质量还好,可以不使用滤波,即该位配置成0000即可,参考代码如下
cpp
// 若信号质量不错,则输入滤波器不滤波 IC1F-0000
TIM4->CCMR1 &= ~TIM_CCMR1_IC1F;
以上就是输入捕获功能中CCMR捕获/比较寄存器中涉及到的需要配置的位。
4.3 TIMx_CCER寄存器
接着,按框图走向,滤波器前面说过了,然后应该就是边沿检测的设置 ,这其实对应的通道极性的设置,在CCER捕获/比较使能寄存器中,如下图所示

可见该寄存器包含了4个通道的配置,就是专门配置通道极性和使能的,对于通道1就是上图红框的两位,分别是CC1P和CC1E位。
首先说CC1P ,捕获/比较1极性,对于输入捕获就是配置捕获上升沿还是下降沿的,介绍如下图所示。

若希望捕获发生在上升沿,则CC1P设置为0即可,参考代码如下
cpp
// 设置边沿检测器 0上升沿 1下降沿
TIM4->CCER &= ~TIM_CCER_CC1P;
然后是CC1E ,捕获/比较1使能,对于输入捕获就是用于开启捕获的,介绍如下图所示

若要使用捕获功能,则将CC1E置1即可,参考代码如下
cpp
// 使能CH1通道捕获使能 0: 关闭 1:开启
TIM4->CCER |= TIM_CCER_CC1E;
以上就是输入捕获功能中CCER捕获/比较使能寄存器中涉及到的需要配置的位的介绍。
4.4 TIMx_DIER寄存器
然后基本上通道1一条路相关的配置就完了,捕获后会产生捕获事件,开了中断会产生捕获中断事件,而开启捕获中断涉及到的位就在DIER寄存器中,如下图所示,即CC1IE位。

可以看到IDER寄存器,也就是DMA/中断使能寄存器,其中有四个通道的捕获中断或普通的更新中断使能位以及DMA请求使能位等。
本次通道1的捕获中断使能位 即上图红框的CC1IE位,其介绍如下图所示。

显然,若开启捕获中断,则CC1IE位置1即可,参考代码如下
cpp
TIM4->DIER |= TIM_DIER_CC1IE;
好了,到这里,输入捕获新涉及到的一些寄存器的位就基本介绍完了。
五、输入捕获案例实操
接下来,就进入实操环节了,我们通过一个案例来使用通用定时器的输入捕获功能,熟悉其编码流程。
5.1 PWM波的周期与频率测量
5.1.1 需求描述
前面输出比较案例LED-2的呼吸灯生成了PWM波形,本次要求使用通用定时器的输入捕获功能来测量该PWM波形的周期/频率。
5.1.2 硬件电路设计
实际上就是使用PA1的TIM5_CH2复用功能生成PWM波,查看数据手册可得

然后我决定选择PB6来捕获测量PA1生成的PWM波形的周期频率,查看数据手册可知

利用PB6的TIM4_CH1复用功能即可,同时为了将PWM波输入给PB6,我们需要利用一根杜邦线,将PA1与PB6相连,使得PA1输出的PWM波可输入至PB6。
5.1.3 需求分析
根据需求描述可知,本次要实现的其实就是测量输入的PWM波形的周期频率,而PWM波形的来源就是前面输出比较由PA1生成的,所以这里我们可以直接使用就行。
而测量PWM波形使用的是PB6,复用TIM4_CH1,即可以利用TIM4的通道1来输入PA1输出的PWM波形,然后捕获其中的边沿信号,进而获取PWM波形周期频率。只不过我们需要自己额外利用杜邦线连接PA1和PB6。
因此,本次实现有前面的铺垫,实现起来主要在于对TIM4输入捕获的配置,思路还比较清晰。
5.1.4 软件设计(寄存器方式)
按照前面的分析,接下来就可以正式开始编写代码了。
本次编码,我们直接使用前面呼吸灯的工程进行修改 ,因为反正还要使用PA1生成的PWM波形。然后还要新加一个TIM4的头源文件,进行TIM4的输入捕获的配置等。因此本次工程的目录结构如下

主要涉及到的就是TIM5(生成PWM波)和TIM4(捕获PWM波并测量周期频率),其实还有串口重定向实现,因为后面会讲获取的周期频率通过串口打印出来显示在PC端。
对于TIM5生成PWM波的实现以及串口重定向实现,可参考前面输出比较功能的案例和串口重定向案例,链接如下
通用定时器_输出比较介绍及案例实践-CSDN博客https://blog.csdn.net/2301_79475128/article/details/152515745?spm=1001.2014.3001.5502STM32调试手段:重定向printf串口_stm32 printf重定向-CSDN博客
https://blog.csdn.net/2301_79475128/article/details/145305160 本次核心在于实现输入捕获的相关内容。
5.1.4.1 TIM4的初始化实现
那么首先,TIM4使用肯定得初始化一下。而定时器的初始化最开始那几步都差不多,因为复用的GPIO口,所以额外多了GPIO的配置,大概前几步一般都是开启时钟、配置GPIO工作模式、设置计数器时钟预分频数值、设置自动重装载值。
开启时钟很简单,就是开启TIM4的时钟和GPIO时钟,因为是使用PB6,所以开启个GPIOB时钟;
配置GPIO工作模式。PB6用于输入捕获,也就是做一个输入模式,等待PA1生成的PWM波形过来,因此设置一个浮空输入即可;
设置计数器时钟预分频数值。本次机械能输入捕获,所以尽可能让计数频率高点,为了计算方便,就进行72分频,即预分频数值设置为72-1即可;
设置自动重装载值。同理,为了尽可能在计数器溢出前捕获到PWM波的边沿,因此自动重装载值尽可能大,所以给最大65535即可。
这些代码实现起来比较简单,而且经过前面定时器案例后应该也比较熟练了,所以不再赘述,该部分参考代码如下
cpp
// 初始化
void TIM4_Init(void)
{
// 1. 开启时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
// 2. 配置GPIO工作模式 浮空输入 MODE-00 CNF-01
GPIOB->CRL &= ~GPIO_CRL_MODE6;
GPIOB->CRL |= GPIO_CRL_CNF6_0;
GPIOB->CRL &= ~GPIO_CRL_CNF6_1;
// 3. 时基单元
// 3.1 设置预分频值 72 1MHz
TIM4->PSC = 72 - 1;
// 3.2 设置重装载值 最大,避免溢出 65535
TIM4->ARR = 65536 - 1;
// 3.3 设置计数方向
TIM4->CR1 &= ~TIM_CR1_DIR;
/* ***** */
}
注:这是初始化的前一小部分内容啊,基本上定时器配置都会涉及,也就是时钟配置、GPIO配置以及时基部分(PSC、ARR)。
接着,就是TIM4初始化的重点了------输入捕获的核心配置部分。
关于输入捕获的配置,最简单的方式就是按照框图中信号输入到完成一次捕获的流程来,基本上对应着配置就好了,也就是前面我们介绍寄存器时所说的信号走向。本次使用的是TIM4通道1,我就截通道1部分的框图如下

如果还记得前面介绍的原理的话,如上图看就很清楚了,首先选择输入信号 为通道1引脚进来的形成TI1信号,然后进行滤波器配置 (本次直接输入芯片生成的PWM信号,质量还行,不必滤波以及后续的分频)、接着进行捕获的边沿设置(即通道极性) 形成TI1FP1/2信号,然后我们将IC1信号与TI1FP1直接映射 ,即使用TI1FP1信号去生成输入捕获1信号,然后设置预分频器 ,经过后形成IC1PS信号,最后我会利用捕获中断,所以还会开启捕获中断,产生捕获中断事件以及输入捕获事件,捕获到上升沿时将计数器值复制给捕获寄存器然后就完成一次捕获。
总结一下,配置的步骤即:
1、设置选择的输入信号:直接选择通道1引脚来的输入信号即可,即TI1S位配置0;
2、设置滤波器:芯片自身产生的PWM波,信号质量应该不错,所以不用滤波,即IC1F位配置为0000;
3、配置和使能通道极性(捕获的边沿信号):本次选择捕获上升沿信号,所以配置CC1P为0,然后使能通道即CC1E置1即可;
4、设置输入模式(对应图中"4"):使用TI1FP1直接映射给IC1,用TI1FP1信号作为输入捕获的信号,即CC1S位配置为01;
5、设置预分频器:本次不用作分频处理,即IC1PSC位给00即可;
6、开启输入捕获中断 ,配置中断:将IC1IE位置1即可开启捕获中断,然后配置中断,在NVIC中配置中断优先级相关内容。
上述即为完整的带捕获中断的输入捕获配置流程,还比较清楚,只是相对要配置的内容多一些,参考代码如下
cpp
// 4. 配置通道
// 4.1 选择通道1
TIM4->CR2 &= ~TIM_CR2_TI1S;
// 4.2 设置通道1滤波
TIM4->CCMR1 &= ~TIM_CCMR1_IC1F;
// 4.3 设置极性 上升沿
TIM4->CCER &= ~TIM_CCER_CC1P;
// 4.4 配置通道1为输入模式,直接映射IT1 CC1S-01
TIM4->CCMR1 &= ~TIM_CCMR1_CC1S_1;
TIM4->CCMR1 |= TIM_CCMR1_CC1S_0;
// 4.5 设置预分频 触发上升沿直接捕获一次IC1PSC-00
TIM4->CCMR1 &= ~TIM_CCMR1_IC1PSC;
// 4.6 使能通道1
TIM4->CCER |= TIM_CCER_CC1E;
// 4.7 使能捕获中断
TIM4->DIER |= TIM_DIER_CC1IE;
// 4.8 配置NVIC
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(TIM4_IRQn, 2);
NVIC_EnableIRQ(TIM4_IRQn);
注:这是TIM4初始化后半部分的代码,也就是通道1实现输入捕获功能的内容。关于TIM4的定时器使能开启和关闭问题,我们学HAL库的做法,单独使用函数进行封装。
到这里,关于TIM4的初始化就完成了。接下来我们应该是使能定时器,因为单独用函数封装了,所以我们分开后面来说。
5.1.4.2 TIM4定时器的开启与关闭
我们在写一下TIM4的开启和关闭,这与前面的定时器案例做法一样的,就是配置控制寄存器1的CEN位,来控制TIM4的使能或禁止,分别一句搞定,很简单,这里不再赘述。参考代码如下
cpp
// 开启定时器
void TIM4_Start(void)
{
TIM4->CR1 |= TIM_CR1_CEN;
}
// 关闭定时器
void TIM4_Close(void)
{
TIM4->CR1 &= ~TIM_CR1_CEN;
}
5.1.4.3 获取PWM周期/频率实现
TIM4的配置都完成了,接着就是获取捕获时放进捕获寄存器的数值了。按照前面原理介绍,捕获一次上升沿后,计数器的值CNT会马上复制一份给捕获寄存器CCR。那么我们获取连续两个上升沿之间的时间间隔的思路就是:因为我们开启了捕获中断,所以每捕获到一次就会触发一次捕获中断,那么我们就可以在第一次捕获到上升沿的时候进入中断处理程序中清零计数器的值,然后捕获到第二次上升沿的时候再次进入中断处理程序,此时我们判断这是第几次捕获到上升沿,如果是第二次,就记录此时的CCR的值,也就是连续俩上升沿的时间间隔即周期。
整理一下,测量的步骤就是:
1、要有一个变量记录捕获到上升沿的次数 ,还要有一个变量记录连续边沿之间的计数次数;
2、在TIM4的中断处理程序中自增捕获的次数 (因为进入一次中断处理程序表明捕获到一次边沿信号),并在出现边沿次数为1时清零计数器CNT ;并在次数为2时记录CCR的值,作为连续上升沿的计数次数,用于周期换算,最后清零次数;
3、定义获取PWM周期函数,将计数次数变成单位ms的周期时间并返回,要注意周期单位用ms;
4、定义获取PWM频率函数,返回周期的倒数,同时注意单位Hz。
这就是最容易想到的一个测量周期的思路。接下来就开始编写代码。
按照步骤,一是定义俩变量,我这定义cnt记录捕获边沿信号的次数、cycle记录连续边沿之间的计数次数;二是重写TIM4的中断处理程序,其中断名可在STM32的汇编文件中找到,如下图所示。

然后其中的逻辑就比较简单了,参考代码如下
cpp
uint8_t cnt = 0;
// 捕获计数
uint16_t cycle = 0;
// 捕获 中断处理函数 判断第几个上升沿产生
void TIM4_IRQHandler(void)
{
if (TIM4->SR & TIM_SR_CC1IF)
{
// 清除中断标志位
TIM4->SR &= ~TIM_SR_CC1IF;
// 计数增加
cnt ++;
if (cnt == 1)
{
// 第一次捕获周期,清零计数器
TIM4->CNT = 0;
}
else if (cnt == 2)
{
// 第二次捕获,读取周期
cycle = TIM4->CCR1;
// 清零计数
cnt = 0;
}
}
}
值得注意的是,不要忘记中断处理程序中的中断标志位判断和清除。
由于cycle是获取的计数次数,而计数频率为1MHz也就是1us,所以最后还需要就是将获取到的cycle换算成ms的周期时间,然后定义PWM周期获取函数返回,频率获取同理,不再赘述。参考代码如下
cpp
// 读取pwm的周期,单位ms
double TIM4_GetDutyCycle(void)
{
return cycle / 1000.0;
}
// 读取频率
double TIM4_GetFreq(void)
{
return 1000000.0 / cycle;
}
注:PWM周期频率可能为小数,所以返回值类型为浮点型;cycle记录的是连续边沿之间计数的值,TIM4定时器配置的计数频率为1us,所以返回周期前要换成ms,由于定义的周期返回值类型为浮点型,所以除以1000.0实现浮点转换,频率返回前同理要注意该问题。
5.1.4.4 main.c完善
最后,在main.c中进行相关初始化然后串口打印获取的周期频率即可。参考代码如下
cpp
#include "usart.h"
#include "tim5.h"
#include "tim4.h"
#include "Delay.h"
int main(void)
{
// 1. 初始化
USART_Init();
TIM5_Init();
TIM4_Init();
// 2. 开启定时器
TIM5_Start();
TIM4_Start();
// 死循环保持状态
while (1)
{
// 打印周期频率数据
printf("T = %.2f ms, freq = %.2f Hz\n", TIM4_GetDutyCycle(), TIM4_GetFreq());
// 延时1s
Delay_ms(1000);
}
}
最后编译,然后烧录后打开串口助手即可查看实验现象,如下图所示。

5.1.4.5 测量周期频率代码优化
回顾前面编写的测量周期频率的代码逻辑,会发现其中定义的全局变量cycle只是在中断中使用,然后直接做返回值返回了,而cycle使用也只是存CCR的值,然而实际CCR的值除了出现捕获事件的时候会复制CNT计数器的值以外,其他时候都不会发生改变 ,换句话说,CCR的值也只是每次发生捕获中断那小段时间会出现变化,所以这样看来我们完全没必要多定义一个cycle去单独存一次CCR的值,而是直接换算返回周期即可。
不过,并不是这么简单就结束了。还需要注意的是,输入捕获的整个流程,我们说过捕获寄存器中存入出现捕获事件时CNT计数器的值不是软件控制的,而是硬件自动设置的;而我们进行的中断处理程序这本质上是一个软件代码,也就是软件控制的,在实际捕获处理过程中或者任何处理中硬件处理的速度是远快于软件的,也就是说,对于产生捕获事件(计数器值复制给捕获寄存器CCR)、捕获中断的顺序是先出现捕获事件,CCR记录当前计数器数值,然后进入中断处理程序,执行其中的逻辑。
总结一下就是:
1、捕获事件由硬件自动产生,而中断由软件控制,且硬件处理远比软件处理快;
2、出现边沿信号时,首先硬件控制输入捕获,将计数器中的值捕获存至CCR;
3、其次,进入中断处理程序,将计数器CNT清零,重新开始计数。
那么,我们希望获取连续边沿信号的计数值,逻辑还是如此:即先清零,再返回下一次捕获发生时给CCR的计数值。
所以,由于进入中断晚于硬件自动将计数器值给CCR ,且CCR值只会在每次出现捕获事件时变化一下 ,因此基于上述两条,我们完全可以合并 清零计数器、拿CCR值转换 这两步操作,软件代码只需每次在中断清零计数器CNT即可(CCR在这之前已经取到捕获时的计数值,所以不必担心CCR每拿到就被清除),保证每次取到的计数值是前一个边沿信号到当前边沿信号之间的计数,这样就可以拿CCR的值转换成周期值了。
这个时候我们的代码就会简单不少,即中断里面只需要每次清零计数器CNT即可,然后在获取PWM周期频率的函数中对CCR值进行换算并返回测量的周期频率就完事了。
参考代码如下
cpp
void TIM4_IRQHandler(void)
{
// 触发捕获中断类型
if (TIM4->SR & TIM_SR_CC1IF)
{
// 清除中断标志位
TIM4->SR &= ~TIM_SR_CC1IF;
// 计数器清零
TIM4->CNT = 0;
}
}
// 读取pwm的周期,单位us,返回为ms
double TIM4_GetDutyCycle(void)
{
return TIM4->CCR1 / 1000.0;
}
// 读取频率
double TIM4_GetFreq(void)
{
return 1000000.0 / TIM4->CCR1;
}
main.c内容不用修改,最后再次编译,然后烧录后打开串口助手即可查看实验现象,如下图所示。

可见效果没有问题,仍然正确。
5.1.5 软件设计(HAL方式)
接下来,我们再使用HAL库方式实现一下该案例。
5.1.5.1 STM32CubeMX配置
在STM32CubeMX中进行图形化配置,主要包括芯片选择、工程创建、系统配置如时基、调试模式,时钟配置,然后是使用的定时器TIM5和TIM4的配置,最后串口打印所以还有USART1的配置,然后进行工程管理的设置即可。
为节省篇幅,本次开始我们采用视频的方式展示完整的图形化配置流程 ,然后相关陌生点以及重点内容再逐帧单独拿出来进行介绍。
HAL库图形化配置部分
如上视频可见。
其中,串口配置和TIM5生成PWM波形的配置在前面介绍的案例中均已做相关说明,就不再赘述。这里要着重介绍的是TIM4配置输入捕获功能的过程:
如下图所示,这时TIM4的模式配置部分

其实看起来和之前介绍TIM5是一样的,主要是其中配置的内容不一样,所以如果不熟悉,可参见输出比较功能介绍
通用定时器_输出比较介绍及案例实践-CSDN博客https://blog.csdn.net/2301_79475128/article/details/152515745?spm=1001.2014.3001.5501 对于本次配置,这里的从模式我们也不用配置,因为这是定时器间的级联,是定时器控制定时器计数,而我们这里虽然也是俩定时器,但是另一个定时器是产生波形,本质上说还是一个定时器接收外部的波形,所以并不是级联,因此Slave Mode不用开启;触发源也是不需要;
然后时钟源设置,即使用常用的内部时钟 即可,然后我们复用PB6的TIM5通道1,所以对Channel1设置输入捕获直连模式,即直接用通道1引脚输入的信号即可。
接着是对相关参数配置,与前面寄存器配置一样,预分频给71,默认向上计数,ARR自动重装载值给最大65535即可,不做CKD时钟分频,触发输出不需要,然后输入捕获通道1的设置基本默认就行(极性选择即通道极性,上升沿捕获、IC选择就是直通的映射即IC1映射在TI1上、预分频器不做分频,滤波也不用),如下图所示。

然后对TIM4的NVIC使能,因为要使用捕获中断,优先级暂不影响,因为就一个中断配置。如下图所示

然后最需要注意的是,从视频中可看出,刚配置好的TIM4,其复用的GPIO引脚是PD12,如下图所示

实际原因是HAL库优先使用了TIM4_CH1的重映射功能引脚,我们查看数据手册就明白了,如下图所示。

什么是重映射或者是重定义功能呢?
简单来说,就是当一个引脚复用了多项功能时,会通过重映射的方式将其中排在后面的复用功能重新对应到另外的引脚上,避免引脚冲突。
我们本来使用的PB6引脚,他实际先是复用I2C的功能,然后才是TIM4_CH1的功能,所以HAL库中会优先让PB6作为I2C的复用引脚,所以我们这时候使用TIM4_CH1复用功能时,就会重映射到PD12上,大概就是这个意思。

因此,我们还需要自己在图形化的芯片上修改引脚,重置PD12引脚,然后选择PB6并使用其TIM4_CH1的功能,此时PB6可能显示黄色,意味着PB6还需要重新配置一下TIM4的通道等内容,因此还需要回头看TIM4的模式配置部分,在通道1选项重新选择好输入捕获直连模式,最后检查其他配置是否需要重新设置,没问题就基本上配置完成了。
至此,HAL库的图形化配置部分就说完了,接着是在keil中打开工程,在里面做一些配置。
5.1.5.2 Keil中的配置
由于我们要使用串口重定向printf的功能,所以要多做一些配置,即进入【魔法棒】在【Target】中勾选【Use MicroLIB】,这样我们到时候重写fputc函数才不会有问题。

然后配置Debug的内容。进入【魔法棒】,然后选择【Debug】,进入【Settings】,然后选择【Flash Download】,勾选上【Reset and Run】;然后进入【Pack】,取消勾选【Enable】即可。


最后不要直接叉掉,点击确认/Ok后退出。
4.1.5.3 Vscode完善代码
接着,我们就可以使用VScode打开工程,进行代码分析并且补充一下代码了。

那么首先,我们就先把串口重定向的代码补充一下,避免后面忘记了。
在usart.h中先引入头文件stdio.h,如下图所示

接着在usart.c中重写fputc函数即可,如下图所示。

然后就可以继续看一下tim.c内容,检查一下TIM4和5的配置有没有问题,若有直接在代码中修改即可。


可见没啥问题,那么我们先补充一下main.c,因为HAL库把定时器的开启关闭单独封装成函数了,所以在主函数我们要自己开启一下定时器,如下图所示。

注:因为TIM5用于输出比较生成PWM波,所以是OCxxx,而TIM4是使用了捕获中断,所以是IC..._IT这样的名字,不要弄错函数名了。
最后我们进入中断文件(文件名后缀带_it.c)补充TIM4的捕获中断处理程序。

显然,TIM4的中断处理程序和前面基本定时器的中断处理程序结构类似,也就是说我们仍然要去找到相应的回调函数,对其进行重写即可。如果不记得的可以移步基本定时器案例的HAL库实现内容再看看,这里不再赘述。
基本定时器的寄存器介绍及案例实践-CSDN博客https://blog.csdn.net/2301_79475128/article/details/152452750?spm=1001.2014.3001.5501 所以,直接进HAL_TIM_IRQHandler实现的文件里面,去找一下本次要涉及的回调函数原型吧。
如下图,这是基本定时器的中断当时重写的回调函数,由于特别多,所以我们直接给出,并做过相关解释(因为基本定时器的中断是计数周期(计数次数)溢出或者消逝后产生的,所以名称为PeriodElapsedCallback)如下图所示。

那么,按照这个理解,我们本次的捕获中断就不同了,捕获中断就不希望溢出,而且是输入捕获后产生的中断,即此时应该是出现输入捕获信号后产生的捕获回调函数,也就是下图所示

当然,可以看见紧跟着的还有一个输入捕获的,不过他是捕获一半的时候产生的回调,所以我们使用的是IC_CaptureCallback,所以直接搜索这个名字找到对应的函数原型,如下图所示。

接着,复制一下到中断文件去重写它。显然按照前面寄存器方式的逻辑,在这里就是对CNT计数器做一下清零即可 。当然做这件事之前,还要先判断传入的结构体指针htim所对应的定时器是不是TIM4,若是则再清零,示例代码如下
cpp
/* USER CODE BEGIN 1 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM4)
{
// 计数器清零
__HAL_TIM_SetCounter(&htim4, 0);
}
}
/* USER CODE END 1 */
注:这里设置计数器值的函数原型如下:传入的是一个TIM的那个结构体名字的指针,以及设置的新计数值。
最后,我们应该是获取输入波形的周期和频率,为了提高可读性,我们再向寄存器实现那样再定义两个函数用于返回换算得到的周期频率。直接在tim.c中定义两个函数,并在tim.h中声明一下,参考代码如下
tim.h中声明:

tim.c中实现:
cpp
/* USER CODE BEGIN 1 */
// 获取周期
double TIM4_GetPWMCycle(void)
{
return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1) / 1000.0;
}
// 获取频率
double TIM4_GetPWMFreq(void)
{
return 1000000.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
}
/* USER CODE END 1 */
最后,在主函数中循环串口打印出来即可,参考代码如下

至此,HAL库代码就实现完毕,最后编译烧录,打开串口助手即可看见实验现象,如下图。

六、小结
本文介绍了STM32通用定时器的输入捕获功能,并重点讲述了其工作原理及实现方法。输入捕获通过检测外部信号的上升沿/下降沿,记录定时器计数值来测量PWM波形的周期和频率。然后利用测量PWM波形的周期频率案例从寄存器配置角度详细介绍了实现方法,同时再使用HAL库实现方式,通过CubeMX图形化配置展示了输入捕获的应用过程,最后通过实际案例,实现了对PWM波形的周期和频率测量,并将结果通过串口输出验证。
以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!
鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!