20. 基本定时器

一、基本定时器简介

  STM32F407 有两个基本定时器 TIM6 和 TIM7,它们的功能完全相同,资源是完全独立的,可以同时使用。其主要特性如下:16 位自动重载递增计数器,16 位可编程预分频器,预分频系数 1 ~ 65536,用于对计数器时钟频率进行分频,还可以触发 DAC 的同步电路,以及生成中断/DMA 请求。

二、基本定时器框图

基本定时器框图

①、时钟源

  定时器的核心就是计算器,要实现计数功能,首先要给它一个时钟源。基本定时器时钟挂载在 APB1 总线,所以它的时钟来自于 APB1 总线,但是基本定时器时钟不是直接由 APB1 总线直接提供,而是先经过一个倍频器。当 APB1 的预分频器系数为 1 时,这个倍频器系数为 1,即定时器的时钟频率等于 APB1 总线时钟频率;当 APB1 的预分频器系数 ≥2 分频时,这个倍频器系数就为 2 , 即定时器的时钟频率等于 APB1 总线时钟频率的两倍。我们在 System_Clock_Init() 时钟设置函数已经设置 APB1 总线时钟频率为 42Mhz,APB1 总线的预分频器分频系数是 2,所以挂载在 APB1 总线的定时器时钟频率为 84Mhz。

②、控制器

  控制器除了控制定时器复位、使能、计数等功能之外,还可以用于触发 DAC 转换。

③、时基单元

  时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 。基本定时器的这三个寄存器都是 16 位有效数字,即可设置值范围是 0~65535。

  时基单元中的预分频器 PSC,它有一个输入和一个输出。输入 CK_PSC 来源于控制器部分,实际上就是来自于内部时钟(CK_INT),即 2 倍的 APB1 总线时钟频率(84MHz)。输出 CK_CNT是分频后的时钟,它是计数器实际的计数时钟,通过设置预分频器寄存器(TIMx_PSC)的值可以得到不同频率 CK_CNT,计算公式如下:

\[f_{CK} = \frac{f_{CK\_PSC}}{PSC[15:0] + 1} \]

  上式中,PSC[15:0] 是写入预分频器寄存器(TIMx_PSC)的值。

  预分频器寄存器(TIMx_PSC)可以在运行过程中修改它的数值,新的预分频数值将在下一个更新事件时起作用。因为更新事件发生时,会把 TIMx_PSC 寄存器值更新到其影子寄存器中,这才会起作用。

  影子寄存器是一个实际起作用的寄存器,不可直接访问。举个例子:我们可以把预分频系数写入预分频器寄存器(TIMx_PSC),但是预分频器寄存器只是起到缓存数据的作用,只有等到更新事件发生时,预分频器寄存器的值才会被自动写入其影子寄存器中,这时才真正起作用。

  自动重载寄存器及其影子寄存器的作用和上述同理。不同点在于自动重载寄存器是否具有缓冲作用还受到 ARPE 位的控制,当该位置 0 时,ARR 寄存器不进行缓冲,我们写入新的 ARR 值时,该值会马上被写入 ARR 影子寄存器中,从而直接生效;当该位置 1 时,ARR 寄存器进行缓冲,我们写入新的 ARR 值时,该值不会马上被写入 ARR 影子寄存器中,而是要等到更新事件发生才会被写入 ARR 影子寄存器,这时才生效。预分频器寄存器则没有这样相关的控制位,这就是它们不同点。

三、基本定时器溢出条件

  更新事件的产生 有两种情况,一是由 软件产生将 TIMx_EGR 寄存器的位 UG 置 1,产生更新事件后,硬件会自动将 UG 位清零。二是由 硬件产生,满足以下条件即可:计数器的值等于自动重装载寄存器影子寄存器的值

  基本定时器的计数器(CNT)是一个递增的计数器,当寄存器(TIMx_CR1)的 CEN 位置 1,即使能定时器,每来一个 CK_CNT 脉冲,TIMx_CNT 的值就会递增加 1。当 TIMx_CNT 值与 TIMx_ARR 的设定值相等时,TIMx_CNT 的值就会被自动清零并且会生成更新事件(如果开启相应的功能,就会产生 DMA 请求、产生中断信号或者触发 DAC 同步电路),然后下一个CK_CNT 脉冲到来,TIMx_CNT 的值就会递增加 1,如此循环。在此过程中,TIMx_CNT 等于TIMx_ARR 时,我们称之为定时器溢出,因为是递增计数,故而又称为 定时器上溢。定时器溢出就伴随着更新事件的发生。

  由上述可知,我们只要设置预分频寄存器和自动重载寄存器的值就可以控制定时器更新事件发生的时间。自动重载寄存器(TIMx_ARR)是用于存放一个与计数器作比较的值,当计数器的值等于自动重载寄存器的值时就会生成更新事件,硬件自动置位相关更新事件的标志位,如:更新中断标志位。

  定时器的溢出时间公式为:

\[T_{out} = \frac{(ARR + 1) * (PSC + 1)}{F_{t}} \]

  其中,\(T_{out}\) 是定时器溢出时间,\(F_{t}\) 是定时器的时钟源频率。\(ARR\) 是自动重装载寄存器的值。\(PSC\) 是预分频寄存器的值。

  比如我们需要一个 500ms 周期的定时器更新中断,一般思路是先设置预分频寄存器,然后才是自动重载寄存器。考虑到我们设置的 CK_INT 为 84MHz,我们把预分频系数设置为 8400,即写入预分频寄存器的值为 8399,那么 \(f_{CK_CNT} = \frac{84MHz}{8400} = 10KHz\)。这样就得到计数器的计数频率为 10KHz,即计数器 1 秒钟可以计 10000 个数。我们需要 500ms的中断周期,所以我们让计数器计数 5000 个数就能满足要求,即需要设置自动重载寄存器的值为 4999,另外还要把定时器更新中断使能位 UIE 置 1,CEN 位也要置 1。

四、基本定时器常用寄存器

4.1、TIM6和TIM7控制寄存器

TIM6和TIM7控制寄存器1

  该寄存器位 0(CEN)用于使能或者禁止计数器,该位置 1 计数器开始工作,置 0 则停止。还有位 7(APRE)用于控制自动重载寄存器 ARR 是否具有缓冲作用,如果 ARPE 位置 1,ARR 起缓冲作用,即只有在更新事件发生时才会把 ARR 的值写入其影子寄存器里;如果 ARPE 位置 0,那么修改自动重载寄存器的值时,该值会马上被写入其影子寄存器中,从而立即生效。

4.2、TIM6和TIM7 DMA/中断使能寄存器

TIM6和TIM7DMA中断使能寄存器

  该寄存器位 0(UIE)用于使能或者禁止更新中断。

4.3、TIM6和TIM7状态寄存器

TIM6和TIM7状态寄存器

  该寄存器位 0(UIF)是中断更新的标志位,当发生中断时由硬件置 1,然后就会执行中断服务函数,需要软件去清零,所以我们必须在中断服务函数里把该位清零。如果中断到来后,不把该位清零,那么系统就会一直进入中断服务函数。

4.4、TIM6和TIM7事件生成寄存器

TIM6和TIM7事件生成寄存器

4.5、TIM6和TIM7计数器

TIM6和TIM7计数器

  该寄存器位 [15:0] 就是计数器的实时的计数值。

4.6、TIM6和TIM7预分频器

TIM6和TIM7预分频器

  该寄存器是 TIM6/TIM7 的预分频寄存器,比如我们要 8400 分频,就往该寄存器写入 8399。注意这是 16 位的寄存器,写入的数值范围是 0 到 65535,分频系数范围:1 到 65536。

4.7、TIM6和TIM7自动重载寄存器

TIM6和TIM7自动重载寄存器

五、基本定时器中断配置步骤

5.1、使能定时器的时钟

  使能对应的定时器的时钟。

#define __HAL_RCC_TIM6_CLK_ENABLE()   do { \
                                      __IO uint32_t tmpreg = 0x00U; \
                                      SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM6EN);\
                                      /* Delay after an RCC peripheral clock enabling */ \
                                      tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM6EN);\
                                      UNUSED(tmpreg); \
                                      } while(0U)
#define __HAL_RCC_TIM7_CLK_ENABLE()   do { \
                                      __IO uint32_t tmpreg = 0x00U; \
                                      SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM7EN);\
                                      /* Delay after an RCC peripheral clock enabling */ \
                                      tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM7EN);\
                                      UNUSED(tmpreg); \
                                      } while(0U)

5.2、配置定时器工作参数

  HAL 库提供基本定时器初始化函数,它的说明如下:

HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);

  其中,htimTIM_HandleTypeDef 结构体类型指针变量(亦称定时器句柄),结构体定义如下:

typedef struct
{
    TIM_TypeDef *Instance;                              // 寄存器基地址
    TIM_Base_InitTypeDef Init;                          // 定时器初始化结构体
    HAL_TIM_ActiveChannel Channel;                      // 定时器通道
    DMA_HandleTypeDef *hdma[7];                         // DMA管理结构体

    HAL_LockTypeDef Lock;                               // 锁对象
    __IO HAL_TIM_StateTypeDef State;                    // 定时器状态
    __IO HAL_TIM_ChannelStateTypeDef ChannelState[4];   // 定时器通道状态
    __IO HAL_TIM_ChannelStateTypeDef ChannelNState[4];  // 定时器互补通道状态
    __IO HAL_TIM_DMABurstStateTypeDef DMABurstState;    // DMA溢出状态
} TIM_HandleTypeDef;

  Instance指向定时器寄存器基地址,可选值如下:

#define TIM6                ((TIM_TypeDef *) TIM6_BASE)
#define TIM7                ((TIM_TypeDef *) TIM7_BASE)

  Init定时器初始化结构体,用于配置定时器的相关参数,它的定义如下:

typedef struct
{
    uint32_t Prescaler;             // 预分频系数
    uint32_t CounterMode;           // 计数模式
    uint32_t Period;                // 自动重装载值
    uint32_t ClockDivision;         // 时钟分频因子
    uint32_t RepetitionCounter;     // 重复计数器值
    uint32_t AutoReloadPreload;     // 自动重装载值预载入功能
} TIM_Base_InitTypeDef;

  Prescaler预分频系数,即写入预分频寄存器的值,范围 0 到 65535。

  CounterMode计数器计数模式,这里基本定时器只能向上计数。

#define TIM_COUNTERMODE_UP                 0x00000000U                          /*!< Counter used as up-counter   */

  Period自动重载值,即写入自动重载寄存器的值,范围 0 到 65535。

  AutoReloadPreload自动重载预装载使能,即控制寄存器 1(TIMx_CR1)的 ARPE 位。

#define TIM_AUTORELOAD_PRELOAD_DISABLE                0x00000000U               /*!< TIMx_ARR register is not buffered */
#define TIM_AUTORELOAD_PRELOAD_ENABLE                 TIM_CR1_ARPE              /*!< TIMx_ARR register is buffered */

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

typedef enum 
{
    HAL_OK = 0x00U,             // 成功
    HAL_ERROR = 0x01U,          // 错误
    HAL_BUSY = 0x02U,           // 忙碌
    HAL_TIMEOUT = 0x03U         // 超时
} HAL_StatusTypeDef;

5.3、使能中断

5.3.1、设置中断优先级分组

  HAL_NVIC_SetPriorityGrouping() 函数是设置中断优先级分组函数。其声明如下:

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

  其中,参数 PriorityGroup中断优先级分组号,可以选择范围如下:

#define NVIC_PRIORITYGROUP_0         0x00000007U /*!< 0 bits for pre-emption priority
                                                      4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1         0x00000006U /*!< 1 bits for pre-emption priority
                                                      3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2         0x00000005U /*!< 2 bits for pre-emption priority
                                                      2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3         0x00000004U /*!< 3 bits for pre-emption priority
                                                      1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4         0x00000003U /*!< 4 bits for pre-emption priority
                                                      0 bits for subpriority */

  这个函数在一个工程里基本只调用一次,而且是在程序 HAL 库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。如果调用了多次,则以最后一次为准。

5.3.2、设置中断优先级

  HAL_NVIC_SetPriority() 函数是设置中断优先级函数。其声明如下:

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);

  其中,参数 IRQn中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。

typedef enum
{
  TIM6_DAC_IRQn               = 54,     /*!< TIM6 global and DAC1&2 underrun error  interrupts                 */
  TIM7_IRQn                   = 55,     /*!< TIM7 global interrupt                                             */
} IRQn_Type;

  参数 PreemptPriority抢占优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。

  参数 SubPriority响应优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。

5.3.3、使能中断

  HAL_NVIC_EnableIRQ() 函数是中断使能函数。其声明如下:

void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);

  其中,参数 IRQn中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。

5.4、使能更新中断并启动计数器

  HAL_TIM_Base_Start_IT() 函数是更新定时器中断和使能定时器的函数。其声明如下:

HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);

  该函数调用了 __HAL_TIM_ENABLE_IT 和 __HAL_TIM_ENABLE 两个函数宏定义,分别是 更新定时器中断使能定时器 的宏定义。

  形参 htimTIM_HandleTypeDef 结构体类型指针变量,即定时器句柄。

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

  当我们启动定时器时,会立即进入更新事件。这是因为我们刚启动定时器时,它的预分频器寄存器的预分频值默认为 0,而新的预分频数值将在下一个更新事件时起作用,即更新事件发生时,会把 TIMx_PSC 寄存器值更新到其影子寄存器中,这才会起作用。

  此时,我们可以手动产生一个更新事件,但是,手动产生更新事件后,状态寄存器 SR 的 UIF 位也会置 1。如果此时,使能了更新中断,还是会进入中断。这需要我们在使能更新中断之前手动清除 UIF 位。

TIM6->EGR |= TIM_EGR_UG;                                // 预分频寄存器和重装载寄存器的值更新到影子寄存器
TIM6->SR &= ~TIM_SR_UIF;                                // 手动清除状态寄存器SR的UIF位

  HAL 库中 HAL_TIM_Base_Init() 函数的底层会产生一个更新事件,把预分频寄存器和重装载寄存器的值更新到影子寄存器。此时,我们需要在使能更新中断之前手动清除中断标志位。

__HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);                        // 清除更新中断标志位

5.5、编写中断服务函数

  定时器中断服务函数为:TIMx_IRQHandler() 等,当发生中断的时候,程序就会执行中断服务函数。

void TIM6_DAC_IRQHandler(void);
void TIM7_IRQHandler(void);

  HAL 库提供了一个定时器中断公共处理函数 HAL_TIM_IRQHandler(),该函数又会调用 HAL_TIM_PeriodElapsedCallback() 等一些回调函数,需要用户根据中断类型选择重定义对应的中断回调函数来处理中断程序。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);     // 更新中断回调函数

六、程序源码

  定时器初始化函数内容如下:

TIM_HandleTypeDef g_tim6_handle;

/**
 * @brief 定时器定时功能初始化函数
 * 
 * @param htim 定时器句柄
 * @param TIMx 定时器寄存器基地址,可选值: TIMx, x可选范围: 1 ~ 14
 * @param prescaler 预分频系数,可选值: 0 ~ 65535
 * @param period 自动重装载值,可选值: 0 ~ 65535
 * 
 * @note 默认为向上计数模式
 */
void TIM_Base_Init(TIM_HandleTypeDef *htim, TIM_TypeDef *TIMx, uint16_t prescaler, uint16_t period)
{
    htim->Instance = TIMx;                                                      // 定时器寄存器基地址
    htim->Init.CounterMode = TIM_COUNTERMODE_UP;                                // 计数模式
    htim->Init.Prescaler = prescaler;                                           // 预分频系数
    htim->Init.Period = period;                                                 // 自动重装载值
    HAL_TIM_Base_Init(htim);
}

  基本定时器底层初始化函数内容如下:

/**
 * @brief 基本定时器底层初始化函数
 * 
 * @param htim 定时器句柄
 */
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        __HAL_RCC_TIM6_CLK_ENABLE();                                            // 使能定时器6的时钟

        HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);                                      // 使能定时器6中断
        HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 4, 0);                              // 设置中断优先级
    }
}

  定时器 6 中断服务函数内容如下:

/**
 * @brief 定时器6中断服务函数
 * 
 */
void TIM6_DAC_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&g_tim6_handle);                                         // 调用HAL库公共处理函数
}

  定时器更新中断回调函数内容如下:

/**
 * @brief 定时器更新中断回调函数
 * 
 * @param htim 定时器句柄
 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
    }
}

  main() 函数内容如下:

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
  
    TIM_Base_Init(&g_tim6_handle, TIM6, 8399, 9999);

    LED_Init();

    __HAL_TIM_CLEAR_IT(&g_tim6_handle, TIM_IT_UPDATE);                          // 清除更新中断标志位
    HAL_TIM_Base_Start_IT(&g_tim6_handle);                                      // 使能更新中断,并启动计数器

    while (1)
    {

    }
  
    return 0;
}
posted @ 2023-11-28 19:16  星光樱梦  阅读(76)  评论(0编辑  收藏  举报