文章目录
- 一、STM32F429 通用定时器简介
- 二、硬件设计
- 三、软件设计
- 四、实验现象
- 五、STM32CubeMX 配置定时器更新中断功能
这一章介绍如何使用
STM32F429
的通用定时器,
STM32F429
的定时器功能十分强大,有
TIME1
和
TIME8
等高级定时器,也有
TIME2~TIME5
,
TIM9~TIM14
等通用定时器,还有
TIME6
和
TIME7
等基本定时器,总共达 14 个定时器之多。在本章中,我们将使用
TIM3
的定时器中断来控制
DS1
的翻转,在主函数用
DS0
的翻转来提示程序正在运行。
一、STM32F429 通用定时器简介
STM32F429
的通用定时器包含一个 16 位或 32 位自动重载计数器(CNT
),该计数器由可编程预分频器(PSC
)驱动。STM32F429
的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM
)等。 使用定时器预分频器和 RCC
时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。STM32F429
的每个通用定时器都是完全独立的,没有互相共享的任何资源。
STM32
的通用 TIMx
(TIM2~TIM5
和 TIM9~TIM14
)定时器功能包括:
- 16 位/32 位(仅
TIM2
和TIM5
)向上、向下、向上/向下自动装载计数器(TIMx_CNT
),注意:TIM9~TIM14
只支持向上(递增)计数方式。 - 16 位可编程(可以实时修改)预分频器(
TIMx_PSC
),计数器时钟频率的分频系数为1~65535
之间的任意数值。 - 4 个独立通道(
TIMx_CH1~4
,TIM9~TIM14
最多 2 个通道),这些通道可以用来作为:
A.输入捕获
B.输出比较
C.PWM
生成(边缘或中间对齐模式) ,注意:TIM9~TIM14
不支持中间对齐模式
D.单脉冲模式输出 - 可使用外部信号(
TIMx_ETR
)控制定时器和定时器互连(可以用 1 个定时器控制另外一个定时器)的同步电路。 - 如下事件发生时产生中断
/DMA
(TIM9~TIM14
不支持DMA
):
A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
B.触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
C.输入捕获
D.输出比较
E.支持针对定位的增量(正交)编码器和霍尔传感器电路(TIM9~TIM14
不支持)
F.触发输入作为外部时钟或者按周期的电流管理(TIM9~TIM14
不支持)
下面我们介绍一下与我们这章的实验密切相关的几个通用定时器的寄存器。
-
控制寄存器 1:
TIMx_CR1
该寄存器的各位描述如图所示:
在本实验中,我们只用到了TIMx_CR1
的最低位,也就是计数器使能位,该位必须置 1,才能让定时器开始计数。 -
DMA/
中断使能寄存器:TIMx_DIER
该寄存器的各位描述如图所示:
这里我们同样仅关心它的第 0 位,该位是更新中断允许位,本章用到的是定时器的更新中断,所以该位要设置为 1,来允许由于更新事件所产生的中断。\ -
预分频寄存器:
TIMx_PSC
该寄存器用来设置对时钟进行分频,然后提供给计数器,作为计数器的时钟。该寄存器的各位描述如图所示:
这里,定时器的时钟来源有 4 个:- 内部时钟
CK_INT
:默认情况下,TIM3
的时钟来源是内部时钟CK_INT
- 外部时钟模式 1:外部输入脚
TIx
- 外部时钟模式 2:外部触发输入
ETR
,仅适用于TIM2
、TIM3
、TIM4
- 内部触发输入
ITRx
:使用A
定时器作为B
定时器的预分频器(A
为B
提供时钟)。
这些时钟,具体选择哪个可以通过
TIMx_SMCR
寄存器的相关位来设置。这里的CK_INT
时钟是从APB1
倍频来的,除非APB1
的时钟分频数设置为 1(一般都不会是 1),否则通用定时器TIMx
的时钟是APB1
时钟的 2 倍,当APB1
的时钟不分频的时候,通用定时器TIMx
的时钟就等于APB1
的时钟。这里还要注意的就是高级定时器以及TIM9~TIM11
的时钟不是来自APB1
,而是来自APB2
的。 - 内部时钟
-
TIMx_CNT
寄存器
该寄存器是定时器的计数器,该寄存器存储了当前定时器的计数值。 -
自动重装载寄存器:
TIMx_ARR
该寄存器在物理上实际对应着 2 个寄存器。一个是程序员可以直接操作的,另外一个是程序员看不到的,这个看不到的寄存器在被叫做影子寄存器。事实上真正起作用的是影子寄存器。根据TIMx_CR1
寄存器中APRE
位的设置:APRE=0
时,预装载寄存器的内容可以随时传送到影子寄存器,此时 2 者是连通的;而APRE=1
时,在每一次更新事件(UEV
)时,才把预装载寄存器(ARR
)的内容传送到影子寄存器。
自动重装载寄存器的各位描述如图所示:
-
状态寄存器:
TIMx_SR
该寄存器用来标记当前与定时器相关的各种事件/中断是否发生。该寄存器的各位描述如图所示:
只要对以上几个寄存器进行简单的设置,我们就可以使用通用定时器了,并且可以产生中断。
这一章,我们将使用定时器产生中断,然后在中断服务函数里面翻转 DS1
上的电平,来指示定时器中断的产生。接下来我们以通用定时器 TIM3
为实例,来说明要经过哪些步骤,才能达到这个要求,并产生中断。这里我们就对每个步骤通过库函数的实现方式来描述。首先要提到的是,定时器相关的库函数主要集中在 HAL
库文件 stm32f4xx_hal_tim.h
和 stm32f4xx_hal_tim.c
文件中。定时器配置步骤如下:
-
TIM3 时钟使能
HAL
中定时器使能是通过宏定义标识符来实现对相关寄存器操作的,方法如下:__HAL_RCC_TIM3_CLK_ENABLE(); //使能 TIM3 时钟
-
初始化定时器参数:设置自动重装值,分频系数,计数方式等。
在HAL
库中,定时器的初始化参数是通过定时器初始化函数HAL_TIM_Base_Init
实现的:HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);
该函数只有一个入口参数,就是
TIM_HandleTypeDef
类型结构体指针,其定义为:typedef struct { TIM_TypeDef *Instance; TIM_Base_InitTypeDef Init; HAL_TIM_ActiveChannel Channel; DMA_HandleTypeDef *hdma[7]; HAL_LockTypeDef Lock; __IO HAL_TIM_StateTypeDef State; }TIM_HandleTypeDef;
- 第一个参数
Instance
是寄存器基地址。和串口,看门狗等外设一样,一般外设的初始化结构体定义的第一个成员变量都是寄存器基地址。 - 第二个参数
Init
为真正的初始化结构体TIM_Base_InitTypeDef
类型。该结构体定义如下:typedef struct { uint32_t Prescaler; //预分频系数 uint32_t CounterMode; //计数方式 uint32_t Period; //自动装载值 ARR uint32_t ClockDivision; //时钟分频因子 uint32_t RepetitionCounter; } TIM_Base_InitTypeDef
- 参数
Prescaler
是用来设置分频系数的。 - 参数
CounterMode
是用来设置计数方式,可以设置为向上计数,向下计数方式还有中央对齐计数方
式,比较常用的是向上计数模式TIM_CounterMode_Up
和向下计数模式TIM_CounterMode_Down
。 - 参数
Period
是设置自动重载计数周期值。 - 参数
ClockDivision
是用来设置时钟分频因子,也就是定时器时钟频率CK_INT
与数字滤波器所使用的采样时钟之间的分频比。 - 参数
RepetitionCounter
用来设置重复计数器寄存器的值,用在高级定时器中。
- 参数
- 第三个参数
Channel
用来设置活跃通道。每个定时器最多有四个通道可以用来做输出比较,输入捕获等功能之用。这里的Channel
就是用来设置活跃通道的,取值范围为:HAL_TIM_ACTIVE_CHANNEL_1~ HAL_TIM_ACTIVE_CHANNEL_4
。 - 第四个
hdma
是定时器的DMA
功能时用到 - 第五个参数
Lock
和State
,是状态过程标识符,是 HAL库用来记录和标志定时器处理过程。
定时器初始化范例如下:
TIM_HandleTypeDef TIM3_Handler; //定时器句柄 TIM3_Handler.Instance=TIM3; //通用定时器 3 TIM3_Handler.Init.Prescaler=8999; //分频系数 TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器 TIM3_Handler.Init.Period=4999; //自动装载值 TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子 HAL_TIM_Base_Init(&TIM3_Handler);
- 第一个参数
-
使能定时器更新中断,使能定时器
HAL
库中,使能定时器更新中断和使能定时器两个操作可以在函数HAL_TIM_Base_Start_IT()
中一次完成的,该函数声明如下:HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
调用该定时器之后,会首先调用
__HAL_TIM_ENABLE_IT
宏定义使能更新中断,然后调用宏定义__HAL_TIM_ENABLE
使能相应的定时器。这里分别列出单独使能/关闭定时器中断和使能/关闭定时器方法:__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 指定的定时器
-
TIM3 中断优先级设置
在定时器中断使能之后,因为要产生中断,必不可少的要设置NVIC
相关寄存器,设置中断优先级。
和串口等其他外设一样,HAL
库为定时器初始化定义了回调函数HAL_TIM_Base_MspInit
。一般情况下,与MCU
有关的时钟使能,以及中断优先级配置我们都会放在该回调函数内部。函数声明如下:void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim);
-
编写中断服务函数
通过该函数来处理定时器产生的相关中断。通常情况下,在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,所以在状态寄存器SR
的最低位。在处理完中断之后应该向TIM3_SR
的最低位写 0,来清除该中断标志。
以定时器 3的更新中断为例,首先,定时器 3 的中断服务函数为:TIM3_IRQHandler();
一般情况下我们是在中断服务函数内部编写中断控制逻辑。但是
HAL
库为我们定义了新的定时器中断共用处理函数HAL_TIM_IRQHandler
,在每个定时器的中断服务函数内部,我们会调用该函数。该函数声明如下:void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim);
而函数
HAL_TIM_IRQHandler
内部,会对相应的中断标志位进行详细判断,判断确定中断来源后,会自动清掉该中断标志位,同时调用不同类型中断的回调函数。所以我们的中断控制逻辑只用编写在中断回调函数中,并且中断回调函数中不需要清中断标志位。
定时器更新中断回调函数为:void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
对于其他类型中断,
HAL
库同样提供了几个不同的回调函数,这里我们列出常用的几个回调函数:void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);//更新中断 void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim);//输出比较 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);//输入捕获 void HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim);//触发中断
通过以上几个步骤,我们就可以达到我们的目的了,使用通用定时器的更新中断,来控制 DS1
的亮灭。
二、硬件设计
本实验用到的硬件资源有:
- 指示灯
DS0
和DS1
- 定时器
TIM3
本章将通过 TIM3
的中断来控制 DS1
的亮灭,DS1
是直接连接到 PB0 上的。 TIM3
属于 STM32F429 的内部资源,只需要软件设置即可正常工作。
三、软件设计
我们直接复制“窗口门狗(WWDG
)实验”的工程模板,将复制过来的模板文件夹重新命名为“7-定时器外部中断实验”。在HARDWARE->TIME
文件夹下面新建timer.c
文件以及头文件 timer.h
。
打开time.c
文件,代码如下:
#include "timer.h"
#include "led.h"
TIM_HandleTypeDef TIM3_Handler; //定时器句柄
//通用定时器3中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器3!(定时器3挂在APB1上,时钟为HCLK/2)
void TIM3_Init(u16 arr,u16 psc)
{
TIM3_Handler.Instance=TIM3; //通用定时器3
TIM3_Handler.Init.Prescaler=psc; //分频系数
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM3_Handler.Init.Period=arr; //自动装载值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子
HAL_TIM_Base_Init(&TIM3_Handler);
HAL_TIM_Base_Start_IT(&TIM3_Handler); //使能定时器3和定时器3更新中断:TIM_IT_UPDATE
}
//定时器底册驱动,开启时钟,设置中断优先级
//此函数会被HAL_TIM_Base_Init()函数调用
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE(); //使能TIM3时钟
HAL_NVIC_SetPriority(TIM3_IRQn,1,3); //设置中断优先级,抢占优先级1,子优先级3
HAL_NVIC_EnableIRQ(TIM3_IRQn); //开启ITM3中断
}
}
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
HAL_TIM_IRQHandler(&TIM3_Handler);
}
//回调函数,定时器中断服务函数调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==(&TIM3_Handler))
{
LED1=!LED1; //LED1反转
}
}
其头文件timer.h
为
#ifndef _TIMER_H
#define _TIMER_H
#include "sys.h"
extern TIM_HandleTypeDef TIM3_Handler; //定时器句柄
void TIM3_Init(u16 arr,u16 psc);
#endif
第一个函数 TIM3_Init
用来初始化定时器 3,使能定时器 3 更新中断以及使能定时器。该函数的 2 个参数用来设置 TIM3
的溢出时间。因为我们在 Stm32_Clock_Init
函数里面已经初始化 APB1
的时钟为 4 分频,所以 APB1
的时钟为 45M
,而从 STM32F429
的内部时钟树图中得知:当 APB1
的时钟分频数为 1 的时候,TIM2~7
以及 TIM12~14
的时钟为 APB1
的时钟,而如果 APB1
的时钟分频数不为 1,那么TIM2~7
以及 TIM12~14
的时钟频率将为 APB1
时钟的两倍。因此,TIM3
的时钟为 90M
,再根据我们输入的 arr
和 psc
的值,就可以计算中断时间了。计算公式如下:
T
o
u
t
=
(
(
a
r
r
+
1
)
∗
(
p
s
c
+
1
)
)
/
T
c
l
k
Tout= ((arr+1)*(psc+1))/Tclk
Tout=((arr+1)∗(psc+1))/Tclk
其中:
Tclk
:TIM3
的输入时钟频率(单位为Mhz
)。Tout
:TIM3
溢出时间(单位为 us)。
第二个函数 HAL_TIM_Base_MspInit
是定时器初始化回调函数,主要是使能定时器 3 时钟以及定时器 3 的 NVIC
配置。
第三个函数TIM3_IRQHandler
是中断服务入口函数,该函数内部只有一行代码就是调用定时器中断共用处理函数 HAL_TIM_IRQHandler
。函数 HAL_TIM_IRQHandler
内部会判断中断来源,根据中断来源调用不同的中断处理回调函数。这里我们开启的是定时器 3 的更新中断,所以我们需要重定义更新中断回调函数HAL_TIM_PeriodElapsedCallback
。
第四个函数HAL_TIM_PeriodElapsedCallback
就是更新中断回调函数,也就是真正的中断处理函数,该函数内部通过判断中断是定时器 3 之后,然后控制 LED1
翻转。
主函数main.c
代码如下:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "timer.h"
int main(void)
{
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180Mhz
delay_init(180); //初始化延时函数
uart_init(115200); //初始化USART
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
TIM3_Init(5000-1,9000-1); //定时器3初始化,定时器时钟为90M,分频系数为9000-1,
//所以定时器3的频率为90M/9000=10K,自动重装载为5000-1,那么定时器周期就是500ms
while(1)
{
LED0=!LED0; //LED0翻转
delay_ms(200); //延时200ms
}
}
这段代码对 TIM3
进行初始化之后,进入死循环等待 TIM3
溢出中断,当 TIM3_CNT
的值等于 TIM3_ARR
的值的时候,就会产生 TIM3
的更新中断,然后在中断里面取反 LED1
,TIM3_CNT
再从 0 开始计数。
这里定时器定时时长 500ms 是这样计算出来的,定时器的时钟为 90Mhz
,分频系数为 8999
,所以分频后的计数频率为 90Mhz/(8999+1)=10KHz
,然后计数到 4999
,所以时长为(4999+1)/10000=0.5s
,也就是 500ms
。
四、实验现象
使用 USB
线将开发板和电脑连接成功后(电脑能识别开发板上 CH340
串口),把编译后产生的.hex
文件烧入到芯片内。可以看到:DS0
不停闪烁,而 DS1
也是不停的闪烁,但是闪烁时间较DS0
慢。
五、STM32CubeMX 配置定时器更新中断功能
首先使能 TIM3
的方法,
然后进入 Configuration->TIM3
配置页,在弹出的界面中点击 Parameter Settings
选项卡,Counter Settings
配置栏下面的四个选项就是用来配置定时器的预分频系数,自动装载值,计数模式以及时钟分频因子。操作方法如下图所示:
最后,进入 Configuration->NVIC
配置页,在弹出的界面中点击 NVIC
选项卡,配置 Interrupt Table
中的 TIM3 global interrupt
,使能中断,配置抢占优先级和响应优先级。
经过上面三个步骤,生成代码,大家对比生成的代码和实验工程的区别。这里需要说明的是,默认情况下,TIM3
的时钟来源是内部时钟 CK_INT
,所以在我们实验中使用的是默认配置,没有额外在程序中提现。
/* TIM3 init function */
void MX_TIM3_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig;
TIM_MasterConfigTypeDef sMasterConfig;
htim3.Instance = TIM3;
htim3.Init.Prescaler = 8999;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 4999;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim3);
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig);
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);
}
void TIM3_IRQHandler(void)
{
/* USER CODE BEGIN TIM3_IRQn 0 */
/* USER CODE END TIM3_IRQn 0 */
HAL_TIM_IRQHandler(&htim3);
/* USER CODE BEGIN TIM3_IRQn 1 */
/* USER CODE END TIM3_IRQn 1 */
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{
if(htim_base->Instance==TIM3)
{
/* USER CODE BEGIN TIM3_MspInit 0 */
/* USER CODE END TIM3_MspInit 0 */
/* Peripheral clock enable */
__HAL_RCC_TIM3_CLK_ENABLE();
/* Peripheral interrupt init */
HAL_NVIC_SetPriority(TIM3_IRQn, 1, 3);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
/* USER CODE BEGIN TIM3_MspInit 1 */
/* USER CODE END TIM3_MspInit 1 */
}
}