【嵌入式STM32】TIM定时器总结
stm32F427,定时器的划分如下(参考官方数据手册):stm32f103定时器:这里以TIM4高级定时器为例。先看MX配置:这里的配置选项非常多,下面整理一下。可选的如下:- Disable 关闭从模式,定时器独立运行。- External Clock Mode 1,【外部时钟模式1,很特殊】 定时器时钟时钟源由外部触发信号驱动,用外部脉冲信号作为计数时钟。当设置该项时,Clock Sourc
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
更多推荐



所有评论(0)