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份 ·

实验所需软件

  • MDK5 安装步骤

  • ST-LINK 驱动安装步骤

  • Git 软件下载(可选)

ST-Link下载器 & ST-Link下载器连接线

STLink下载器

STM32底座:HIVE PRO STM32是一种基于STM32F103C8T6芯片的蜂巢底座。

STM32底座

LED模块:LED模块共有四个按键,4个LED灯,可供完成流水灯、按键处理等相关实验。按键的触发为低电平,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   
}

常见问题

  1. 弹出警告窗口,不能下载程序。

    • 请确认STLink驱动、STM32F103C8的DFP包是否安装。
    • STLink仿真器是否正常接入。
  2. 下载代码后程序没观察到实验现象。

    • 请重新上电,或者按下底座上的复位按键。
    • 模块没有安装稳妥。

实验思考

  1. 在原代码的基础上修改定时器中断周期为500ms。

  2. 在原代码的基础上修改定时器中断周期为5000ms。