目录

前言

一、STM32定时器架构与基础原理

1.1 定时器系统架构

1.2 定时器分类与特性

二、定时器定时中断模式深度解析

2.1 定时中断工作原理

2.1.1 基本定时器

2.1.2 通用定时器

2.1.3 高级定时器

2.2 定时中断基本结构

2.3 预分频器时序

2.4 计数器时序

2.5 计数器无预装时序​编辑

2.5 计数器有预装时序

2.6 RCC时钟树

三、定时器定时中断配置代码实现

3.1 定时中断接线图

3.2 封装定时器模块

3.2.1 System\Timer.h

3.2.2 System\Timer.c

3.3 主程序实现

3.3.1 User\main.c

四、定时器外部时钟

4.1 外部时钟模式原理

五、定时器外部时钟配置代码实现

5.1 外部时钟接线图

5.2 封装外部时钟模块

5.2.1 System\Timer.h

5.2.2 System\Timer.c

5.3 主程序实现

5.3.1 User\main.c

六、总结展望与源码分享

6.1 核心收获

6.2 未来发展方向

6.3 实践建议

6.4 源码分享


前言

在嵌入式系统开发中,STM32定时器是最核心的外设之一,它不仅能实现精确的定时功能,还能处理外部时钟信号,为各种复杂应用场景提供支持。本文基于江协科技STM32入门教程系列,将深入探讨定时器的定时中断外部时钟两大核心功能,帮助开发者全面掌握这一强大外设。

通过本文的学习,你将能够:

  • 理解STM32定时器的工作原理和架构
  • 掌握定时中断的配置和应用技巧
  • 深入理解定时器外部时钟模式的工作机制
  • 学会将两种模式结合应用到实际项目中

一、STM32定时器架构与基础原理

1.1 定时器系统架构

STM32定时器是基于ARM Cortex-M内核的复杂外设系统,其架构如下:

系统时钟 → AHB总线 → APB总线 → 定时器时钟 → 预分频器 → 计数器 → 比较/捕获寄存器

关键组成部分:

  • 时钟源:可选择内部时钟或外部时钟
  • 预分频器(PSC):对输入时钟进行分频
  • 自动重装载寄存器(ARR):设定计数周期
  • 计数器(CNT):实际计数值
  • 控制寄存器:配置工作模式和中断

1.2 定时器分类与特性

STM32F103系列包含多种定时器:

  • 基本定时器:TIM6、TIM7(仅定时中断功能)
  • 通用定时器:TIM2-TIM5、*TIM9-TIM14(功能最丰富)
  • 高级定时器:TIM1、TIM8(支持电机控制等高级功能)

STM32F103C8T6定时器资源:TIM1TIM2TIM3TIM4(无基本定时器)

二、定时器定时中断模式深度解析

2.1 定时中断工作原理

定时中断模式是最基础的应用,其工作原理如下:

  1. 时钟配置:选择内部时钟源(通常为系统时钟)
  2. 预分频:通过PSC寄存器将高频时钟分频为低频
  3. 自动重载:ARR寄存器设定计数最大值
  4. 计数过程:CNT寄存器从0开始向上计数
  5. 中断触发:当CNT=ARR时,产生更新中断,CNT重置为0

定时时间计算公式:

定时时间 = (ARR + 1) × (PSC + 1) / 时钟频率

2.1.1 基本定时器

2.1.2 通用定时器

2.1.3 高级定时器

其中对于通用定时器和高级定时器,支持向上计数、向下计数、中央对齐三种模式;而基本定时器仅支持向上计数这一种模式。

2.2 定时中断基本结构

2.3 预分频器时序

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

2.4 计数器时序

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

2.5 计数器无预装时序

2.5 计数器有预装时序

2.6 RCC时钟树

三、定时器定时中断配置代码实现

3.1 定时中断接线图

3.2 封装定时器模块

我们这里使用模块化编程的思路,先将STM32的内部时钟模块封装到System文件夹

3.2.1 System\Timer.h

#ifndef __TIMER_H
#define __TIMER_H

void Timer_Init(void);
void TIM2_IRQHandler(void);

#endif

3.2.2 System\Timer.c

定时器的实现逻辑:开 TIM2 时钟(RCC_APB1PeriphClockCmd) → 设内部时钟(TIM_InternalClockConfig) → 配时基参数 PSC/ARR(TIM_TimeBaseInit) → 开更新中断(TIM_ITConfig) → 配 NVIC(NVIC_PriorityGroupConfig/NVIC_Init) → 启动 TIM2(TIM_Cmd) → 在中断函数里处理并清中断标志(TIM2_IRQHandler)

#include "stm32f10x.h"                  // Device header

extern uint16_t Num;

void Timer_Init(void)
{
	// 1. 开启 TIM2 时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	
    // 2. 选择内部时钟
	TIM_InternalClockConfig(TIM2);

    // 3. 配置时基单元	
	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);
	
    // 4. 清除更新标志位
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);

    // 5. 开启更新中断
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    // 6. 配置 NVIC
	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);
	
    // 7. 启动 TIM2
	TIM_Cmd(TIM2, ENABLE);
}

void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) ==  SET)
	{
		Num ++;
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

需要注意的是,由于预分频器中缓冲寄存器的存在,会导致按下复位按钮后,更新事件和更新中断同时发生(即从1开始计数),所以为了避免刚初始化完成后就立即进中断,需要手动把更新中断标志位清除一下(TIM_ClearFlag)(即从0开始计数)

3.3 主程序实现

3.3.1 User\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:");
	
	while (1)
	{
		OLED_ShowNum(1, 5, Num, 5);
		OLED_ShowNum(2, 5, TIM_GetCounter(TIM2), 5);// CNT计数器的变化情况
	}
	
}

/*
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) ==  SET)
	{
		Num ++;
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}
*/

这里对于main.c中的全局变量Num,在执行中断自增的操作时有两种处理方式,一种是在Timer.c文件中使用extern函数来声明外部变量Num,使其能够正常被中断函数调用;另一种是直接把中断函数void TIM2_IRQHandler(void)写在主函数中,这样就可以在主函数中直接调用Num了。

四、定时器外部时钟

4.1 外部时钟模式原理

外部时钟模式是本视频的核心内容,它允许定时器使用外部信号作为时钟源,实现对外部事件的精确计数和测量。

工作原理:

  • 外部信号通过GPIO引脚输入
  • 信号经过滤波和边沿检测
  • 每检测到一个有效边沿,计数器加1
  • 可配置上升沿、下降沿或双边沿触发

外部时钟模式应用场景:

  • 旋转编码器测速
  • 脉冲信号计数
  • 频率测量
  • 位置检测

五、定时器外部时钟配置代码实现

5.1 外部时钟接线图

5.2 封装外部时钟模块

5.2.1 System\Timer.h

#ifndef __TIMER_H
#define __TIMER_H

void Timer_Init(void);
void TIM2_IRQHandler(void);
uint16_t Timer_GetCounter(void);

#endif

这里我们将CNT监视器的函数模块也进行了封装,使代码更加规范

5.2.2 System\Timer.c

外部时钟配置流程与内部时钟基本相同,仅需将原来配置内部时钟的函数改为配置外部时钟的函数,并且把外部时钟外设对应的GPIO引脚配置好即可(这里使用对射式红外传感器作为外部时钟进行配置,由于传感器本身的性质,建议将滤波调整为0x05

#include "stm32f10x.h"                  // Device header

extern uint16_t Num;

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, 0x05);
	
	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)
	{
		Num ++;
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

5.3 主程序实现

5.3.1 User\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);// CNT计数器的变化情况
	}
	
}

/*
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) ==  SET)
	{
		Num ++;
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}
*/

六、总结展望与源码分享

通过本文的学习,我们深入掌握了STM32定时器的两大核心功能:定时中断模式外部时钟模式。这两种模式各有特点,可以独立使用,也可以结合应用,为嵌入式系统开发提供强大的时间管理和外部信号处理能力。

6.1 核心收获

  1. 定时中断模式:适用于精确的时间控制、周期性任务调度、软件定时器实现
  2. 外部时钟模式:适用于脉冲信号计数、频率测量、编码器接口、外部事件触发
  3. 结合应用:通过两种模式的结合,可以实现复杂的实时控制系统,如电机控制、数据采集等

6.2 未来发展方向

  • 深入学习定时器的PWM输出功能,实现电机精确控制
  • 掌握输入捕获功能,实现高精度频率和占空比测量
  • 学习定时器的主从模式,实现多定时器协同工作
  • 结合DMA和定时器,实现高效的数据采集系统

6.3 实践建议

  1. 动手实验:按照本文代码,在开发板上实际运行并观察效果
  2. 项目应用:将定时器功能应用到实际项目中,如智能家居控制系统、工业监测设备等
  3. 性能优化:通过逻辑分析仪等工具,分析和优化定时器性能
  4. 深入学习:阅读STM32参考手册中定时器章节,理解底层硬件细节

6.4 源码分享

6-1 定时器定时中断https://gitee.com/Li-Cheng-Ze/stm32/tree/master/6-1%20%E5%AE%9A%E6%97%B6%E5%99%A8%E5%AE%9A%E6%97%B6%E4%B8%AD%E6%96%AD6-2 定时器外部时钟https://gitee.com/Li-Cheng-Ze/stm32/tree/master/6-2%20%E5%AE%9A%E6%97%B6%E5%99%A8%E5%A4%96%E9%83%A8%E6%97%B6%E9%92%9F定时器作为STM32最强大的外设之一,其应用场景远不止本文所述。希望本文能为你打开STM32定时器应用的大门,助你在嵌入式开发道路上更进一步!


作者:Auto_Dev_06
开发环境:Keil MDK-ARM + STM32F103C8T6
参考资料:江协科技STM32入门教程系列、STM32F10x参考手册
版权声明:本文为CSDN原创文章,转载请注明出处
发布时间:2026年4月12日

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐