STM32的定时器

STM32的定时器概述

STM32F1 系列的定时器资源十分丰富,除了上一节介绍过的系统定时器外,还有若干外设的定时器,本章继续介绍这些外设的定时器。

在 STM32F103 系列中,最多可以有 8 个外设定时器,这 8 个定时器一般被编号为 TIM1 ~ TIM8 ,TIM 即 timer 的缩写。

这 8 个定时器并不完全一样,按结构和功能可以分为基本定时器、通用定时器和高级定时器三类。其中基本定时器最简单,只有定时的功能;通用定时器在基本定时器的基础上多了很多处理其它时间相关任务的功能;高级定时器则进一步在通用定时器的基础上多出了更专业性的功能。这 8 个定时器中,TIM6 和 TIM7 为基本定时器,TIM2 ~ TIM5 为通用定时器,TIM1 和 TIM8 为高级定时器。

对于 STM32F103x8 等小容量设备,只有 TIM1 ~ TIM4 这 1 个高级定时器和 3 个通用定时器。没有通用定时器 TIM5 、高级定时器 TIM8 和基本定时器。

除此之外,在标准库中还出现了 TIM9 以后的定时器,它们是互联型产品 STM32F105 、STM32F107 的通用定时器。

由于 STM32 定时器的功能很复杂,因此这里从基本定时器的功能开始,逐步向上介绍定时器的应用。

基本定时器

基本定时器的功能概述

基本定时器的结构非常简单,只有最基本的部分,如下图所示:

不论多高级还是低级的定时器,都有和这个结构类似的基本定时单元,称为时基(time base)单元。

在 STM32 的标准库中,定时器的初始化被拆分为了多个初始化函数和初始化结构体。基本定时器只用到了其中用于初始化时基部分的结构体和函数,并且只用到了其中的两个成员:

typedef struct {
    uint16_t TIM_Prescaler;
    uint32_t TIM_Period;
    // ...
} TIM_TimeBaseInitTypeDef;

为了理解这两个成员的含义,需要理解基本定时器时基单元的工作原理。

  • 计数器

基本定时器的核心就是计数器 CNT ,它是一个 16 位的计数器,只能往上计数,最大计数值为 65535 。

自动重装载寄存器 ARR 是一个 16 位的寄存器,这里面装着计数器能计数的最大数值。当 CNT 的计数达到 ARR 值的时候,清零并产生更新事件,开始新一轮的计数。

可以通过 TIM 时基初始化结构体成员 .TIM_Period 配置这个计数值。这里要注意一个细节问题,因为定时器在装载到最大值后,它还需要一个时钟周期来将计数器清 0 ,这导致实际的计数周期会比预想的多 1 。所以如果想要使计数器每 100 个时钟周期产生 1 次中断,那么应该设置重装载值为 99 。

  • 时钟来源

基本定时器和通用定时器的时钟都由 APB1 总线时钟提供,具体规则为:如果 APB1 预分频系数为 1 ,则频率不变,否则频率为 APB1 时钟的 2 倍。一般情况下,设置系统时钟为 72MHz ,APB1 预分频系数为 2 ,则 APB1 时钟为 36MHz ,定时器时钟翻倍为 72MHz 。

基本定时器时钟还会经过一个 PSC 预分频器。PSC 是一个 16 位的预分频器,可以再次对定时器时钟以 1~65536 之间的任何一个数为系数的分频。

TIM 时基初始化结构体成员 .TIM_Prescaler 用于配置这个预分频器。不过要注意的是,向预分频寄存器内写入 0 ,实际上得到的是 1 分频(不分频),写入 1 得到的是 2 分频,也就是说实际的分频系数会比写入的值多 1 。标准库在初始化时并没有考虑到这方面的问题,所以如果期望对输入时钟实现 72 分频,那么应该将这个成员赋值为 71 。

  • 定时时间的计算

定时器的定时时间等于计数器的计数间隔乘以计数个数。计数间隔即时钟的倒数,等于 1 / (TIMxCLK / (PSC+1)) ;计数个数等于 (ARR+1) 。在最大的预分频系数和最大的自动重装值的情况下,定时器的最大定时时间约为 59.65 秒。

关于触发控制

STM32 的定时器都有一个触发控制的功能,它可以在定时器定时完毕时,产生事件直接输出到其它外设上,这样可以通过硬件直接控制另一个硬件,这一过程不需要内核的参与,实现了硬件流程的自动化。

基本定时器程序

有了以上知识以后,就可以编写基本定时器的定时程序了。以下只展示了程序的核心部分代码,完整的程序可以在 GitHub 仓库获取。

定时任务

本节介绍基本定时器最简单的用法,通过使定时器定时产生中断,实现每隔固定时间执行一段程序。

为了使用基本定时器,首先需要通过 RCC 打开外设时钟。注意,基本定时器和通用定时器挂载在 APB1 总线上,而高级定时器挂载在频率更高的 APB2 总线上。

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

然后需要初始化定时器。在使用基本定时器时,只需要初始化时基单元就可以了:

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = {
    .TIM_Prescaler = 72 - 1,
    .TIM_Period = 1000 - 1,
};
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);

以上初始化结构体表示将 TIM6 的输入时钟 72 分频,由于 TIM6 的输入时钟为 72MHz ,分频之后计数器的时钟为 1MHz ,即每 1μs 输入一个脉冲;同时,定时器的重装载值为 999 ,这样它会在输入 1000 个脉冲后产生更新中断;即每隔 1ms ,产生一次中断。

为了使定时器能产生中断,需要使能定时器 6 和它的更新中断:(然后还需要在 NVIC 中使能中断源 TIM6_IRQn

TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM6, ENABLE);

然后,在中断服务函数的更新中断内执行指定的定时任务。这里任务执行的周期为 1ms ,如果要适当延长任务执行的周期,可以通过增大预分频器的分频系数或增加重装载值。

void TIM6_IRQHandler(void) {
    if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) {
        // user task
        TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
    }
}

这种中断函数在执行时有一个细节上的问题:STM32 定时器的预分频器和重装寄存器是有缓存的,在向预分频器和重装寄存器写入新的值后,它不会立即生效,而是在发生一次更新事件后,这些新的值才会生效。

为此,TIM_TimeBaseInit() 函数在配置完成后,会主动触发一次更新事件,以使预分频器和重装寄存器能够立即生效。但是,这样做的副作用就是定时器使能后,发现更新标志位存在,会立即进入更新中断内执行任务。

这个副作用的解决方法也非常简单,只需要在定时器使能前,调用

TIM_ClearFlag(TIM6, TIM_FLAG_Update);

清除中断状态标志位,就可以避免这个一开始就执行的定时任务。

测量时间

接下来的示例使用定时器完成复杂一些的任务:使用定时器测量某个事件的持续时间,例如某个引脚输入高电平的维持时间。

使用定时器测量时间有许多处理思路,这里提供一种比较简单的:在程序中,维护一个全局变量用于记录当前定时器的更新次数:

volatile uint32_t TimerCount;

这样,程序中通过计算事件开始与结束两个时刻定时器更新次数之差,再结合定时器更新的周期,就可以计算出事件的持续时间了。同时,这个更新次数使用 32 位无符号整型,可以避免定时器的计数器只有 16 位不够用的问题。

为此,在初始化时基单元时,可以将更新周期减小一些,至少应该满足测量的精度要求。例如,以下时基配置的更新周期为 10us ,只要被测量事件的持续时间大于 10us ,程序就能检测出来:

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = {
    .TIM_Prescaler = 72 - 1,
    .TIM_Period = 10 - 1,
};
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);

在定时器的更新中断内,只需要递增更新次数变量即可:

void TIM6_IRQHandler(void) {
    if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) {
        TimerCount++;
        TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
    }
}

因为这里只有一个测量任务,所以可以在高电平开始时归零更新次数,那么在高电平结束时更新变量的值就是高电平期间定时器的更新次数,再结合定时器的更新间隔就可以计算出高电平的持续时间:

while (GPIO_READ(HCSR04_ECHO) == 0);
TimerCount = 0;
while (GPIO_READ(HCSR04_ECHO) == 1);
CalcFromCounter(TimerCount);

通用定时器

通用定时器的结构

通用定时器的功能相比基本定时器强大很多。在 STM32F1 官方参考手册中,对通用定时器的功能描述足足编排了 16 个小节:

同时,通用定时器的结构相比基本定时器也复杂得多,以下展示了通用定时器的完整结构:

这个结构主要可以分为时基单元、时钟选择、定时器输入和定时器输入这四个关键部分理解。

基本的时基单元

通用定时器的核心时基单元和基本定时器一样,输入的时钟由预分频器实现 1~65536 的分频,驱动计数器改变计数值。当计数值达到自动重装载值时,清零计数器并产生更新中断/事件:

不过,通用定时器相比基本定时器,增加了几种新的计数模式。在基本定时器中,只有一个向上计数模式,计数值从 0 计数到 ARR 的自动重装载值时发生中断,同时重新从 0 开始计数。而通用定时器还拥有以下几种计数模式:

  • 向下计数模式:该模式中,计数器从 ARR 的自动重装载值开始,向下计数到 0 时发生中断,同时重新装入 ARR 开始计数
  • 中央对齐模式:计数器从 0 开始计数到 ARR-1 值时发生中断,产生上溢事件;然后从 ARR 值向下计数到 1 并产生下溢事件;接着再从 0 开始新一轮循环

这三种模式中,计数值的可视化变化过程如下图所示:

通用定时器的时基单元相比基本定时器,可以在初始化结构体中通过成员 .TIM_CounterMode 选择不同的计数方式。

定时器时钟

基本定时器的时钟源只能是 RCC 的内部时钟,而通用定时器的一大特点是,可以选择不同的时钟源。

通用定时器的时钟源不仅仅可以是 APB1 系统总线的时钟,还可以是外部时钟:可以在定时器指定的外部引脚上输入一个方波信号,作为定时器的输入脉冲。这称为定时器的外部触发(ETR)。除此之外,定时器的时钟甚至还可以是另一个定时器的输出,这称为定时器的内部触发(ITR):通过内部触发,可以实现定时器的级联,使定时器指数型地延长所能定时的时长。

下图展示了通用定时器时钟选择部分的结构:

通用定时器的时钟可以是以下几个时钟:

  • 内部时钟

来自 RCC 的时钟由 APB1 总线提供,通常工作频率为 72MHz 。

  • 外部触发输入

通用定时器的时钟可以是定时器指定的外部引脚上输入的方波信号。每个定时器的外部触发(ETR)输入引脚都是固定的,ETR 引脚的对应关系可以参考下表:

定时器 ETR 引脚
TIM1 PA12
TIM2 PA0
TIM3 PD2
TIM4 PE0

外部输入首先经由极性选择、边沿检测和输入滤波。极性选择的作用是选择高电平作为输入脉冲还是低电平作为输入脉冲;边沿检测和输入滤波使输入信号变为清晰的方波。这一过程中还可以提前对外部输入信号分频,但分频系数只能选择为 1/2/4/8 。

通用定时器使用数字滤波器来滤除外部引脚的高频信号。数字滤波器通过对输入数字信号采样,并使用一定数量的连续相同状态来评估信号状态是否改变(只有当输入信号在较长的时间内保持稳定状态时,滤波器才会认为这不是噪声)。由此可见,数字滤波器的采样频率越慢,对短暂噪声干扰的判断越准确。

通用定时器将来自 RCC 的输入时钟提供给数字滤波器工作。初始化结构体的成员 .TIM_ClockDivision 用于控制数字滤波器的分频系数,它的分频系数可以是 1/2/4 。如果使用的外部触发信号有噪声,使用 TIM_CKD_DIV2TIM_CKD_DIV4 可以降低滤波器的采样频率,帮助滤波器更有效地抑制噪声。

外部输入可以直接进入控制器并作为定时器的时钟,这就是外部时钟模式 2 ;也可以经由另一条会产生触发信号的路径进入控制器,但是这一条路径会和其它输入时钟重合,需要提前通过开关切换,这就是外部时钟模式 1 。两种模式的主要区别就在于有无触发事件 TRGI(Trigger Input) 的产生。

  • 内部触发输入

可以产生触发输入的时钟还可以是内部输入。通用定时器都可以通过触发控制器产生触发信号,这个触发信号可以进入另一个定时器,作为另一个定时器的输入。这种输入方式称为内部触发输入。内部触发输入模式和外部时钟模式 1 作用相同,只不过时钟源一个在内部一个在外部。

  • 外部捕获输入

可以产生触发输入的时钟还可以是定时器的输入捕获通道。有关输入捕获的内容将在下一节介绍。

接下来通过具体的代码介绍通用定时器时钟选择的方式。

通用定时器时钟选择

使用外部输入计数输入脉冲

在本节中,使用外部输入作为定时器的时钟:外部引脚上每产生一个方波,定时器的计数值就增加 1 ,这样便可以将定时器用作外部信号的计数功能。

将外部时钟作为定时器时钟的关键代码在于调用以下函数:

  • void TIM_ETRClockMode2Config(TIM_TypeDef *TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);

该函数可以将外部触发输入以外部时钟模式 2 的方式作为定时器时钟。它的参数有:

  1. TIM_ExtTRGPrescaler 可以对外部输入信号提前分频
  2. TIM_ExtTRGPolarity 可以配置输入信号的极性,如果是 TIM_ExtTRGPolarity_NonInverted ,那就是从低电平到高电平的上升沿会作为输入脉冲;而 TIM_ExtTRGPolarity_Inverted 则将下降沿作为输入脉冲
  3. ExtTRGFilter 则配置数字滤波器采样时,多少个连续相同状态会作为稳定的判别。它的取值可以是 0x000x0F 的数值

在调用了以上函数后,外部时钟便成为了通用定时器的输入时钟。在配置定时器的时基时,如果还是不分频,那么外部每输入一个脉冲,定时器的计数值就增加 1 :

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = {
    .TIM_Prescaler = 1 - 1,
    .TIM_Period = 65535,
    .TIM_ClockDivision = TIM_CKD_DIV1,
    .TIM_CounterMode = TIM_CounterMode_Up,
};
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

在纯粹作为输入计数的情况下,定时器不怎么需要重装载和更新中断功能,因此可以将重装载值增大一些。在程序中,如果要获取当前计数器的计数值,可以通过以下函数:

  • uint16_t TIM_GetCounter(TIM_TypeDef *TIMx);

这样就实现了外部脉冲的计数个数测量。

使用内部输入实现定时器级联

STM32 的定时器还支持内部触发的功能,可以将一个定时器更新事件产生的触发事件作为另一个定时器的输入,也就是定时器的级联。

在两个定时器级联的情况下,第一个定时器的输入时钟会经过两次分频和两次更新中断,并由第二个定时器输出。级联的定时器定时时间的调整范围呈指数级的增长,在 72MHz 的输入下,最多可以拥有约 8123 年的定时时长。

如果要将 TIM2 和 TIM3 级联,需要先打开 TIM2 的输出触发功能,使 TIM2 定时完毕时,产生的更新事件直接输出到其它外设上。使用输出触发功能可以通过调用以下函数实现:

  • void TIM_SelectOutputTrigger(TIM_TypeDef *TIMx, uint16_t TIM_TRGOSource);

例如,如果要将 TIM2 的更新事件作为输出触发,可以这样调用该函数:

TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);

然后,还需要配置 TIM3 的输入时钟为 TIM2 更新事件产生的内部触发。和外部触发一样,输入时钟的配置也是通过类似的函数实现的:

  • void TIM_ITRxExternalClockConfig(TIM_TypeDef *TIMx, uint16_t TIM_InputTriggerSource);

这里选择 ITR1 通道(对应 TIM2 ,ITR2 则对应 TIM3 ,ITR3 对应 TIM4 )通过外部时钟模式 1 作为时钟源,对应的函数调用为:

TIM_ITRxExternalClockConfig(TIM3, TIM_TS_ITR1);

这就完成了 TIM2 到 TIM3 的级联。然后可以分别配置两个定时器的时基来得到合适的更新周期。首先配置 TIM2 的时基,使其更新周期为 10ms :

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = {
    .TIM_Prescaler = 720 - 1,
    .TIM_Period = 1000 - 1,
};
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

这样 TIM3 输入时钟的间隔就为 10ms 。然后再给 TIM3 提供合适的分频系数和重装载值,就可以实现更长的定时周期。在以下的时基配置下,TIM3 每 600s 产生一次更新事件:

TIM_TimeBaseStructure.TIM_Prescaler = 100 - 1;
TIM_TimeBaseStructure.TIM_Period = 600 - 1;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

以上配置过程可以通过下图描述:


在标准外设库中,定时器在时钟选择时,只需要调用相关的函数即可。除了以上介绍的两种时钟选择外,定时器有以下时钟选择相关函数:

  • void TIM_InternalClockConfig(TIM_TypeDef *TIMx);
    配置定时器时钟为 RCC 内部时钟。这个是默认的配置
  • void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
    配置定时器时钟为外部时钟模式 1 的外部时钟,该模式下包含触发输入功能。如果不需要触发输入的功能,可以换成上文模式 2 的外部时钟,两者的参数完全一样
  • void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource, uint16_t TIM_ICPolarity, uint16_t ICFilter);
    使用 TIx 捕获通道的时钟,按前文这里还可以进行极性选择和滤波器

除了时钟选择外,通用定时器还有很多强大的功能,例如它可以和输入输出功能交互,来处理更复杂的时间相关任务。受限于篇幅,这些功能将在下一节详细介绍。

京ICP备2021034974号
contact me by hello@frozencandles.fun