2.12 TIM定时中断(定时器第一部分)

TIM定时器

TIM(Timer)意思是定时器,定时器的基本功能就是可以对输入的时钟进行计数,并在计数值达到设定值时触发中断,这就是定时器最基本的功能,就是定时触发中断。同时,也可以看出来,定时器就是一个计数器,当这个计数器的输入是一个准确可靠的基准时钟的时候,那它在对这个基准时钟进行计数的过程,实际上就是计时的过程。

STM32的定时器,不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。由于定时器的这个基本结构是非常通用的,很多模块电路都能用到,所以STM32的定时器上扩展了非常多的功能。

STM32的定时器,根据复杂度和应用场景分为了高级定时器、通用定时器和基本定时器三种类型。其中高级定时器最为复杂,通用定时器中等复杂、最常用,基本定时器最简单,我们主要讲的就是通用定时器。学会了通用定时器,再学高级定时器和基本定时器,那都应该没问题了。

编号:因为同一个芯片一般都有很多个定时器,所以TIM后面会跟一个数字。当然除了图上的这些TIM1~8,在库函数中还出现了TIM9、10、11等等,这些一般都用不到,只需要知道一下TIM1~8,每个都是什么定时器就好了。

总线:高级定时器连接的是性能更高的APB2总线,通用定时器和基本定时器连接的是APB1总线,这个在开启RCC时钟的时候要注意下。

基本定时器

基本定时器可以完成定时中断和主模式触发DAC的功能。

这一部分,有三个最重要的寄存器,分别是预分频器、计数器和自动重装寄存器,它们构成了最基本的计数计时电路,所以这一块电路就叫做时基单元。

预分频器之前,连接的就是基准计数时钟的输入,由于基本定时器只能选择内部时钟,所以你可以直接认为这根线直接连到了内部时钟CK_INT,内部时钟的来源是RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz,所以通向时基单元的计数基准频率就是72MHz。

再来看时基单元,首先是预分频器,它可以对这个72MHz的计数时钟进行预分频,比如PSC预分频器的寄存器写0,那就是不分频或者说1分频,这时候预分频器的输出频率 = 输入频率 = 72MHz;如果预分频器写1,那就是2分频,这时候预分频器的输出频率 = 输入频率/2 = 32MHz;如果预分频器写2,那就是3分频,这时候预分频器的输出频率 = 输入频率/3 = 24MHz,以此类推。所以预分频器的值和实际的分频系数相差了1,即实际分频系数 = 预分频器的值+1。这个预分频器是16位的,所以最大值可以写65535,也就是65536分频,这就是预分频器,就是对输入的基准频率提前进行一个分频操作。

然后是计数器,这个计数器可以对预分频器后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值就加1,这个计数器也是16位的,所以里面的值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始。所以计数器的值在计时过程中会不断的自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务。

所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了,自动重装寄存器也是16位,它存的就是我们写入的计数目标,在运行的过程中,计数值不断自增,自动重装值是固定的目标,当计数值等于自动重装值时,也就是计时时间到了,那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时。

在这里图上画了一个向上的折现箭头,就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,我们一般把它叫做“更新中断”,这个更新中断之后就会通往NVIC,我们再配置好NVIC的定时器通道,那定时器的更新中断就能够得到CPU的响应了。

这里向下的箭头,代表着会产生一个事件,这里对应的事件就叫做“更新事件”,更新事件不会触发中断,但可以触发内部其他电路的工作,以上这些就是定时器定时中断的全部流程了。

从基准时钟,到预分频器,再到计数器,计数器计数自增,同时不断地与自动重装寄存器进行比较,他俩值相等时,即计时时间到,这时会产生一个更新中断和更新事件,CPU响应更新中断,就完成了我们定时中断的任务了。


我们再来简单的介绍一下主模式触发DAC的功能,STM32定时器的一大特色,就是这个主从触发模式,它能让内部的硬件在不受程序的控制下实现自动运行,如果能把这个主从触发模式掌握好,那在某些情境下将会极大地减轻CPU的负担,这个我们后面再详细讲。

这个主模式触发DAC有啥用呢?这个用途就是在我们使用DAC的时候,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点,如果用正常的思路来实现的话,就是先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次DAC转换,然后DAC输出,这样也是没问题的,但是这样会使主程序处于频繁被中断的状态,这会影响主程序的运行和其他中断的响应,所以定时器就设计了一个主模式,使用这个主模式可以把这个定时器的更新事件映射到触发输出TRGO(Trigger Out)的位置,然后TRGO直接接到DAC的触发转换引脚上,这样,定时器的更新就不需要再通过中断来触发DAC转换了,仅需要把更新事件通过主模式映射到TRGO,然后TRGO就会直接去触发DAC了,整个过程不需要软件的参与,实现了硬件的自动化,这就是主模式的作用。当然除了这个主模式外,还有更多的硬件自动化设计,这些我们后续再继续讲。


到此为止,有关基本定时器的部分我们就讲完了,接下来我们看一下通用定时器。

通用定时器


首先中间最核心的这一部分,还是时基单元,这一部分和基本定时器的结构是一样的,由于分频器、计数器、自动重装寄存器构成。

每部分的工作流程和基本定时器也是一样的,预分频器对时钟进行预分频,计数器自增计数,当计数值达到自动重装值时,计数值清零同时产生更新中断和更新事件。不过对于通用定时器而言,这个计数器的技术模式就不止向上计数这一种了,之前解释的时候,都是用的向上计数模式,也就是计数器从0开始,向上自增,计到重装值,清零同时申请中断,然后开始下一轮,依次循环,这是向上计数。除了这种向上计数的模式外,通用定时器和高级定时器还支持向下计数模式和中央对其模式,向下技术模式就是从重装值开始,向下自减,减到零后,回到重装值同时申请中断,然后继续下一轮,依次循环,这就是向下计数。还有中央对齐的计数模式,就是从0开始,先向上自增,计到重装值,申请中断,然后再向下自减,减到0,再申请中断,然后进行下一轮,依次循环,这是中央对齐模式。


另外,我们再看一下上面的结构,这些就是内外时钟源选择和主从触发模式的结构了。

我们先看一下内外时钟源选择,对于基本定时器而言,定时只能选择内部时钟,也就是系统频率72MHz,到了通用定时器这里,时钟源不仅可以选择72MHz时钟,还可以选择外部时钟,具体有哪些呢?

第一个外部时钟就是来自TIMx_ETR引脚上的外部时钟,这个ETR(External)引脚的位置,可以参考一下引脚定义表:

可以看到这里有个TIM2_CH1_ETR,意思就是这个TIM2的CH1和ETR都是复用在了这个位置,也就是PA0引脚。那这里我们可以在TIM2的ETR引脚,也就是PA0上接一个外部的方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置一下输入滤波电路。这两块电路可以对外部时钟进行一定的整形,因为是外部引脚的时钟,所以难免会有一些毛刺,那这些电路就可以对输入的波形进行滤波,同时也可以选择一下极性和预分频器。

最后,滤波后的信号兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了。如果你想在ETR外部引脚提供时钟,或者想对ETR时钟进行计数,把这个定时器当做计数器来用过的话,那就可以配置这一路电路,在STM32中,这一路也叫做“外部时钟模式2”。

除了外部ETR引脚可以提供时钟外,下面这里还有一路可以提供时钟,就是TRGI(Trigger In),这一路从名字上来看的话,它主要用作触发输入来使用的,这个触发输入可以触发定时器的从模式,关于触发输入和从模式,我们后续的课程再讲。本小节我们讲的是这个触发输入作为外部时钟来使用的情况,你暂且把这个TRGI当做外部时钟的输入来看,当这个TRGI当做外部时钟来使用的时候,这一路叫做“外部时钟模式1”。那通过这一路的外部时钟都有哪些呢?往左看,第一个,就是ETR引脚的信号,ETR在经过输入通道后有两路,这里ETR引脚既可以通过上面这一路进来当做时钟,也可以通过下面这一路,两种情况对于时钟输入而言是等价的。只不过下面这一路输入会占用触发输入的通道(TRGI)而已。

 

第二个外部时钟,就是ITR信号,这一部分的时钟信号是来自其他定时器的。从右边可以看出,这个主模式的输出TRGO可以通向其他定时器。那通向其他计时器的时候,就接到了其他定时器器的ITR引脚上来了,这个ITR0~ITR3分别来自其他4个定时器的TRGO输出,至于具体的连接方式是怎么样的,手册的这个位置有一张表:这就是ITR和定时器的连接关系,通过这一路我们就可以实现定时器的级联的功能。比如我们可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,这里选择ITR2,对应的就是TIM3的TRGO,然后后面再选择时钟为“外部时钟模式1”,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就实现了定时器的级联。

那我们继续看,这里还可以选择TI1F_ED,这里连接的是这里输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,这里后缀加一个ED(Edeg)就是边沿的意思,也就是通过这一路输入的时钟,上升沿和下降沿均有效,最后,这个时钟还能通过TI1FP1和TI2FP2获得,其中TI1FP1是连接到了CH1引脚的时钟,TI2FP2连接到了CH2引脚的时钟。到这里外部时钟模式1的输入就介绍完了。总结一下就是,外部时钟模式1的输入可以是ETR引脚、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚,这还是比较复杂的,一般情况下外部时钟通过ETR引脚就可以了。下面设置这么复杂的输入,不仅仅是为了扩大时钟输入的范围,更多的还是为了某些特殊应用场景而设计的。对于时钟输入而言,最常用的还是内部的72MHz的时钟。如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入,这一路最简单最直接,那有关时钟输入的部分,到这里就讲完了。

最后还有一块没有讲到,这个是定时器的一个编码器接口,可以读取正交编码器的输出波形,这个我们后续也会再讲。

接下来看右边,这个就是定时器的主模式输出了,这部分电路可以把内部的一些事件映射到这个TRGO引脚上,比如我们刚才讲的基本定时器分析的,将更新事件映射到TRGO,用于触发DAC,这里也是一样的,它可以把定时器内部的一些事件映射到这里来,用于触发其他定时器、DAC或者ADC,可见这个触发输出的范围是比基本定时器更广一些的。上面的这一部分已经解释完了,我们看下面的这一部分。


右边这是一块输出比较电路,总共有四个通道,分贝对应CH1到CH4的引脚,可以用于输出PWM波形,驱动电机。

左边这一块是输入捕获电路,也是有四个通道,对应的也是CH1到CH4的引脚,可以用于测量输入方波的频率等。

中间这个寄存器是捕获/比较寄存器,是输入捕获和输出比较电路共用的。

因为输入捕获和输出比较不能同时使用,所以这里的寄存器是共用的,引脚也是共用的。

那有关输入捕获和输出比较这部分的电路,我们留到之后再做具体分析。我们本节课主要讲的是定时中断和内外时钟源选择,也用不到这部分电路。

高级定时器

对比通用寄存器,左上的这一大部分都没有变化,主要改动的就是右边这几个部分。

和通用定时器不同的地方,第一个是,申请中断的地方,增加了一个重复次数计数器,有了这个计数器之后,就可以实现每隔几个计数周期,才发生一次更新事件和更新中断。原来的结构是每个计数周期完成后就都会发生更新,现在有个计数器在这里,就可以实现每隔几个周期更新一次,这里相当于对输出的更新信号又做了一次分频,那对于高级定时器的话,我们之前计算的最大定时时间59秒多,在这里就还需要乘以一个65536,这就又提升了很多定时时间,这个就是重复次数计数器的工作流程。

下面这一些就是高级定时器对输出比较模块的升级了,这些内容简单说一下,大家有印象就行,现在还不比深入了解。我们看一下,这个DTG(Dead Time Generate)是死区生成电路。右边这里的输出引脚由原来的一个变为了两个互补的输出,可以输出一对互补的PWM波,这些电路 是为了驱动三相无刷电机的,三相无刷电机还是比较常用的,比如四轴飞行器、电动车的后轮、电钻等,里面都可能是这个三相无刷电机。因为三相无刷电机的驱动电路一般需要三个桥臂,每个桥臂两个大功率开关管来控制,总共需要六个大功率开关管来控制,所以这里的输出PWM引脚的前三路就变为了互补的输出,而第四路却没什么变化,因为三相电机只需要三路就行了。另外,为了防止互补输出的PWM驱动桥臂时,在开关切换的瞬间,由于器件的不理想,造成短暂的直通现象,所以前面这里就加上了死区生成电路,在开关切换的瞬间,产生一定时长的死区,让桥臂的上下管全都断开,防止直通现象。

最后一部分就是刹车输入的功能了,这个是为了给电机驱动提供安全保障,如果外部引脚BKIN(Break IN)产生了刹车信号,或者内部时钟失效,产生了故障,那么控制电路就会自动切断电机的输出,防止意外的发生,这就是刹车输入的功能。

 

定时器功能

定时中断和内外时钟源的选择

上面是定时中断和内外时钟源的选择这些所涉及的结构。

其中,时基单元下面的运行控制,就是控制寄存器的一些位,比如启动停止,向上或向下计数等等,我们操作这些寄存器技能控制时基单元的运行了。

在本小节示例程序里,第一个定时器定时中断就是用的内部时钟这一路;第二个定时器外部时钟就是用的外部时钟模式2这一路,当然还可以选择这里的触发输入当做外部时钟,即外部时钟模式1(对应的有ETR外部时钟、ITRx其他定时器、TIx输入捕获通道)。

最后这里还有个编码器模式,这一般是编码器独用的模式,普通的时钟用不到这个。

当信号从时基单元出来以后,在中断输出控制之前,中断信号会在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。

为什么会有个中断输出控制呢?因为这个定时器模块有很多地方都要申请中断,比如更新中断会申请中断,触发信号也会申请中断,输入捕获和输出比较匹配时也会申请中断,所以这些中断都要经过中断输出控制,如果需要这个中断,那就允许,如果不需要,那就禁止。简单来说,这个中断输出控制就是一个中断输出的允许位,如果需要某个中断,就记得允许一下。


接下来我们来看几个时序图,研究一下时基单元运行的一些细节问题。

预分频器时序

计数器计数频率:CK_CNT = CK_PSC / (PSC+1)

这个图示表示的是当预分频器的参数从1变到2时,计数器的时序图。

第一行CK_PSC,预分频器的输入时钟,选内部时钟的话一般是72MHz,然后这个时钟在不断的运行。

第二行CNT_EN,计数器使能,高电平计数器正常运行,低电平计数器停止。

第三行CK_CNT,计数器时钟,它既是预分频器的时钟输出,也是计数器的时钟输入。

这里可以看到,开始时,计数器未使能,计数器时钟不运行,然后使能后,前半段,预分频器的系数为1,计数器的时钟等于预分频器前的时钟;后半段预分频器的系数变为2,计数器的时钟等于预分频器前的时钟一半了。

在计数器时钟的驱动下,下面的计数器寄存器也跟随着时钟的上升沿不断地自增。可以看到在FC之后,计数值变为0了,这里虽然没写,但是可以推断出ARR自动重装值就是FC,当计数值计到和重装值相等,并且下一个时钟来临时,计数值才清零,同时下面产生一个更新事件,这就是一个计数周期的工作流程。

然后下面还有三行时序(预分频控制寄存器、预分频缓冲器、预分频计数器),这三行时序什么意思呢?

这里描述的其实是这个预分频寄存器的一种缓冲机制,也就是这个预分频寄存器实际上是有两个,一个是预分频控制寄存器,供我们读写用的,他并不直接决定分频系数。另外一个是缓冲寄存器(影子寄存器)这个预分频缓冲器才是真正起作用的寄存器。比如我们在某个时刻,把预分频寄存器由0改成了1,如果在此时立刻改变时钟的分频系数,那么就会导致这个地方,,在一个计数周期内,前半部分和后半部分的频率不一样,这里计数计到一半,计数的频率突然就会改变了,这虽然一般并不会有什么问题,但是STM32的定时器比较严谨,设计了这个缓冲寄存器,这样,当我在计数计到一半的时候改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时,产生了更新事件,预分频寄存器的值才会被传递到缓冲寄存器里面去,才会生效。所以可以看到,即使我在计数中途改变了预分频值,技术频率仍然会保持为原来的频率,直到本轮计数完成,在下一轮计数时,改变后的分频值才会起作用。

最后一行,预分频计数器,当预分频值为0时,计数器就一直为0,直接输出原频率;当预分频值为1时,计数器就0、1、0、1、0、1这样计数,在回到0的时候,输出一个脉冲,这样输出频率就是输入频率的2分频。预分频器的值和实际的分频系数之间有一个数的偏移,所以计数器计数频率:CK_CNT = CK_PSC / (PSC+1)。

计数器时序

计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)
ARR:AutoReloadRegister,自动重装寄存器。

这个图时计数器时序图,内部时钟分频因子为2,就是分频系数为2。

第一行时内部时钟72MHz。

第二行是时钟使能,高电平启动。

第三行是计数器时钟,因为分频系数为2,所以这个频率是内部时钟的二分之一。

然后计数器在这个时钟每个上升沿自增,当增到0036的时候,发生溢出,计到36之后再来一上升沿,计数器清零,计数器溢出,产生一个更新事件脉冲,另外还会置一个更新中断标志位UIF。这个标志位只要置1,就会申请中断。中断响应之后,需要在中断程序中手动清零,这就是计数器的工作流程。

计数器溢出频率 :CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1) (计算定时的时间只需要取个倒数即可)

那刚才说了,预分频器为了防止计数中途更改数值造成错误,设计了缓冲寄存器,这个计数器肯定也少不了这样的设计了,计数器的这个ARR自动重装寄存器也是有一个缓冲寄存器的,并且这个缓冲寄存器用还是不用,可以自己设置的,下面的两个时序,一个是计数器无预装时序,就是没有缓冲寄存器的情况下;另一个是有预装时序,就是有缓冲寄存器的情况。通过设置这个ARPE位,就可以选择是否使用预装功能。

计数器无预装时序

计数器无预装时序,就是没有缓冲寄存器的情况,在这里,计数器正在进行自增,我突然更改了自动加载寄存器,就是自动重装寄存器,由FF改成了36。那计数值的目标值就由FF变成了36,所以这里计到36之后就直接更新开启下一轮计数,不再计数到FF了。

计数器有预装时序

由图可以看出,在有缓冲寄存器的情况下,在计数的中途,我突然把计数目标从F5更改成了36,可以看到下面有个影子寄存器,这个影子寄存器才是真正起作用的,影子寄存器的计数目标还是F5,并且产生更新事件,产生更新事件之后,要更改的36才被传递到影子寄存器,在下一个计数周期这个更改的36才有效。

所以可以看出,引入这个影子寄存器的目的实际上是为了通同步,就是让值的变化和更新事件同步发生,防止在运行途中更改造成错误。

在这个例子也可以看出,如果在这里不使用影子寄存器的话,F5改到36立刻生效,但此时计数值已经到了F1,已经超过36了,F1只能增加,但它的目标值却是36,比它还小,这样F1就只能一直加一直加,加到FFFF,再回到0,再加36,才能产生更新,这就会造成一些小问题。


RCC时钟树结构图

这个时钟树,就是STM32用来产生和配置时钟,并且把配置好的时钟发送到各个外设的系统。时钟是所有外设运行的基础,所以时钟也是最先需要配置的东西。我们之前说过,程序中主函数之前还会执行一个SystemInit函数,这个函数就是用来配置这个时钟树的。ST公司已经帮我们写好了配置这个时钟树的SystemInit函数。

我们看一下这个时钟树,从这里划一个界限,左边的都是时钟的产生电路,右边的时钟的分配电路。

中间的这个SYSCLK就是系统时钟72MHz,在时钟产生电路中,有四个震荡源,分别是内部的8MHz高速RC振荡器、外部的4-16MHz高速石英晶体振荡器,也就是晶振,一般都是接8MHz、外部的32.768KHz低俗晶振,这个一般是给RTC提供时钟的、内部的40KHz低俗RC振荡器,这个可以给看门狗提供时钟。

上面的两个高速晶振,是用来提供系统时钟的,我们AHB、APB1、APB2的时钟都是来源于这两个高速晶振,这里内部和外部都有一个8MHz的晶振,都是可以用的,只不过外部的石英振荡器比内部的RC振荡器更加稳定,所以一般我们都用外部晶振,但是如果你的系统很简单而且不需要那么精确的时钟那也是可以使用内部的RC振荡器,这样就可以省下外部晶振的电路了。

在SystemInit函数里,ST是这样来配置时钟的,首先他会启动内部时钟,选择内部8MHz为系统时钟,暂时以内部8MHz的时钟运行,然后再启动外部时钟,配置外部时钟这样走进入PLL锁相环进行倍频,8MHz倍频9倍,得到72MHz,等到锁相环输出稳定后,选择锁相环输出为系统时钟。这样就把系统时钟由原来的8MHz切换为了72MHz,这是ST配置的流程。

这样分析之后,就可以解决实际应用的一个问题,那就是如果你的外部晶振出问题了,可能会导致一个现象,就是你会发现,你程序的时钟大概慢了10倍,比如你用定时器定一个1s的时间,结果过了大概10s才进中断,这个问题就出现在这里,如果外部晶振出问题了,系统时钟就无法切换到72MHz,那它就会以内部的8MHz运行,8MHz相比较72MHz,大概慢了10倍。

这个CSS是时钟安全系统,他也是负责切换时钟的,它可以监测外部时钟的运行状态,一旦外部时钟失效,他就会自动把外部时钟切换回内部时钟,保证系统时钟的运行,防止程序卡死造成事故。另外,在高级定时器中,实际上也有CSS的身影在这个刹车输入里,一旦CSS检测到外部时钟失效,这里通过或门就会立刻反映到输出比较这里,让这个输出控制的电机立刻停止,防止意外。这就是STM32中的一些安全保障措施。

我们再来看一下右边的时钟分配电路,首先系统时钟72MHz进入AHB总线,AHB总线有个预分频器,在SystemInit里配置的分配系数为1,那AHB的时钟就是72MHz。然后进入AHB1总线,这里配置的分配系数是2,,所以APB1总线的时钟为36Mhz,现在大家可能会有一个问题,就是通用定时器和基本定时器是接在APB1上的,而APB1的时钟是36MHz,按理说它们的时钟应该是36MHz,但是我在讲定时器的时候,一直都说的是所有定时器的时钟都是72MHz,这是为什么?原因是这下面还有一条支路,上面写的是如果APB1预分频系数=1,则频率不变,否则频率*2。这一路是单独为定时器2~7开通的。这里就有个结论,无论是高级定时器、通用定时器、基本定时器,他们的内部基准时钟都是72MHz。当然前提是你不乱改SystemInit里面的默认配置,要是改了,这里的时钟还需要另行分析。

我们再看下面APB2的时钟,这里给的分频系数为1,所以APB2的时钟和AHB一样,都是72MHz,这里APB2上的高级定时器也单开了一路,给定时器1和8,上面写也是如果APB2预分频系数=1,则频率不变,否则频率*2。

在时钟输出这里都有一个与门进行输出控制,控制位写的是外部时钟使能,这就是我们在程序中写RCC_APB2/1PeriphClockCmd作用的地方,打开时钟就是在这个位置写1,让左边的时钟能够通过与门输出给外设。