🚀 我的STM32学习专栏:[点击直达 | 持续更新中]


前言

之前学习的delay_ms()阻塞延时会占用 CPU,程序等待期间无法执行其他任务。但是今天我们学习的定时器中断可以实现后台计时,不阻塞主循环,是嵌入式开发非常常用的功能。本文跟着江科大教程,使用 TIM2 通用定时器完成定时中断实验,讲解配置流程,同时记录调试过程遇到的问题,适合新手入门学习。

一.开启定时器的步骤

如果不知道怎么配置,为什么这么配置定时器可以参考下面的图会更好的理解

图中大致可以分为五步,但是我将通过这个图片为你提供更加详细的配置步骤(以TIM2为例)

  • 第一步:RCC 开启定时器时钟 调用RCC_APB1PeriphClockCmd()开启 TIM2 外设时钟。

    ⚠️所有外设工作前,必须开启对应总线时钟。

  • 第二步:选择时基单元的时钟源 TIM_InternalClockConfig(TIM2),配置定时器使用内部时钟。

    ⚠️固件库默认就是内部时钟,此行可省略,建议写上便于理解。

  • 第三步:配置时基单元 填充TIM_TimeBaseInitTypeDef结构体:预分频 PSC、自动重装载 ARR、计数模式、时钟分频,调用TIM_TimeBaseInit()写入寄存器。

    ⚠️决定定时器计数快慢、定时周期。

  • 第四步:配置输出中断控制,允许更新中断输出到 NVIC TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE),使能定时器更新溢出中断。

    ⚠️定时器产生溢出事件后,允许发出中断信号。

  • 第五步:配置 NVIC,打开定时器中断通道、分配优先级 先设置中断优先级分组NVIC_PriorityGroupConfig(); 填充NVIC_InitTypeDef,指定中断通道 TIM2_IRQn、抢占优先级、响应优先级,执行NVIC_Init()

    ⚠️配置内核如何接收、处理定时器发出的中断信号。

  • 第六步:运行控制(可选预处理) TIM_ClearFlag()清除初始状态的更新标志位,防止上电瞬间误触发中断。

  • 第七步:使能计数器 TIM_Cmd(TIM2, ENABLE),定时器计数器正式开始计数。

二.定时器常用的库函数讲解

我们可以在工程的User文件夹里面找到关于TIM的库函数,可以通过图片中的小图标来标记我们需要的函数,后面方便我们查找

下面整理学习江科大教程时记录的定时器相关库函数,我分成基础必备函数拓展函数两部分,我把基础·必备函数加红方便大家查看和方便区分哪些是本次定时中断实验要用的。

一、基础核心函数(TIM2 更新中断实验重点)

  1. void TIM_InternalClockConfig(TIM_TypeDef* TIMx);用来设置定时器时钟源为内部时钟。我们这次 TIM2 实验调用了这个函数,不过标准库定时器上电默认就是内部时钟,就算删掉这行代码程序也能正常跑。这个函数主要是方便后续做外部时钟、输入捕获的时候切换时钟源。

  2. void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct); 时基单元初始化函数,算是定时器最重要的函数。预分频 PSC、自动重装载值 ARR、计数模式这些决定定时时间的参数,都靠填充这个结构体,再调用该函数写入寄存器。

  3. void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct); 给时基结构体填充默认参数。平时简单实验我很少写,但规范的工程里最好加上,防止结构体里面残留随机值,出现一些莫名其妙的问题。

  4. void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState); 定时器开关,控制计数器启停。ENABLE 开启计数,DISABLE 关闭。一定要注意使用顺序,所有参数都配置完成之后,最后再调用这个函数启动定时器。

  5. void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState); 开启定时器对应的中断信号。本次实验配置TIM_IT_Update,作用就是允许定时器溢出更新事件产生中断并送往 NVIC。除了更新中断,它还能开启捕获中断、比较中断。

  6. FlagStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT); 在中断服务函数里判断中断来源,只识别已经开启的中断。注意和TIM_GetFlagStatus区分,后者只是单纯读取事件标志,不会判断中断有没有使能。

  7. void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT); 清除中断标志位,很容易踩坑。中断逻辑执行完必须调用,如果不清标志位,退出中断之后会立刻再次进中断,程序直接卡死。

  8. void TIM_DeInit(TIM_TypeDef* TIMx); 把定时器恢复成上电默认状态。本次实验没有用到,一般在需要重复初始化、切换定时器功能的项目里才会使用。

二、拓展函数(后续输入捕获、动态改定时参数使用)

时钟源配置相关

TIM_ITRxExternalClockConfig():选择其他定时器作为时钟源 TIM_TIxExternalClockConfig():使用通道 TIx 外部信号充当定时器时钟 TIM_ETRClockMode1Config()TIM_ETRClockMode2Config():ETR 外部时钟两种模式 TIM_ETRConfig():单独配置 ETR 引脚的预分频、极性、滤波参数

这一组本次定时中断用不上,等到后面学习脉冲计数、外部时钟模式的时候再细看。

运行时动态修改定时器参数

  1. TIM_PrescalerConfig() 运行过程中修改预分频 PSC,可以选择新参数立刻生效,或是等到更新事件到来再生效。
  2. TIM_CounterModeConfig() 动态切换计数模式,向上计数、向下计数、中央对齐模式都可以设置。
  3. TIM_ARRPreloadConfig() 控制 ARR 预装载功能。ENABLE 时,新写入的 ARR 先放到缓冲器,更新事件产生后才生效;DISABLE 模式下写入 ARR 马上生效。
  4. TIM_SetAutoreload():直接写入 ARR 寄存器
  5. TIM_SetCounter():手动给 CNT 计数器赋值

寄存器读取函数

uint16_t TIM_GetCounter(TIM_TypeDef* TIMx); 读取当前计数器 CNT 实时数值。

uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx); 读取当前生效的预分频 PSC 数值。

三.TIM2工程代码实战

一.接线部分

在代码配置之前有一件非常重要的操作就是接线,如果线接错了,轻则烧录这后无反应或烧录不进去,重则烧板子,所以接线还是很重要的,本程序的接线还是非常简单的,下面图片是接线示意图

二.代码部分

我们在System文件夹下单独封装一个TIimer.h和Timer.c文件

void Timer_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//开启TIM2时钟
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;//ARR自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;//PSC预分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//高级定时器专用,通用定时器填0
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);//写入参数
TIM_ClearFlag(TIM2, TIM_FLAG_Update);//清除更新标志位
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);//使能更新中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断优先级分组2
NVIC_InitTypeDef NVIC_InitStructure;//NVIC结构体
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);//开启定时器
}

逐段解析

  1. 开启时钟:所有外设使用前必须开启对应总线时钟,TIM2 属于 APB1。
  2. TIM_InternalClockConfig:配置定时器使用内部时钟,固件库默认模式,注释掉代码依旧正常运行。
  3. 时基单元配置
    • TIM_Prescaler:预分频,对 72MHz 时钟进行分频;
    • TIM_Period:ARR 寄存器,计数器计数上限;
    • 向上计数模式:计数器从 0 增加到 ARR 后清零,产生溢出事件。
  4. 中断配置
    • TIM_ITConfig:打开定时器更新中断;
    • NVIC 配置:设置中断优先级,保证内核能够响应 TIM2 中断。
  5. TIM_Cmd:最终使能定时器,计数器开始运行。

⚠️我们写的值只有在更新事件时,才会真正地起作用,所以在这里 为了让值立刻起作用,就在这最后手动生成了一个更新时间,这样,预分频设置就会有效了,但同时,它的副作用就是,更新事件和更新中断是同时发生的,更新中断 会置更新中断标志位,当我们最后一旦初始化完成了,更新中断就会立刻进入,这是我们刚一上电就立刻进中断的原因,就会在 TimeBaseInit 的后面,开启中断的前面,可以手动调用一下 TIM_ClearFlag 函数,来清除更新标志位

三、中断服务函数讲解

void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//判断是否溢出中断
    {
        Num++;
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);//清除中断标志位
}
}

重点说明

  1. 中断函数名称固定不能写错 TIM2_IRQHandler,启动文件内定义;
  2. TIM_GetITStatus:判断中断是否满足条件(区分普通标志位读取函数TIM_GetFlagStatus);
  3. TIM_ClearITPendingBit:清除中断挂起位,必不可少

⚠️如果不清除标志位,退出中断后会立刻再次进入中断,程序卡死在中断内。

⚠️void TIM2_IRQHandler(void)这个函数可以和上面函数一同放在Timer.c里面,也可以放在main.c文件里面,但放在Timer.c里面用到Num时要声明一下外部变量extern uint16_t Num;

四、实验现象

实验现象:TIM2 配置为 1s 中断一次。主循环持续运行,每隔 1 秒进入中断服务函数。

Num会从1开始一直累加

完整程序代码请看STM32_Project: 基于江科大的STM32的教程编写的程序

五、调试踩坑记录

坑 1:进入中断后不断重复触发

现象:程序一直卡在中断函数 原因:忘记调用TIM_ClearITPendingBit清除中断标志位 解决:每次中断处理完成,清除对应中断挂起位

坑 2:定时器完全不进中断

排查方向:

  1. 是否开启 APB1 TIM2 时钟;
  2. 是否执行TIM_Cmd(TIM2,ENABLE)打开定时器;
  3. 是否调用TIM_ITConfig开启更新中断;
  4. NVIC 中断通道、优先级是否正确配置。

坑 3:定时时间和计算结果不一致

原因:PSC、ARR 忘记 -1;定时公式两个寄存器都需要减一 解决:严格遵循公式 (PSC+1)*(ARR+1)

总结

本文系统性地介绍了 STM32 定时器中断的配置与应用,以 TIM2 通用定时器为例,从理论到实践完整走通了定时中断的开发流程。

核心要点回顾

  1. 定时器配置七步法:从开启时钟、选择时钟源、配置时基单元、使能中断、配置 NVIC,到清除标志位和使能计数器,每一步都有其明确的作用和注意事项。

  2. 关键库函数掌握:重点掌握了 TIM_TimeBaseInitTIM_ITConfigTIM_CmdTIM_GetITStatusTIM_ClearITPendingBit 等核心函数,理解了它们在定时中断中的作用。

  3. 中断服务函数规范:中断函数名必须正确,中断标志位的判断与清除是确保中断正常执行的关键,避免程序卡死在中断中。

  4. 定时时间计算:牢记公式 定时时间 = (PSC+1)*(ARR+1)/时钟频率,注意寄存器值需要减一。

写这篇文章的时候,我把江科大的视频又回顾了一遍,还对照标准库源码和参考手册逐个函数验证,确实比自己单纯听课理解得更深了。如果你也在学STM32,强烈建议你试着把学到的知识点写成博客,教别人是最好的学习方式!


📌 开发环境:Keil5 + STM32F103C8T6最小系统板 + ST-Link
📌 参考资料:江科大STM32入门教程、《STM32F10xxx参考手册》
📌 前置文章STM32定时器超详解:从时钟树到影子寄存器,一文吃透定时中断

如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

Logo

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

更多推荐