通用定时器简介(以F429为例)

(部分图引自于ATK)

前情提要(基本定时器)

点此进入

通用定时器类别

CubeMX配置计时器_stm32


通用定时器和基本定时器相比大致的工作方式是相似的,不过通用定时器比基本定时器多了一些很好用的功能,比如:

  • 外部输入捕获
  • 输出比较
  • 输出PWM

时钟源

CubeMX为我们提供了配置时钟的非常方便的工具。首先还是看这张图:

CubeMX配置计时器_stm32_02


通用定时器的时钟源可以选择以下四种:

  • 内部时钟
  • 外部时钟模式1
  • 外部时钟模式2
  • 内部触发输入

一般用的最多的还是内部时钟源。以TIM3为例,由上图,TIM3挂载于APB1总线。在Cube MX中可以设置APB1总线定时器的时钟频率。

CubeMX配置计时器_stm32_03


本文内APB1定时器频率均设置为75MHz。

计数模式

通用定时器的计数模式有三种,分别为:

  • 向上(递增)计数
  • 向下(递减)计数
  • 中心对齐

    这部分和基本定时器的内容基本一致。

功能

通用定时器可以实现输入捕获、输出比较等功能。以输入捕获为例,其顾名思义是定时器对输入的某个信号的上升沿、下降沿或者双边沿进行检测。输入信号通常来自于定时器的4个通道中的某一通道,其通过GPIO的复用功能引入。比如我们在CubeMX中随便选中一个GPIO,以PB0为例:

CubeMX配置计时器_寄存器_04


TIM3_CH3表明该GPIO可以被复用为TIM3的第3通道。

输入捕获功能可以测量输入信号的脉冲宽度、测量 PWM 输入信号的频率和占空比等。其他功能同理都基于这些定时器通道实现。

通用定时器中断应用(TIM3)

预期功能

使用通用定时器TIM3在定时器中断中以2s的间隔点亮LED0,在main.c的While(1)中以500ms间隔点亮LED1,比较二者的点亮间隔。

CubeMX配置

CubeMX配置计时器_stm32_05


时钟源选择内部时钟。此处没用到定时器通道,因此4个通道都是禁用状态。在Parameter Settings里配置PSC、ARR寄存器的写入值和计数方式。这一部分和基本定时器相同。(截图里的PSC写错了,应该是15000)

CubeMX配置计时器_嵌入式硬件_06


NVIC中开启TIM3全局中断。抢占优先级设为1.

CubeMX配置计时器_嵌入式硬件_07

tim.c配置

重写HAL_TIM_PeriodElapsedCallback()

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  UNUSED(htim);
  if (htim->Instance == TIM3)	//确认中断由TIM3申请
  {
    HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);		//反转LED0电平
  }
}

main.c配置

打开TIM3时基:

HAL_TIM_Base_Start_IT(&htim3);

重写死循环:

while (1)
  {
     HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_1);
     HAL_Delay(500);
  }

编译下载

实物演示就不放了。这一部分和基本定时器的使用是一致的,只不过改了定时器的编号。

通用定时器输出PWM应用(TIM3)

简介

生成PWN是定时器最为实用的功能之一,PWM可以用在很多地方,最常见的就是驱动调速。

在计数器频率固定时,PWM的频率由ARR确定,其占空比由CCR(捕获/比较寄存器)确定。产生原理如下图:

CubeMX配置计时器_寄存器_08


显然上图中的定时器工作在向上计数模式,纵轴为CNT。当CNT小于CCR写入值时,IO视为输出低电平;CNT大于等于CCR写入值时,IO视为输出高电平。CNT=ARR时定时器事件更新,CNT清零,进入下一个PWM周期。定时器可以通过多种方式产生PWM,一般使用边沿对齐模式:

CubeMX配置计时器_嵌入式硬件_09

预期功能

使用TIM3通道3(复用PB0)输出PWM至LED,通过PWM控制LED亮度。

CubeMX配置

GPIO(PB0)选择复用推挽输出,上拉,高速。

CubeMX配置计时器_单片机_10


定时器选择TIM3,时钟源选择内部时钟,通道3选择PWM Generation CH3.

接下来开始配置定时器参数。根据以下顺序一步步的进行:

1.配置TIM3时钟频率:这部分在Clock Configuartion中设置。根据上文,APB2总线定时器频率为75MHz。

2.配置PWM频率:根据
CubeMX配置计时器_寄存器_11
而KaTeX parse error: Expected '}', got 'EOF' at end of input: {f_{clk}为75MHz,如果我们要生成的PWM频率为2000Hz,那么对应的定时器计数间隔即为0.0005s,可以推算出ARR为500,PSC为75.

3.配置占空比:占空比由捕获/比较寄存器CCR写入值确定。CCR的值可以固定也可以不固定。CubeMX中提供的CCR写入值算是一个初始值。占空比还和输出极性的高低有关。当输出极性为高时,只有当ARR大于CCR时(即有效电平),IO才输出高电平。假设CCR写入值为200,则此时的占空比为:
CubeMX配置计时器_stm32_12
当输出极性为低,当ARR大于CCR(即有效电平)时,IO输出低电平。此时占空比显然为40%。

配置好的参数如下图所示:

CubeMX配置计时器_嵌入式硬件_13


其中,

  • Prescaler (PSC - 16 bits value):PSC寄存器写入值
  • Counter Mode:计数模式
  • **Counter Period **:ARR寄存器写入值
  • Internal Clock Division:二次分频,此处禁用
  • auto-reload preload:影子寄存器
  • Trigger Output (TRGO)Parameters:此处设置当计数器溢出事件更新后是否输出事件,这里用不到
  • Mode:PWM输出模式。Mode2是Mode1的反相
  • Pulse:比较时钟数,即CCRx寄存器写入值
  • Output compare preload:在程序运行中实时更改CCRx写入值时,是立即有效还是等当前计数周期结束后才生效
  • Fast Mode:快驱模式,这里用不到
  • CH Polarity:输出比较极性。LED0为低电平有效,因此此处选Low,表明当ARR>CCRx时有效电平输出低电平

本应用没用到定时器中断,因此NVIC中的TIM3全局中断可以不用开启。

main.c

通过PWM控制LED亮度的基本思路为,设置一个变量ledpwmval来控制CCRx写入值从而控制占空比,通过变量direction来设置当前占空比是从大变小还是从小变大。初始的CCR3写入值为0,direction为1,表明有效电平(低电平)时间占整个周期时间的100%,且LED亮度变换方向为越来越暗。之后每个计数事件结束后都将CCR3写入值逐步增大直至等于ARR,此时LED熄灭。然后,调转direction的值为0,表明LED亮度变换方向为越来越亮,在之后的计数事件中,每个计数结束后都将CCR3写入值逐渐减小至0,直至LED最亮。如此重复。

在HAL库提供了一些关于PWM的函数,主要的有:

HAL_TIM_PWM_Init

声明为

HAL_StatusTypeDef HAL_TIM_PWM_Init (TIM_HandleTypeDef * htim)

形参为htim句柄,用于确认是哪个定时器申请的PWM。该函数根据TIM_HandleTypeDef中指定的参数初始化TIM PWM时基以及初始化关联句柄。

HAL_TIM_PWM_MspInit

声明为

void HAL_TIM_PWM_MspInit (TIM_HandleTypeDef * htim)

用于初始化TIM PWM MSP。

HAL_TIM_PWM_Start

声明为

HAL_StatusTypeDef HAL_TIM_PWM_Start (TIM_HandleTypeDef * htim, uint32_t Channel)

形参为:

  • htim:TIM句柄
  • Channel:TIM需要启用的通道。可为以下选项之一:
    —TIM_CHANNEL_1:选择TIM通道1
    —TIM_CHANNEL_2:选择TIM通道2
    —TIM_CHANNEL_3:选择TIM通道3
    —TIM_CHANNEL_4:选择TIM通道4

该函数用于启动对应TIM的对应通道的PWM输出功能。

HAL_TIM_PWM_Stop

声明为

HAL_StatusTypeDef HAL_TIM_PWM_Stop (TIM_HandleTypeDef * htim, uint32_t Channel)

形参为:

  • htim:TIM句柄
  • Channel:TIM需要禁用的通道。可为以下选项之一:
    —TIM_CHANNEL_1:选择TIM通道1
    —TIM_CHANNEL_2:选择TIM通道2
    —TIM_CHANNEL_3:选择TIM通道3
    —TIM_CHANNEL_4:选择TIM通道4

该函数用于关闭对应TIM的对应通道的PWM输出功能。

然后我们开始编辑main.c中的代码。

首先,定义pwmledvaldirection

uint16_t pwmledval = 0;  
uint8_t direction = 1;

注意CCR3是一个16位的寄存器,因此要把pwmledval定义成uint16_t类型。

然后打开TIM3 PWM:

HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);

不需要在main.c中再次初始化PWM,不然会使得PWM无法输出。

然后,重写While(1):

while (1)
  {
    delay_ms(10);

    if (direction) pwmledval++;
    else pwmledval--;

    if (pwmledval > 500) direction = 0;
    if (pwmledval == 0) direction = 1;


    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, pwmledval);
  }

注意最后的这个 __HAL_TIM_SET_COMPARE() 。它是HAL库提供的宏,原始声明为:

#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
  (((__CHANNEL__) == TIM_CHANNEL_1) ? ((__HANDLE__)->Instance->CCR1 = (__COMPARE__)) :\
   ((__CHANNEL__) == TIM_CHANNEL_2) ? ((__HANDLE__)->Instance->CCR2 = (__COMPARE__)) :\
   ((__CHANNEL__) == TIM_CHANNEL_3) ? ((__HANDLE__)->Instance->CCR3 = (__COMPARE__)) :\
   ((__HANDLE__)->Instance->CCR4 = (__COMPARE__)))

HANDLE为对应实例(哪个定时器)、CHANNEL是对应通道号,COMPARE是比较值(CCRx寄存器写入值)。有了这个宏,我们就可以很方便在主函数中实时修改定时器PWM比较值。

编译下载

此处略。

通用定时器输入捕获(TIM5)

简介

输入捕获功能可以用来测量外部输入的脉冲宽度,或者测量外部输入信号的频率。如图:

CubeMX配置计时器_stm32_14


假设外部输入的电平信号在t1-t2时间段内为高电平,那么这一段时间就是我们要测量的高电平时间。若TIM工作在向上计数模式,那么首先需要设置TIM通道x为上升沿捕获。当t1的外部电平上升沿到来时即触发捕获事件,同时打开捕获中断,在中断内将CNT清零,并设置通道x为下降沿捕获。t2下降沿到来时会触发捕获事件同时进入捕获中断。在捕获事件内,CNT的值会被锁存到CCRx中。那么我们只要在捕获中断内读取CCRx的值就可以获取到在外部高电平时间段内CNT的值,从而计算得到外部高电平所持续的时间。

不过,在这段时间内,TIM可能已经产生了N次溢出事件。那么在高电平时间内,计数器个数的计算方法为:
CubeMX配置计时器_CubeMX配置计时器_15
此处的CCRx是t2时刻CCRx的值。
计算得到计数个数后,将其乘以计数器的计数周期,即可得到高电平持续时间。

预期功能

使用 TIM5_CH1做输入捕获,捕获 PA0上的高电平脉宽,并将脉宽时间通过串口打印,通过按 WK_UP按键,模拟输入高电平。LED0闪烁指示程序运行。

CubeMX配置

直接跳到TIM5参数设置,打开内部时钟,将通道1设置为输入捕获模式:

CubeMX配置计时器_寄存器_16


设置PSC、ARR,IC配置选择上升沿捕获、直接映射到引脚、不分频、不滤波注意这里的PSC和ARR值。计数器计数频率为:

CubeMX配置计时器_stm32_17

而TIM5的ARR是32位寄存器,理论最大值可以到0xFFFFFFFF。不过为了通用,我们还是按照16位寄存器的格式给它赋值,即0xFFFF(65535)

CubeMX配置计时器_寄存器_18


打开TIM5定时器中断

CubeMX配置计时器_stm32_19


配置串口

CubeMX配置计时器_单片机_20

uart.c部分

重写printf定向至串口:

int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}

int fgetc(FILE *f)
{
  uint8_t ch = 0;
  HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
  return ch;
}

main.c部分

首先根据需要声明以下变量:

uint8_t cap_flag = 0;   //标志位,0为未捕获到第一次上升沿,1为捕获到了上升沿但还没有捕获到下降沿,2为捕获到下降沿(即捕获高电平成功)	
uint16_t cap_over = 0;		//定时器溢出次数
uint16_t cap_cnt = 0;		//定时器计数值(CCRx的值)
uint32_t temp = 0;		//临时变量
uint8_t t = 0;		//程序运行时间

启用TIM2时基和TIM2CH1输入捕获

HAL_TIM_Base_Start_IT(&htim5);
HAL_TIM_IC_Start_IT(&htim5,TIM_CHANNEL_1);

然后进入主要的代码部分。首先需要知道以下两个函数:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)	//输入捕获中断调用函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)		//定时器溢出事件中断调用函数

当定时器在中断模式内成功捕获到了一次上升沿/下降沿后,将会调用HAL_TIM_IC_CaptureCallback(),然后执行函数内的代码。不过这个函数本身并不能判断捕捉到的是上升沿还是下降沿,因此我们需要之前声明的标志位cap_flag来手动标记当前的捕获状态。
当cap_flag=0的时候,此时定时器应当是还没有捕捉到第一次上升沿的状态。但如果我们把回调函数写成这样:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM5)
  {
      if (cap_flag == 0)
      {
			..........
      }

    }
}

显然HAL_TIM_IC_CaptureCallback()只会在定时器捕捉到了一次上升沿/下降沿的时候才会被调用。如果当该函数被调用的时候,cap_flag的值是0,那么就说明造成这一次回调的捕获事件所抓到的东西一定是上升沿。于此同理,当cap_flag不为0时,抓到的一定是下降沿。理解了这点之后事情就好办了:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM2)
  {
      if (cap_flag == 0)
      {
        __HAL_TIM_SET_COUNTER(&htim5,0);	//将CCRx清零
        TIM_RESET_CAPTUREPOLARITY(&htim5,TIM_CHANNEL_1);	//重设定时器通道输入捕获极性
        __HAL_TIM_SET_CAPTUREPOLARITY(&htim5,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_FALLING);	//设置为下降沿捕获模式
        cap_flag = 1;	//标记为已捕获到上升沿,等待捕获下降沿
        cap_over = 0;	//清空定时器溢出次数
        cap_cnt = 0;	//清空计数值,标记为上升沿起点,开始计数
      }
      else
      {
        cap_cnt = HAL_TIM_ReadCapturedValue(&htim5,TIM_CHANNEL_1);	//将CCRx中的计数值转存到cap_cnt里
        TIM_RESET_CAPTUREPOLARITY(&htim5,TIM_CHANNEL_1);	//重设定时器通道输入捕获极性
        __HAL_TIM_SET_CAPTUREPOLARITY(&htim5,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_RISING);	//设置为上升沿捕获模式
        cap_flag = 2;	//标记为高电平捕获成功
      }
    }
}

然后我们要处理高电平时间过长导致定时器可能在高电平持续时间内触发N次溢出事件的情况。根据上文:
CubeMX配置计时器_CubeMX配置计时器_15
显然第一个需要获取的值就是定时器的溢出次数N。好在我们有定时器溢出事件中断回调函数HAL_TIM_PeriodElapsedCallback(),他会帮助我们在每次定时器发生溢出之后统计溢出的总次数。当溢出次数达到上限(cap_over是16位无符号整型,所以在这里上限是0xFFFF)时,则强制结束此次高电平捕获,标记cap_flag为2。也就是说,在本应用里,能够检测的高电平最长时间为:
CubeMX配置计时器_stm32_22
这个最长时间和ARR寄存器的写入值,以及cap_over的最大值都有关。不过在这里设置得太大没有什么必要。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM2)
  {
    if (cap_flag == 1)
    {
      if (cap_over == 0xFFFF)	//如果溢出达到最大次数
      {
        cap_flag = 2;	//强制标记高电平捕获结束
        cap_cnt = 0xFFFF;	//计数值为0xFFFF
        TIM_RESET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1);	//重设定时器通道输入捕获极性
        __HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_RISING);	//设置为上升沿捕获模式
      }
      else
      {
        cap_over ++;	//溢出次数+1
      }
    }
  }
}

最后,我们把while(1)写完。

while (1)
  {
    if (cap_flag == 2)		//如果高电平捕获已完成
    {
      temp = (uint32_t)cap_over * (0xFFFF+1) + cap_cnt;		//计算总的计数值并转存至temp
      printf("high time: %d us\r\n",temp);		//打印至串口
      cap_flag = 0;		//清空标志位
    }
}

编译下载

CubeMX配置计时器_stm32_23