第5章
定时器与蜂鸣器

CHAPTER5 

5.2F03定时器概述
1 
STM31

MCU 中的定时器实质上是一个计数器,可以对内部脉冲或者外部输入进行计数。相
比传统51 单片机,STM32F103 的定时器要完善和复杂很多,它们是专为工业控制应用量
身定做的,具有延时、频率测量、PWM 输出、电机控制及编码接口等诸多功能。

STM32F103 内部集成了8个可编程定时器,分为基本定时器、通用定时器和高级定时
器三种类型,它们都具有16 位定时器分辨率和16 位可编程的预分频系数,都可以产生
DMA 请求,如表5-1所示。

表5-
1 
STM32F103 
定时器分类

主要特点基本定时器通用定时器高级定时器
定时器TIM6 、TIM7 TIM2 、TIM3 、TIM4 、TIM5 TIM1 、TIM8 
内部时钟CK_INT 的来源APB1 分频器APB1 分频器APB2 分频器
计数类型向上向上、向下、向上/向下向上、向下、向上/向下
比较/捕捉通道0 4 4 
互补输出没有没有有
定时器分辨率16 位
预分频系数16 位(1~65536) 
产生DMA 请求可以

基本定时器TIM6/7只能向上计数,没有外部I/O功能。通用定时器TIM2/3/4/5可
以向上、向下、向上/向下计数,有四个外部I/O,具有输入捕捉、输出比较功能。高级定时器
TIM1/8除具有定时、输入捕捉、输出比较功能外,还具有三相电机互补输出。本章主要以
基本定时器TIM6 为例来学习STM32F103 定时器。

5.基本定时器原理
2 

STM32F103 的两个基本定时器TIM6/7的核心是由可编程预分频器驱动的16 位自动


73 

重装载计数器。在更新事件(计数器溢出)发生时,基本定时器TIM6/7会产生中断/DMA 
请求。此外,它们还具有触发DAC 的同步电路,如图5-1所示。时基单元是基本定时器最
重要的组成部分,由自动重装载寄存器(TIMx_ARR )、预分频寄存器(TIMx_PSC)和计数
器寄存器(TIMx_CNT)组成,在运行时可以通过软件读写这三个寄存器。基本定时器只有
一个内部时钟(CK_INT),TIMxCLK 来源于APB1 预分频器的输出,默认情况下时钟频率
为72MHz 。


图5-1 基本定时器的功能框图

1. 
自动重装载寄存器(TIMx_ARR) 
如图5-2所示,TIMx_ARR 在物理上实际对应了两个寄存器:一个是用户可以读写的
寄存器,称为预装载寄存器;另一个是用户看不见但真正起
作用的寄存器,称为影子寄存器,影子寄存器就是预装载寄
存器的一份复制。自动重装载寄存器是预加载的,每次读写
它时,实际上是通过读写预装载寄存器实现的。

将基本定时器的控制寄存器(TIMx_CR1)的自动重装
载预装载允许位(ARPE)使能,写入预装载寄存器的内容在
下一次更新事件发生后才传送到影子寄存器,否则写入自动
重装载寄存器的值会被立即更新到影子寄存器。通过软件
可以使能或禁止定时器更新事件的产生。

为什么要设计预装载寄存器和影子寄存器呢? 因为软
件不能在一个相同的时刻同时更新多个寄存器,也就不能同步多个通道的时序,若再有其他
因素(如中断)影响,多个通道的时序关系就不可预知了。设计两个寄存器则可以在同一个
时刻(发生更新事件时), 将预装载寄存器的内容更新到所对应的真正起作用的影子寄存器
中,这样就能够准确地同步多个通道的操作了。

2. 
预分频器
预分频寄存器带有缓冲器,定时器运行时就能改变它的值,新值会在下一次更新事件到
来时生效。预分频器可以以系数为1~65536 的任意整数值对预分频器的输入时钟进行分
频,得到新的计数器时钟CK_CNT,计数器时钟频率的计算公式如下: 

图5-2 自动重装载寄存器
组成示意图

计数器时钟fCK_CNT=计数器预分频时钟fCK_PSC/(PSC[15:0]+1) 

如图5-3所示,预分频值PSC[15:0]=0时,预分频系数为1,预分频时钟与计数器时钟

相同。当预分频寄存器(TIMx_PSC)写入新的预分频值PSC[15:0]=3时,预分频系数并

没有立即更改,而是在更新事件到来时才从1变为4。


图5-3 预分频系数从1变为4的计数器时序图

3. 
计数器
TIMxCNT 是一个带有自动重装载的16 位累加计数器,存储了当前定时器的计数值, 
它的计数时(_) 钟是通过预分频器(PSC)得到,由PSC 输出的时钟CK_CNT 驱动。计数器
(CNT)只能往上计数,最大计数值为65535 。计数器从0累加计数到自动重装载数值后,将
产生一个计数器溢出事件并重新从0开始计数,每次计数器溢出时可以产生更新事件。

如图5-4所示,当预分频系数为4且寄存器TIMxARR 的值为0x36 时,每4个预分频
时钟计数器向上计数1次,当计数值达到0x0036 时发生(_) 计数溢出,计数值清零并产生更新
事件,同时设置更新中断标志位。


图5-4 预分频系数为4时计数器时序图
寄存器TIMx_CR1 中的计数器使能位(CEN)用于使能或关闭计数器计数。


75 
4.定时器的定时时间计算
在时钟CK_CNT的驱动下,计数器计一个数的时间是1/CK_CNT=1/(TCLK/(PSC+ 
1)),产生一次中断的时间为(1/CK_CNT)(ARR+1),因此定时器的定时时间为
(1/CK_CNT)(ARR+1)=((PSC+1)(ARR+1))/TCLK 
其中,PSC为预分频器的值(为0~65535),ARR 为自动重装载寄存器的值(为0~65535), 
TCLK为输入时钟频率。
5.3 定时器的HAL库用法
5.3.1 定时器寄存器结构体TIM_TypeDef 
结构体数据类型TIM_TypeDef将与定时器相关的寄存器进行了封装,该结构体与定
时器的宏都是在文件stm32f103xe.h中定义的,代码如下: 
typedef struct 
{ 
__IO uint32_t CR1; /*TIM 控制寄存器1*/ 
__IO uint32_t CR2; /*TIM 控制寄存器2*/ 
__IO uint32_t SMCR; /*TIM 从机模式控制寄存器*/ 
__IO uint32_t DIER; /*TIM DMA/中断使能寄存器*/ 
__IO uint32_t SR; /*TIM 状态寄存器*/ 
__IO uint32_t EGR; /*TIM 事件生成寄存器*/ 
__IO uint32_t CCMR1; /*TIM 捕获/比较模式寄存器1*/ 
__IO uint32_t CCMR2; /*TIM 捕获/比较模式寄存器2*/ 
__IO uint32_t CCER; /*TIM 捕获/比较使能寄存器*/ 
__IO uint32_t CNT; /*TIM 计数器寄存器*/ 
__IO uint32_t PSC; /*TIM 预分频器寄存器*/ 
__IO uint32_t ARR; /*TIM 自动重加载寄存器*/ 
__IO uint32_t RCR; /*TIM 重复计数器寄存器*/ 
__IO uint32_t CCR1; /*TIM 捕获/比较寄存器1*/ 
__IO uint32_t CCR2; /*TIM 捕获/比较寄存器2*/ 
__IO uint32_t CCR3; /*TIM 捕获/比较寄存器3*/ 
__IO uint32_t CCR4; /*TIM 捕获/比较寄存器4*/ 
__IO uint32_t BDTR; /*TIM 中断和停滞时间寄存器*/ 
__IO uint32_t DCR; /*TIM DMA 控制寄存器*/ 
__IO uint32_t DMAR; /*TIM DMA 完整传输寄存器地址*/ 
__IO uint32_t OR; /*TIM 选项寄存器*/ 
} TIM_TypeDef; 
#define APB1PERIPH_BASE PERIPH_BASE /*总线基地址*/ 
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000UL) 
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000UL) 
#define TIM1_BASE (APB2PERIPH_BASE + 0x00002C00UL) 
#define TIM2_BASE (APB1PERIPH_BASE + 0x00000000UL) 
#define TIM3_BASE (APB1PERIPH_BASE + 0x00000400UL) 
#define TIM4_BASE (APB1PERIPH_BASE + 0x00000800UL)

76 
#define TIM5_BASE (APB1PERIPH_BASE + 0x00000C00UL) 
#define TIM6_BASE (APB1PERIPH_BASE + 0x00001000UL) 
#define TIM7_BASE (APB1PERIPH_BASE + 0x00001400UL) 
#define TIM8_BASE (APB2PERIPH_BASE + 0x00003400UL) 
#define TIM1 ((TIM_TypeDef *)TIM1_BASE) 
#define TIM2 ((TIM_TypeDef *)TIM2_BASE) 
#define TIM3 ((TIM_TypeDef *)TIM3_BASE) 
#define TIM4 ((TIM_TypeDef *)TIM4_BASE) 
#define TIM5 ((TIM_TypeDef *)TIM5_BASE) 
#define TIM6 ((TIM_TypeDef *)TIM6_BASE) 
#define TIM7 ((TIM_TypeDef *)TIM7_BASE) 
#define TIM8 ((TIM_TypeDef *)TIM8_BASE) 
下面以TIM6为例说明这些宏定义的作用,对TIM6结构体变量的操作就等于对TIM6寄
存器的操作,如访问TIM6的CR1寄存器可以这样操作:TIM6->CR1&= ~(1<<0)(实现
关闭TIM6的计数功能)。
从定时器基地址的宏定义可以看出TIM1和TIM8挂接在APB2总线上,其他定时器
则挂接在APB1总线上。
5.3.2 定时器句柄结构体TIM_HandleTypeDef 
HAL库在TIM_TypeDef的基础上封装了一个结构体数据类型TIM_HandleTypeDef,该
结构体也可以称为定时器的句柄(handle),定义如下: 
typedef struct 
{ 
TIM_TypeDef *Instance; / * 寄存器基地址*/ 
TIM_Base_InitTypeDef Init; /* TIM 时基初始化所需参数*/ 
HAL_TIM_ActiveChannel Channel; / * 活动通道*/ 
DMA_HandleTypeDef *hdma[7]; / * DMA 处理器数组*/ 
HAL_LockTypeDef Lock; / * 锁定对象*/ 
__IO HAL_TIM_StateTypeDef State; /* T IM 的运行状态*/ 
#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1) 
… /*回调函数,略*/ 
#endif /*USE_HAL_TIM_REGISTER_CALLBACKS*/ 
} TIM_HandleTypeDef; 
这里重点介绍一下该结构体的前四个成员,其他参数主要是HAL库内部使用的。
(1)成员Instance是定时器寄存器的实例化指针,方便操作寄存器,比如使能计数器可
以这样操作:SET_BIT(htim6->Instance->CR1,TIM_CR1_CEN)。
(2)成员Init是用户接触最多的,用于配置定时器的基本参数,它的结构体数据类型定
义如下: 
typedef struct 
{

77 
uint32_t Prescaler; /*设置定时器预分频器值,范围是0x0000 到0xFFFF*/ 
uint32_t CounterMode; /*计数模式:向上计数模式、向下计数模式和中心对齐模式*/ 
uint32_t Period; /*设置定时器周期,范围是0x0000 到0xFFFF*/ 
/*设置定时器时钟频率CK_INT 与数字滤波器所使用的采样时钟之间的预分频系数*/ 
uint32_t ClockDivision; 
uint32_t RepetitionCounter; /*设置重复计数器寄存器的值,用在高级定时器中*/ 
/*设置自动重装载寄存器是更新事件产生时写入有效,还是立即写入有效*/ 
uint32_t AutoReloadPreload; 
} TIM_Base_InitTypeDef; 
(3)成员Channel用来设置活跃通道,取值范围为HAL_TIM_ACTIVE_CHANNEL_ 
1~HAL_TIM_ACTIVE_CHANNEL_4。
(4)成员hdma用来关联DMA,用于定时器的DMA 功能。
5.3.3 TIM 相关HAL库函数
TIM 的操作函数及宏定义在文件stm32f1xx_hal_tim.c和stm32f1xx_hal_tim.h中。
1.函数HAL_TIM_Base_Init 
函数HAL_TIM_Base_Init说明如表5-2所示。
表5-2 函数HAL_TIM_Base_Init说明
函数原型HAL_StatusTypeDefHAL_TIM_Base_Init(TIM_HandleTypeDef*htim) 
功能描述根据htim 中设定的参数初始化TIM 的时基单元,并初始化关联的句柄
输入参数htim:时基句柄指针
返回值 HAL状态
应用示例
TIM_HandleTypeDef htim6; /*定时器句柄*/ 
htim6.Instance= TIM6; / * 通 用 定 时器6*/ 
htim6.Init.Prescaler=72-1; / * 预 分 频 系 数 */ 
htim6.Init.CounterMode= TIM_COUNTERMODE_UP; /*向上计数*/ 
htim6.Init.Period=1000-1; / * 自 动 重 装 载 值*/ 
/*自动重装载寄存器是更新事件产生时写入有效*/ 
htim6.Init.AutoReloadPreload= TIM_AUTORELOAD_PRELOAD_ENABLE; 
HAL_TIM_Base_Init(&htim6); 
2.函数HAL_TIM_Base_MspInit 
函数HAL_TIM_Base_MspInit说明如表5-3所示。
表5-3 函数HAL_TIM_Base_MspInit说明
函数原型voidHAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) 
功能描述定时器时基Msp初始化,配置与MCU 有关的时钟使能以及中断优先级
输入参数htim:TIM 句柄
应用示例HAL_TIM_Base_MspInit(&htim6);

3.函数HAL_TIM_Base_Start_IT 
函数HAL_TIM_Base_Start_IT说明如表5-4所示。

表5-
4 
函数HAL_TIM_Base_Start_IT说明

函数原型

功能描述

输入参数

返回值

应用示例

HAL_StatusTypeDefHAL_TIM_Base_Start_IT(TIM_HandleTypeDef*htim) 
使能定时器更新中断(TIM_IT_UPDATE)和使能定时器
htim:TIM句柄

HAL状态

HAL_TIM_Base_Start_IT(&htim6); 

4.函数HAL_TIMEx_MasterConfigSynchronization 
函数HAL_TIMEx_MasterConfigSynchronization说明如表5-5所示。

表5-
5 
函数HAL_TIMEx_MasterConfigSynchronization说明

函数原型

功能描述

输入参数

返回值

应用示例

5.
4 

HAL_StatusTypeDefHAL_TIMEx_MasterConfigSynchronization(TIM_HandleTypeDef*
htim,TIM_MasterConfigTypeDef*sMasterConfig)
配置TIM在主模式下工
作


htim:TIM句柄

sMatCfig:指向TIM_MasterConfigTypeDef结构的指针,包含所选触发器输出TRGO 和主/(s) 从(e) 模(r) 式(n) (o) 

HAL状态

TIM_MasterConfigTypeDefsMasterConfig={0}; 

/*TIMx_EGR寄存器中的UG位用作触发输出*/
sMatCfig.MatOutputTrigger=TIM_TRGO_RESET;/*禁(s) 止(e) 主(r) /(o) 从(n) 模式*/(r) (e) (s) sMasterConfig.MasterSlaveMode=TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim6,&sMasterConfig); 

基本定时器应用示例

本示例使用定时器中断方式实现LED1每1000ms闪烁一次,以延时的方式实现LED8 
每1000ms闪烁一次。

5.1 
STM3ueMX工程配置
4.2Cb

定时器的时钟TCLK(即内部时钟CK_INT)是HCLK经过APB1预分频器后提供的,如
果APB1预分频系数为1,则频率不变,否则频率乘以2。如图5-5所示,APB1的预分频系
数是/2,所以定时器时钟TCLK=36MHz×2=72MHz 。

如图5-6所示,在Timers中选择基本定时器TIM6,勾选激活(Activated)复选框,在
ParameterSetings中配置TIM6的参数。时钟预分频系数(Prescaler)设置为72-1,计数


79 
图5-5 TIM6时钟配置
模式(CounterMode)为向上计数(Up),自动重装载寄存器(ARR)设置为1000-1,根据定
时器的溢出时间公式,定时周期Tout=((71+1)×(999+1))/72MHz=1ms。使能自动重
装载,禁止触发输出。
图5-6 TIM6参数配置
如图5-7所示,配置NVIC,使能TIM6全局中断,抢占优先级(PreemptionPriority)和
响应优先级(SubPriority)均采用默认值0。
图5-7 TIM6中断配置
5.4.2 定时器配置与中断服务函数
1.定时器配置函数 
函数MX_TIM6_Init初始化TIM6的基本参数,配置定时时间为1ms,代码如下: 
TIM_HandleTypeDef htim6; 
static void MX_TIM6_Init(void) 
{ 
TIM_MasterConfigTypeDef sMasterConfig = {0}; 
htim6.Instance = TIM6; /*通用定时器6*/

80 
htim6.Init.Prescaler = 72 - 1; /*预分频系数*/ 
htim6.Init.CounterMode = TIM_COUNTERMODE_UP; /*向上计数器*/ 
htim6.Init.Period = 1000 - 1; /*定时器计数周期值*/ 
/*使能自动重装载*/ 
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; 
if (HAL_TIM_Base_Init(&htim6) != HAL_OK) 
{ 
Error_Handler(); 
} 
/*主/从模式配置*/ 
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; 
/*禁止主/从模式*/ 
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; 
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, 
&sMasterConfig) != HAL_OK) 
{ 
Error_Handler(); 
} 
}
文件stm32f1xx_hal_msp.c中,函数HAL_TIM_Base_MspInit实现了TIM6的时钟使
能、中断优先级和中断使能配置,代码如下: 
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim_base) 
{ 
if (htim_base->Instance == TIM6) 
{ 
__HAL_RCC_TIM6_CLK_ENABLE(); /*使能TIM6 时钟*/ 
HAL_NVIC_SetPriority(TIM6_IRQn, 0, 0); /*设置TIM6 中断优先级*/ 
HAL_NVIC_EnableIRQ(TIM6_IRQn); /*TIM6 中断使能*/ 
} 
}
函数HAL_TIM_Base_MspInit名称中的Msp是MCUspecificpackage(MCU 的具体
方案)的缩写,理解Msp函数的运行逻辑是搞懂HAL库的关键,下面来分析一下该函数的
运行逻辑。
HAL库初始化TIM6的流程:MX_TIM6_Init()→HAL_TIM_Base_Init()→HAL_ 
TIM_Base_MspInit()。库文件stm32f1xx_hal_tim.c中默认的HAL_TIM_Base_MspInit 
是一个弱函数,没有任何实际的控制逻辑,因此函数HAL_TIM_Base_Init优先调用在文件
stm32f1xx_hal_msp.c中重定义的函数HAL_TIM_Base_MspInit。MX_TIM6_Init初始化
与MCU 无关的TIM6配置,HAL_TIM_Base_MspInit初始化与MCU 相关的TIM6配置, 
这样做的好处是,当移植代码到其他MCU 平台时,只需要修改函数HAL_TIM_Base_ 
MspInit中的内容,而不需要修改函数MX_TIM6_Init中的内容。可见,相对于标准库,因
为有Msp函数,HAL库具备非常强的代码移植性。

81 
与之对应,复位函数HAL_TIM_Base_MspDeIni实现了与函数HAL_TIM_Base_ 
MspInit相反的操作,用于重置TIM,它在函数HAL_TIM_Base_DeInit中被调用,也是个
弱函数。
2.TIM 中断服务函数
在文件stm32f1xx_it.c中生成了TIM6的中断服务函数TIM6_IRQHandler,它调用了
定时器公用中断处理函数HAL_TIM_IRQHandler,代码如下: 
void TIM6_IRQHandler(void) 
{ 
HAL_TIM_IRQHandler(&htim6); 
}
HAL库函数HAL_TIM_IRQHandler封装了定时器的中断处理过程:通过判断中断
标志位确定中断来源(更新中断)后,会自动清除该中断标志位,然后调用定时器更新中断回
调函数HAL_TIM_PeriodElapsedCallback,HAL库在文件stm32f1xx_hal_tim.c中定义了
该弱回调函数,代码如下: 
__weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 
{ 
UNUSED(htim); /*防报错的定义*/ 
/*这个函数不应该被改变,如果需要使用回调函数,请重新在用户文件中实现该函数*/ 
}
为了实现用户功能,需要重定义该回调函数,这样做的好处是不需要去理会中断服务函
数怎么实现,只需要重写这个回调函数并添加用户功能代码即可。更便利的是,当同时有多
个定时器中断时,HAL库自动将它们的服务函数规整到一起并调用同一个回调函数,也就
是无论几个中断,只需要重写一个回调函数并判断传入的定时器编号即可。
5.4.3 用户代码
在文件main.c中重定义回调函数HAL_TIM_PeriodElapsedCallback,实现每1s将LED1 
的状态翻转一次,定时时间为1ms,定时器中断1000次的时间才是1s,具体的代码如下: 
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 
{ 
static uint32_t time = 0; 
if (htim->Instance == TIM6) /*定时器6*/ 
{ 
time++; /*每1ms 累加1 次*/ 
if (time == 1000) /*1s 时间到*/ 
{ 
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); /*翻转LED1*/ 
time = 0; 
} 
} 
}