前言:第六章 TIM 定时器 包含以下四个部分内容:
- 第一部分:介绍定时器的基本定时功能(本篇内容)
- 第二部分:讲解定时器的输出比较功能(PWM)
- 第三部分:探讨定时器的输入捕获功能
- 第四部分:学习定时器的编码器接口
目录
[1 定时器简介](#1 定时器简介)
[2 STM32 定时器类型](#2 STM32 定时器类型)
[2.1 基本定时器(Basic Timer)](#2.1 基本定时器(Basic Timer))
[2.1.1 基本时基单元](#2.1.1 基本时基单元)
[2.1.2 工作原理与流程](#2.1.2 工作原理与流程)
[2.1.3 工程提示与注意点](#2.1.3 工程提示与注意点)
[2.2 通用定时器(General-Purpose Timer)](#2.2 通用定时器(General-Purpose Timer))
[2.2.1 主要功能概览](#2.2.1 主要功能概览)
[2.2.2 工作原理(分模块说明)](#2.2.2 工作原理(分模块说明))
[2.2.3 工程注意点](#2.2.3 工程注意点)
[2.3 高级定时器(Advanced-Control Timer)](#2.3 高级定时器(Advanced-Control Timer))
[2.3.1 主要功能概览](#2.3.1 主要功能概览)
[2.3.2 工作原理(按模块说明)](#2.3.2 工作原理(按模块说明))
[2.3.3 工程提示与注意点](#2.3.3 工程提示与注意点)
[3 定时器中断配置流程](#3 定时器中断配置流程)
[3.1 内部时钟触发定时器中断(使用 RCC 内部时钟)](#3.1 内部时钟触发定时器中断(使用 RCC 内部时钟))
[3.1.1 时钟开启](#3.1.1 时钟开启)
[3.1.2 时钟源配置](#3.1.2 时钟源配置)
[3.1.3 时基单元初始化](#3.1.3 时基单元初始化)
[3.1.4 中断输出使能](#3.1.4 中断输出使能)
[3.1.5 NVIC 配置](#3.1.5 NVIC 配置)
[3.1.6 运行控制](#3.1.6 运行控制)
[3.2 外部时钟触发定时器中断](#3.2 外部时钟触发定时器中断)
[3.2.1 时钟开启](#3.2.1 时钟开启)
[3.2.2 时钟源配置](#3.2.2 时钟源配置)
[3.2.3 时基单元初始化](#3.2.3 时基单元初始化)
[3.2.4 中断输出使能](#3.2.4 中断输出使能)
[3.2.5 NVIC 配置](#3.2.5 NVIC 配置)
[3.2.6 运行控制](#3.2.6 运行控制)
[4 相关元器件简介](#4 相关元器件简介)
[4.1 对射式红外传感器](#4.1 对射式红外传感器)
[4.1.1 传感器概述](#4.1.1 传感器概述)
[4.1.2 输出逻辑](#4.1.2 输出逻辑)
[5 本章节实验](#5 本章节实验)
[5.1 定时器定时中断](#5.1 定时器定时中断)
[5.1.1 实验目标](#5.1.1 实验目标)
[5.1.2 硬件设计](#5.1.2 硬件设计)
[5.1.3 软件设计](#5.1.3 软件设计)
[5.1.3.1 解析初始化后中断提前触发的"诡异"现象](#5.1.3.1 解析初始化后中断提前触发的“诡异”现象)
[5.1.4 实验现象](#5.1.4 实验现象)
[5.2 定时器外部时钟](#5.2 定时器外部时钟)
[5.2.1 实验目标](#5.2.1 实验目标)
[5.2.2 硬件设计](#5.2.2 硬件设计)
[5.2.3 软件设计](#5.2.3 软件设计)
[5.2.4 实验现象](#5.2.4 实验现象)
1 定时器简介
定时器(TIM)本质上是一个受控的计数器单元。它通过对基准时钟信号进行分频和累加,实现精确的时间管理功能。虽然名为"定时",但其底层逻辑其实是对时钟脉冲进行计数。
STM32 的定时器架构主要由时钟源、预分频器(PSC)和计数器(CNT)组成。其工作流程如下:
- 计数:在驱动时钟下,计数器(CNT)按步长递增;
- 匹配:当 CNT 达到自动重装载寄存器(ARR)设定的阈值时;
- 响应:硬件产生"更新事件"并触发中断。
这种基于硬件计数的定时方式,不仅精度极高,还能配合 DMA 或其他外设实现复杂的自动化控制逻辑。
2 STM32 定时器类型
STM32 的定时器功能强大、种类丰富。按功能复杂度和应用场景可以分为三大类:高级定时器(Advanced) 、通用定时器(General-purpose) 和 基本定时器(Basic)。不同类型的定时器挂载在不同的总线上,这决定了它们的时钟源和最高运行频率,从而影响实际计时能力与外设联动能力。
| 定时器类型 | 典型编号 | 挂载总线 | 功能概述 |
|---|---|---|---|
| 高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,额外支持重复计数器、死区生成、互补输出、刹车输入等面向电机和电源控制的高级特性。 |
| 通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,额外具备内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等,应用最灵活。 |
| 基本定时器 | TIM6、TIM7 | APB1 | 结构最简单,仅提供基本定时中断和触发 DAC 的主模式输出,通常不暴露外部 I/O 引脚(用于产生稳定的时间基准)。 |
以 STM32F103C8T6 为例,该芯片包含以下计时源:
- 高级定时器 (1个):TIM1
- 通用定时器 (3个):TIM2、TIM3、TIM4
- 基本定时器 (0个) :无(该型号不含TIM6/TIM7)
- 其他 (3个):2个看门狗定时器(IWDG, WWDG)和1个系统滴答定时器(SysTick)。
2.1 基本定时器(Basic Timer)
基本定时器是 STM32 定时器家族中功能最精简的一类,设计目标是提供稳定、低开销的时间基准或作为其它外设的周期触发源。它不暴露比较/捕获通道,硬件实现简单,适用于周期性中断、定时唤醒和驱动 DAC。
2.1.1 基本时基单元
从基本定时器的框图可以看出,其仅包含最核心的时基单元部分。
- PSC(Prescaler,预分频器) :16 位,用于对定时器输入时钟
CK_PSC进行分频,从而改变计数速度。 - CNT(Counter,计数器):16 位,用于记录当前的计数值。计数到 ARR 后触发更新事件,并重新从 0 开始计数。
- ARR(Auto-Reload Register,自动重装载寄存器):16 位,用于定义计数器 CNT 计数的"终点值"。

2.1.2 工作原理与流程
基本定时器通过以下四个步骤循环工作,产生精准的时间基准:
(1)时钟输入与分频
定时器从 RCC 获得基准时钟 CK_PSC。经过预分频器后,生成计数时钟 CK_CNT。计算关系如下:
(2)计数驱动
CK_CNT 推动 CNT 按配置方向(通常向上)递增,直至等于 ARR。
(3)触发更新事件 (UEV):
当 CNT 的值累加至与 ARR 设定值相等时,硬件自动产生一个更新事件 (Update Event, UEV)。
- 中断/DMA 触发:若开启了相关功能,UEV 将触发更新中断或 DMA 请求。
- 影子寄存器刷新:UEV 到来时,预装载的 ARR 或 PSC 值会被正式同步到活跃寄存器中。
- TRGO 输出:UEV 可通过主模式控制器输出 TRGO 信号,直接触发 ADC 或 DAC。
(4)自动重置与循环:
产生 UEV 后,CNT 会自动清零(0)并重新开始下一轮计数,从而实现连续的周期性定时。
注:STM32F103C8T6 不含基本定时器(TIM6/TIM7),但在配置通用定时器的定时中断时,其核心逻辑与上述流程完全一致。
2.1.3 工程提示与注意点
- PSC 实际分频系数为
PSC + 1;写PSC=0表示 1 分频(即不分频)。 - ARR/PSC 通常采用影子寄存器机制,写入后在下一次 UEV 才会生效。
- 基本定时器不直接驱动外设引脚;若需 PWM 或捕获功能,请使用通用或高级定时器。
2.2 通用定时器(General-Purpose Timer)
通用定时器(如 TIM2~TIM5)是在基本定时功能基础上的增强型,支持多通道信号处理与外设联动,是 STM32 中使用频率最高的一类定时器。
通用定时器框图如图所示:

2.2.1 主要功能概览
通用定时器通常具备以下核心能力:
- 最多 4 个独立通道:每个通道可配置为输出比较、PWM 输出或输入捕获。
- 多种计数模式:支持向上计数、向下计数、中心对齐模式。
- 外部时钟与触发:支持 ETR / TIx 外部时钟输入与内部触发。
- 主从同步机制:可与其他定时器级联或同步启动。
2.2.2 工作原理(分模块说明)
(1)计数核心与更新机制
通用定时器的底层时基结构与基本定时器一致。它从 RCC / APB 总线获得输入时钟(CK_APB),经预分频器 PSC 分频后形成计数节拍 CK_CNT。PSC 的实际分频系数为 PSC + 1,因此当 PSC = 0 时表示不分频。
CK_CNT 持续驱动计数器 CNT 按配置模式运行。CNT 可以向上计数、向下计数,或采用中心对齐(上下计数)模式运行。当 CNT 达到 ARR 所设定的自动重载值,或完成一次上下计数循环时,硬件产生更新事件 UEV。
UEV 是周期边界的重要同步点。它不仅标志一个计数周期结束,同时还承担寄存器更新功能------若启用了预装载机制,ARR 和 CCRx 的新值会在 UEV 时刻统一生效。此外,UEV 还可以触发更新中断、DMA 请求,或输出 TRGO 信号作为其他外设或定时器的触发源。
(2)比较单元与 PWM 输出
在计数运行过程中,硬件会实时比较 CNT 当前值与各通道的 CCRx 值。当 CNT 等于某个 CCRx 时产生比较匹配事件(Compare Match)。
这一事件可以直接控制对应的 OCx 输出引脚状态,例如置高、置低或翻转;也可以仅作为内部事件触发中断或 DMA。比较机制与自动重载寄存器 ARR 结合,就形成了 PWM 输出结构。
ARR 决定整个计数周期长度,因此决定 PWM 频率;CCRx 决定比较发生的时刻,因此决定占空比。只要修改 CCRx 的值,就可以改变输出脉冲的高电平持续时间,而不影响周期本身。
在边缘对齐模式下,CNT 单向计数,每个周期只发生一次比较匹配,结构简单、应用最广。中心对齐模式下,CNT 在上升和下降阶段各触发一次比较事件,输出波形围绕周期中心对称,谐波特性更优,常用于电机驱动等对波形质量要求较高的场景。
为了避免运行过程中直接写 ARR 或 CCRx 导致波形瞬态跳变,通用定时器通常启用影子寄存器机制。写入的新值首先进入缓冲区,只有在下一次 UEV 到来时才同步到工作寄存器,从而保证参数在周期边界更新,维持输出连续性。
(3)输入捕获与信号测量
当某个通道被配置为输入捕获模式时,其外部引脚的指定边沿到来时,会把当前 CNT 的数值锁存到对应的 CCRx 中。由于 CNT 本质上是时间计数器,因此锁存的值可以直接用于计算脉冲宽度、周期或频率。
输入路径通常带有数字滤波器和采样分频功能,用于抑制毛刺或抖动信号,提升测量稳定性。捕获事件发生后,可以产生中断通知 CPU,也可以通过 DMA 自动搬运数据,实现高频测量而不占用大量处理器资源。
(4)外部时钟与主从同步机制
通用定时器不仅可以使用内部 CK_APB 作为时基,还可以选择外部信号(如 ETR 或 TIx)作为计数时钟,从而实现对外部脉冲的计数功能。
此外,它支持主从同步模式。一个定时器可以通过 TRGO 输出触发信号,另一个定时器作为从设备接收该触发信号并同步启动或复位。通过这种方式,可以构建多个定时器组成的同步系统,例如多相 PWM 输出、同步 ADC 采样或级联计数结构。
这种硬件级联机制避免了软件触发带来的延迟和抖动,提高了系统整体时序一致性。
2.2.3 工程注意点
- 高速 PWM 不宜完全依赖中断,应结合 DMA 以减轻 CPU 负担。
- 中心对齐模式下,一个周期内比较事件会发生两次,需注意逻辑控制。
- APB 分频改变时,定时器时钟可能存在倍频规则(如 APB1 预分频不为 1 时,定时器时钟会自动×2)。
2.3 高级定时器(Advanced-Control Timer)
高级定时器(典型:TIM1 / TIM8)在通用定时器基础上引入面向功率与电机控制的硬件特性,除了支持输出比较、输入捕获、PWM 和编码器接口外,还提供互补输出、可编程死区、重复计数器和刹车保护等功能,适用于三相逆变、电机驱动和高可靠性电源管理场景。
高级控制定时器框图如图所示:

2.3.1 主要功能概览
- 具备通用定时器的全部功能。
- 互补输出 (OCx 与 OCxN)及硬件死区时间(Dead-time)插入。
- 重复计数器(Repetition Counter),用于控制寄存器更新的频率。
- 刹车(Break)输入,用于硬件故障快速关断保护。
2.3.2 工作原理(按模块说明)
(1)计数核心与更新
时钟(CK_APB)经 PSC 分频生成 CK_CNT,驱动 CNT 按配置的对齐模式计数(向上/向下/中心对齐)。当 CNT 达到 ARR 或完成计数周期时产生 UEV,用于刷新影子寄存器并可输出 TRGO。
(2)比较与互补输出
每个通道的比较匹配(CNT = CCRx)不仅控制主输出 OCx 的翻转,也可同时或条件性地控制互补输出 OCx̄。互补输出在切换时钟入会按用户设定自动插入死区时间,确保主/互补在开关瞬间不会同时导通,防止桥臂直通故障。
(3)重复计数与寄存器更新
重复计数器允许把"寄存器更新/输出更新"的操作按 N 次 UEV 下采样,例如每 N 次 UEV 才真正将影子寄存器内容转入工作寄存器或触发外部事件,从而实现更细粒度的输出时序控制。
(4)刹车(Break)与故障保护
当检测到外部故障(如过流)或收到刹车信号时,硬件能立即以最高优先级将所有输出置于安全状态(强制关断或设为预定义电平),同时生成中断以通知软件处理。刹车路径是硬件级别的快速保护机制,响应时间短且可靠性高。
2.3.3 工程提示与注意点
- 死区时间需根据驱动器与功率器件特性精确配置,过短会导致直通,过长会增加开关损耗/畸变。
- 重复计数器与影子寄存器配合使用可避免输出更新时的瞬态错误,但会增加输出更新延迟,控制策略需兼顾。
- 刹车输入通常与外部监测电路(过流/过压)联动,应制定明确的保护与恢复策略(例如硬件解除、手动复位或软件确认)。
- 高级定时器通常挂载在较高速的 APB2 总线上(具体以芯片手册为准),因此在高频 PWM 场景下能提供更高的分辨率与实时性。
- 在功率控制场景中,建议把关键保护逻辑尽可能放在硬件路径(刹车、死区)上,软件作为次级处理与状态记录。
3 定时器中断配置流程
无论时钟源来自哪里,配置定时器中断的通用标准流程如下:
- 时钟开启:使能 TIM 外设及相关外设的时钟。
- 时钟源配置:选择内部时钟或外部触发源(ETR/ITR/TIx)。
- 配置时基单元:设置预分频器(PSC)、自动重装载寄存器(ARR)、计数模式。
- 使能更新中断:开启定时器到 NVIC 的中断信号输出,使能定时器更新中断输出位。
- 配置 NVIC 中断通道:设定中断向量优先级分组,配置 TIM 中断通道并使能。
- 启动定时器:使能定时器计数器(CEN),定时器开始工作。
3.1 内部时钟触发定时器中断(使用 RCC 内部时钟)
在本章的第一个实验《定时器定时中断》中,定时器使用RCC内部时钟(72MHz)作为时钟源,实现精确的定时功能。
配置的流程图如图所示:

其配置流程大致包括以下关键步骤:
3.1.1 时钟开启
首先需要开启定时器的外设时钟。STM32 的通用定时器 TIM2 挂载在 APB1 总线上。开启时钟后,TIM2 的基准时钟和外设时钟才会工作。
代码示例如下:
cpp
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 开启TIM2的时钟
3.1.2 时钟源配置
调用相关得时钟源选择函数确定计数器的驱动基准。对于定时中断,通常选择内部时钟模式。
代码示例如下:
cpp
TIM_InternalClockConfig(TIM2); // 选择TIM2为内部时钟(默认即为此配置)
3.1.3 时基单元初始化
此步骤直接决定了中断触发的频率。实验的目标是实现 1 秒(1Hz) 的定时中断,让变量 Num 每秒自增并在 OLED 上显示。
定时器的更新频率(即进入中断的频率)由以下公式决定:
其中各参数含义如下:
- f_CLK :定时器的输入时钟频率(STM32F103 中 TIM2 默认由内部 RCC 提供,频率为 72MHz)。
- PSC (Prescaler):预分频器数值。
- ARR (Auto-Reload Register):自动重装载寄存器数值。
- f:最终触发中断的频率(单位:Hz)。
若要实现 1 秒定时,即目标更新频率 f = 1Hz。我们将已知项代入公式:
通过移项可知,需要让 (PSC + 1) * (ARR + 1) = 72,000,000。为了计算方便且不超出 16 位寄存器的范围(最大 65535),通常进行如下组合:
- 设定 PSC = 7200 - 1:这样预分频后的时钟频率为 72\\text{MHz} / 7200 = 10,000\\text{Hz}(即每秒计 10000 个数)。
- 设定 ARR = 10000 - 1:计数器从 0 计到 9999,刚好经历 10000 个计数值。
**为什么在代码中要写 "- 1" ?**可能大家会由此疑问。底层逻辑只有一句话:STM32 的硬件计数是从 0 开始的。实际写入寄存器的值 = 你的目标计算结果 - 1
预分频器 (PSC) :如果想实现 7200 分频,硬件会从 0 数到 7199 。如果填入 0 ,则代表 1 分频(不分频)。
自动重装载 (ARR) :如果想计 10000 个数,硬件会从 0 累加到 9999 。当从 9999 回到 0 的那一瞬间,触发中断。
最终结果:
频率 1Hz 对应的周期 T = 1 / f = 1秒。
代码示例如下:
cpp
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; // 高级定时器专用
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
3.1.4 中断输出使能
开启定时器的更新中断(Update Interrupt)信号通道。在开启中断前,需手动清除更新标志位。
代码示例如下:
cpp
TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 清除由于初始化产生的更新标志位
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 开启定时器更新中断
注意:TIM_TimeBaseInit 在底层执行时会手动产生一次更新事件以装载预分频值,这会导致 UIF 标志位提前置 1。若不调用 TIM_ClearFlag,程序初始化后会立刻进入一次中断。
3.1.5 NVIC 配置
在内核中断控制器(NVIC)中设置定时器中断的优先级(抢占/响应优先级),并使能对应中断通道,确保 CPU 能够响应中断信号。
代码示例如下:
cpp
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设置优先级分组
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 定时器2中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 响应优先级
NVIC_Init(&NVIC_InitStructure);
3.1.6 运行控制
最后,使能定时器计数器,定时器正式开始计时,并在达到设定值(ARR)时触发中断服务函数。
代码示例如下:
cpp
TIM_Cmd(TIM2, ENABLE); // 启动定时器
3.2 外部时钟触发定时器中断
在本章的第二个实验《定时器外部时钟》中,定时器TIM2使用来自 ETR 引脚(对应 PA0 引脚)输入的外部脉冲信号作为时钟源。

外部时钟触发模式允许用外部脉冲信号驱动定时器计数,从而在外部事件达到设定次数时产生中断。在该模式下,配置流程在上节基础上增加了 GPIO 和时钟源设置,配置的流程图如图所示:

其配置流程大致包括以下关键步骤:
3.2.1 时钟开启
除定时器时钟外,还需开启对应输入引脚的 GPIO 时钟。
代码示例如下:
cpp
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
3.2.2 时钟源配置
配置 GPIO 引脚为输入模式,并将 TIM2 设置为外部时钟模式2。参数说明:
- TIM_ExtTRGPSC_OFF 表示不使用外部预分频器,
- TIM_ExtTRGPolarity_NonInverted 表示高电平或上升沿有效,
- 滤波参数 0x0F 根据需要设置消抖。
这样 TIM2 的时钟源就切换为外部输入(ETR),每次外部脉冲到来时计数器增加 1。
代码示例如下:
cpp
// GPIO 初始化
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);
// 0x0F 为滤波器参数,用于滤除外部信号抖动
提示:STM32 参考手册中推荐把 TIM2 的 ETR(PA0)引脚配置为浮空输入,因为这能最真实地反映外部逻辑。如果引脚悬空或信号不稳定,浮空输入极易受电磁干扰产生"毛刺",导致计数器误触发。因此,为了增强抗干扰能力,通常推荐配置为上拉输入 (IPU) 或下拉输入。
只有在外部信号源阻抗很高或信号驱动力很弱(例如某些传感器或微弱模拟信号)时,内部上拉电阻才可能会参与分压,干扰原始信号,无法达到 MCU 识别的逻辑阈值。此时,必须配置为浮空输入,以确保不影响外部信号的原始电平。
3.2.3 时基单元初始化
本章第二个实验《定时器外部时钟》中,TIM2 的计数时钟来自外部触发输入 ETR(对应引脚 PA0),外部脉冲由对射式红外传感器的 DO 引脚输出------我们通过遮挡/移开挡光片产生高低电平变化,模拟方波脉冲以驱动定时器计数。因此在时基单元的配置上应以"每个外部脉冲为一次计数"的思路来设置 PSC 与 ARR。
首先明确几个概念:
- 定时器的计数器 CNT 在每个计数脉冲到来时递增一次;
- ARR(Auto-Reload Register)定义CNT计数周期的上限,计数器从 0 计数到 ARR,然后产生一次更新事件(UEV);
- PSC(Prescaler)是预分频器,实际分频系数为 PSC + 1。
对于外部时钟模式,计数脉冲源是来自 ETR 的外部信号,内部的 PSC 仍可用于进一步分频,但外部还有专门的外部触发预分频/滤波器设置(由 TIM_ETRClockMode2Config 的参数控制)。在本实验中,因外部脉冲来源为手动遮挡产生,频率较低且不可太快,因此选择不对外部脉冲再做分频,直接以每个外部脉冲计一次数。
基于上述考虑,示例中将 PSC 设置为 1 - 1(即 PSC = 0,表示不分频),ARR 设置为 10 - 1(即 ARR = 9),这意味着:计数器从 0 计数到 9(共 10 个外部脉冲)时产生一次更新事件并触发中断,进而在中断服务函数中将 Num 自增一次。
代码示例如下:
cpp
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 10 - 1; // 计满10个脉冲触发中断
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; // 外部时钟不分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
3.2.4 中断输出使能
和内部时钟模式一样,清除标志位并使能更新中断:
cpp
TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 清除标志,避免初始触发
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断
3.2.5 NVIC 配置
同内部时钟模式,对 TIM2_IRQn 设置优先级并使能
cpp
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
3.2.6 运行控制
使能定时器。此后,TIM2 将响应外部输入脉冲,当计数到 ARR 时触发中断。
cpp
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
4 相关元器件简介
4.1 对射式红外传感器
4.1.1 传感器概述
提示:对射式红外传感器在上一篇【江科大STM32学习笔记-05】EXTI外部中断中有详细说明,这里只简单介绍一下。
对射式红外传感器是一种常见的光电开关型传感器,通常由红外发射管 和 红外接收管 组成,两者相对安装,中间形成一个固定宽度的"光槽"。当光路未被遮挡时,接收端可以正常接收到红外光;当有物体进入槽中遮挡光路时,接收状态发生变化,从而在输出端产生明确的电平跳变信号。

| 引脚名 | 功能说明 |
| VCC | 电源正极(3.3V~5V) |
| GND | 电源负极 |
| DO | 数字输出(TTL 电平) |
| AO | 模拟输出(本模块未接出/不起作用) |
|---|
4.1.2 输出逻辑
在本实验所使用的对射式红外传感器模块中,其输出逻辑关系如下表所示:
| 光路状态 | 接收管状态 | DO 输出电平 | 指示灯状态 |
|---|---|---|---|
| 无遮挡 | 导通 | 低电平 | 亮 |
| 有遮挡 | 截止 | 高电平 | 灭 |
即:
- 当物体进入光路、产生遮挡时,DO 引脚电平由低电平跳变为高电平,产生一个上升沿;
- 当物体离开光路、遮挡消失时,DO 引脚电平由高电平跳变为低电平,产生一个下降沿;
由此可知:遮挡时产生上升沿,移开遮挡时产生下降沿。它可以很好地作为定时器的外部脉冲源。
5 本章节实验
本章通过两个实验来讲解 STM32 定时器的基本定时功能与中断使用:第一个实验用内部时钟产生周期性定时中断并在 OLED 上显示计数;第二个实验把定时器作为计数器,使用外部时钟(PA0/ETR)驱动计数,并在外部事件达到设定次数时产生中断。两个实验代码结构相似,差别主要在时钟源与 GPIO 配置上,便于初学者理解"定时器 = 时钟来源 + 时基配置 + 中断/触发"的思想。
5.1 定时器定时中断
5.1.1 实验目标
掌握使用内部时钟配置 STM32 定时器以产生周期性更新中断;理解并实践以下要点:
- 如何打开定时器外设时钟(RCC);
- 如何设置时基单元(PSC、ARR)以得到所需周期;
- 如何使能并配置 TIM 更新中断(TIM_IT_Update);
- 如何在 NVIC 中设置中断通道与优先级;
- 中断服务例程(ISR)中如何判断、处理并清除中断标志;
- 在中断中更新状态并在主程序(OLED 显示)中读取显示。
完成后,能用定时中断驱动一个变量 Num 周期性自增,并在 OLED 上实时显示该数值,验证中断触发与计时精度。
5.1.2 硬件设计

5.1.3 软件设计
本实验软件分为两大阶段:一次性初始化和循环显示(主循环),中断处理放在独立 ISR 中。
(1)初始化阶段(只执行一次)
- 打开定时器时钟:调用 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); 打开 TIM2 的时钟,使能外设寄存器可写、定时器可工作。
- 选择时钟源:显式调用 TIM_InternalClockConfig(TIM2);(可选,因为默认也是内部时钟),明确使用内部时钟作为计时源。
- 配置时基单元:使用 TIM_TimeBaseInitTypeDef 设置 TIM_Prescaler(PSC)和 TIM_Period(ARR)。
- 清除更新标志并使能更新中断:先 TIM_ClearFlag(TIM2, TIM_FLAG_Update); 防止初始化时立刻触发中断,然后 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); 打开更新中断。
- 配置 NVIC:调用 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 设置分组(只需全工程调用一次),然后通过 NVIC_InitTypeDef 设置 NVIC_IRQChannel = TIM2_IRQn、抢占优先级与子优先级,最后 NVIC_Init(&NVIC_InitStructure); 使能。
- 启动定时器:TIM_Cmd(TIM2, ENABLE); 使能 TIM2 开始计数。
(2)主循环阶段(无限循环)
- 主程序不做计时工作,仅负责显示。示例中 main() 先初始化 OLED 与定时器,然后在 while(1) 中不断调用 OLED_ShowNum(1,5,Num,5); 将中断中自增的 Num 显示出来。这样可以把定时/计数逻辑与显示逻辑解耦。
(3)中断处理(TIM2_IRQHandler)
- 在中断函数中先调用 TIM_GetITStatus(TIM2, TIM_IT_Update) 判断是否为更新中断,然后执行用户逻辑(示例为 Num++),最后调用 TIM_ClearITPendingBit(TIM2, TIM_IT_Update) 清除挂起位。清除中断标志是必须的,否则中断会被反复触发导致主程序无法运行。
5.1.3.1 解析初始化后中断提前触发的"诡异"现象
(1) 现象描述
在初始化流程中,若未在调用 TIM_TimeBaseInit() 函数后、开启中断前手动执行 TIM_ClearFlag() 操作,会出现一个异常现象:每次按下复位键(Reset)后,OLED 显示屏上的 Num 数值并非从 0 开始计数,而是直接显示为 1。
(2)原因分析
这表明初始化函数在内部生成了一次"更新事件(UEV)",并把更新中断标志置位了,因此在真正开启中断使能后会马上进入一次 ISR。
追踪库函数源码,在 TIM_TimeBaseInit 的末尾发现这样一行代码和注释:
/* 向事件产生寄存器(EGR)写入 1,手动产生一个更新事件 */
TIMx->EGR = TIM_PSCReloadMode_Immediate;
它的目的是:立即产生一个更新事件(UEV)来重新装载预分频器(PSC)和自动重装载寄存器(ARR)的值。
这是由定时器的硬件架构决定的。预分频器(PSC)具有**影子寄存器(Shadow Register)**机制。当我们写入新的分频值时,它并不会立刻生效,只有在产生"更新事件"时,值才会被真正加载到硬件逻辑中。
库函数为了保证初始化完成后,定时器能立刻按照你设定的频率运行,所以贴心地在最后"手动"触发了一次更新事件,但它同时也带来了一个副作用:产生更新事件(UEV)的同时,硬件会自动将更新中断标志位(UIF)置为 1。
这意味着,当你随后执行 TIM_ITConfig 开启中断使能时,中断系统发现 UIF 已经是 1 了,于是 CPU 认为中断条件已达成,初始化一结束就立刻飞奔进中断函数执行了 Num++。
(3)解决方案
要解决这个问题,只需在初始化时基单元之后、开启中断使能之前,手动调用一次清除标志位的函数即可:
// 1. 时基单元初始化(底层会手动产生一次更新事件,导致 UIF = 1)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// 2. 关键:手动清除掉初始化产生的更新中断标志位,避免误进中断
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
// 3. 此时再开启中断输出使能,逻辑就是干净的了
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
总结: 这一行 TIM_ClearFlag 相当于给中断系统做了一次"归零"校准,确保实验中的 Num 严格从 0 开始递增。
具体代码如下:
main.c文件:
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num; //定义在定时器中断里自增的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Timer_Init(); //定时中断初始化
/*显示静态字符串*/
OLED_ShowString(2, 1, "Num:"); //2行1列显示字符串Num:
OLED_ShowString(3,1,"CNT:"); //3行1列显示字符串Num:
while (1)
{
OLED_ShowNum(2, 5, Num, 5); //不断刷新显示Num变量
OLED_ShowNum(3,5,Timer_GetCounter(),5); //不断刷新显示CNT的值
}
}
/**
* 函 数:TIM2中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Num ++; //Num变量自增,用于测试定时中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
Timer.c文件:
cpp
#include "stm32f10x.h" // Device header
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
*/
void Timer_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
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; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
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);
}
}
*/
5.1.4 实验现象
运行程序后,OLED 屏上会显示静态字符串 "Num:" 以及不断增长的数值 Num。以示例 PSC/ARR 配置为目标,Num 将以约固定周期(示例为 1 秒)递增一次。主循环不断刷新 OLED,读者可通过观察数值变化验证定时器中断触发、ISR 正常执行以及标志位清除是否正确。

5.2 定时器外部时钟
5.2.1 实验目标
理解并掌握将 STM32 定时器配置为外部时钟输入模式的方法;学会配置外部引脚作为 ETR(外部触发),并观察:当外部脉冲达到预设次数(ARR)时,定时器产生更新中断。通过该实验学会:
- 配置 GPIO 为外部时钟输入(上拉/下拉选择);
- 使用
TIM_ETRClockMode2Config将 TIM 切换到外部时钟模式(ETR); - 结合 PSC/ARR 设置在 N 个外部脉冲后产生中断;
- 在主程序中读取并显示当前 CNT 值以验证外部计数行为。
5.2.2 硬件设计

5.2.3 软件设计
外部时钟模式的软件流程与内部时钟模式类似,但在初始化阶段需增加 GPIO 配置与外部时钟模式设置,主要步骤如下:
- 打开时钟与 GPIO :启用 TIM2 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);并启用 PA0 所在 GPIO 的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);。 - 配置 PA0 为输入 :通过
GPIO_InitTypeDef将 PA0 设置为上拉输入(GPIO_Mode_IPU),保证无外部信号时的确定电平。 - 设置外部时钟模式(ETR) :调用
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);将 TIM2 切换到外部时钟模式 2,使定时器以 ETR 管脚输入的脉冲作为计数时钟。参数中TIM_ExtTRGPSC_OFF表示不使用外部预分频,TIM_ExtTRGPolarity_NonInverted表示非反向极性(上升沿/高电平有效,具体以芯片手册为准),0x0F为滤波器寄存器用于消除毛刺(示例使用较大滤波值以提高鲁棒性)。 - 配置时基单元(PSC/ARR) :与内部计数相同,使用
TIM_TimeBaseInit配置计数模式、PSC 与 ARR。示例中为了让定时器在每 10 个外部脉冲后产生一次更新中断,设PSC = 1-1(无分频)、ARR = 10-1。实际应用中可根据脉冲频率与期望响应频率调整。 - 清除标志并使能中断 :调用
TIM_ClearFlag(TIM2, TIM_FLAG_Update);清初始标志,然后TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);使能更新中断。 - 设置 NVIC 并启动定时器 :按内部时钟实验一样配置
NVIC_PriorityGroupConfig与NVIC_Init,最后TIM_Cmd(TIM2, ENABLE);启动计数。 - ISR 与主循环 :中断服务函数
TIM2_IRQHandler检查TIM_GetITStatus(TIM2, TIM_IT_Update),在触发时对Num自增并TIM_ClearITPendingBit清标志。主循环可以通过Timer_GetCounter()(示例提供了该函数)读取当前 CNT 值并显示在 OLED 上,便于实时观察外部脉冲计数与中断触发。
具体代码如下:
main.c文件:
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num; //定义在定时器中断里自增的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Timer_Init(); //定时中断初始化
/*显示静态字符串*/
OLED_ShowString(2, 1, "Num:"); //1行1列显示字符串Num:
OLED_ShowString(3, 1, "CNT:"); //2行1列显示字符串CNT:
while (1)
{
OLED_ShowNum(2, 5, Num, 5); //不断刷新显示Num变量
OLED_ShowNum(3, 5, Timer_GetCounter(), 5); //不断刷新显示CNT的值
}
}
/**
* 函 数:TIM2中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Num ++; //Num变量自增,用于测试定时中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
Timer.c文件:
cpp
#include "stm32f10x.h" // Device header
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数配置为外部时钟,定时器相当于计数器
*/
void Timer_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
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); //将PA0引脚初始化为上拉输入
/*外部时钟配置*/
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
//选择外部时钟模式2,时钟从TIM_ETR引脚输入
//注意TIM2的ETR引脚固定为PA0,无法随意更改
//最后一个滤波器参数加到最大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; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/**
* 函 数:返回定时器CNT的值
* 参 数:无
* 返 回 值:定时器CNT的值,范围:0~65535
*/
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2); //返回定时器TIM2的CNT
}
/* 定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
5.2.4 实验现象
程序运行后,OLED 第2行显示 Num:,第三行显示 CNT:。
当不断用挡光片来回遮挡对射式红外传感器的光耦时, DO端口输出脉冲信号,可以观察到:
CNT数值随着遮挡的次数不断递增;- 当
CNT计数达到设定值9之后自动清零,同时Num数值加 1;
若一直不断的用挡光片来回遮挡,则:
CNT周期性递增并回零;CNT每回零一次,Num递增一次。
实验现象表明,定时器能够对外部输入脉冲进行计数,并在达到设定值后产生一次中断更新。
