前言
STM32 单片机的定时器分为高级定时器、 通用定时器 、基本定时器三种。这三个定时器成上下级的关系,即基本定时器有的功能通用定时器都有,而且还增加了向下、向上/向下计数器、PWM生成、输出比较、输入捕获等功能;
而高级定时器又包含了通用定时器的所有功能,另外还增加了死区互补输出、刹车信号。(此处我们暂时忽略,暂时我们暂时还不需要接触这些功能)
一、STM32 通用定时器简介
通用定时器是一个通过可编程预分频器驱动的16位自动装载计数器构成。它适用于多种场合,包括测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和PWM)。使用定时器预分频器和RCC时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。每个定时器都是完全独立的,没有互相共享任何资源。
STM32F1的通用定时器(TIM2、TIM3、TIM4、TIM5)功能与特点包括:
16位向上、向下、向上/向下(中心对齐)计数模式,自动装载计数器( TIMx_CNT)。
向下计数模式:一个是从某个数减到零;
向上计数模式:是从零加到某个数;
中央对齐模式:向上/向下计数模式;
在中央对齐模式,计数器从0开始计数到自动加载的值(TIMx_ARR寄存器)−1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器下溢事件;然后再从0开始重新计数。
注:为什么需要中间对齐模式:
在永磁同步电机的控制中,需要对电机的三相定子施加一定的电压,才能控制电机转动。现在用的较多的是SVPWM(SVPWM的具体原理会在后面另写一篇博客说明),要想产生SVPWM波形,需要控制的三相电压呈如下形式,即A、B、C三相的电压是中间对齐的,这就需要用到stm32定时器的中间对齐模式了。
16位可编程预分频器(TIMx_PSC),计数器时钟频率的分频系数为1~65535之间的任意数值。
4个独立通道(TIMx_CH1-4)可以用来作为:
a) 输入捕获
b) 输出比较
c) PWM生成
d) 单脉冲模式输出
可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可用1个定时器控制另外一个定时器)
如下事件发生时产生中断/DMA(6个独立的IRQ/DMA请求生成器)
a) 更新,计数器溢出,计数器初始化
b) 触发事件
c) 输入捕获
d) 输出比较
e) 支持针对定位的增量编码器和霍尔传感器电路
f) 触发输入作为外部时钟或者按周期的电流管理
通用定时器框图
二、stm32的时钟树
想说定时器,绕不开时钟。简单介绍一下stm32时钟的配置过程。以外部时钟作为时钟源为例。HSE代表外部时钟(假设为8M)、SYSCLK为系统时钟,经过倍频器之后变成168M、SYSCLK经过AHB预分频器(假设分频系数为1)后变成HCLK时钟等于系统时钟SYSCLK,HCLK即AHB外部总线时钟,经过APB预分频器分出APB1时钟(分频系数为2,低速设备SYSCLK/4)与APB2时钟(分频系数为1,高速设备SYSCLK/2)
HSE->SYSCLK->HCLK->APB1、APB2。
在我们的iBox的工程中:有一个文件 system_stm32f10x.c
中,我们去配置我们的时钟
通过宏定义,我们看到,通过不同的时钟频率宏定义的选择,在后面的代码中通过不同的宏定义,选择对应的时钟配置函数进行运行,在对应的函数中对时钟寄存器进行不同的选择配置:
配置函数中用到的寄存器,可以通过:
《STM32中文参考手册》进行查阅,本文不再重复描述。
stm32定时器的时钟
stm32定时器分为高级定时器(TIM1与TIM8)、通用定时器(TIM2-TIM5、TIM9-TIM14)、基本定时器(TIM6、TIM7)。不同的定时器使用不同的时钟。
其中TIM1、TIM8、TIM10、TIM11使用的是APB2时钟,而其余定时器使用的是APB1时钟。
在stm32手册中有这么一段话
根据前面RCC配置可以知道TIM1、TIM8、TIM10、TIM11使用的时钟频率为SYSCLK,其他定时器使用的时钟频率为SYSCLK/2。
二、TIMx功能描述
时基单元
可编程通用定时器的主要部分是一个16位计数器和与其相关的自动装载寄存器。这个计数器可以向上计数、向下计数或者向上向下双向计数。此计数器时钟由预分频器分频得到。计数器、自动装载寄存器和预分频器寄存器可以由软件读写,在计数器运行时仍可以读写。
时基单元包含:
计数器寄存器(TIMx_CNT)
预分频器寄存器 (TIMx_PSC)
自动装载寄存器 (TIMx_ARR)
自动装载寄存器是预先装载的,写或读自动重装载寄存器将访问预装载寄存器。根据在TIMx_CR1 寄存器中的自动装载预装载使能位(ARPE)的设置,预装载寄存器的内容被立即或在每次的更新事件UEV时传送到影子寄存器。当计数器达到溢出条件(向下计数时的下溢条件)并当TIMx_CR1 寄存器中的UDIS位等于’0’时,产生更新事件。更新事件也可以由软件产生。随后会详细描述每一种配置下更新事件的产生。
2.计数模式
通用定时器可以向上计数、向下计数、向上向下双向计数模式。
①向上计数模式:计数器从0计数到自动加载值(TIMx_ARR),然后重新从0开始计数并且产生一个计数器溢出事件。
②向下计数模式:计数器从自动装入的值(TIMx_ARR)开始向下计数到0,然后从自动装入的值重新开始,并产生一个计数器向下溢出事件。
③中央对齐模式(向上/向下计数):计数器从0开始计数到自动装入的值-1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器溢出事件;然后再从0开始重新计数。
设置TIMx_CR1 寄存器中的UDIS位,可以禁止更新事件;这样可以避免在向预装载寄存器中写入新值时更新影子寄存器。在UDIS位被清’0’之前,将不产生更新事件。但是在应该产生更新事件时,计数器仍会被清’0’,同时预分频器的计数也被请0(但预分频器的数值不变)。此外,如果设置了 TIMx_CR1 寄存器中的URS位(选择更新请求),设置UG位将产生一个更新事件UEV,但硬件不设置UIF标志(即不产生中断或DMA请求)。这是为了避免在捕获模式下清除计数器时,同时产生更新和捕获中断。
3.时钟选择
计数器时钟可由下列时钟源提供:
1)内部时钟(CK_INT)
2)外部时钟模式1 :外部输入脚(TIx)
3)外部时钟模式2:外部触发输入(ETR)
4)内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1 而作为另一个定时器Timer2的预分频器。
内部时钟源(CK_INT)
如果禁止了从模式控制器(TIMx_SMCR寄存器的SMS=000),则CEN、 DIR(TIMx_CR1 寄存器)和UG位(TIMx_EGR寄存器)是事实上的控制位,并且只能被软件修改(UG位仍被自动清除)。只要CEN位被写成’1’,预分频器的时钟就由内部时钟CK_INT提供。
外部时钟源模式1
当TIMx_SMCR寄存器的SMS=111 时,此模式被选中。计数器可以在选定输入端的每个上升沿或下降沿计数。
外部时钟源模式2
选定此模式的方法为:令TIMx_SMCR寄存器中的ECE=1。计数器能够在外部触发ETR的每一个上升沿或下降沿计数。
4.输入捕获模式
输入捕获模式可以用来测量脉冲宽度或者测量频率。STM32 的定时器,除了 TIM6 和 TIM7,其他定时器都有输入捕获功能。 STM32 的输入捕获,简单的说就是通过检测 TIMx_CHx 上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)里面,完成一次捕获。同时还可以配置捕获时是否触发中断/DMA 等。
5.PWM模式
脉冲宽度调制模式可以产生一个由TIMx_ARR寄存器确定频率、由TIMx_CCRx寄存器确定占空比的信号。
例如:产生一个10KHZ的PWM信号,在定时器3的时钟为72MHZ下,占空比为50%,则需把预分频寄存器设置为0,自动重载寄存器设置为7199,CCRx寄存器设置为3599
根据TIMx_CR1 寄存器中 CMS位的状态,定时器能够产生边沿对齐的PWM信号或中央对齐的PWM信号。
对齐模式的图示如下图所示,可以看到在中心对齐模式下产生的PWM波形的周期比实际计数周期要大1倍,所以假设要使用中间对齐模式,并且需要产生的PWM波频率为20K,那么对应的定时器时基应该设为40K。
三、定时器相关寄存器
计数器当前值寄存器CNT
2.预分频寄存器TIMx_PS
3.自动重装载寄存器(TIMx_ARR)
4.控制寄存器1(TIMx_CR1)
5.DMA中断使能寄存器(TIMx_DIER)
四、通用定时器常用库函数
1) TIM3 时钟使能
TIM3 是挂载在 APB1 之下,所以我们通过 APB1 总线下的使能使能函数来使能 TIM3。调
用的函数是:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
2)初始化定时器参数,设置自动重装值,分频系数,计数方式
在库函数中,定时器的初始化参数是通过初始化函数 TIM_TimeBaseInit 实现的:
void TIM_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。第三个参数是设置自动重载计数周期值。第四个参数是用来设置时钟分频因子。TIM_ClockDivision的作用就是在未分频之前根据要求建立新的分频器,确定定时器,确定一定的延时时间,在此时间内完成一定预期的功能,一般不太用,所以无论是定义上文中的哪个值对原本定时器的频率都毫无影响,不过并不是没有作用的,以后会有能用到的地方。
针对 TIM3 初始化范例代码格式:
3) 设置 TIM3_DIER 允许更新中断
因为我们要使用 TIM3 的更新中断, 寄存器的相应位便可使能更新中断。 在库函数里面定
时器中断使能是通过 TIM_ITConfig 函数来实现的:
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
第一个参数是选择定时器号,这个容易理解,取值为 TIM1~TIM17。
第二个参数非常关键,是用来指明我们使能的定时器中断的类型,定时器中断的类型有很
多种,包括更新中断 TIM_IT_Update,触发中断 TIM_IT_Trigger,以及输入捕获中断等等。
第三个参数就很简单了, 就是失能还是使能。
例如我们要使能 TIM3 的更新中断,格式为:
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE );
4) TIM3 中断优先级设置
在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器, 设置中
断优先级。之前已讲解过用 NVIC_Init 函数实现中断优先级的设置,这里就不重复讲解。
5)使能 TIM3
我们在配置完后要开启定时器,
通过 TIM3_CR1 的 CEN 位来设置。 在固件库里面使能定时器的函数是通过 TIM_Cmd 函数来实现的:
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)
这个函数非常简单,比如我们要使能定时器 3,方法为:
TIM_Cmd(TIM3, ENABLE); //使能 TIMx 外设
6) 编写中断服务函数。
最后,编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,所以在状态寄存器 SR 的最低位。在处理完中断之后应该向 TIM3_SR 的最低位写 0,来清除该中断标志。
在固件库函数里面, 用来读取中断状态寄存器的值判断中断类型的函数是:
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t)
该函数的作用是,判断定时器 TIMx 的中断类型 TIM_IT 是否发生中断。 比如,我们要判断定
时器 3 是否发生更新(溢出)中断,方法为:
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){}
固件库中清除中断标志位的函数是:
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT)
该函数的作用是,清除定时器 TIMx 的中断 TIM_IT 标志位。 使用起来非常简单,比如我们在
TIM3 的溢出中断发生后,我们要清除中断标志位,方法是:
TIM_ClearITPendingBit(TIM3, TIM_IT_Update );
这里需要说明一下,固件库还提供了两个函数用来判断定时器状态以及清除定时器状态标志位的函数 TIM_GetFlagStatus 和 TIM_ClearFlag,他们的作用和前面两个函数的作用类似。 只是在 TIM_GetITStatus 函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而TIM_GetFlagStatus 直接用来判断状态标志位。
五、程序设计
本例为定时器中断,①~⑥为设置步骤, 其中arr 和 psc 两个参数用来设置 TIM3 的溢出时间。
系统初始化的时候在默认的系统初始化函数 SystemInit 函数里面已经初始化 APB1 的时钟为 2 分频,所以 APB1 的时钟为 36M, 而从 STM32 的内部时钟树图得知:当 APB1 的时钟分频数为 1 的时候, TIM2~7 的时钟为 APB1 的时钟,而如果 APB1 的时钟分频数不为 1,那么 TIM2~7 的时钟频率将为 APB1 时钟的两倍。TIM3 的时钟为 72M,再根据我们设计的 arr 和 psc 的值,就可以计算中断时间了。计算公式如下:
Tout= ((arr+1)*(psc+1 ))/Tclk;
其中:
Tclk: TIM3 的输入时钟频率(单位为 Mhz)。
Tout: TIM3 溢出时间(单位为 us)。
烧入IBOX达到的效果如下图所示,我们定的时间是0.5s LED状态翻转一次,如串口打印加了时间戳,相邻两个OK的打印时间正好是500ms