1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html
第二十章 基本定时器实验
STM32F103有众多的定时器,其中包括2个基本定时器(TIM6和TIM7)、4个通用定时器(TIM2~TIM5)、2个高级控制定时器(TIM1和TIM8),这些定时器彼此完全独立,不共享任何资源。本章我们学习如何使用STM32F103的基本定时器中断。我们将使用TIM6的定时器中断来控制LED1的翻转,在主函数用LED0的翻转来提示程序正在运行。
本章分为如下几个小节:
20.1 基本定时器简介
20.2 硬件设计
20.3 程序设计
20.4 下载验证
20.1 基本定时器简介
STM32F103有两个基本定时器TIM6和TIM7,它们的功能完全相同,资源是完全独立的,可以同时使用。其主要特性如下:16位自动重载递增计数器,16位可编程预分频器,预分频系数1~65536,用于对计数器时钟频率进行分频,还可以触发DAC的同步电路,以及生成中断/DMA 请求。
20.1.1 基本定时器框图
下面先来学习基本定时器框图,通过学习基本定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。
图20.1.1.1 基本定时器框图
①时钟源
定时器的核心就是计算器,要实现计数功能,首先要给它一个时钟源。基本定时器时钟挂载在APB1总线,所以它的时钟来自于APB1总线,但是基本定时器时钟不是直接由APB1总线直接提供,而是先经过一个倍频器。当APB1的预分频器系数为1时,这个倍频器系数为1,即定时器的时钟频率等于APB1总线时钟频率;当APB1的预分频器系数≥2分频时,这个倍频器系数就为2,即定时器的时钟频率等于APB1总线时钟频率的两倍。我们在sys_stm32_clock_init时钟设置函数已经设置APB1总线时钟频率为36M,APB1总线的预分频器分频系数是2,所以挂载在APB1总线的定时器时钟频率为72Mhz。
②控制器
控制器除了控制定时器复位、使能、计数等功能之外,还可以用于触发DAC转换。
③时基单元
时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 。基本定时器的这三个寄存器都是16位有效数字,即可设置值范围是0~65535。
时基单元中的预分频器PSC,它有一个输入和一个输出。输入CK_PSC来源于控制器部分,实际上就是来自于内部时钟(CK_INT),即2倍的APB1总线时钟频率(72MHz)。输出CK_CNT是分频后的时钟,它是计数器实际的计数时钟,通过设置预分频器寄存器(TIMx_PSC)的值可以得到不同频率CK_CNT,计算公式如下:
fCK_CNT= fCK_PSC / (PSC[15:0]+1)
上式中,PSC[15:0]是写入预分频器寄存器(TIMx_PSC)的值。
另外:预分频器寄存器(TIMx_PSC)可以在运行过程中修改它的数值,新的预分频数值将在下一个更新事件时起作用。因为更新事件发生时,会把TIMx_PSC寄存器值更新到其影子寄存器中,这才会起作用。
什么是影子寄存器?从框图上看,可以看到图20.1.1.1中的预分频器PSC后面有一个影子,自动重载寄存器也有个影子,这就表示这些寄存器有影子寄存器。影子寄存器是一个实际起作用的寄存器,不可直接访问。举个例子:我们可以把预分频系数写入预分频器寄存器(TIMx_PSC),但是预分频器寄存器只是起到缓存数据的作用,只有等到更新事件发生时,预分频器寄存器的值才会被自动写入其影子寄存器中,这时才真正起作用。
自动重载寄存器及其影子寄存器的作用和上述同理。不同点在于自动重载寄存器是否具有缓冲作用还受到ARPE位的控制,当该位置0时,ARR寄存器不进行缓冲,我们写入新的ARR值时,该值会马上被写入ARR影子寄存器中,从而直接生效;当该位置1时,ARR寄存器进行缓冲,我们写入新的ARR值时,该值不会马上被写入ARR影子寄存器中,而是要等到更新事件发生才会被写入ARR影子寄存器,这时才生效。预分频器寄存器则没有这样相关的控制位,这就是它们不同点。
值得注意的是,更新事件的产生有两种情况,一是由软件产生,将TIMx_EGR寄存器的位UG置1,产生更新事件后,硬件会自动将UG位清零。二是由硬件产生,满足以下条件即可:计数器的值等于自动重装载寄存器影子寄存器的值。下面来讨论一下硬件更新事件。
基本定时器的计数器(CNT)是一个递增的计数器,当寄存器(TIMx_CR1)的CEN位置1,即使能定时器,每来一个CK_CNT脉冲,TIMx_CNT的值就会递增加1。当TIMx_CNT值与 TIMx_ARR的设定值相等时,TIMx_CNT的值就会被自动清零并且会生成更新事件(如果开启相应的功能,就会产生 DMA请求、产生中断信号或者触发 DAC 同步电路),然后下一个CK_CNT脉冲到来,TIMx_CNT的值就会递增加1,如此循环。在此过程中,TIMx_CNT等于TIMx_ARR时,我们称之为定时器溢出,因为是递增计数,故而又称为定时器上溢。定时器溢出就伴随着更新事件的发生。
由上述可知,我们只要设置预分频寄存器和自动重载寄存器的值就可以控制定时器更新事件发生的时间。自动重载寄存器(TIMx_ARR)是用于存放一个与计数器作比较的值,当计数器的值等于自动重载寄存器的值时就会生成更新事件,硬件自动置位相关更新事件的标志位,如:更新中断标志位。
下面举个例子来学习如何设置预分频寄存器和自动重载寄存器的值来得到我们想要的定时器上溢事件发生的时间周期。比如我们需要一个500ms周期的定时器更新中断,一般思路是先设置预分频寄存器,然后才是自动重载寄存器。考虑到我们设置的CK_INT为72MHz,我们把预分频系数设置为7200,即写入预分频寄存器的值为7199,那么fCK_CNT=72MHz/7200=10KHz。这样就得到计数器的计数频率为10KHz,即计数器1秒钟可以计10000个数。我们需要500ms的中断周期,所以我们让计数器计数5000个数就能满足要求,即需要设置自动重载寄存器的值为4999,另外还要把定时器更新中断使能位UIE置1,CEN位也要置1。
20.1.2 TIM6/TIM7寄存器
下面介绍TIM6/TIM7的几个重要的寄存器,具体如下:
控制寄存器 1(TIMx_CR1)
TIM6/TIM7的控制寄存器1描述如图20.1.2.1所示。
图20.1.2.1 TIMx_CR1寄存器
该寄存器,我们需要注意的是:位0(CEN)用于使能或者禁止计数器,该位置1计数器开始工作,置0则停止。还有位7(APRE)用于控制自动重载寄存器ARR是否具有缓冲作用,如果ARPE位置1,ARR起缓冲作用,即只有在更新事件发生时才会把ARR的值写入其影子寄存器里;如果ARPE位置0,那么修改自动重载寄存器的值时,该值会马上被写入其影子寄存器中,从而立即生效。
DMA/中断使能寄存器(TIMx_DIER)
图20.1.2.2 TIMx_DIER寄存器
该寄存器位0(UIE)用于使能或者禁止更新中断,因为本实验我们用到中断,所以该位需要置1。位8(UDE)用于使能或者禁止更新DMA请求,我们暂且用不到,置0即可。
状态寄存器(TIMx_SR)
TIM6/TIM7的状态寄存器描述如图20.1.2.3所示:
图20.1.2.3 TIMx_SR寄存器
该寄存器位0(UIF)是中断更新的标志位,当发生中断时由硬件置1,然后就会执行中断服务函数,需要软件去清零,所以我们必须在中断服务函数里把该位清零。如果中断到来后,不把该位清零,那么系统就会一直进入中断服务函数,这显然不是我们想要的。
计数器寄存器(TIMx_CNT)
TIM6/TIM7的计数器寄存器描述如图20.1.2.4所示:
图20.1.2.4 TIMx_CNT寄存器
该寄存器位[15:0]就是计数器的实时的计数值。
预分频寄存器(TIMx_PSC)
TIM6/TIM7的预分频寄存器描述如图20.1.2.5所示:
图20.1.2.5 TIMx_PSC寄存器
该寄存器是TIM6/TIM7的预分频寄存器,比如我们要7200分频,就往该寄存器写入7199。注意这是16位的寄存器,写入的数值范围是0到65535,分频系数范围:1到65536。
自动重载寄存器(TIMx_ARR)
TIM6/TIM7的自动重载寄存器描述如图20.1.2.6所示:
图20.1.2.6 TIMx_ARR寄存器
该寄存器可以由APRE位设置是否进行缓冲。计数器的值会和ARR寄存器影子寄存器进行比较,当两者相等,定时器就会溢出,从而发生更新事件,如果打开更新中断,还会发生更新中断。
20.1.3 基本定时器中断应用
本实验,我们主要配置定时器产生周期性溢出,从而在定时器更新中断中做周期性的操作,如周期性翻转LED灯。假设计数器计数模式为递增计数模式,那么实现周期性更新中断原理示意图如下所示:
图20.1.3.1 基本定时器中断示意图
如图20.1.3.1所示,CNT计数器从0开始计数,当CNT的值和ARR相等时(t1),产生一个更新中断,然后CNT复位(清零),然后继续递增计数,依次循环。图中的t1、t2、t3就是定时器更新中断产生的时刻。
通过修改ARR的值,可以改变定时时间。另外,通过修改PSC的值,使用不同的计数频率(改变图中CNT的斜率),也可以改变定时时间。
20.2 硬件设计
- 例程功能
LED0用来指示程序运行,每200ms翻转一次。我们在更新中断中,将LED1的状态取反。LED1用于指示定时器发生更新事件的频率,500ms取反一次。 - 硬件资源
1)LED灯
LED0 – PB5
LED1 – PE5
2)定时器6 - 原理图
定时器属于STM32F103的内部资源,只需要软件设置好即可正常工作。我们通过LED1来指示STM32F103的定时器进入中断的频率。
20.3 程序设计
20.3.1 定时器的HAL库驱动
定时器在HAL库中的驱动代码在STM32F1xx_hal_tim.c和STM32F1xx_hal_tim_ex.c文件(以及它们的头文件)中。 - HAL_TIM_Base_Init函数
定时器的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);
函数描述:
用于初始化定时器。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量(亦称定时器句柄),结构体定义如下:
typedef struct
{
TIM_TypeDef *Instance; /* 外设寄存器基地址 */
TIM_Base_InitTypeDef Init; /* 定时器初始化结构体*/
HAL_TIM_ActiveChannel Channel; /* 定时器通道 */
DMA_HandleTypeDef *hdma[7]; /* DMA管理结构体 */
HAL_LockTypeDef Lock; /* 锁定资源 */
__IO HAL_TIM_StateTypeDef State; /* 定时器状态 */
}TIM_HandleTypeDef;
1)Instance:指向定时器寄存器基地址。
2)Init:定时器初始化结构体,用于配置定时器的相关参数。
3)Channel:定时器的通道选择,基本定时器没有该功能。
4)hdma[7]:用于配置定时器的DMA请求。
5)Lock:ADC锁资源。
6)State:定时器工作状态。
我们主要看TIM_Base_InitTypeDef这个结构体类型定义:
typedef struct
{
uint32_t Prescaler; /* 预分频系数 */
uint32_t CounterMode; /* 计数模式 */
uint32_t Period; /* 自动重载值ARR */
uint32_t ClockDivision; /* 时钟分频因子 */
uint32_t RepetitionCounter; /* 重复计数器 */
uint32_t AutoReloadPreload; /* 自动重载预装载使能 */
} TIM_Base_InitTypeDef;
1)Prescaler:预分频系数,即写入预分频寄存器的值,范围0到65535。
2)CounterMode:计数器计数模式,这里基本定时器只能向上计数。
3)Period:自动重载值,即写入自动重载寄存器的值,范围0到65535。
4)ClockDivision:时钟分频因子,也就是定时器时钟频率CK_INT与数字滤波器所使用的采样时钟之间的分频比,基本定时器没有此功能。
5)RepetitionCounter:设置重复计数器寄存器的值,用在高级定时器中。
6)AutoReloadPreload:自动重载预装载使能,即控制寄存器 1 (TIMx_CR1)的ARPE位。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_TIM_Base_Start_IT函数
HAL_TIM_Base_Start_IT函数是更新定时器中断和使能定时器的函数。其声明如下:
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef htim);
函数描述:
该函数调用了__HAL_TIM_ENABLE_IT和__HAL_TIM_ENABLE两个函数宏定义,分别是更新定时器中断和使能定时器的宏定义。
函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,即定时器句柄。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
注意事项:
下面分别列出单独使能/关闭定时器中断和使能/关闭定时器方法:
__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE); / 使能句柄指定的定时器更新中断 /
__HAL_TIM_DISABLE_IT (htim, TIM_IT_UPDATE); / 关闭句柄指定的定时器更新中断 /
__HAL_TIM_ENABLE(htim); / 使能句柄htim指定的定时器 /
__HAL_TIM_DISABLE(htim); / 关闭句柄htim指定的定时器 /
定时器中断配置步骤
1)开启定时器时钟
HAL中定时器使能是通过宏定义标识符来实现对相关寄存器操作的,方法如下:
__HAL_RCC_TIMx_CLK_ENABLE(); / x=1~8 */
2)初始化定时器参数,设置自动重装值,分频系数,计数方式等
定时器的初始化参数是通过定时器初始化函数HAL_TIM_Base_Init实现的。
注意:该函数会调用:HAL_TIM_Base_MspInit函数,我们可以通过后者存放定时器时钟和中断等初始化的代码。
3)使能定时器更新中断,开启定时器计数,配置定时器中断优先级
通过HAL_TIM_Base_Start_IT函数使能定时器更新中断和开启定时器计数。
通过HAL_NVIC_EnableIRQ函数使能定时器中断,通过HAL_NVIC_SetPriority函数设置中断优先级。
4)编写中断服务函数
定时器中断服务函数为:TIMx_IRQHandler等,当发生中断的时候,程序就会执行中断服务函数。HAL库提供了一个定时器中断公共处理函数HAL_TIM_IRQHandler,该函数又会调用HAL_TIM_PeriodElapsedCallback等一些回调函数,需要用户根据中断类型选择重定义对应的中断回调函数来处理中断程序。
20.3.2 程序流程图
图20.3.2.1 基本定时器中断实验程序流程图
程序开始先进行一系列初始化,然后在main中让LED0每过200ms翻转一次,用于指示系统代码正在运行。LED1的翻转将在定时器更新中断里进行,请看程序解析。
20.3.3 程序解析
- 基本定时器中断驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。基本定时器驱动代码包括两个文件:btim.c和btim.h。
首先看btim.h头文件的几个宏定义:
/* 基本定时器 定义 */
/* TIMX 中断定义
* 默认是针对TIM6/TIM7.
* 注意: 通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器.
*/
#define BTIM_TIMX_INT TIM6
#define BTIM_TIMX_INT_IRQn TIM6_DAC_IRQn
#define BTIM_TIMX_INT_IRQHandler TIM6_DAC_IRQHandler
/* TIM6 时钟使能 */
#define BTIM_TIMX_INT_CLK_ENABLE() do{ __HAL_RCC_TIM6_CLK_ENABLE(); }while(0)
通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器。
下面我们解析btim.c的程序,先看定时器的初始化函数,其定义如下:
/**
* @brief 基本定时器TIMX定时中断初始化函数
* @note
* 基本定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
* 基本定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟 = 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void btim_timx_int_init(uint16_t arr, uint16_t psc)
{
g_timx_handle.Instance = BTIM_TIMX_INT; /* 定时器x */
g_timx_handle.Init.Prescaler = psc; /* 预分频 */
g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数器 */
g_timx_handle.Init.Period = arr; /* 自动装载值 */
HAL_TIM_Base_Init(&g_timx_handle);
HAL_TIM_Base_Start_IT(&g_timx_handle); /* 使能定时器x和定时器x更新中断 */
}
btim_timx_int_init函数用来初始化定时器,我们可以通过修改宏定义BTIM_TIMX_INT来初始化TIM1~TIM8中的任意一个,本章我们是初始化基本定时器6。该函数的2个形参:arr设置自动重载寄存器(TIMx_ARR),psc设置预分频器寄存器(TIMx_PSC)。HAL_TIM_Base_Init函数初始化定时器后,再调用HAL_TIM_Base_Start_IT函数使能定时器和更新定时器中断。
因为我们在sys_stm32_clock_init函数里面已经初始化APB1的时钟为HCLK的2分频,所以APB1的时钟为36M,而从STM32F1的内部时钟树图得知:当APB1的时钟分频数为1的时候,TIM27的时钟为APB1的时钟,而如果APB1的时钟分频数不为1,那么TIM27的时钟频率将为APB1时钟的两倍。因此,TIM6的时钟为72M,再根据我们设计的arr和psc的值,就可以计算中断时间了。计算公式如下:
Tout= ((arr+1)*(psc+1))/Tclk
其中:
Tout:定时器溢出时间(单位为s)。
Tclk:定时器的时钟源频率(单位为MHz)。
arr:自动重装寄存器(TIMx_ARR)的值。
psc:预分频器寄存器(TIMx_PSC)的值
定时器底层驱动初始化函数如下:
/**
* @brief 定时器底层驱动,开启时钟,设置中断优先级
此函数会被HAL_TIM_Base_Init()函数调用
* @param 无
* @retval 无
*/
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == BTIM_TIMX_INT)
{
BTIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟 */
/* 设置中断优先级,抢占优先级1,子优先级3 */
HAL_NVIC_SetPriority(BTIM_TIMX_INT_IRQn, 1, 3);
HAL_NVIC_EnableIRQ(BTIM_TIMX_INT_IRQn); /* 开启ITMx中断 */
}
}
HAL_TIM_Base_MspInit函数用于存放GPIO、NVIC和时钟相关的代码,这里首先判断定时器的寄存器基地址,满足条件后,首先设置使能定时器的时钟,然后设置定时器中断的抢占优先级为1,响应优先级为3,最后开启定时器中断。这里没有用到IO引脚,所以不用初始化GPIO。
接着是定时器中断服务函数的定义,这里用的是宏名,其定义如下:
/**
* @brief 定时器中断服务函数
* @param 无
* @retval 无
*/
void BTIM_TIMX_INT_IRQHandler(void)
{
HAL_TIM_IRQHandler(&timx_handle);
}
这个函数实际上调用HAL库的定时器中断公共处理函数HAL_TIM_IRQHandler。HAL库的中断公共处理函数,会根据中断标志位调用各个中断回调函数,中断要处理的逻辑代码就写到这个回调函数中。比如这里我们使用到的是更新中断,定义的更新中断回调函数如下:
/**
* @brief 定时器更新中断回调函数
* @param htim:定时器句柄指针
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == BTIM_TIMX_INT)
{
LED1_TOGGLE();
}
}
更新中断回调函数是所有定时器公用的,所以我们就需要在更新中断回调函数中对定时器寄存器基地址进行判断,只有符合对应定时器发生的更新中断,才能进行相应的处理,从而避免多个定时器同时使用到更新中断,导致更新中断代码的逻辑混乱。这里我们使用定时器6的更新中断,所以进入更新中断回调函数后,先判断是不是定时器6的寄存器基地址,当然这里使用宏的形式,BTIM_TIMX_INT的原型就是TIM6,执行的逻辑代码是翻转LED1。
2. main.c代码
在main.c里面编写如下代码:
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
btim_timx_int_init(5000 - 1, 7200 - 1); /* 10Khz的计数频率,计数5K次为500ms */
while (1)
{
LED0_TOGGLE();
delay_ms(200);
}
}
在main函数里,先初始化系统和用户的外设代码,然后在wilhe(1)里每200ms翻转一次LED0。由前面的内容知道,定时器6的时钟频率为72MHZ,而调用btim_timx_int_init初始化函数之后,就相当于写入预分频寄存器的值为7199,写入自动重载寄存器的值为4999。由公式得:
Tout = ((4999+1)*(7199+1))/72000000 = 0.5s = 500ms
20.4 下载验证
下载代码后,可以看到LED0不停闪烁(每400ms一个周期),而LED1也是不停的闪烁,但是闪烁时间较LED0慢(每1s一个周期)。