第10章 STM32F4通用定时器简介

第十章 STM32F4通用定时器简介

1. 通用定时器简介

STM32F4 的通用定时器包含一个 16 位或 32 位自动重载计数器(CNT),该计数器由可编程预分频器(PSC) 驱动。 STM32F4 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)等。 使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。 STM32F4 的每个通用定时器都是完全独立的,没有互相共享的任何资源。

STM32 的通用 TIMx (TIM2~TIM5 和 TIM9~TIM14)定时器功能包括:

  1. 16 位/32 位(仅 TIM2 和 TIM5)向上、向下、向上/向下自动装载计数器(TIMx_CNT),注意: TIM9~TIM14 只支持向上(递增)计数方式。

  2. 16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~ 65535 之间的任意数值。

  3. 4 个独立通道(TIMx_CH1~4, TIM9~TIM14 最多 2 个通道),这些通道可以用来作为:

  • 输入捕获

  • 输出比较

  • PWM生成(边缘或中间对齐模式),注意:TIM9-TIM14不支持中间对齐模式

  • 单脉冲模式输出

  1. 可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外一个定时器)的同步电路。

  2. 如下事件发生时产生中断/DMA(TIM9~TIM14 不支持 DMA):

  • 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)

  • 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)

  • 输入捕获

  • 输出比较

  • 支持针对定位的增量(正交)编码器和霍尔传感器电路(TIM9~TIM14 不支持)

  • 触发输入作为外部时钟或者按周期的电流管理(TIM9~TIM14 不支持)

2. 定时器常用寄存器

2.1 控制寄存器TIMx_CR1

屏幕截图 2024 09 14 090918

  1. CEN (Counter Enable)
  • 位位置:0
  • 功能:启用定时器计数器。当该位设置为1时,定时器开始计数。
  1. UDIS (Update Disable)
  • 位位置:1
  • 功能:禁止更新事件产生。当该位为1时,计数器更新事件被禁用。
  1. URS (Update Request Source)
  • 位位置:2
  • 功能:更新请求源配置。当该位为1时,仅当计数器达到最大值时才会产生更新事件。
  1. OPM (One Pulse Mode)
  • 位位置:3
  • 功能:单脉冲模式。当该位为1时,定时器在接收到触发事件后只会计数一次,然后自动停止。
  1. DIR (Direction)
  • 位位置:4
  • 功能:计数方向选择。0表示向上计数,1表示向下计数。
  1. CMS (Center-Aligned Mode Selection)
  • 位位置:5-6
  • 功能:选择中心对齐模式。当设置为特定值时,可以实现不同的对齐模式。
  1. ARPE (Auto-Reload Preload Enable)
  • 位位置:7
  • 功能:自动重载预装使能。当该位为1时,计数器的自动重载值在更新事件发生时会被预加载。
  1. CKD (Clock Division)
  • 位位置:8-9
  • 功能:时钟分频选择,影响计数器的计数速度。

在使用TIMx_CR1寄存器时,通常需要先配置其它相关寄存器(如预分频器和自动重载值),然后再配置CR1寄存器以启动定时器。以下是一个简单的代码示例:

TIM_HandleTypeDef htimx;

void MX_TIMx_Init(void) {
    // 初始化定时器
    htimx.Instance = TIMx;
    htimx.Init.Prescaler = 8399; // 预分频
    htimx.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
    htimx.Init.Period = 9999; // 自动重载值
    htimx.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_Base_Init(&htimx);
    // 配置控制寄存器
    TIMx->CR1 |= TIM_CR1_CEN; // 启动计数器
}

2.2 DMA/中断使能寄存器(TIMx_DIER)

屏幕截图 2024 09 14 091258

  1. UIE (Update Interrupt Enable)
  • 位位置:0
  • 功能:使能计数器更新中断。当计数器溢出或达到自动重载值时,会产生更新中断。
  1. CC1IE (Capture/Compare 1 Interrupt Enable)
  • 位位置:1
  • 功能:使能通道1的捕获/比较中断。当通道1触发捕获或比较事件时,会产生中断。
  1. CC2IE (Capture/Compare 2 Interrupt Enable)
  • 位位置:2
  • 功能:使能通道2的捕获/比较中断。
  1. CC3IE (Capture/Compare 3 Interrupt Enable)
  • 位位置:3
  • 功能:使能通道3的捕获/比较中断。
  1. CC4IE (Capture/Compare 4 Interrupt Enable)
  • 位位置:4
  • 功能:使能通道4的捕获/比较中断。
  1. TIE (Trigger Interrupt Enable)
  • 位位置:6
  • 功能:使能触发中断。当定时器接收到触发信号时,会产生中断。
  1. COMIE (COM Interrupt Enable)
  • 位位置:7
  • 功能:使能通用中断。当比较模式产生中断时会触发。
  1. UDE (Update DMA Request Enable)
  • 位位置:8
  • 功能:使能更新事件的DMA请求。
  1. CC1DE (Capture/Compare 1 DMA Request Enable)
  • 位位置:9
  • 功能:使能通道1的DMA请求。
  1. CC2DE (Capture/Compare 2 DMA Request Enable)
  • 位位置:10
  • 功能:使能通道2的DMA请求。
  1. CC3DE (Capture/Compare 3 DMA Request Enable)
  • 位位置:11
  • 功能:使能通道3的DMA请求。
  1. CC4DE (Capture/Compare 4 DMA Request Enable)
  • 位位置:12
  • 功能:使能通道4的DMA请求。

在配置TIMx_DIER寄存器时,通常需要根据应用需求选择需要使能的中断和DMA请求。以下是一个简单的代码示例,展示如何使能更新中断和通道1的捕获中断:

void MX_TIMx_Init(void) {
    // 初始化定时器
    htimx.Instance = TIMx;
    htimx.Init.Prescaler = 8399; // 预分频
    htimx.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
    htimx.Init.Period = 9999; // 自动重载值
    HAL_TIM_Base_Init(&htimx);

    // 配置中断使能寄存器
    TIMx->DIER |= (TIM_DIER_UIE | TIM_DIER_CC1IE); // 使能更新中断和通道1中断

    // 启动定时器
    HAL_TIM_Base_Start(&htimx);
}

2.3 预分频寄存器(TIMx_PSC)

该寄存器用设置对时钟进行分频,然后提供给计数器,作为计数器的时钟。

屏幕截图 2024 09 14 091340

这里,定时器的时钟来源有四个:

  • 内部时钟(CK_INT)

  • 外部时钟模式 1:外部输入脚(TIx)

  • 外部时钟模式 2:外部触发输入(ETR),仅适用于 TIM2、 TIM3、 TIM4

  • 内部触发输入(ITRx):使用 A 定时器作为 B 定时器的预分频器(A 为 B 提供时钟)。

这些时钟,具体选择哪个可以通过 TIMx_SMCR 寄存器的相关位来设置。这里的 CK_INT时钟是从 APB1 倍频的来的,除非 APB1 的时钟分频数设置为 1(一般都不会是 1),否则通用定时器 TIMx 的时钟是 APB1 时钟的 2 倍,当 APB1 的时钟不分频的时候,通用定时器 TIMx的时钟就等于 APB1 的时钟。这里还要注意的就是高级定时器以及 TIM9~TIM11 的时钟不是来自 APB1,而是来自 APB2 的。

使用示例

假设我们希望使用 84 MHz 的时钟来生成 1 秒的延迟。

  1. 计算预分频值
  • 输入时钟:84 MHz
  • 目标计数频率:1 Hz
  • 预分频值(PSC)计算:

屏幕截图 2024 10 02 102530

  1. 配置定时器
  • 在初始化代码中,设置 TIMx_PSC 为 83,999,999。

以下是一个简单的示例,展示如何在 STM32 中配置 TIMx_PSC:

#include "stm32f4xx_hal.h"

void Timer_Config(void)
{
    TIM_HandleTypeDef htim2;
    // 启用 TIM2 时钟
    __HAL_RCC_TIM2_CLK_ENABLE();
    // 配置定时器基本参数
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 83999; // 84MHz / (83999 + 1) = 1kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 1000 - 1; // 1s = 1000ms
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    // 初始化定时器
    HAL_TIM_Base_Init(&htim2);
    // 启动定时器
    HAL_TIM_Base_Start(&htim2);
}

int main(void)
{
    HAL_Init();
    Timer_Config();
    while (1)
    {
        // 主循环
        if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
        {
            __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
            // 每秒执行的代码
        }
    }
}

2.4 计数寄存器(TIMx_CNT)

该寄存器是定时器的计数器,该寄存器存储了当前定时器的计数值。

  1. 计数方向
  • 定时器可以配置为向上计数(从0到设定的自动重载值)或向下计数(从自动重载值降到0)。这一点通过定时器的配置寄存器中的相关位来设置。
  1. 计数值范围
  • 计数值的范围通常取决于定时器的位宽。例如,对于16位定时器,计数范围为0到65535;对于32位定时器,计数范围为0到4294967295。
  1. 读写操作
  • 可以通过读取TIMx_CNT寄存器来获得当前计数值,也可以通过写入该寄存器来重置计数器,或者设定一个新的起始计数值。

以下是一个简单的代码示例,展示如何读取和设置TIMx_CNT寄存器的值:

#include "stm32f4xx_hal.h"

void MX_TIMx_Init(void) {
    // 初始化定时器结构体
    TIM_HandleTypeDef htimx;
    htimx.Instance = TIMx;
    htimx.Init.Prescaler = 8399; // 预分频
    htimx.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
    htimx.Init.Period = 9999; // 自动重载值
    HAL_TIM_Base_Init(&htimx);
    // 启动定时器
    HAL_TIM_Base_Start(&htimx);
}
void SomeFunction(void) {
    // 读取当前计数值
    uint32_t currentCount = TIMx->CNT;
    // 打印当前计数值(假设有打印函数)
    printf("Current Timer Count: %lu\n", currentCount);
    // 重置计数器
    TIMx->CNT = 0; // 将计数器重置为0
}

2.5 自动重装载寄存器(TIMx_ARR)

接着我们介绍自动重装载寄存器(TIMx_ARR),该寄存器在物理上实际对应着 2 个寄存器。一个是程序员可以直接操作的,另外一个是程序员看不到的,这个看不到的寄存器在《STM32F4xx 中文参考手册》里面被叫做影子寄存器。事实上真正起作用的是影子寄存器。

根据 TIMx_CR1 寄存器中 APRE 位的设置: APRE=0 时,预装载寄存器的内容可以随时传送到影子寄存器,此时 2 者是连通的;而 APRE=1 时,在每一次更新事件(UEV)时,才把预装载寄存器(ARR) 的内容传送到影子寄存器。

屏幕截图 2024 09 14 091622

  1. 功能
  • TIMx_ARR定义了定时器的周期。当计数器(TIMx_CNT)达到此值时,会触发更新事件(Update Event),并根据配置可以产生中断或其他动作。
  1. 计数模式
  • 计数模式可以设置为向上计数、向下计数或中心对称计数,这些模式将影响计数过程中如何处理ARR值。
  1. 配置
  • 可以通过定时器的初始化函数来设置ARR的值。通常在配置定时器时一起设置。
  1. 范围限制
  • 对于16位定时器,ARR的有效值范围为0到65535;对于32位定时器,范围为0到4294967295。

以下是一个简单的代码示例,展示如何设置和读取TIMx_ARR寄存器的值:

#include "stm32f4xx_hal.h"

void MX_TIMx_Init(void) {
    // 初始化定时器结构体
    TIM_HandleTypeDef htimx;
    htimx.Instance = TIMx; // 定时器实例
    htimx.Init.Prescaler = 8399; // 预分频
    htimx.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
    htimx.Init.Period = 9999; // 设置自动重载值
    HAL_TIM_Base_Init(&htimx);
    // 启动定时器
    HAL_TIM_Base_Start(&htimx);
}

void SomeFunction(void) 
{
    // 获取当前自动重载寄存器的值
    uint32_t autoReloadValue = TIMx->ARR;
    // 打印自动重载值(假设有打印函数)
    printf("Auto Reload Value: %lu\n", autoReloadValue);
    // 修改自动重载值
    TIMx->ARR = 4999; // 将自动重载值修改为4999
}

2.6 状态寄存器(TIMx_SR)

该寄存器用来标记当前与定时器相关的各种事件/中断是否发生。

屏幕截图 2024 09 14 091744

  1. 更新标志(UIF - Update Interrupt Flag)
  • 功能:指示定时器溢出或更新事件。
  • 操作:当计数器达到预设值并发生溢出时,该标志被置位(设为1)。用户可以通过软件清除该标志。
  • 用途:常用于定时任务的调度。
  1. 捕获标志(CCxIF - Capture/Compare Interrupt Flag)
  • 功能:用于指示输入捕获或输出比较事件。
  • 操作:在发生输入捕获或输出比较时,相应的CCxIF标志位会被置位。
  • 用途:可用于精确测量信号的时间或者生成精确的定时输出。
  1. 比较溢出标志(CCxOF - Capture/Compare Overcapture Flag)
  • 功能:指示在捕获模式下,新的捕获事件发生时,之前的捕获数据未能被读取而导致覆盖。
  • 操作:当新的捕获事件发生而未读取之前的事件时,该标志会被置位。
  • 用途:用于避免数据丢失,程序员需要及时读取捕获数据以清除该标志。

以下是一个简单的STM32定时器使用的示例代码:

// 假设TIM2为定时器
void TIM2_IRQHandler(void) {
    // 检查更新标志
    if (TIM2->SR & TIM_SR_UIF) {
        // 清除标志
        TIM2->SR &= ~TIM_SR_UIF; // 或者可以通过执行相关操作来清除
        // 执行定时器溢出处理
        handle_timer_overflow();
    }
    // 检查捕获标志
    if (TIM2->SR & TIM_SR_CC1IF) {
        // 处理捕获事件
        uint32_t captured_value = TIM2->CCR1;   
        // 清除捕获标志
        TIM2->SR &= ~TIM_SR_CC1IF;
        handle_capture_event(captured_value);
    }
}

3. 定时器基本配置步骤

这一章,我们将使用定时器产生中断,然后在中断服务函数里面翻转 DS1 上的电平,来指示定时器中断的产生。接下来我们以通用定时器 TIM3 为实例,来说明要经过哪些步骤,才能达到这个要求,并产生中断。 这里我们就对每个步骤通过库函数的实现方式来描述。

3.1 TIM3时钟使能

HAL 中定时器使能是通过宏定义标识符来实现对相关寄存器操作的,方法如下:

__HAL_RCC_TIM3_CLK_ENABLE(); // 使能 TIM3 时钟

3.2 初始化定时器参数

在 HAL 库中,定时器的初始化参数是通过定时器初始化函数 HAL_TIM_Base_Init 实现的:

HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);

该函数只有一个入口参数,就是 TIM_HandleTypeDef 类型结构体指针,结构体类型为下面我们看看这个结构体的定义:

typedef struct
{
    TIM_TypeDef              *Instance;
    TIM_Base_InitTypeDef      Init;
    HAL_TIM_ActiveChannel     Channel;
    DMA_HandleTypeDef         *hdma[7];
    HAL_LockTypeDef           Lock;
    __IO HAL_TIM_StateTypeDef State;
}TIM_HandleTypeDef;

第一个参数 Instance 是寄存器基地址。和串口,看门狗等外设一样,一般外设的初始化结构体定义的第一个成员变量都是寄存器基地址。这在HAL中都定义好了,比如要初始化串口1,那么 Instance 的值设置为 TIM1 即可。

第二个参数 Init 为真正的初始化结构体 TIM_Base_InitTypeDef 类型。该结构体定义如下:

typedef struct
{
    uint32_t Prescaler;     // 预分频系数
    uint32_t CounterMode;   // 计数方式
    uint32_t Period;        // 自动装载值 ARR
    uint32_t ClockDivision; // 时钟分频因子
    uint32_t RepetitionCounter;
} TIM_Base_InitTypeDef;

该初始化结构体中, 参数 Prescaler 是用来设置分频系数的,刚才上面有讲解。参数CounterMode 是用来设置计数方式,可以设置为向上计数,向下计数方式还有中央对齐计数方式 , 比较常用的是向上计数模式 TIM_CounterMode_Up 和向下计数模式 TIM_CounterMode_Down。参数 Period 是设置自动重载计数周期值。参数 ClockDivision 是用来设置时钟分频因子,也就是定时器时钟频率 CK_INT 与数字滤波器所使用的采样时钟之间的分频比。 参数 RepetitionCounter 用来设置重复计数器寄存器的值,用在高级定时器中。

第三个参数 Channel 用来设置活跃通道。前面我们讲解过,每个定时器最多有四个通道可以用来做输出比较,输入捕获等功能之用。这里的 Channel 就是用来设置活跃通道的,取值范围为: HAL_TIM_ACTIVE_CHANNEL_1~ HAL_TIM_ACTIVE_CHANNEL_4。

第四个 hdma 是定时器的 DMA 功能时用到。

第五个参数 Lock 和 State,是状态过程标识符,是 HAL 库用来记录和标志定时器处理过程。定时器初始化范例如下:

TIM_HandleTypeDef TIM3_Handler;    // 定时器句柄
TIM3_Handler.Instance = TIM3;      // 通用定时器 3
TIM3_Handler.Init.Prescaler = 8999;// 分频系数
TIM3_Handler.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数器
TIM3_Handler.Init.Period = 4999;   // 自动装载值
TIM3_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟分频因子
HAL_TIM_Base_Init(&TIM3_Handler);  // 初始化

3.3 使能定时器更新中断,使能定时器

HAL 库中,使能定时器更新中断和使能定时器两个操作可以在函数HAL_TIM_Base_Start_IT()中一次完成的,该函数声明如下:

HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);

该函数非常好理解,只有一个入口参数。调用该定时器之后,会首先调用__HAL_TIM_ENABLE_IT 宏定义使能更新中断,然后调用宏定义__HAL_TIM_ENABLE 使能相应的定时器。这里我们分别列出单独使能/关闭定时器中断和使能/关闭定时器方法:

__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);  // 使能句柄指定的定时器更新中断
__HAL_TIM_DISABLE_IT (htim, TIM_IT_UPDATE);// 关闭句柄指定的定时器更新中断
__HAL_TIM_ENABLE(htim);  // 使能句柄 htim 指定的定时器
__HAL_TIM_DISABLE(htim); // 关闭句柄 htim 指定的定时器

3.4 TIM3中断优先级设置

在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器,设置中断优先级。之前多次讲解到中断优先级的设置,这里就不重复讲解。

和串口等其他外设一样, HAL 库为定时器初始化定义了回调函数 HAL_TIM_Base_MspInit。一般情况下,与 MCU 有关的时钟使能,以及中断优先级配置我们都会放在该回调函数内部。函数声明如下:

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim);

对于回调函数,这里我们就不做过多讲解,大家只需要重写这个函数即可。

3.5 编写中断服务函数

在最后,还是要编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。 通常情况下, 在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,所以在状态寄存器 SR 的最低位。在处理完中断之后应该向 TIM3_SR 的最低位写 0,来清除该中断标志。

跟串口一样,对于定时器中断, HAL 库同样为我们封装了处理过程。这里我们以定时器 3的更新中断为例来讲解。

首先,中断服务函数是不变的,定时器 3 的中断服务函数为:

TIM3_IRQHandler();

一般情况下我们是在中断服务函数内部编写中断控制逻辑。但是 HAL 库为我们定义了 新的定时器中断共用处理函数 HAL_TIM_IRQHandler,在每个定时器的中断服务函数内部,我们会调用该函数。该函数声明如下:

void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim);

而函数 HAL_TIM_IRQHandler 内部,会对相应的中断标志位进行详细判断,判断确定中断来源后,会自动清掉该中断标志位,同时调用不同类型中断的回调函数。所以我们的中断控制逻辑只用编写在中断回调函数中,并且中断回调函数中不需要清中断标志位。

比如定时器更新中断回调函数为:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);

跟串口中断回调函数一样,我们只需要重写该函数即可。对于其他类型中断, HAL 库同样提供了几个不同的回调函数,这里我们列出常用的几个回调函数:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);  // 更新中断
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim);// 输出比较
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);     // 输入捕获
void HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim);        // 触发中断

通过以上几个步骤,我们就可以达到我们的目的了,使用通用定时器的更新中断,来控制DS1 的亮灭。


2024.10.2 第一次修订,后期不再维护

posted @ 2024-10-02 10:41  hazy1k  阅读(17)  评论(0编辑  收藏  举报