STM32 定时器实验
实验内容
- 利用定时器做为时间基准,LED1、LED2、LED3、LED4每隔一秒就翻转一次状态,即亮1秒、灭1秒。
实验目的
- 认识定时器,了解定时器工作原理。
- 认识定时器中断,掌握定时器中断的应用。
- 区别循环延时和使用定时器的区别。
实验环境
实验所需硬件
序号 | 名称 | 数量 | 备注 |
---|---|---|---|
1 | 电脑 | 1台 | 电脑安装有Keil 5和ST-Link驱动 |
2 | STM32底座模块 | 1个 | · |
3 | LED模块 | 1个 | · |
4 | ST-Link下载器 | 1个 | · |
5 | ST-Link下载器连接线 | 1根 | · |
6 | 配套定时器实验代码 | 1份 | · |
实验所需软件
ST-Link下载器 & ST-Link下载器连接线
STM32底座:HIVE PRO STM32是一种基于STM32F103C8T6芯片的蜂巢底座。
LED模块:LED模块共有四个按键,4个LED灯,可供完成流水灯、按键处理等相关实验。按键的触发为低电平,LED灯低电平点亮。
实验要求
- 了解STM32F103的片上定时器以及功能;
- 了解计时器工作的不同计数模式;
- 能够自行编程完成定时器的配置以及使用。
实验原理
0
定时器介绍
在STM32F10xx系列的32位MCU上,定时器资源十分丰富,包括高级定时器、通用定时器和基本定时器。此外,还有能够实现定时功能的系统滴答定时器,实时时钟以及看门狗。关于这些定时器的介绍,占据了STM32F10xx参考手册1/5的篇幅,可见其功能的强大。
在低容量和中容量的STM32F103xx产品,以及互联型产品STM32F105xx和STM32F107xx中,只有一个高级控制定时器TIM1。而在高容量和超大容量的STM32F103xx产品中,有两个高级控制定时器TIM1和TIM8。
STM32的定时器是个强大的模块,定时器使用的频率也是很高的,定时器可以做一些基本的定时,还可以做PWM输出或者输入捕获功能。
STM32有八个定时器TIMx,其中TIM1和TIM8挂在APB2总线上,而TIM2-TIM7则挂在APB1总线上。其中TIM1 & TIM8称为高级控制定时器(advanced control timer)。APB2可以工作在72MHz下,而APB1最大是36MHz。如图所示:
定时器的时钟不是直接来自于APB1或APB2,而是APB1或APB2经过一个倍频器,当APB1的预分频系数为1时,这个倍频器不起作用,定时器的时钟频率等于APB1的频率;当APB1的预分频系数为其它数值(即预分频系数为2、4、8或16)时,这个倍频器起作用,定时器输出的时钟频率等于APB1的频率两倍。假设AHB=36MHz,当APB1预分频系数=1时,APB1=36MHz,这时,TIM2 ~ 7的时钟频率=36MHz(倍频器不起作用);当APB1预分频系数=4时,APB1=9MHz,在倍频器的作用下,TIM2 ~ 7的时钟频率=2x9=18MHz。APB2上的定时器频率配置与此类似。
本节,将使用定时器产生中断,然后在中断服务函数里面翻转LED1的电平用于指示定时器中断的产生。下面以通用定时器TIM2为例,介绍通用定时器的使用步骤。
通用定时器配置步骤如下:
- TIM2时钟使能。
HAL库中定时器使能是通过宏定义标识符来实现对相关寄存器操作的,方法如下:
__HAL_RCC_TIM2_CLK_ENABLE(); //使能 TIM2 时钟
- 初始化定时器参数,设置自动重装值,分频系数,计数方式等。
在HAL库中,定时器的初始化参数是通过定时器初始化函数HAL_TIM_Base_Init实现的:
HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);
该函数只有一个入口参数,就是TIM_HandleTypeDef类型结构体指针,结构体定义如下:
typedef struct
{
/*!< Register base address*/
TIM_TypeDef *Instance;
/*!< TIM Time Base required parameters */
TIM_Base_InitTypeDef Init;
/*!< Active channel */
HAL_TIM_ActiveChannel Channel;
/*!<DMA Handlers arrayThis array is acces sed by a @ref TIM_DMA_Handle_
index */
DMA_HandleTypeDef *hdma[7];
/*!< Locking object */
HAL_LockTypeDef Lock;
/*!< TIM operation state */
__IO HAL_TIM_StateTypeDef State;
}TIM_HandleTypeDef;
第一个参数Instance是寄存器基地址。一般外设的初始化结构体定义的第一个成员变量都是寄存器基地址。这在HAL库中都定义好了,比如要初始化定时器2,那么Instance的值设置为TIM2即可。
第二个参数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 TIM2_Handler; //定时器句柄
TIM2_Handler.Instance=TIM2;//通用定时器2
TIM2_Handler.Init.Prescaler=6399;//分频系数
TIM2_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM2_Handler.Init.Period=4999;//自动装载值
TIM2_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//分频因子
HAL_TIM_Base_Init(&TIM2_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 指定的定时器
- TIM2中断优先级设置
在定时器中断使能之后,因为要产生中断,必不可少的要设置NVIC相关寄存器,设置中断优先级。HAL库为定时器初始化定义了回调函数HAL_TIM_Base_MspInit。函数声明如下:
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim);
对于回调函数就不做过多介绍,只需要重写这个函数即可。
- 编写中断服务函数
最后,编写的是定时器中断服务函数,通过该函数来处理定时器产生的相关中断。通常情况下,在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,这里使用的是更新(溢出)中断,所以在状态寄存器SR的最低位。在处理完中断之后应该向TIM2_SR的最低位写0,来清除该中断标志。对于定时器中断,HAL库同样封装了处理过程。这里以定时器2的更新中断为例来介绍:
首先,中断服务函数名称是不变的,定时器2的中断服务函数为:
TIM2_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);//触发中断
通过以上几个步骤,就可以达到使用定时器的目的。
实验步骤
① LED模块安装于STM32底座上,ST-Link连接底座与计算机。如下图:
② 访问github,进入github界面后点击Code,Clone HTTPS安全链接,如下图所示:
③ 打开电脑终端,进入工作目录workspace (workspace 为工程文件夹所在目录):
cd workspace
④ 运行clone命令:
git clone https://github.com/aiotcom/eps.git
下载目录至指定文件夹下。
如果提示“command not found”表示电脑没有安装Git,请至Git官网下载。
如果电脑没有安装 Git 软件,也可以进入Github,点击 Code
-> DownLoad ZIP
下载所有工程代码。如下图所示:
如果电脑没有公网,可以进入D:\实验教程与代码 选择相应的代码。
⑤ 打开 Keil 5 工程软件,点击工具栏: ` Project ->
Open Project,选择工程文件:
STM32基础实验\4.定时器实验\USER\TIMER.uvprojx` 并打开。
如果没有安装该软件,请至Keil官网下载。
⑥ 工程启动后,点击 Rebuild
重新编译。如下图:
⑦ 编译成功,如下图:
⑧ 点击 Download
按钮下载程序,如下图所示:
⑨ 下载完成后,将USB线进行重连操作(即:将STLink的USB线从底座上取下,再重新接上)。
⑩ 仔细观察LED模块上的实验现象,LED1~LED4将以1秒为间隔闪烁。其中1秒的间隔是以定时器计时为基准。
代码讲解
① 程序目录结构,如下图。CORE文件夹为STM32内核代码,HALLIB文件文件夹为底层HAL库文件。我们主要关心main.c及HARDWARE中的代码。
② main.c中进行硬件的初始化、定时器的中断周期初始及整个代码的逻辑控制。重点了解定时器中断的中断服务函数中的代码。当定时器溢出后,程序就会运行到TIM2_IRQHandler ()中,改变四个LED灯的状态。
extern TIM_HandleTypeDef TIM2_Handler;//定时器句柄
int main(void)
{
HAL_Init();//初始化HAL库
LED_Init();//初始化LED控制接口
/* 系统时钟64MHz 64MHz/6400/10000 = 1HZ*/
TIM2_Init(10000-1,6400-1);//定时器2初始化中断周期1秒
while(1);
}
定时器2中断服务函数:
void TIM2_IRQHandler(void)
{
static uint8_t state = 0;
state = 1 - state;
if(state){
LED_ON();//LED灯全亮
}
else{
LED_OFF();//LED灯全灭
}
__HAL_TIM_CLEAR_IT(&TIM2_Handler, TIM_IT_UPDATE);//清中断标志位,必须需要
}
③ LED.c为LED驱动代码,代码实现对LED灯控制的IO进行初始化设置IO口为推挽上拉模式。重点不要忘记使用能IO接口的时钟及设置输出的速度。
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOA_CLK_ENABLE();//开启GPIOA时钟
GPIO_Initure.Pin=LED1_PIN|LED2_PIN|LED3_PIN|LED4_PIN;
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;//推挽输出
GPIO_Initure.Pull=GPIO_PULLUP;//上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH;//高速
HAL_GPIO_Init(LED_PORT,&GPIO_Initure);
LED_OFF();
}
④ timer.c 设置定时器计数时钟及定时器溢出时间,设置中断抢占优先级。
void TIM2_Init(unsigned int arr,unsigned int psc)
{
TIM2_Handler.Instance=TIM2;//通用定时器2
TIM2_Handler.Init.Prescaler=psc;//分频系数
TIM2_Handler.Init.CounterMode=TIM_COUNTERMODE_UP;//向上计数器
TIM2_Handler.Init.Period=arr;//自动装载值
TIM2_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子
HAL_TIM_Base_Init(&TIM2_Handler);
HAL_TIM_Base_Start_IT(&TIM2_Handler);//使能定时器2和定时器2更新中断:TIM_IT_UPDATE
}
常见问题
-
弹出警告窗口,不能下载程序。
- 请确认STLink驱动、STM32F103C8的DFP包是否安装。
- STLink仿真器是否正常接入。
-
下载代码后程序没观察到实验现象。
- 请重新上电,或者按下底座上的复位按键。
- 模块没有安装稳妥。
实验思考
-
在原代码的基础上修改定时器中断周期为500ms。
-
在原代码的基础上修改定时器中断周期为5000ms。