0.基本情况介绍

stm32F427,定时器的划分如下(参考官方数据手册):
在这里插入图片描述
stm32f103定时器:
在这里插入图片描述

1.定时器MX配置

这里以TIM4高级定时器为例。
先看MX配置:
在这里插入图片描述
这里的配置选项非常多,下面整理一下。

1.1 Slave Mode 从模式配置项

可选的如下:
在这里插入图片描述
- Disable 关闭从模式,定时器独立运行。
- External Clock Mode 1,【外部时钟模式1,很特殊】 定时器时钟时钟源由外部触发信号驱动, 用外部脉冲信号作为计数时钟。当设置该项时,Clock Source变为灰色:
- 在这里插入图片描述
- Reset Mode:触发信号使定时器计数器重置,计数从0重新开始。此时和Trigger名副其实,就是触发一下。
- Gated Mode: 触发信号有效时才计数,测脉冲宽度使用
- Trigger Mode:触发信号启动计数,计数完成后停止,可用来对多个定时器进行同步启动。

1.2 Trigger Mode 触发源设置

可以有如下选项,
在这里插入图片描述
- ITR0-ITR3 :所有TIMx定时器在内部相连,用于定时器同步或链接。当一个定时器处于主模式时,它可以通过在定时器溢出时输出TRGO事件,从而对另一个处于从模式的定时器的计数器进行复位、启动、停止或提供时钟等操作。
下面是F103/F407的定时器主从关系图:在这里插入图片描述
- ETR1:外部引脚,在MX中搜索ETR,可以看到所有的ETR引脚:
在这里插入图片描述
比如PE0端口对应可设置为TIM4的ETR引脚:
在这里插入图片描述
- Tl1_ED: 直接使用通道1输入的边沿触发。
- Tl1FP1: 采集通道1输入信号,滤波后触发。

1.3 Clock Source 时钟源配置

可选的有:
在这里插入图片描述
- Disable:关闭时钟源,不使用该定时器作为时钟源,也就是说定时器不会计数或触发事件。
- Internal Clock:使用内部时钟作为定时器的时钟源,通常是来自系统时钟(如 APB 时钟)经过预分频后的频率。是最常用的时钟源。
- ETR2 :即外部触发输入2,定时器从外部引脚(ETR2)接收时钟或触发信号,适合外部事件驱动计数。这里的ETR2对应的还是上面的各个ETR引脚!

1.4 Channel1配置

在这里插入图片描述

选项 说明
Disable 禁用该通道,不进行任何输入捕获或输出比较操作。
Input Capture direct mode 直接输入捕获模式,定时器在该通道捕获输入信号的定时器计数值,用于测量输入信号的时间特性。
Input Capture indirect mode 间接输入捕获模式,通常用于测量两个通道之间的时间差或配置组合模式。
Input Capture triggered by TRC 由触发控制器(TRC)触发的输入捕获模式,较少用,通常用于高级定时器的同步触发。
Output Compare No Output 输出比较模式,但不产生任何输出信号(仅用于软件事件或计数比较)。
Output Compare CH1 输出比较模式并输出信号,输出信号与通道1的比较结果相关联(如用于同步输出)。
PWM Generation No Output 产生 PWM 波形,但不输出到引脚,仅用于内部逻辑或触发其他事件。
PWM Generation CH1 产生 PWM 波形并输出到通道1关联的引脚。

在这里插入图片描述
单脉冲模式,启动后执行一次即停止。

其他介绍(略)

在这里插入图片描述

2. 关键寄存器

可以通过keil如下方式查看运行时各个外设寄存器的数值:
在这里插入图片描述

2.1 TIMx_CR1 — 控制寄存器1

  • CEN 计数器使能,使能后计数即开始

2.2 TIMx_SR — 状态寄存器

  • UIF(Update Interrupt Flag) 更新中断或计数器溢出标记,如果中断被使能,会触发中断回调;
  • TIF(Trigger Interrupt Flag) 表示定时器触发事件发生,比如TIM2被TIM1触发启动,TIM2该标记设1;

2.3 其他

  • TIMx_CNT:计数器自动递增/递减的数值。
  • TIMx_ARR:计数器达到该值时,产生更新事件。

3.定时器应用

3.1 系统时基(Timebase Sourece)

3.1.1 两个时基

STM32的hal项目中,存在两个“时基”:HAL时基操作系统OS时基。HAL时基为了给HAL_Delay()函数计算延时时间用,OS时基给操作系统进行调度使用。
在这里插入图片描述

MX配置中有个选项Timebase Source,默认是SysTick,它的意思是HAL时基选择的是SysTick中断。如果选择默认的SysTick,会生成如下代码:

void SysTick_Handler(void)
{
  HAL_IncTick(); //驱动HAL时基
  //...省略
  xPortSysTickHandler(); //驱动OS时基
  
}

SysTick_Handler是SysTick中断的回调函数,其中驱动了两个时基,MX推荐我们在有操作系统时,Timebase Source选择某个TIM定时器,比如这里选择TIM6:

在这里插入图片描述
在定时器中断响应函数中,可以看到:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6) {
    HAL_IncTick(); //驱动HAL时基
  }
}

那么此时OS时基在哪驱动呢?搜一下xPortSysTickHandler,发现在cmsis_os2.c中有如下定义:

#if (USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION == 0)
void SysTick_Handler (void) {
  /* Clear overflow flag */
  SysTick->CTRL;

  if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {
    /* Call tick handler */
    xPortSysTickHandler();
  }
}

该USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION 宏在MX时基选择SysTick时,是定义为1的。也就是说OS时基还是靠systick驱动!

3.1.1 超时时间的配置方式

以该TIM6为例,在stm32f4xx_hal_timebase_tim.c中,我们可以看到对TIM6的配置方式,注意该函数在运行时会进入两遍 !第一遍Hal_Init()进入,第二遍HAL_RCC_ClockConfig时进入,第二遍时钟才是对的:

HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  RCC_ClkInitTypeDef    clkconfig;
  uint32_t              uwTimclock, uwAPB1Prescaler = 0U;

  uint32_t              uwPrescalerValue = 0U;
  uint32_t              pFLatency;
  HAL_StatusTypeDef     status;

  /* Enable TIM6 clock */
  __HAL_RCC_TIM6_CLK_ENABLE();
  /* Get clock configuration */
  HAL_RCC_GetClockConfig(&clkconfig, &pFLatency);
  /* Get APB1 prescaler */
  uwAPB1Prescaler = clkconfig.APB1CLKDivider;
  /* Compute TIM6 clock */
  if (uwAPB1Prescaler == RCC_HCLK_DIV1)
  {
    uwTimclock = HAL_RCC_GetPCLK1Freq();
  }
  else
  {
    uwTimclock = 2UL * HAL_RCC_GetPCLK1Freq();
  }

  /* Compute the prescaler value to have TIM6 counter clock equal to 1MHz */
  uwPrescalerValue = (uint32_t) ((uwTimclock / 1000000U) - 1U);

  /* Initialize TIM6 */
  htim6.Instance = TIM6;

  /* Initialize TIMx peripheral as follow:

  + Period = [(TIM6CLK/1000) - 1]. to have a (1/1000) s time base.
  + Prescaler = (uwTimclock/1000000 - 1) to have a 1MHz counter clock.
  + ClockDivision = 0
  + Counter direction = Up
  */
  htim6.Init.Period = (1000000U / 1000U) - 1U;
  htim6.Init.Prescaler = uwPrescalerValue;
  htim6.Init.ClockDivision = 0;
  htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

  status = HAL_TIM_Base_Init(&htim6);
  if (status == HAL_OK)
  {
    /* Start the TIM time Base generation in interrupt mode */
    status = HAL_TIM_Base_Start_IT(&htim6);
    if (status == HAL_OK)
    {
    /* Enable the TIM6 global Interrupt */
        HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
      /* Configure the SysTick IRQ priority */
      if (TickPriority < (1UL << __NVIC_PRIO_BITS))
      {
        /* Configure the TIM IRQ priority */
        HAL_NVIC_SetPriority(TIM6_DAC_IRQn, TickPriority, 0U);
        uwTickPrio = TickPriority;
      }
      else
      {
        status = HAL_ERROR;
      }
    }
  }

 /* Return function status */
  return status;
}

定时时间怎么计算的呢?下面代码中,

  • Period 表示定时周期,计数器计到该值时产生更新事件(溢出中断)。计数范围是从0计数到Period。这里表示1000个周期
  • Prescaler 表示对时钟的预分频系数。定时器周期的最终公式:

Tout=((ARR+1)*(PSC+1)) / Tclk
注意最终结果是,因为Tclk单位是MHz。
比如Tclk 84MHz,PSC设置为83,ARR设置为999,计算为0.001s即1ms。
要注意,ARR和PSC都为16bit(有的定时器是32bit),最大为65535,这样如果要定时周期为1s,那么PSC设置为83,ARR必须设置为1000000,这样就超了范围了,此时必须加大PSC的数量级,比如PSC设置为83999(不行,也超了),设置为41999,ARR为2000-1,最终即为1s

  • CounterMode 计数方向 向上计数。
  • AutoReloadPreload ARR寄存器预装载使能,这里是关闭的,ARR立即生效。

TIM_AUTORELOAD_PRELOAD_DISABLE 关闭预装载,写入 ARR 的值会立即生效。
TIM_AUTORELOAD_PRELOAD_ENABLE 开启预装载,新写入的 ARR 值暂存到预装载寄存器,计数器溢出时更新 ARR。
当你在定时器运行过程中修改 ARR 值:
关闭预装载时,ARR 立即改变,可能导致计数器行为异常(提前溢出或延迟溢出)
开启预装载时,ARR 改变会在计数器溢出时统一更新,保证计数器行为稳定,不会产生瞬时异常。
【应用场景】:需要动态修改定时周期,但保证计数器计数连续稳定时,建议开启预装载。简单固定周期时,关闭预装载即可。

  htim6.Init.Period = (1000000U / 1000U) - 1U;
  htim6.Init.Prescaler = uwPrescalerValue;
  htim6.Init.ClockDivision = 0;
  htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

3.1.3 定时器中断触发和DMA触发

以该TIM6为例,这里调用的是HAL_TIM_Base_Start_IT(&htim6);使用的是中断触发的方式,当定时器计数溢出时,触发中断:

/* Start the TIM time Base generation in interrupt mode */
HAL_TIM_Base_Start_IT(&htim6);

/* Enable the TIM6 global Interrupt */
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn); //TIM6_DAC_IRQn为TIM6对应的中断向量,对应的中断函数是TIM6_DAC_IRQHandler

/* Configure the TIM IRQ priority */
 HAL_NVIC_SetPriority(TIM6_DAC_IRQn, TickPriority, 0U);

中断向量对应的函数:

void TIM6_DAC_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim6);
}

最终会调用weak声明的可重定义的函数:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6) {
    HAL_IncTick(); //驱动HAL时基
  }
}

中断对应的优先级为#define TICK_INT_PRIORITY 15U /*!< tick interrupt priority */15,为最低的优先级。

DMA方式(略)。

3.2 定时器同步(一个启动拉起另一个)

3.2.1 配置方式

比如从定时器为TIM2,模式设置为Trigger mode,设置触发源为ITR0(对应TIM1),观察TIM1溢出后,TIM2的计数情况。
预期效果:TIM1溢出中断后会产生TRGO 触发信号,TIM2 作为从定时器,等待 TIM1 的 TGRO
信号触发。TGRO信号触发后TIM2即启动了。

TIM1的MX配置:
在这里插入图片描述

  • Master/Slave Mode (MSM bit) :主从模式使能,设置为Enable
  • Trigger Event Selection:触发事件源选择,决定定时器产生触发输出(TRGO)的条件。Update Event(更新事件,计数器溢出);
    在这里插入图片描述
    TIM2的配置:
    在这里插入图片描述

代码变化:

  void MX_TIM1_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  //略...
  
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE;
  HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig);
}
/* TIM2 init function */
void MX_TIM2_Init(void)
{
  TIM_SlaveConfigTypeDef sSlaveConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  //略...
  //从模式启用
  sSlaveConfig.SlaveMode = TIM_SLAVEMODE_TRIGGER;
  sSlaveConfig.InputTrigger = TIM_TS_ITR0;
  if (HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig) != HAL_OK)
  {
    Error_Handler();
  }
  //主模式禁用
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }

}

中断配置:
在这里插入图片描述

3.2.2 启动及踩坑

main.c中启动代码如下(错误代码)

	  MX_TIM2_Init(); //!!!这两句MX生成的代码顺序导致的诡异问题
	  MX_TIM1_Init();
	  	/* USER CODE BEGIN 2 */
	  __HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE); 
	  HAL_TIM_Base_Start_IT(&htim1); 
	  __HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
3.2.2.1 坑1 - TIM2幽灵启动

USER CODE BEGIN 2之间的代码都 注释掉运行,此时由于中断未使能(HAL_TIM_Base_Start_IT调用或者__HAL_TIM_ENABLE_IT可以使能中断),只能通过寄存器查看定时器的运行情况,结果发现定时器TIM2初始化后就开始计数了,如图:
在这里插入图片描述
这是为什么?百思不得其解。TIM2到底是谁将CEN标记打上的呢?查了下只有__HAL_TIM_ENABLE这个宏才能打上该标记,关键是也没人调用啊。经过高人指点后才知道,问题在于TIM1初始化后会立即产生一个触发更新事件,即SR寄存器的UIF会被打上标记,并产生一个TGRO信号,此时如果TIM2初始化完了,接着就启动了!,这是硬件特性导致的,原因是产生该信号,将影子寄存器的数值写入真正的寄存器!
下面框中的即为影子寄存器!
在这里插入图片描述
TIM1初始化后UIF被打上标记:
在这里插入图片描述

如何解决?简单,将下面两句倒换一下位置就行了:

	  MX_TIM1_Init();
	  MX_TIM2_Init(); //!!!这两句MX生成的代码顺序导致的诡异问题

这样TIM1初始化后该事件并不会被TIM2接收到。但这里是MX生成的,所以最好把MX_TIMx_Init()函数重新封装一下,不要依赖MX生成的代码。

3.2.2.1 坑2 - TIM2中断不进

如果改为如下启动代码,只启动TIM1的中断,此时TIM2中断不会进入,必须将__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);放开,手动启动TIM2的溢出更新中断:

	  MX_TIM1_Init();
	  MX_TIM2_Init(); //!!!这两句MX生成的代码顺序导致的诡异问题
	  	/* USER CODE BEGIN 2 */
	 // __HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE); 
	  HAL_TIM_Base_Start_IT(&htim1); 
	  //__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
3.2.2.2 坑3 - TIM1中断立即进入

将__HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE);注释掉,TIM1的中断会在调用完HAL_TIM_Base_Start_IT(&htim1); 后立即进入,这还是因为SR状态寄存器UIF标记导致中断会立即进入,因此在启用之前,请务必先清除该标记!【这个方法适用于任何时候!】
最终正确的代码:

MX_TIM1_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  __HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE); 
  HAL_TIM_Base_Start_IT(&htim1); 
  __HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);

3.3 测量频率

  • 测频法
    两个定时器,一个接内部时钟源进行计时,一个输入外部输入信号用进行计数,测试一定时间段内的计数个数来计算。
    比如将上面例子中的TIM2改为如下配置,时钟源设置为外部:
    在这里插入图片描述
    对应的IO端口为下图。这样可以从该端口输入信号源:
    在这里插入图片描述
    启动TIM1后会同步启动TIM2,TIM1溢出后统计TIM2的计数__HAL_TIM_GET_COUNTER(htim);,当然计数可能溢出,因此需在TIM2的溢出中断中进行次数记录!
    测频法适用于频率比较高的信号的测量。

  • 测周法
    对于频率比较低的信号,比如10Hz、1Hz甚至更小,可以采用测周期法。
    用输入捕获模式,外接外部输入信号到定时器某个输入捕获通道,此时定时器时钟源为内部时钟源,捕获到上升沿后会跳中断,此时记录计数器(基于内部时钟源的)计数(该计数通过HAL_TIM_ReadCapturedValue()获得),再跳下一个上升沿中断后再记录计数,作差然后依据内部时钟源频率进行计算。
    下图是TIM2设置1通道为捕获模式,上升沿捕获即外部信号上升沿时,会产生该定时器的中断。
    在这里插入图片描述

if(htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
    {
        uint32_t captured_value = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        // 定时器硬件自动将当前计数器 CNT 的值保存到对应的捕获比较寄存器 CCR(比如 CCR1 对应通道1)
        // HAL_TIM_ReadCapturedValue() 就是读取这个寄存器的值。
    }

这个值也可以通过设置定时器中断存储计数器值的方式来获得,这两种方式都是比较精确的。

32.4 输出比较(输出PWM)

待完善
只能ETR引脚输入 ,纯计数

3.5 DAC(定时器触发)

https://doc.embedfire.com/mcu/stm32/f103zhinanzhe/std/zh/latest/book/DAC.html

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐