【嵌入式STM32】TIM定时器总结
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关联的引脚。 |

单脉冲模式,启动后执行一次即停止。
其他介绍(略)
更多推荐
所有评论(0)