【ARM Cortex-M 开发实战指南(基础篇)】第7章 定时器

开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F103ZET6

7.1定时器的工作原理概述

系统滴答定时器一般用来提供“心跳”作用,而STM32定时器最基本功能也是定时,可以设置不同时间长度的定时。定时器除了最基本的定时功能外,定时器与GPIO有挂钩使得它可以发挥强大的作用,比如可以输出不同频率、不同占空比的方波信号、PWM信号,同时做为输入捕获功能时,可以测量脉冲宽度、实现电容按键检测等等。

STM32有三类定时器,基本定时器就是单纯的定时计数器,通用定时器多了四个通道,相对应的增加了功能,高级定时器具有基本,通用定时器的所有的功能,并且添加了其他功能。定时器的对比特性如下表所示。

XSmlsf.md.png

7.1.1基本定时器

TIM6和TIM7定时器的主要功能包括:

● 16位自动重装载累加计数器

● 16位可编程(可实时修改)预分频器,用于对输入的时钟按系数为1~65536之间的任意数值分频

● 触发DAC的同步电路

● 在更新事件(计数器溢出)时产生中断/DMA请求

总的说来,基本定时器 TIM6 和 TIM7 只具备最基本的定时功能,就是累加的时钟脉冲数超过预定值时,能触发中断或触发 DMA 请求。由于在芯片内部与 DAC 外设相连,可通过触发输出驱动 DAC,也可以作为其他通用定时器的时钟基准。

这两个基本定时器使用的时钟源都是 TIMxCLK,时钟源经过 PSC 预分频器输入至脉冲计数器 TIMx_CNT,基本定时器只能工作在向上计数模式,在重载寄存器 TIMx_ARR 中保存的是定时器的溢出值。

工作时,脉冲计数器 TIMx_CNT 由时钟触发进行计数,当 TIMx_CNT 的计数值 X 等于重载寄存器 TIMx_ARR 中保存的数值 N 时,产生溢出事件,可触发中断或 DMA 请求。然后 TIMx_CNT 的值重新被置为 0,重新向上计数。

XSm1L8.md.png

7.1.2通用定时器

通用TIMx (TIM2、TIM3、TIM4和TIM5)定时器功能包括:

● 16位向上、向下、向上/向下自动装载计数器;

● 16位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为1~65536之间的任意数值;

● 4个独立通道:输入捕获,输出比较,PWM生成(边缘或中间对齐模式),单脉冲模式输出;

● 使用外部信号控制定时器和定时器互连的同步电路;

● 如下事件发生时产生中断/DMA:更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发), 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数),输入捕获,输出比较;

● 支持针对定位的增量(正交)编码器和霍尔传感器电路

● 触发输入作为外部时钟或者按周期的电流管理

相比之下,通用定时器 TIM2 ~ TIM5 就比基本定时器复杂得多了。除了基本的定时,它主要用在测量输入脉冲的频率、脉冲宽与输出 PWM 脉冲的场合,还具有编码器的接口。

XSm8eS.md.png

1.捕获/比较寄存器

通用定时器的基本计时功能与基本定时器的工作方式是一样的,同样把时钟源经过预分频器输出到脉冲计数器 TIMx_CNT 累加,溢出时就产生中断或 DMA 请求。而通用定时器比基本定时器多出的强大功能,就是因为通用定时器多出了一种寄存器——捕获 / 比较寄存器 TIMx_CCR(capture/compare register),它在输入时被用于捕获(存储) 输入脉冲在电平发生翻转时脉冲计数器 TIMx_CNT 的当前计数值,从而实现脉冲的频率测量 ;在输出时被用来存储一个脉冲数值,把这个数值用于与脉冲计数器TIMx_CNT 的当前计数值进行比较,根据比较结果进行不同的电平输出。

2. PWM输出过程分析

通用定时器可以利用 GPIO 引脚进行脉冲输出,在配置为比较输出、PWM 输出功能时,捕获 /比较寄存器 TIMx_CCR 被用作比较功能,下面把它简称为比较寄存器。这里直接举例说明定时器的 PWM 输出工作过程 :若配置脉冲计数器 TIMx_CNT 为向上计数,而重载寄存器 TIMx_ARR 被配置为 N,即 TIMx_CNT 的当前计数值数值 X 在TIMxCLK 时钟源的驱动下不断累加,当 TIMx_CNT 的数值 X 大于 N 时,会重置TIMx_CNT 数值为 0 并重新计数。

而在 TIMx_CNT 计数的同时,TIMx_CNT 的计数值 X 会与比较寄存器 TIMx_CCR 预先存储的数值 A 进行比较。当脉冲计数器 TIMx_CNT 的数值 X 小于比较寄存器TIMx_CCR 的值 A 时,输出高电平(或低电平);相反地,当脉冲计数器的数值 X 大于或等于比较寄存器的值 A 时,输出低电平(或高电平)。如此循环,得到的输出脉冲周期就为重载寄存器 TIMx_ARR 存储的数值(N+1)乘以触发脉冲的时钟周期,其脉冲宽度则为比较寄存器 TIMx_CCR 的值 A 乘以触发脉冲的时钟周期,即输出 PWM 的占空比为 A/(N+1)。见图3 PWM 输出模式,图中为重载寄存器 TIMx_ARR 被配置为 N=8,向上计数 ;比较寄存器 TIMx_CCR 的值被设置为 4、8、大于 8、等于 0 时的输出时序图。图中OCxREF 即为 GPIO 引脚的输出时序、CCxIF 为触发中断的时序。

XSmGdg.md.png

3. PWM输入过程分析

而当定时器被配置为输入功能时,可以用于检测输入到 GPIO 引脚的信号(频率检测、输入 PWM 检测),此时捕获/比较寄存器 TIMx_CCR 被用作捕获功能,下面把它简称为捕获寄存器。见图4,为 PWM 输入时的脉冲宽检测时序图。

XSmJoQ.md.png

按照上图所示时序图来分析 PWM 输入脉冲宽检测的工作过程 :要测量的 PWM 脉冲通过 GPIO 引脚输入到定时器的脉冲检测通道,其时序为图中的 TI1。把脉冲计数器TIMx_CNT 配置为向上计数,重载寄存器 TIMx_ARR 的 N 值配置为足够大。

在输入脉冲 TI1 的上升沿到达时,触发 IC1 和 IC2 输入捕获中断,这时把脉冲计数器TIMx_CNT 的计数值复位为 0,于是 TIMx_CNT 的计数值 X 在 TIMxCLK 的驱动下从 0 开始不断累加,直到 TI1 出现下降沿,触发 IC2 捕获事件,此时捕获寄存器 TIMx_CCR2 把脉冲计数器 TIMx_CNT 的当前值 2 存储起来,而 TIMx_CNT 继续累加,直到TI1出现第二个上升沿,触发了IC1捕获事件,此时TIMx_CNT的当前计数值4被保存到TIMx_CCR1。

很明显 TIMx_CCR1(加 1)的值乘以 TIMxCLK 的周期,即为待检测的 PWM 输入脉冲周期,TIMx_CCR2(加 1)的值乘以 TIMxCLK 的周期,就是待检测的 PWM 输入脉冲的高电平时间,有了这两个数值就可以计算出 PWM 脉冲的频率、占空比了。可以看出,正因为捕获/比较寄存器的存在,才使得通用定时器变得如此强大。

4.定时器的时钟源

从时钟源方面来说,通用定时器比基本定时器多了一个选择,它可以使用外部脉冲作为定时器的时钟源。使用外部时钟源时,要使用寄存器进行触发边沿、滤波器带宽的配置。如果选择内部时钟源的话则与基本定时器一样,也为 TIMxCLK。但要注意的是,所有定时器(包括基本、通用和高级)使用内部时钟时,定时器的时钟源都被称为TIMxCLK,但 TIMxCLK 的时钟来源并不是完全一样的,见下图。

XSmNJs.png

TIM2 ~ 7也就是基本定时器和通用定时器,TIMxCLK 的时钟来源是 APB1 预分频器的输出。当 APB1 的分频系数为 1 时,则 TIM2 ~ 7 的 TIMxCLK 直接等于该APB1 预分频器的输出,而 APB1 的分频系数 不 为 1 时,TIM2 ~ 7 的 TIMxCLK 则 为APB1 预分频器输出的 2 倍。

如在常见的配置中,AHB=72 MHz,而 APB1 预分频器的分频系数被配置为 2,则PCLK1 刚好达到最大值 36 MHz,而此时 APB1 的分频系数不为 1,则 TIM2 ~ TIM7的时钟 TIMxCLK = (AHB/2) x 2 = 72 MHz。

而对于 TIM1 和 TIM8 这两个高级定时器,TIMxCLK 的时钟来源则是 APB2 预分频器的输出,同样它也根据分频系数分为两种情况。

常见的配置中 AHB=72 MHz,APB2 预分频器的分频系数被配置为1, 此时PCLK2刚好达到最大值72 MHz,而 TIMxCLK 则直接等于APB2分频器的输出,即TIM1和 TIM8 的时钟 TIMxCLK=AHB=72 MHz。

虽然这种配置下最终 TIMxCLK 的时钟频率相等,但必须清楚实质上它们的时钟来源是有区别的。还要强调的是 :TIMxCLK 是定时器内部的时钟源,但在时钟输出到脉冲计数器 TIMx_CNT 前,还经过一个预分频器 PSC,最终用于驱动脉冲计数器 TIMx_CNT 的时钟频率根据预分频器 PSC 的配置而定。

7.1.3高级定时器

TIM1和TIM8定时器的功能包括:

● 16位向上、向下、向上/下自动装载计数器;

● 16位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为1 ~ 65535之间的任意数值;

● 多达4个独立通道:输入捕获,输出比较,PWM生成(边缘或中间对齐模式),单脉冲模式输出;

● 死区时间可编程的互补输出;

● 使用外部信号控制定时器和定时器互联的同步电路;

● 允许在指定数目的计数器周期之后更新定时器寄存器的重复计数器;

● 刹车输入信号可以将定时器输出信号置于复位状态或者一个已知状态;

● 如下事件发生时产生中断/DMA:更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发),触发事件(计数器启动、停止、初始化或者由内部/外部触发计数),输入捕获,输出比较,刹车信号输入;

● 支持针对定位的增量(正交)编码器和霍尔传感器电路;

● 触发输入作为外部时钟或者按周期的电流管理。

XSmUWn.md.png

总的来说,TIM1 和 TIM8 是两个高级定时器,它们具有基本、通用定时器的所有功能,还具有三相 6 步电机的接口、刹车功能(break function)及用于 PWM 驱动电路的死区时间控制等,使得它非常适合于电机的控制。如图6 所示为高级定时器结构。

相比于通用定时器,主要多出了 BRK、DTG 两个结构,因而具有了死区时间的控制功能。首先,死区时间是什么呢?在 H 桥、三相桥的 PWM 驱动电路中,上下两个桥臂的PWM 驱动信号是互补的,即上下桥臂轮流导通,但实际上为了防止出现上下两个臂同时导通(会造成短路),在上下两臂切换时留一小段时间,上下臂都施加关断信号,这个上下臂都关断的时间称为死区时间。

STM32 的高级定时器可以配置出输出互补的 PWM 信号,并且在这个 PWM 信号中加入死区时间,为电机的控制提供了极大的便利。见图7。图中的 OCxREF 为参考信号(可理解为原信号), OCx 和 OCxN 为定时器通过 GPIO 引脚输出的 PWM 互补信号。

XSmwQ0.md.png

若不加入死区时间,当 OCxREF 出现下降沿,OCx 同时输出下降沿,OCxN 则同时输出相反的上升沿,即这三个信号的跳变是同时的。

加入死区时间后,当 OCxREF 出现下降沿,OCx 同时输出下降沿,但 OCxN 则过了一小段延迟再输出上升沿,OCxREF 出现上升沿后,OCx 要经过一段延时再输出上升沿。假如 OCx、 OCxN 分别控制上、下桥臂,有了延迟后,就不容易出现上、下桥臂同时导通的情况。这个延迟时间与 PWM 信号驱动的电子器件特性相关,从事工控领域的读者对此应该比较熟悉。

在保证不出现短路的情况下,死区时间越短越好。

XSm0yV.md.png

XSmBLT.md.png

7.2定时器计数模式

定时器可以向上计数、向下计数、向上向下双向计数模式。

  • 向上计数模式:计数器从0计数到自动加载值(TIMx_ARR),然后重新从0开始计数并且产生一个计数器溢出事件。

  • 向下计数模式:计数器从自动装入的值(TIMx_ARR)开始向下计数到0,然后从自动装入的值重新开始,并产生一个计数器向下溢出事件。

  • 中央对齐模式(向上/向下计数):计数器从0开始计数到自动装入的值-1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器溢出事件;然后再从0开始重新计数。

简单地理解三种计数模式,可以通过下面的图形:

XSmreU.png

计数器时钟可由下列时钟源提供:

  • 内部时钟(TIMx_CLK);

  • 外部时钟模式1:外部捕捉比较引脚(TIx);

  • 外部时钟模式2:外部引脚输入(TIMx_ETR)仅适用TIM2,3,4;

  • 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1而作为另一个定时器Timer2的预分频器。

在前文对三类定时器做了讲解,下面列出定时器功能引脚分布。

XSmyo4.md.png

7.3定时器的寄存器分析

为了深入了解 STM32 的通用寄存器, 下面我们先介绍一下与我们这章的实验密切相关的几个通用定时器的寄存器。首先是控制寄存器 1( TIMx_CR1),该寄存器的各位描述如下图。

XSmcFJ.md.png

XSmgY9.md.png

首先我们来看看 TIMx_CR1 的最低位,也就是计数器使能位,该位必须置1,才能让定时器开始计数。 从第 4 位 DIR 可以看出默认的计数方式是向上计数, 同时也可以向下计数,第 5,6位是设置计数对齐方式的。从第 8 和第 9 位可以看出,我们还可以设置定时器的时钟分频因子为1,2,4。

接下来介绍第二个与我们这章密切相关的寄存器:DMA/中断使能寄存器( TIMx_DIER)。该寄存器是一个 16 位的寄存器,其各位描述如下图。

XSm2WR.md.png

这里我们同样仅关心它的第 0 位,该位是更新中断允许位,本章用到的是定时器的更新中断,所以该位要设置为1。

接下来我们看第三个与我们这章有关的寄存器:预分频寄存器( TIMx_PSC)。该寄存器用设置对时钟进行分频,然后提供给计数器,作为计数器的时钟。该寄存器的各位描述如下图。

XSmWS1.md.png

这里,定时器的时钟来源有 4 个:

1) 内部时钟( CK_INT)

2) 外部时钟模式 1:外部输入脚( TIx)

3) 外部时钟模式 2:外部触发输入( ETR)

4) 内部触发输入( ITRx):使用 A 定时器作为 B 定时器的预分频器( A 为 B 提供时钟)。

这些时钟,具体选择哪个可以通过 TIMx_SMCR 寄存器的相关位来设置。这里的 CK_INT时钟是从 APB1 倍频的来的,除非 APB1 的时钟分频数设置为 1,否则通用定时器 TIMx 的时钟是 APB1 时钟的 2 倍,当 APB1 的时钟不分频的时候,通用定时器 TIMx 的时钟就等于 APB1的时钟。这里还要注意的就是高级定时器的时钟不是来自 APB1,而是来自 APB2 的。

这里顺带介绍一下TIMx_CNT 寄存器,该寄存器是定时器的计数器,该寄存器存储了当前定时器的计数值。

接着我们介绍自动重装载寄存器( TIMx_ARR),该寄存器在物理上实际对应着 2 个寄存器。一个是程序员可以直接操作的,另外一个是程序员看不到的,这个看不到的寄存器在《STM32参考手册》里面被叫做影子寄存器。事实上真正起作用的是影子寄存器。根据 TIMx_CR1 寄存器中 APRE 位的设置: APRE=0 时,预装载寄存器的内容可以随时传送到影子寄存器,此时 2者是连通的;而 APRE=1 时,在每一次更新事件( UEV)时,才把预装在寄存器的内容传送到影子寄存器。自动重装载寄存器的各位描述如下图。

XSmfQx.md.png

最后,我们要介绍的寄存器是:状态寄存器( TIMx_SR)。该寄存器用来标记当前与定时器相关的各种事件/中断是否发生。该寄存器的各位描述如下图。

XSmhy6.md.png

关于这些位的详细描述,请参考《STM32 参考手册》。只要对以上几个寄存器进行简单的设置,我们就可以使用通用定时器了,并且可以产生中断。

这一章,我们将使用定时器产生中断,然后在中断服务函数里面翻转 DS1 上的电平,来指示定时器中断的产生。

7.4定时器代码实现-标准库

接下来我们以通用定时器 TIM2 为实例,来说明要经过哪些步骤,才能达到这个要求,并产生中断。

7.4.1定时器配置步骤

这里我们就对每个步骤通过库函数的实现方式来描述。首先要提到的是,定时器相关的库函数主要集中在固件库文件 stm32f10x_tim.h 和 stm32f10x_tim.c 文件中。

1) TIM2 时钟使能。

TIM2 是挂载在 APB1 之下,所以我们通过 APB1 总线下的使能使能函数来使能 TIM2。调用的函数是:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //时钟使能

2)初始化定时器参数,设置自动重装值,分频系数,计数方式等。

在库函数中,定时器的初始化参数是通过初始化函数 TIM_TimeBaseInit 实现的:

voidTIM_TimeBaseInit(TIM_TypeDef*TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

第一个参数是确定是哪个定时器,这个比较容易理解。 第二个参数是定时器初始化参数结构体指针,结构体类型为 TIM_TimeBaseInitTypeDef,下面我们看看这个结构体的定义:

typedef struct
{
uint16_t TIM_Prescaler;
uint16_t TIM_CounterMode;
uint16_t TIM_Period;
uint16_t TIM_ClockDivision;
uint8_t TIM_RepetitionCounter;
} TIM_TimeBaseInitTypeDef;

这个结构体一共有 5 个成员变量,要说明的是,对于通用定时器只有前面四个参数有用,最后一个参数 TIM_RepetitionCounter 是高级定时器才有用的,这里不多解释。

第一个参数 TIM_Prescaler 是用来设置分频系数的,刚才上面有讲解。

第二个参数 TIM_CounterMode 是用来设置计数方式,上面讲解过,可以设置为向上计数,向下计数方式还有中央对齐计数方式, 比较常用的是向上计数模式 TIM_CounterMode_Up 和向下计数模式 TIM_CounterMode_Down。

第三个参数是设置自动重载计数周期值,这在前面也已经讲解过。

第四个参数是用来设置时钟分频因子。

针对 TIM2初始化范例代码格式:

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 999;
TIM_TimeBaseStructure.TIM_Prescaler =71;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

3)设置TIM2_DIER允许更新中断。

因为我们要使用 TIM2 的更新中断, 寄存器的相应位便可使能更新中断。 在库函数里面定时器中断使能是通过 TIM_ITConfig 函数来实现的:

void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);

第一个参数是选择定时器号,这个容易理解,取值为 TIM1~TIM17。

第二个参数非常关键,是用来指明我们使能的定时器中断的类型,定时器中断的类型有很多种,包括更新中断 TIM_IT_Update,触发中断 TIM_IT_Trigger,以及输入捕获中断等等。

第三个参数就很简单了, 就是失能还是使能。

例如我们要使能 TIM2 的更新中断,格式为:

TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE );

4)TIM2中断优先级设置。

在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器, 设置中断优先级。之前多次讲解到用 NVIC_Init 函数实现中断优先级的设置,这里就不重复讲解。

中断优先级配置代码如下:

/**
  * @brief  TIM2中断优先级配置
  * @param  None
  * @retval None
  */
void TIM2_NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure; 

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

5)允许TIM2工作,也就是使能TIM2。

光配置好定时器还不行,没有开启定时器,照样不能用。我们在配置完后要开启定时器,通过 TIM2_CR1 的 CEN 位来设置。在固件库里面使能定时器的函数是通过 TIM_Cmd 函数来实现的:

void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)

这个函数非常简单,比如我们要使能定时器2,方法为:

TIM_Cmd(TIM2, ENABLE); //使能 TIMx 外设

6)编写中断服务函数。

在最后,还是要编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,所以在状态寄存器 SR 的最低位。在处理完中断之后应该向 TIM2_SR 的最低位写 0,来清除该中断标志。

在固件库函数里面, 用来读取中断状态寄存器的值判断中断类型的函数是:

ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t)

该函数的作用是,判断定时器 TIMx 的中断类型 TIM_IT 是否发生中断。 比如,我们要判断定时器2是否发生更新(溢出)中断,方法为:

if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET){}

固件库中清除中断标志位的函数是:

void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT)

该函数的作用是,清除定时器 TIMx 的中断 TIM_IT 标志位。 使用起来非常简单,比如我们在TIM2 的溢出中断发生后,我们要清除中断标志位,方法是:

TIM_ClearITPendingBit(TIM2, TIM_IT_Update );

这里需要说明一下,固件库还提供了两个函数用来判断定时器状态以及清除定时器状态标志位的函数 TIM_GetFlagStatus 和 TIM_ClearFlag,他们的作用和前面两个函数的作用类似。只是在 TIM_GetITStatus 函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而TIM_GetFlagStatus 直接用来判断状态标志位。

通过以上几个步骤,我们就可以达到我们的目的了,使用通用定时器的更新中断,来控制LED的亮灭。

最后定时器核心配置代码如下:

/**
  * @brief  TIMX配置
  * @param  None
  * @retval None
  * @attention
    TIM_Period / Auto Reload Register(ARR) = 999   TIM_Prescaler--71 
    中断周期为 = 1/(72MHZ /72) * 1000 = 1ms
    TIMxCLK/CK_PSC --> TIMxCNT --> TIM_Period(ARR) --> 中断 且TIMxCNT重置为0重新计数 
  */
void TIM2_Configuration()
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;

    /* 设置TIM2CLK 为 72MHZ */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 , ENABLE);
    //TIM_DeInit(TIM2);

    /* 自动重装载寄存器周期的值(计数值) */
    TIM_TimeBaseStructure.TIM_Period=999;

    /* 累计 TIM_Period个频率后产生一个更新或者中断 */
    /* 时钟预分频数为72 */
    TIM_TimeBaseStructure.TIM_Prescaler= 71;

    /* 对外部时钟进行采样的时钟分频,这里没有用到 */
    TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; 
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    TIM_ClearFlag(TIM2, TIM_FLAG_Update);
    TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);

    /* 配置定时器的中断优先级 */
    TIM2_NVIC_Configuration();

    TIM_Cmd(TIM2, ENABLE);

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 , DISABLE); /*先关闭等待使用*/    
}

中断代码如下:

/**
  * @brief  This function handles TIM2 interrupt request.
  * @param  None
  * @retval None
  */
void TIM2_IRQHandler(void)
{
    if ( TIM_GetITStatus(TIM2 , TIM_IT_Update) != RESET ) 
    {
        time++;
        TIM_ClearITPendingBit(TIM2 , TIM_FLAG_Update);
    }
}

主函数如下:

/**
  * @brief     主函数
  * @param     None
  * @retval    int
  */
int main(void)
{
    /* config the led */
    LED_GPIO_Config();

    /* TIM2 定时配置 */
    TIM2_Configuration();

    /* TIM2 重新开时钟,开始计时 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 , ENABLE);

    while(1)
    {
        if ( time == 1000 ) /* 1000 * 1 ms = 1s 时间到 */
        {
            time = 0;
            /* LED1 取反 */
            LED1_TOGGLE;
        }
    }
}

接下来分析下定时器溢出时间。

7.4.2定时器溢出时间计算

1.定时器的时钟源

定时器时钟 TIMxCLK经 APB1 预分频器后分频提供,如果 APB1 预分频系数等于 1,则频率不变,否则频率乘以 2,库函数中 APB1 预分频的系数是 2,即 PCLK1=36M,所以定时器时钟 TIMxCLK=36*2=72M。

其时钟初始化代码在system_stm32f10x.c定义的,这里使用的默认配置,具体时钟设置函数是SetSysClockTo72(),代码如下:

/**
  * @brief  Sets System clock frequency to 72MHz and configure HCLK, PCLK2 
  *         and PCLK1 prescalers. 
  * @note   This function should be used only after reset.
  * @param  None
  * @retval None
  */
static void SetSysClockTo72(void)
{
  __IO uint32_t StartUpCounter = 0, HSEStatus = 0;

  /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/    
  /* Enable HSE */    
  RCC->CR |= ((uint32_t)RCC_CR_HSEON);

  /* Wait till HSE is ready and if Time out is reached exit */
  do
  {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;
    StartUpCounter++;  
  } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));

  if ((RCC->CR & RCC_CR_HSERDY) != RESET)
  {
    HSEStatus = (uint32_t)0x01;
  }
  else
  {
    HSEStatus = (uint32_t)0x00;
  }  

  if (HSEStatus == (uint32_t)0x01)
  {
    /* Enable Prefetch Buffer */
    FLASH->ACR |= FLASH_ACR_PRFTBE;

    /* Flash 2 wait state */
    FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
    FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;    

    /* HCLK = SYSCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;

    /* PCLK2 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;

    /* PCLK1 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

#ifdef STM32F10X_CL
    /* Configure PLLs ------------------------------------------------------*/
    /* PLL2 configuration: PLL2CLK = (HSE / 5) * 8 = 40 MHz */
    /* PREDIV1 configuration: PREDIV1CLK = PLL2 / 5 = 8 MHz */

    RCC->CFGR2 &= (uint32_t)~(RCC_CFGR2_PREDIV2 | RCC_CFGR2_PLL2MUL |
                              RCC_CFGR2_PREDIV1 | RCC_CFGR2_PREDIV1SRC);
    RCC->CFGR2 |= (uint32_t)(RCC_CFGR2_PREDIV2_DIV5 | RCC_CFGR2_PLL2MUL8 |
                             RCC_CFGR2_PREDIV1SRC_PLL2 | RCC_CFGR2_PREDIV1_DIV5);

    /* Enable PLL2 */
    RCC->CR |= RCC_CR_PLL2ON;
    /* Wait till PLL2 is ready */
    while((RCC->CR & RCC_CR_PLL2RDY) == 0)
    {
    }

    /* PLL configuration: PLLCLK = PREDIV1 * 9 = 72 MHz */ 
    RCC->CFGR &= (uint32_t)~(RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL);
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLXTPRE_PREDIV1 | RCC_CFGR_PLLSRC_PREDIV1 | 
                            RCC_CFGR_PLLMULL9); 
#else    
    /*  PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
#endif /* STM32F10X_CL */

    /* Enable PLL */
    RCC->CR |= RCC_CR_PLLON;

    /* Wait till PLL is ready */
    while((RCC->CR & RCC_CR_PLLRDY) == 0)
    {
    }

    /* Select PLL as system clock source */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;    

    /* Wait till PLL is used as system clock source */
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
    {
    }
  }
  else
  { /* If HSE fails to start-up, the application will have wrong clock 
         configuration. User can add here some code to deal with this error */
  }
}

重点关注以下代码:

/* PCLK1 = HCLK */
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

而RCC_CFGR_PPRE1_DIV2宏定义的定义如下:

#define  RCC_CFGR_PPRE1_DIV2                 ((uint32_t)0x00000400)        /*!< HCLK divided by 2 */

因此最终到TIM2上的时钟为72Mhz。

2.定时器频率

TIM2上的时钟为72Mhz,定时器分频系数为71,因此TIM2的频率为

CK_CNT=TIMxCLK/(PSC+1)=1MHz

3.自动重装载值

自动重装载寄存器 ARR 是一个 16 位的寄存器,这里面装着计数器能计数的最大数值。当计数到这个值的时候,如果使能了中断的话,定时器就产生溢出中断,这里设置的是999。

完整配置参数如下:

Prtscaler (定时器分频系数) : 71

Counter Mode(计数模式) :Up(向上计数模式)

Counter Period(自动重装载值) : 999

CKD(时钟分频因子) : No Division 不分频

定时器溢出时间:

Tout=1/(Tclk /psc) *(arr+1)

本文设置参数为: arr=999 psc=71 Tclk=72Mhz ,因此最终的溢出时间如下:

Tout=1/(72MHz /(71+1)) * 1000=1ms

值得注意的是,自动重装载值计算溢出时间要加1,这是因为自动重装载寄存器 ARR是从0开始计数的。

7.5定时器代码实现-HAL库

7.5.1 STM32Cube生成工程

我们在流水灯程序的基础上进行修改即可,不必每次都新建工程。本文介绍在STM32CubeMX进行定时器的配置,产生固定时间中断的方法。这里以TIM2为例。

1.GPIO配置

XSm4OK.md.png

2.设置RCC

设置高速外部时钟HSE,选择外部时钟源。

XSmIeO.md.png

3.时钟配置

笔者的板子使用的外部晶振为8MHz,选择外部时钟HSE 8MHz ,PLL锁相环9倍频后为72MHz,系统时钟来源选择为PLL,设置APB1分频器为 /2,这时候定时器的时钟频率为72Mhz。本文笔者使用的定时器是TIM2,TIM2挂在APB1上,不同的定时器挂在不同总线上的。

XSmowD.md.png

4.Times配置

选择TIM,使能TIM2,指定时钟源。

XSmTTe.md.png

【注】TIM2的时钟源有两个选项

选项1 :Internal Clock 内部时钟

选项2 : ETR2 外部触发输入(ETR)(仅适用TIM2,3,4)

定时器参数配置如下:

XSmbYd.md.png

Prtscaler (定时器分频系数) : 71
Counter Mode(计数模式) :Up(向上计数模式)
Counter Period(自动重装载值) : 999
CKD(时钟分频因子) : No Division 不分频
选项: 可以选择二分频和四分频
auto-reload-preload(自动重装载) : Enable 使能
TRGO Parameters 触发输出 (TRGO) :不使能

TRGO:定时器的触发信号输出 在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换,)

TIM2配置选项的前两个为定时器主从模式配置,很少用到,我们用不到,所以全部关闭。

XSmqfA.md.png

使能定时器中断:

XSmj6P.md.png

定时器溢出时间:

Tout=1/(Tclk /psc) *(arr+1)

这里我们 arr=999 psc=71 Tclk=72Mhz

Tout=1/(72MHz /(71+1)) * 1000=1ms

好了,配置就完成了,生成工程就行了。

7.5.2定时器的具体代码分析

我们先看看主函数,其代码如下:

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  /*使能定时2*/
  HAL_TIM_Base_Start_IT(&htim2);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    if(timer_count == 1000)
    {
      timer_count = 0;
      HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
      HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_6);
      HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_7);
    }
  }
  /* USER CODE END 3 */
}

在主循环前面,需要对TIM2进行初始化配置:

HAL_TIM_Base_Start_IT(&htim2);

在主循环内,通过全局变量timer_count来计算延时时间,TIM2的中断时间是1ms,我们计算1000表示1s,再让LED反转。

在主函数后面还需添加以下函数:

/**
  * @brief  Period elapsed callback in non-blocking mode
  * @param  htim TIM handle
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    timer_count++;
}

7.5.2.1定时器外设结构体

前文提到了初始化定时器TIM2,这里介绍定时器的句柄。

typedef struct
{
  TIM_TypeDef                 *Instance;     /*!< Register base address             */
  TIM_Base_InitTypeDef        Init;          /*!< TIM Time Base required parameters */
  HAL_TIM_ActiveChannel       Channel;       /*!< Active channel                    */
  DMA_HandleTypeDef           *hdma[7];      /*!< DMA Handlers array
                                                  This array is accessed by a @ref DMA_Handle_index */
  HAL_LockTypeDef             Lock;          /*!< Locking object                    */
  __IO HAL_TIM_StateTypeDef   State;         /*!< TIM operation state               */
…/*还有其他未使用的函数*/
} TIM_HandleTypeDef;

l Instance:TIM寄存器地址。

l Init:基本定时器的结伴参数,后文会详细介绍。

l Channel:定时器的通道选择。

l hdma:定时器DMA相关的结构体,在后面会有专门讲DMA,先不管他。

l Lock/ State:锁定机制和定时器操作的状态。

接下来说说配置定时器基本参数的结构体。

typedef struct
{
  uint32_t Prescaler;         /*!< Specifies the prescaler value used to divide the TIM clock.
                                   This parameter can be a number between Min_Data = 0x0000 and Max_Data = 0xFFFF */

  uint32_t CounterMode;       /*!< Specifies the counter mode.
                                   This parameter can be a value of @ref TIM_Counter_Mode */

  uint32_t Period;            /*!< Specifies the period value to be loaded into the active
                                   Auto-Reload Register at the next update event.
                                   This parameter can be a number between Min_Data = 0x0000 and Max_Data = 0xFFFF.  */

  uint32_t ClockDivision;     /*!< Specifies the clock division.
                                   This parameter can be a value of @ref TIM_ClockDivision */

  uint32_t RepetitionCounter;  /*!< Specifies the repetition counter value. Each time the RCR downcounter
                                    reaches zero, an update event is generated and counting restarts
                                    from the RCR value (N).
                                    This means in PWM mode that (N+1) corresponds to:
                                        - the number of PWM periods in edge-aligned mode
                                        - the number of half PWM period in center-aligned mode
                                     GP timers: this parameter must be a number between Min_Data = 0x00 and Max_Data = 0xFF.
                                     Advanced timers: this parameter must be a number between Min_Data = 0x0000 and Max_Data = 0xFFFF. */

  uint32_t AutoReloadPreload;  /*!< Specifies the auto-reload preload.
                                   This parameter can be a value of @ref TIM_AutoReloadPreload */
} TIM_Base_InitTypeDef;

Prescaler:定时器预分频系数,时钟源进过该分频器到大定时器时钟,可以设置的范围为0~65535,我们设置的是71,这也是在STM32Cube设置的分频系数,通过分频后得到的时钟是1MHz。

CounterMode:定时器计数方式,选择的是向上技术,值得注意,基本定时器只能向上计数。

Period:定时器周期,本文设置的是999,可设置范围是0~65535,因此产生中断的频率为:1MHz/1000=1KHz,即1ms的定时周期。

ClockDivision:时钟分频,主要是设置定时器时钟频率与数字滤波器采样时中频率比,基本定时器没有这个功能。

RepetitionCounter:重复计数器,属于高级控制寄存器,利用它可控制PWM,后面的文章会具体讲解。

AutoReloadPreload:自动重装装载。这里使能就行。

7.5.2.2定时器编程流程分析

1.初始化GPIO

MX_GPIO_Init();

这个就不用多讲了吧。

2.初始化定时器

MX_TIM2_Init();

函数原型如下:

static void MX_TIM2_Init(void)
{

  /* USER CODE BEGIN TIM2_Init 0 */

  /* USER CODE END TIM2_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM2_Init 1 */

  /* USER CODE END TIM2_Init 1 */
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 71;
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 999;
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM2_Init 2 */

  /* USER CODE END TIM2_Init 2 */

}

该函数主要初始化定时器参数,比如分频系数等,没啥好讲的。

3.使能定时器

HAL_TIM_Base_Start_IT(&htim2);

4.使用定时器

前面讲了定时器的初始化,使能,那么定时是如何使用的呢?它又是如何进行中断计时的呢?其实和滴答定时器一样,在stm32f1xx_it.c中定义了TIM2_IRQHandler中断服务函数,中断服务函数组中调用了我们自己写的回调函数。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    timer_count++;
}

当然,我们也可以将主函数的循环体中代码放在上面的代码中。值得注意的是,HAL库提供了一个HAL_TIM_PeriodElapsedCallback函数。其原型如下:

__weak void HAL_TIM_PeriodElapsedHalfCpltCallback(TIM_HandleTypeDef *htim)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(htim);

  /* NOTE : This function should not be modified, when the callback is needed,
            the HAL_TIM_PeriodElapsedHalfCpltCallback could be implemented in the user file
   */
}

当用户没有写回调函数时,这个函数就会调用,当用户写了回调函数,这个函数就不会调用,也可在这里加代码,但是不推荐,最好自己重新写一个函数。

7.6实现现象

将编译好的程序下载到看板子中,可以看到LED1不停闪烁。


欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎


资源获取方式

1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码

Related posts

Leave a Comment