STM32CubeMX定时器的配置使用

1 STM32CubeMX

STM32CubeMX是ST官方出的一款针对ST的MCU/MPU跨平台的图形化工具,支持在Linux、MacOS、Window系统下开发,其对接的底层接口是HAL库,另外习惯于寄存器开发的同学们,也可以使用LL库。STM32CubeMX除了集成MCU/MPU的硬件抽象层,另外还集成了像RTOS,文件系统,USB,网络,显示,嵌入式AI等中间件,这样开发者就能够很轻松的完成MCU/MPU的底层驱动的配置,留出更多精力开发上层功能逻辑,能够更进一步提高了嵌入式开发效率。


2 定时器简介

2.1 定时器初探

​ 定时器,顾名思义,就是定时的功能,定时器在单片机中算是除GPIO外最基本的外设。在ST中,定时器分为几种,基础定时器,通用定时器,高级定时器和低功耗定时器。本文重点在于基础定时器的配置,其他类型的定时器在其他篇章中详细说明。
    基础定时器,就是只提供最基础的定时功能,下面我们就从ST这个基础定时器框图来讲解一下定时器原理。

​ 以定时1s为例,如果当前输入的时钟频率为1MHz,那怎么样才能得到1s的间隔呢?众所周知,1MHz,本身的含义就是每秒1000000次的频次,那么很容易就能想到,如果让定时器每个时钟周期计一个数,计到1000000次的时候就是1s的时间。于是第一个参数就出现了——自动重装载值(Auto Reload Value),这个参数的作用就是:当计数达到自动重装载值时,可以触发一个溢出中断标志,用户获取这个溢出中断标志就可以得到1s的时间。
    然而现实并没这么理想,这个自动重装载值只有16位,定时器的计数值也只有16位,也就是说计数最大只能到65535,根本到不了1000000,那应该怎么办呢?想一下,既然计数没办法达到这么大的数,那是不是可以让计数不要计那么快,比如让定时器以1kHz的频率计数,那只要计1000次就可以得到1s的时间。但定时器的输入是1MHz,怎么样才能让它变慢呢?这时候另一个参数就派上用场了——预分频系数(Prescaler Value)。1MHz进行2分频,就是500kHz,进行1000分频,就是1kHz。


2.2 不同芯片所对应的定时器

SMT32F1系列共有8个定时器:

  • 基本定时器(TIM6、TIM7)
  • 通用定时器(TIM2、TIM3、TIM4、TIM5)
  • 高级定时器(TIM1、TIM8)

SMT32F4系列共有14个定时器:

  • 基本定时器(TIM6、TIM7)
  • 通用定时器(TIM2、TIM3、TIM4、TIM5、TIM9~TIM14)
  • 高级定时器(TIM1、TIM8)

基本定时器功能(TIM6、TIM7):

  • 16位向上、向下、向上/下自动装载计数器
  • 16位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为1~65535之间的任意数值
  • 触发DAC的同步电路 注:此项是TIM6/7独有功能.
  • 位于APB1总线上

通用定时器(TIM2~TIM5)的主要功能:

  • 16位向上、向下、向上/下自动装载计数器

  • 16位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为1~65535之间的任意数值

  • 4 个独立通道(TIMx_CH1~4)可以用作:

    测量输入信号的脉冲长度( 输入捕获)
    输出比较
    单脉冲模式输出
    PWM输出(边缘或中间对齐模式)

  • 支持针对定位的增量(正交)编码器和霍尔传感器电路

  • 如下事件发生时产生中断/DMA:
    更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
    触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
    输入捕获
    输出比较

  • 位于APB1总线上

高级定时器(TIM1,TIM8)的主要功能:

  • 高级定时器具有基本,通用定时器的所有的功能,
  • 还具有控制交直流电动机所有的功能,
  • 输出6路互补带死区的信号,刹车功能等等
  • 位于APB2总线上

总括:基本定时器就是单纯的定时计数器,通用定时器多了四个通道,相对应的增加了功能,高级定时器具有基本,通用定时器的所有的功能,并且添加了其他功能。

img


2.3 定时器计数模式

通用定时器可以向上计数、向下计数、向上向下双向计数模式。

向上计数模式:计数器从0计数到自动加载值(TIMx_ARR),然后重新从0开始计数并且产生一个计数器溢出事件。

向下计数模式:计数器从自动装入的值(TIMx_ARR)开始向下计数到0,然后从自动装入的值重新开始,并产生一个计数器向下溢出事件。

中央对齐模式(向上/向下计数):计数器从0开始计数到自动装入的值-1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器溢出事件;然后再从0开始重新计数。

简单地理解三种计数模式,可以通过下面的图形:

img


2.4 计数时钟选择

计数器时钟可由下列时钟源提供:

  • 内部时钟(TIMx_CLK)
  • 外部时钟模式1:外部捕捉比较引脚(TIx)
  • 外部时钟模式2:外部引脚输入(TIMx_ETR) 仅适用TIM2,3,4
  • 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1而作为另一个定时器Timer2的预分频器。

2.5 定时器的主从模式

定时器一般是通过软件设置而启动,STM32的每个定时器也可以通过外部信号触发而启动,还可以通过另外一个定时器的某一个条件被触发而启动。这里所谓某一个条件可以是定时到时间、定时器超时、比较成功等许多条件。

这种通过一个定时器触发另一个定时器的工作方式称为定时器的同步,发出触发信号的定时器工作于主模式,接受触发信号而启动的定时器工作于从模式

触发条件:

img

定时器的四种主从机模式:

  • 外部触发模式1
  • IRC重置模式
  • 门控模式
  • 触发模式

这个我们用的很少,介绍下主要是为了下面的讲解 如果需要全面了解,请参考 《STM32中文参考手册》 275页 定时器主从模式。


2.6 定时器的定时周期

T = (psc+1) * (arr+1) / Tclk

  • psc 为定时器预分频系数
  • arr 为自动重装载值
  • Tclk 为系统时钟频率

通过计算

T = (psc+1) * (arr+1) / Tclk=(31999+1)(499+1) / 32us = 500ms


3 定时器配置

3.1 基本配置

以上面的定时1s钟为例,在CubeMX中应该如何设置。

首先看配置界面,因为这里我们只需要一个定时知会的功能,并不需要使用到外部端口,所以我们可以直接在选项卡”Timers”中选择”TIM6”(TIM6就是基础定时器),勾选”Activated”启用定时器。One Pulse Mode为单次定时模式,勾选该模式则定时器只触发一次,默认定时器为连续触发,触发完一次后自动重载ARR中设置的值重新计数。

在这里插入图片描述

接下来看一下基本的一些配置信息。

在这里插入图片描述

单脉冲模式(One Pulse Mode): 开启这个模式后,只要触发了一次溢出标志,就会自动把计数使能关掉,想再次触发需要手动开启计数使能。所以如果想要实现周期触发事件,就不用勾选这个选项。
预分频系数(Prescaler): 新手杀手之一,虽然这里写的是预分频系数,但实际设置的值是预分频系数-1,也就是需要10分频时,需要设置9。
计数模式(Counter Mode): 计数模式,也称计数方向,决定定时器是递增计数还是递减计数。如果只是用来定时,那递增或递减都没什么影响。
计数周期(Counter Period): 就是当计数值达到计数周期值时,会触发一个溢出标志,新手杀手之二,因为计数是从0开始计的,所以如果想要实现10次计数,这里只需要设置9即可。
自动重装载(auto-reload preload): 如果选择了自动重装载,那么在触发了一次溢出标志后,定时器会自动将计数清0并重新计数。如果不使能该参数,则在使用__HAL_TIM_SET_AUTORELOAD()函数动态修改基本定时器ARR参数值时,修改的值会立马生效;而如果使能该参数,则修改的值会在当前计数溢出之后下次得到修改。
触发事件选择(Trigger Event Selection): 可以选择通过UG标志、计数使能、溢出标志来触发输出。一般是用来设置用作其他外设的触发源的,比如将Trigger Event Selection选择为Update Event,然后在其他外设比如ADC中配置外部触发源时选择该定时器的触发事件(如果可以的话),这样在定时器产生Update Event时就可以启动外设,实现用定时器来控制外设启动的功能。

为了实现1s的计时,我们得先知道当前定时器的时钟频率。先查看下时钟树,TIM6是挂在APB时钟总线上的,这里我们按单片机最高主频设置的,是48MHz。

在这里插入图片描述

为了方便计算,这里我们预分频系数可以填47999,也就是48000分频。计数周期就可以填999,也就是计数1000次。另外再使能自动重装载,这样就可以得到一个1s的周期计数。

在这里插入图片描述

为了让计数周期到时可以立即通知到我们去执行一些操作,这里还需要打开中断服务函数。在”NVIC Setting”选项卡中,勾选”TIM6”的中断使能。

在这里插入图片描述

做完以上配置,选择好使用的库就可以生成工程代码了。

稍微复杂的配置

img

1、选择TIM2

2、定时器时钟选择内部时钟

​ Clock Source(时钟来源)

  • 选项1 :Internal Clock 内部时钟
  • 选项2 : ETR2 外部触发输入(ETR)(仅适用TIM2,3,4)

TRGO Parameters 触发输出 (TRGO) 不使能 与本节无关,之后做详细介绍

TRGO: 定时器的触发信号输出 在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换,)

外设中断配置:

基本定时器的触发有三种模式 ① 轮询方式、② 中断方式和③ DMA方式,这里只介绍前两种方式

① 对于轮询方式,当前设置已经足够,只需要在生成的程序中使用HAL_TIM_Base_Start(&htim6)启动基本定时器,然后不断轮询计数值或UEV事件标志来判断是否发生了计数溢出

② 中断方式是基本定时器最常用的方式,在Pinout & Configuration页面左侧功能分类栏目中点开NVIC栏目,然后选择合适的中断优先级并勾选基本定时器6和7的中断使能

常用函数:

以轮询工作方式启动定时器
HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim)

停止轮询工作方式的定时器
HAL_StatusTypeDef HAL_TIM_Base_Stop(TIM_HandleTypeDef *htim)

以中断工作方式启动定时器
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)

停止中断工作方式的定时器
HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim)

定时器周期回调子函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)

以DMA模式开启定时器

HAL_TIM_Base_Start_DMA()

关闭DMA模式的定时器

HAL_TIM_Base_Stop_DMA()


3.2 LL库代码实现

在生成的工程中添加如下代码,在触发中断时,对一个bool变量进行翻转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/***************************main.c******************************/
int main(void)
{
/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

/* MCU Configuration--------------------------------------------------------*/

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Configure the system clock */
SystemClock_Config();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM6_Init();
/* USER CODE BEGIN 2 */

/* 使能更新中断 */
LL_TIM_EnableIT_UPDATE(TIM6);
/* 使能计数 */
LL_TIM_EnableCounter(TIM6);

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/***********************************************************************/

/***************************stm32f0xx_it.c******************************/
void TIM6_DAC_IRQHandler(void)
{
/* USER CODE BEGIN TIM6_DAC_IRQn 0 */
/* 进中断后判断如果使能了更新中断,且更新中断标志为1,则翻转数据,并清除标志 */
if ( (LL_TIM_IsActiveFlag_UPDATE(TIM6)) && (LL_TIM_IsEnabledIT_UPDATE(TIM6)) )
{
LL_TIM_ClearFlag_UPDATE(TIM6);
TestTimer = (++TestTimer) % 2;
}
/* USER CODE END TIM6_DAC_IRQn 0 */

/* USER CODE BEGIN TIM6_DAC_IRQn 1 */

/* USER CODE END TIM6_DAC_IRQn 1 */
}
/*******************************************************************/

3.3 HAL库代码实现

下述代码是如果TIM6中断,则调用 HAL_TIM_IRQHandler(&htim6);

1
2
3
4
5
6
7
8
9
10
11
/***************************stm32f0xx_it.c******************************/
void TIM6_IRQHandler(void)
{
/* USER CODE BEGIN TIM2_IRQn 0 */

/* USER CODE END TIM2_IRQn 0 */
HAL_TIM_IRQHandler(&htim6);
/* USER CODE BEGIN TIM2_IRQn 1 */

/* USER CODE END TIM2_IRQn 1 */
}

进入HAL_TIM_IRQHandler(&htim6)函数,这里面的代码很长,就是不同的中断类型,进入不同的中断回调函数,这里找到 HAL_TIM_PeriodElapsedCallback(htim)。进入这个回调函数,我们看到是个虚函数,需要我们重写,在这个函数里面,就是我们需要实现的功能,但在这之前,需要在main函数里使能中断 HAL_TIM_Base_Start_IT(&htim6)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/* USER CODE BEGIN 0 */
uint8_t TestTimer = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6) {
/* Prevent unused argument(s) compilation warning */
UNUSED(htim);
TestTimer = (++TestTimer) % 2;
}

/* NOTE : This function should not be modified, when the callback is needed,
the HAL_TIM_PeriodElapsedCallback could be implemented in the user file
*/
}
/* USER CODE END 0 */

/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

/* MCU Configuration--------------------------------------------------------*/

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Configure the system clock */
SystemClock_Config();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM6_Init();
/* USER CODE BEGIN 2 */

/* 使能更新中断 */
HAL_TIM_Base_Start_IT(&htim6);

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}

总结,对于定时器的定时中断,相对比较简单,绝大多数功能图形化配置已经为我们配置好了,我们做的只需两步:

  • 在main函数中,开启TIM6中断,HAL_TIM_Base_Start_IT(&htim6);

  • 重写 HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 函数,然后在里面实现我们需要的功能


3.4 外设初始化调用流程

在工程代码主函数main()中调用MX_TIM6_Init()函数对基本定时器TIM6参数进行了配置

在该MX_TIM6_Init()函数中调用了HAL_TIM_Base_Init()对定时器进行了初始化

然后在HAL_TIM_Base_Init()函数中调用了HAL_TIM_Base_MspInit()函数对TIM6时钟和中断设置/使能

TIM7初始化流程类似,具体定时器TIM6初始化流程如下图所示


3.5 外设中断调用流程

激活了基本定时器并启动TIM6/7全局中断之后,会在stm32f4xx_it.c中新增TIM6/7的中断服务函数TIM6_DAC_IRQHandler()和TIM7_IRQHandler()

该函数均调用HAL库的定时器中断统一处理函数HAL_TIM_IRQHandler(),该函数通过一系列的判断最终得出基本定时器目的为周期回调 (注释4)因此最终调用周期回调函数HAL_TIM_PeriodElapsedCallback(),该函数为虚函数

TIM7中断调用流程类似,具体定时器TIM6中断调用流程如下图所示。

重新在tim.c中实现周期回调函数HAL_TIM_PeriodElapsedCallback(),当定时器TIM6溢出则翻转GREEN_LED引脚状态,当定时器TIM7溢出则翻转RED_LED引脚状态,具体代码如下图所示


3.40 效果演示

烧录软件进行调试,可以看到该变量1s翻转一次电平。

在这里插入图片描述