TIM定时中断(STM32标准库学习笔记)
2026/9/8 15:40:18 网站建设 项目流程

在上一章中,我们学习了 EXTI 外部中断,掌握了如何通过外部引脚的电平变化来触发中断,从而让 CPU 及时响应外部事件。然而在实际的嵌入式开发中,除了需要响应外部事件,我们还经常需要精确地控制时间,比如定时采集传感器数据、周期性刷新显示、产生 PWM 波形等。如果单纯依靠主循环的延时函数,不仅会阻塞 CPU 的执行,而且难以保证时间的准确性。这时,STM32 内置的定时器(TIM)就派上了用场。

定时器是 STM32 中非常重要且常用的外设,它通过对内部或外部时钟进行计数,在计数值达到设定值时触发中断,从而实现精确的定时功能。与 EXTI 外部中断不同,定时器中断是由内部时钟驱动、自主产生的,无需外部信号的参与,因此非常适合用于周期性任务的调度。本章将围绕 TIM 定时中断展开,从定时器的基本概念、三种类型(基本、通用、高级)的差异,到时基单元中预分频器、计数器与自动重装寄存器的工作原理,再到预分频器时序、计数器时序以及缓冲寄存器的使用,最后通过两个实战实例——定时器定时中断与定时器外部时钟,带领大家完整掌握 STM32 定时中断的开发流程。

如果觉得有收获可以为我的文章点点小赞,这将激励我创作更好的内容!谢谢!

一、TIM定时中断

1.1 TIM简介

TIM(Timer)定时器

定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断

16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时

不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能

根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型

1.2 定时器类型

STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4

1.3 基本定时器

下面这一部分,有三个最重要的寄存器,分别是预分频器、计数器和自动重装寄存器,它们构成了最基本的计数计时电路,所以这一块电路就叫做时基单元。预分频器之前,连接的就是基准计数时钟的输入,由于基本定时器只能选择内部时钟,所以你可以直接认为,预分频器直接连到了输入端的内部时钟CK INT,内部时钟的来源是RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz,所以通向时基单元的计数基准频率就是72M

时基单元,首先是预分频器,它可以对这个72MHz的计数时钟进行预分频,比如这个寄存器写0,那就是不分频,或者说是1分频,这时候输出频率=输入频率=72MHz;如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHz;如果写2,就是3分频,输出=输入/3,以此内推;所以预分频器的值和实际的分频系数相差了1,即实际分频系数=预分频器的值+1,这个预分频器是16位的,所以最大值可以写65535,也就是65536分频,这就是预分频器,就是对输入的基准频率提前进行一个分频的操作,

计数器,这个计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值就加1,这个计数器也是16位的,所以里面的值可以从0一直加到65535,如果再加的话,计数器就会回到0重新开始,所以计数器的值在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了,自动重装寄存器也是16位的,它存的就是我们写入的计数目标,在运行的过程中,计数值不断自增,自动重装值是固定的目标,当计数值等于自动重装值时,也就是计时时间到了,那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时,像这种计数值等于自动重装值产生的中断呢,我们一般把它叫做“更新中断”,这个更新中断之后就会通往NVIC,我们再配置好NVIC的定时器通道,那定时器的更新中断就能够得到CPU的响应了,

向下的箭头,代表的是会产生一个事件,这里对应的事件就叫做“更新事件”,更新事件不会触发中断,但可以触发内部其他电路的工作。

基本定时器仅支持向上计数这一种模式

1.4 通用定时器

通用定时器和高级定时器支持向上计数、向下计数、中央对齐这三种模式

外部时钟模式1的输入可以是ETR引脚、其他定时器,CH1引脚的边沿、CH1引脚和CH2引脚,一般情况下外部时钟通过ETR引脚就可以了

1.5 高级定时器

1.6 定时中断基本结构

1.7 预分频器时序

计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)

第一行是CK PSC,预分频器的输入时钟,选内部时钟的话一般是72MHz;

第二行是CNT_EN,计数器使能,高电平计数器正常运行,低电平计数器停止;

第三行是CK_CNT,计数器时钟,既是预分频器的时钟输出,也是计数器的时钟输入;

第四行是计数器寄存器,数值跟随时钟的上升沿不断自增,达到自动重装值时,下一个计数变为0;

第五行是更新事件(UEV),当计数器寄存器达到自动重装值时,更新事件(UEV),产生一个低到高的电平脉冲;

缓冲寄存器:

计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)

当计数寄存器(第四行)计数计到一半的时候改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时,产生了更新事件,预分频寄存器的值才会被传递到缓冲寄存器里面去,才会生效

预分频器内部实际上也是靠计数来分频的,当预分频值为0时,计数器就一直为0,直接输出原频率,当预分频值为1时,计数器就0、1、0、1、0、1、0、1这样计数,在回到0的时候,输出一个脉冲,

1.8 计数器时序

计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1)

= CK_PSC / (PSC + 1) / (ARR + 1)CK_PSC大多数情况下是72M)

更新中断标志位UIF:此标志位只要置1了,就会不断申请中断,然后中断响应后,需要在中断程序中手动清零

1.9 计数器无预装时序(不使用缓冲寄存器)

通过设置ARPE位来配置缓冲寄存器的使用与否

1.10 计数器有预装时序(使用缓冲寄存器)

通过设置ARPE位来配置缓冲寄存器的使用与否

只有在当前计数器寄存器的值在更改重装值后完成最后的溢出时(等寄存器走完更改前的值,上图为F5),要更改的重装值才被传递到影子寄存器,在下一个计数周期这个更改值(上图为36)才有效

1.11RCC时钟树

库函数Systemlnit函数配置时钟树的操作如下:

1. 启动内部时钟,选择内部8MHz为系统时钟,暂时以内部8MHz的时钟运行;

2. 启动外部时钟,配置外部时钟进入PLL锁相环进行倍频,8MHz倍频9倍,得到72MHz;

3. 等到锁相环输出稳定后,选择锁相环输出为系统时钟,这样就把系统时钟由8MHz切换为了72MHz;

以上3步就是ST公司的库函数在时钟部分的配置的流程

如果外部晶振出问题了,系统就无法切换到72MHz,那它就会以内部的8MHz运行,8M相比较72M,大概就慢了10倍

时钟安全系统CSS:可以监测外部时钟的运行状态,一但外部时钟失效,就会自动把外部时钟切换回内部时钟,保证系统时钟的运行,防止程序卡死造成事故

AHB总线有个预分频器,在Systemlnit里AHB预分频器配置的分配系数为1,故AHB的时钟就是72MHz,然后进入APB1总线,APB1预分频器配置的分配系数是2,所以APB1总线的时钟为72MHz/2=36MHz

无论是高级定时器,还是通用定时器,还是基本定时器,它们的内部基准时钟都是72MHz,因为还有下面的一条分支:

APB2外设与定时器1和8也是72MHz

我们在程序中写RCC APB2/1PeriphClockCmd作用是配置外设时钟使能,写1打开定时器

1.12 实例一 6.1.1 定时器定时中断

可能用到的库函数:

//将指定定时器外设恢复为复位默认状态,把 TIM 所有寄存器还原成上电复位值,相当于把定时器整个初始化配置全部清空

void TIM_DeInit(TIM_TypeDef* TIMx);

//时基单元初始化

voidTIM_TimeBaseInit(TIM_TypeDef*TIMx,TIM_TimeBaseInitTypeDef*TIM_TimeBaseInitStruct);

其中TIM_TimeBaseInitTypeDef定义的结构体TIM_TimeBaseInitStructure需要注意:

TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;

TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;

TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;

TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;//重装值(记多少次数溢出一次)

TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;//预分频值(内部时钟电平一上一下多少次计一个数)

TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;

TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);

TIM_Period --> 重装值 --> ARR

TIM_Prescaler --> 预分频值 -->PSC

上面两个值满足

计数器溢出频率:CK_CNT_OV = CK_PSC / (PSC + 1) / (ARR + 1)

CK_PSC大多数情况下是72M)

例如要计时1s,1s对应就是1Hz频率,所以我们可以配成

1 = 72M /7200 - 1+ 1 /10000 -1+1

注意:这个函数在使用时会在最开始手动生成一个更新事件用来让我们给预分频器的值生效。但同时,它的副作用就是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,之后一但初始化完成,更新中断就会立刻进入

解决方案:在TimeBaselnit的后面,开启中断的前面,再手动调用一下TIM_ClearFlag

用TIM_ClearFlag(TIM2,TIM_FLAG_Update);手动把更新中断标志位清除一下,就能避免刚初始化完就进中断的问题了

//把结构体变量赋一个默认值

void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

//使能计数器

void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);

//使能中断输出信号(更新中断)的

void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);

//选择内部时钟

void TIM_InternalClockConfig(TIM_TypeDef* TIMx);

//选择ITRx其他定时器的时钟

void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);

//选择TIx捕获通道的时钟

void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx,uint16_t TIM_TIxExternalCLKSource,uint16_t TIM_ICPolarity, uint16_t ICFilter);

//选择ETR通过外部时钟模式1输入的时钟

void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_tTIM_ExtTRGPolarity,uint16_t ExtTRGFilter);

其中参数ExtTRGPrescaler,外部触发预分频器可以对ETR的外部时钟再提前做一个分频

//选择ETR通过外部时钟模式2输入的时钟

void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,

uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);

//单独配置ETR引脚的预分频器、极性、滤波器参数

void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,

uint16_t ExtTRGFilter);

初始后还要修改参数:

//单独写预分频值的

void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);

//改变计数器的计数模式

void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);

//自动重装器预装功能配置

void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);

//手动给计数器一个计数值

void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);

//手动给自动重装器写入一个值

void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);

//获取当前计数器的值(看当前计数器记到哪里了)

uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);

//获取当前的预分频器的值

uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);

//获取标志位和清除标志位

FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);

void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);

ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);

void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);

Timer代码模块如下:

Timer.c(定时器)↓

-----------------------------

"stm32f10x.h" // Device header void Timer_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); TIM_InternalClockConfig(TIM2); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;//重装值(记多少次数溢出一次) TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;//预分频值(内部时钟电平一上一下多少次计一个数) TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure); TIM_ClearFlag(TIM2,TIM_FLAG_Update); TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2,ENABLE); } /* void TIM2_IRQHandler(void) { if( TIM_GetITStatus(TIM2,TIM_IT_Update)==SET ) { TIM_ClearITPendingBit(TIM2,TIM_IT_Update); } } */

-----------------------------

实例一6.1.1 定时器定时中断

main.c

-----------------------------

#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Timer.h" uint16_t Num; int main(void) { OLED_Init(); Timer_Init(); OLED_ShowString(1,1,"Num:"); OLED_ShowString(2,1,"CNT:"); while(1) { OLED_ShowNum(1,5,Num,5); OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5); } } void TIM2_IRQHandler(void) { if( TIM_GetITStatus(TIM2,TIM_IT_Update)==SET ) { Num ++; TIM_ClearITPendingBit(TIM2,TIM_IT_Update); } }

-----------------------------

1.13 实例二6.2.1 定时器外部时钟

我们删去使用内部时钟的代码:

TIM_InternalClockConfig(TIM2);

改为使用外部时钟2,其中0x0F是参数 ExtTRGFilter 数字滤波,取值 0x00 ~ 0x0F,0x00:关闭滤波,0x0F:最大滤波,需要连续 8 个采样周期电平不变才判定有效边沿,抑制抖动,相应的,响应时间变慢

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(GPIOA,&GPIO_InitStructure);

TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x0F);

Timer.c(定时器)↓

-----------------------------

#include "stm32f10x.h" // Device header void Timer_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStructure); TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x0F); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period = 10 - 1;//重装值(记多少次数溢出一次) TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;//预分频值(内部时钟电平一上一下多少次计一个数) TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure); TIM_ClearFlag(TIM2,TIM_FLAG_Update); TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2,ENABLE); } uint16_t Timer_GetCounter(void) { return TIM_GetCounter(TIM2); } /* void TIM2_IRQHandler(void) { if( TIM_GetITStatus(TIM2,TIM_IT_Update)==SET ) { TIM_ClearITPendingBit(TIM2,TIM_IT_Update); } } */

-----------------------------

main.c

-----------------------------

#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Timer.h" uint16_t Num; int main(void) { OLED_Init(); Timer_Init(); OLED_ShowString(1,1,"Num:"); OLED_ShowString(2,1,"CNT:"); while(1) { OLED_ShowNum(1,5,Num,5); OLED_ShowNum(2,5,Timer_GetCounter(),5); } } void TIM2_IRQHandler(void) { if( TIM_GetITStatus(TIM2,TIM_IT_Update)==SET ) { Num ++; TIM_ClearITPendingBit(TIM2,TIM_IT_Update); } }

-----------------------------

二、总结

本文围绕 STM32 的 TIM 定时器展开,系统梳理了定时中断的完整知识体系。首先介绍了 TIM 的基本概念与三种定时器类型(基本、通用、高级),并重点讲解了时基单元中预分频器、计数器与自动重装寄存器的工作原理。随后通过预分频器时序、计数器时序以及有无预装时序的对比,深入说明了计数频率、溢出频率的计算方法,以及缓冲寄存器在参数更新中的关键作用。

在时钟部分,本文详细解析了 RCC 时钟树的配置流程,包括内部时钟启动、外部晶振倍频到 72MHz 的切换过程,以及 AHB、APB1、APB2 总线的分频关系,帮助读者理解定时器基准时钟的来源。最后通过两个实战实例——定时器定时中断与定时器外部时钟,演示了从库函数初始化、NVIC 中断配置到主程序调用的完整开发流程,并给出了可直接运行的代码示例。

掌握 TIM 定时中断,是学习 STM32 外设开发的重要基础。理解时基单元的计数逻辑、预分频与重装值的配合关系,以及时钟树的时钟来源,是灵活运用定时器的关键。建议读者在理解原理的基础上,动手完成两个实例的代码编写与调试,并尝试修改预分频值和重装值,观察定时时间的变化,从而加深对定时器工作机制的理解。

都看到这了点点赞吧!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询