第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; } } }