引子
STM32中的TIM能够让我们做一些定时操作,TIM就是定时器的简称,这次就来研究一下STM32中定时器是如何工作的。
基本定时器
基本定时器的结构是这样的:

通过来自RCC的时钟,输入到触发控制器。在一般情况下,触发控制器会输出一个计数的信号,然后让CNT计数器自增。当CNT计数器中的数字达到了自动重装载寄存器的值的时候,就产生一个中断或者事件。在这个结构中,预分频器、计数器和自动重装载寄存器都是16位的计数器。
这里,预分频器和自动重装载寄存器都是有阴影的,这表明它们各自是两个寄存器,一个寄存器叫做影子寄存器,我们没有办法修改,这是由内部电路使用的寄存器;另一个寄存器就是单纯供我们读写的寄存器,当满足一定条件的时候,这些寄存器的值会写入影子寄存器。
在这个结构中,预分频器的作用就如同字面意思,是对来自时钟的信号频率进行一个预先的分频,这是因为,来自时钟的信号频率通常都比较大,所以需要进行一个预先的分频来适应比较小的定时间隔。这里,预分频器通过不断地计数,当满足一个值的时候,就输出一个计数信号,让最后的计数器进行计数。
也就是说,每来一个时钟信号,那么预分频器就加1,如果到达了一个预先设定的值,就清零,然后输出一个计数信号,让CNT计数器计数。
那么最后的频率就非常好计算了。最后的频率就等于时钟频率除以预分频的系数,再除以自动重装载寄存器的系数。实际存储的过程中,预分频和自动重装载寄存器的值都要是系数减一。
通用定时器
通用定时器的结构如下图所示:

这里我们从左往右看,首先最左边展示了我们可以使用的时钟源,分别是内部时钟、TIMx_ETR和TIMx_CHx。
当使用内部时钟时,我们发现,如果忽略捕获/比较寄存器的话,整个流程并没有变化。
当使用TIMx_CHx,也就是定时器的输入通道的时候,信号首先经过了输入滤波器和边沿检测器,这是为了对输入信号进行初步的筛选和处理,随后输入会有两条路可以选,分别输入到不同的选择器中,进行不同的计数策略。
这部分的结构抽象起来就是如下所示的样子:

选择时钟源,然后进行基本定时器的计数(也叫做时基单元),随后输出中断,走向NVIC。
代码编写
代码的编写可以分为这么几个步骤:时钟使能->时钟源选择->时基单元的初始化->中断初始化->开启时钟->中断函数的编写。
这里我实现了一个LED小灯1s开1s关交替的代码。
时钟使能
根据不同的定时器选择不同的时钟开启,我这里是使用了APB1总线,所以要开启APB1总线的时钟:
1 | RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); |
时钟源选择
随后选择目标时钟源:
1 | TIM_InternalClockConfig(TIM2); |
时基单元的初始化
这里我们可以设置定时的时间:
1 | TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; |
首先创建一个结构体,然后确认滤波器的分频系数(TIM_ClockDivision),我们这里没有应用,所以随便设置一个即可,随后设置计数的模式,是一直向上计数,向下计数,还是其它的模式,这里我选择了一直向上计数计数到目标值产生中断并且清零,随后确认定时器的自动重装载寄存器的值以及预分频器的值,这里我的目的是计数1s,所以最终的频率要为1Hz,所以我给了自动重装载寄存器的系数为10000,预分频器的系数设为了7200,然后因为实际的值中,要是系数减一,所以做出了这样的设置,对于重复计数器来说,通用寄存器没有这个功能,所以随便设为一个值即可。最后,将计时器进行初始化即可。
中断初始化
在这里,中断的初始化就和之前提到的一样,这里的代码如下所示:
1 | NVIC_InitTypeDef NVIC_InitSturcture; |
这里,我们指定所需要的中断是来自TIM2的中断,随后给相关的总线使能,并且设置一个抢占优先级和响应优先级即可,最后初始化。
开启时钟
代码如下:
1 | TIM_Cmd(TIM2, ENABLE); |
中断函数的编写
对于中断函数来说,如下编写即可:
1 | void TIM2_IRQHandler() { |
这里,我首先检查中断标志位,随后将一个全局变量进行变换,传递给主函数进行LED小灯的开关操作,最后将相关的标志位清零即可。
