0.基本情况介绍

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

1.定时器MX配置

MX配置比较复杂,组合情况太多,这里直接按场景介绍,否则直接迷糊!
这里以TIM4通用定时器为例,它挂在APB1总线上(《stm32f427vi-官方.pdf》-图 4. STM32F427xx 和 STM32F429xx 框图)
在这里插入图片描述
时钟频率:
在这里插入图片描述

1.1 普通定时器

1.1.1 基本配置

这是使用最多的场景,启动一个定时器进行计数,溢出后触发中断。此时的配置如下:
在这里插入图片描述

  • PCS(Prescaler - 16bits):预分频,时钟频率/PCS=计数器的计数频率.
  • Counter Mode:向上还是向下计数。
  • ARR(自动重装载寄存器):计数周期,从0计数,到该数值后触发,可绑定中断。
  • CDK: 时钟分频因子,但它不影响定时器的计数频率,仅用于输入捕获滤波或互补 PWM 死区。
  • ARP(auto-reload preload自动重装载使能):ARR初始时一旦设定,过程中可以动态修改,不过ARP决定了你修改的新的ARR什么时候起作用。如果ARP设置为使能Enable,当计数器溢出时,才会使用你重新配置的ARR计数数值(使用__HAL_TIM_SET_AUTORELOAD(&htim4, 999);),用户新设置的计数值会在影子寄存器先存起来。如果设置为Disable,那么定时器运行中,你通过代码修改ARR数值时,会立即生效,可能会出现毛刺。推荐Enable。

Trigger Output (TRGO) 配置

  • Master/Slave Mode (MSM bit) :作为普通定时应用选 Disable 就可以了。那什么时候设置为Enable呢?典型场景:多相 PWM、高精度 ADC 采样同步。后面再说。
  • Trigger Event Selection 触发事件选择,后面介绍。
    作为普通定时器,这两个用默认的就可以了。

1.1.2 计时周期计算

计时周期的计算公式如下:

T=((ARR+1)*(PSC+1)) / Tclk
注意最终结果是秒,因为Tclk单位是MHz。
这里APB1过来的时钟是Tclk 90MHz,PSC设置为8999,ARR设置为4999,计算为0.5s即500ms。
要注意,ARR和PSC都为16bit(有的定时器是32bit),最大为65535。

那么ARR和PSC是随便设置的吗?答案不是!不同场景使用规则是不一样的。
PSC表示将时钟又进行了分频,降低了频率,而ARR就是计数多少次溢出,当PSC设置的很大时,计数的频率会很低,精度也会降低。

  • 普通定时器场景:PSC尽量和输入的时钟凑整数,这样ARR好算。比如上面的例子中,输入时钟90Mhz,PSC就设置为90的倍数。
  • PWM输出:PSC直接为0或者更小,这样精度更高。
  • 输入捕获 / 编码器计数:PSC直接为0。

1.1.3 中断配置

中断触发配置,如图所示,在NVIC Settings中,可以使能全局中断。:
在这里插入图片描述
程序中启动定时器:

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

/* Enable the TIM4 global Interrupt */
HAL_NVIC_EnableIRQ(TIM4_DAC_IRQn); //TIM4_DAC_IRQn为TIM4对应的中断向量,对应的中断函数是TIM4_DAC_IRQHandler

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

中断向量对应的函数:

void TIM4_DAC_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim4);
}

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

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM4) { //务必加这个
    //500ms具体干的活
  }
}

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

1.1.4 DMA配置

TIM定时器搭配DMA的使用场景主要是:定时事件发生时,自动搬运数据,而不需要 CPU 每次进中断手工处理。

  • 输出PWM,比如让 TIM4 的 CH1 输出 PWM,而且 PWM 的占空比不是固定的,而是不断变化的,中断中可以但中断太多了,可以将占空计数放到一个buffer中,定时器触发一次,自动搬运一个数据到CCR1寄存器。
  • 输入捕获,比如每检测到上升沿,保存当前的计数值,如果用DMA可以自动保存到缓冲区。
    不管什么场景,定时器仅仅起到触发DMA搬运一个数据的作用,要结合不同的场景和外设来配合使用。
    这里不再展开。

1.2 系统时基

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

1.2.1 HAL时基

HAL 里有一个非常核心的函数:HAL_GetTick()。它返回一个不断递增的毫秒计数,通常定义成:1 tick = 1 ms。
例如:HAL_GetTick() 返回1000,表示系统启动以来大约过去了 1000 ms。
HAL_Delay(100); 表示延时100ms,注意此函数内部是循环调用GetTick()来延时的:while ((HAL_GetTick() - tickstart) < Delay),千万别在中断中用。
那么这个时基默认是从SysTick中断产生的。

SysTick 是 Cortex-M 内核自带的一个定时器,不属于 STM32F4 的 TIM1~TIM14 外设,它是一个24位递减计数器,计数时钟是时钟树中的HCLK,计数周期默认会被HAL配置为1ms。也就是说这个周期是可以修改的。

1.2.2 OS时基

OS时基是给给 FreeRTOS打节拍OS Tick用的,每来一次 OS Tick,FreeRTOS 都会做一些事情,例如:

  • 检查延时任务
  • 检查是否有任务到期
  • 决定要不要切换任务

1.2.3 时基分配

通常的默认情况下,两个时基会共用SysTick中断当时基。如下图所示,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) //当定义为0时,下面代码会启用,mx也不会生成SysTick_Handler了
void SysTick_Handler (void) {
  /* Clear overflow flag */
  SysTick->CTRL;
  if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {
    /* Call tick handler */
    xPortSysTickHandler();
  }
}
#endif

该USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION 宏在MX的Timebase Source时基选择SysTick时,是定义为1的,当选择了TIM6,该宏会被定义为0,此时仅仅只有OS时基靠systick驱动了。

所以通常情况下,os的时基默认systick中断定时驱动,周期为1ms。hal时基用TIM6,mx自动配置为了1ms。

1.3 定时器高精度同步(精确延迟启动另一个)

1.3.1 场景说明

先定义场景,比如收到外部触发,100us后启动一个定时器来输出PWM。
这里有两个关键的地方:

  • 一是 外部触发到来后,如何快速、准确地开始100 μs计时,并尽可能减小触发响应带来的抖动;
  • 二是 100 μs计时结束后,如何让另一个定时器准确、低抖动地开始输出PWM。
    那么如何解决呢?

1.3.2 关键一解决方法(外部触发)

对于关键一,可以将定时器的从模式(Slave Mode)改为触发模式(Trigger Mode),同时配置触发源(Trigger Source)。这样外部触发来后,直接快速输入到定时器中来启动定时器。
这里的触发源(Trigger Source)有好多种:
在这里插入图片描述

  • ITR0~ITR3:对应其他定时器的输出的TRGO来触发。
  • ETR1:对接专门的外部触发引脚,一般叫ETRF(External Trigger input),默认是上升沿触发的。
  • TI1FP1:来自CH1通道,经过滤波后,上升沿还是下降沿触发。一般用这个,因为不滤波会有很多毛刺。
  • TI2FP2:来自CH2通道,经过滤波后,上升沿还是下降沿触发。
  • TI1_ED:来自CH1通道,不滤波,上升沿还是下降沿触发。
    在当前的应用这,选择ETR1或者TI1FP1(or TI2FP2)都是可以的,看外围硬件怎么接,还要看当前的TIM定时器是否支持(比如TIM1不支持ETR1,没有引脚留给它)。

当选择ETR1作为触发源时候,可以看到对应的是PE0引脚:
在这里插入图片描述
其他配置如下:
在这里插入图片描述

  • Trigger Polarity:触发极性是否反转,因为默认上升沿触发,改为inverted的话,就会变成下降沿触发。
  • Trigger Prescaler:触发分频,如果不分频,一个上升沿产生一次,如果分频为2,则两个上升沿产生一次。
  • Trigger Filter (触发滤波器):如果输入有毛刺,可以输入一个窗口大小进行滤波,窗口越大延迟越大,但滤波效果越好。

当选择TI1FP1作为触发源时候,GPIO对应如下:
在这里插入图片描述

多出的配置如下:
在这里插入图片描述

  • Trigger Polarity可以指定上升还是下降沿。
  • Trigger Filter滤波器。

1.3.3 关键二解决方法(启动其他定时器)

关键二是说:100 μs计时结束后,如何让另一个定时器准确、低抖动地开始输出PWM。关键就是通过触发TGRO事件,来通过ITRx启动另一个定时器。这里假如TIM4为100us计数的定时器,那么它负责启动另一个定时器TIM2。
为了解决关键一,TIM4要按照上面的方法进行配置外,还需要如下额外的配置来产生TRGO信号:
在这里插入图片描述
MSM:(这个有争议哈)目前的理解是①当前定时器不是主动触发,是接收别的触发源来触发,比如外部或者其他定时器发送的TGRO事件。②只有当Trigger Event Selectiong选择为了Enable,也就是定时器启动时立即发送TRGO 信号。满足这两种情况下,定时器立即发送的TRGO 信号会有延迟,启用 MSM 可以避免这个延迟。当前场景为Disable即可。
Trigger Event Selection可选择的项如下:
在这里插入图片描述
这里因为要延迟100us,所以要用Update Event,当计数器溢出,到了100us了,才产生TRGO事件来启动其他定时器。此时TIM4实际上即是从定时器(从模式),又可以触发TRGO的“主”模式。

那么TIM2怎么配置呢?需要配置为触发模式且触发源为ITR0~ITR3中的一个。那到底哪一个?这个在硬件上是固定的,有下面的表可以查:在这里插入图片描述
查表可知,当从定时器为TIM2时,IRT0到ITR3分别对应TIM1/TIM8/TIM3/TIM4定时器,这里用了定时器TIM4,那么就得选择ITR3.

1.4 定时器高精度同步(多个定时器同步启动)

这个和上面的不同,有些场景需要多个定时器同时启动,比如TIM4启动的时候,TIM2同时启动;或者考虑外部触发源,外部触发TIM4启动,TIM4再同时启动其他定时器。
第一种情况手动启动TIM4,同时启动TIM2,只需要将TIM4的Trigger Event Selection改为Enable即可,TIM2正常配置IRT3就行了。
第二种情况外部触发TIM4启动,此时TIM4Trigger Event Selection改为Enable之外,还需要MSM设置为Enable(前面解释了),减少TIM4发送TGRO的延迟。

1.5 测量频率

1.5.1 测频法

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

1.5.2 测周法

对于频率比较低的信号,比如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() 就是读取这个寄存器的值。
    }

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

1.6 输出比较(输出PWM)

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

1.7 DAC(定时器触发)

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

2. 关键寄存器

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

2.1 TIMx_CR1 — 控制寄存器1

  • CEN 计数器使能,使能后计数即开始。即启动定时器的寄存器,设置为1后定时器启动了就。

2.2 TIMx_SR — 状态寄存器

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

2.3 TIMx_CNT

计数器自动递增/递减的数值。

2.4 TIMx_ARR

计数器达到该值时,产生更新事件。

3 简单实验-启动其他定时器

这里手动启动TIM4,TIM4溢出后会启动TIM2,TIM2正常计数并进入溢出中断。

预期效果:TIM4溢出中断后会产生TRGO 触发信号,TIM2 作为从定时器,等待 TIM4 的TRGO信号触发。TRGO信号触发后TIM2即启动了,TIM2启动后会进行计数,然后溢出后进入中断。

具体的配置其实很简单,TIM4正常的普通定时器,不过需要改TGRO事件为Update,计数溢出后发送TGRO。
TIM2使用ITR3作为触发源即可,其他就是个普通定时器。

main.c中启动代码如下**(注意这是错误代码)**

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

3.1 坑1 - TIM2幽灵启动

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

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

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

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

3.2 坑2 - TIM2中断不进

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

	  MX_TIM1_Init();
	  MX_TIM4_Init(); 
	  	/* USER CODE BEGIN 2 */
	  HAL_TIM_Base_Start_IT(&htim4); //光有这个不行
	  __HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE); //这句话必须加上,使能TIM2的中断,初始化中没干这个使能

也就是说,HAL_TIM_Base_Start_IT(&htim4)自动将TIM4的的中断启用了,但TIM2的需要自己 启用,否则中断是不会进的,需要手动启用。

3.3 坑3 - TIM4中断立即进入

TIM4的中断会在调用完HAL_TIM_Base_Start_IT(&htim4); 后立即进入,这还是因为SR状态寄存器UIF标记导致中断会立即进入(坑1说了,这个UIF标记会在TIM4初始化后自动打上),因此在启用之前,请务必先清除该标记!【这个方法适用于任何时候!】
最终正确的代码:

  MX_TIM4_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  __HAL_TIM_CLEAR_FLAG(&htim4, TIM_FLAG_UPDATE); //清除UIF标记,否则会自动进入TIM4中断
  HAL_TIM_Base_Start_IT(&htim4); //启动TIM4,开始计数
  __HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);//使能TIM2中断,否则TIM2收到TIM4的TGRO后启动了,计数器溢出也不进入中断。

4.其他配置

4.1 Clock Source 时钟源配置

定时器的时钟来源,注意当从模式Slave Mode配置为External Clock Mode 1时该项禁用!
当从模式Slave Mode配置为
可选的有:
在这里插入图片描述
- Disable:彻底关闭时钟源,定时器不会计数或触发事件了,相当于定时器废了。
- Internal Clock:使用内部时钟作为定时器的时钟源,通常是来自系统时钟(如 APB 时钟)经过预分频后的频率。是最常用的时钟源。
- ETR2 :即External Clock Mode 2,定时器从外部引脚(ETR2)接收时钟或触发信号,适合外部事件驱动计数。

4.2 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关联的引脚。

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

其他介绍(略)

更多推荐