一、定时器中断原理概述

STM32定时器本质是一个16位/32位的向上/向下计数器,其时钟源来自内部时钟(如APB1 Timer Clock)或外部信号。当计数器计数达到设定的自动重装载值(ARR)时,会产生更新事件(Update Event)。若使能了更新中断,CPU将暂停当前任务,跳转到中断服务函数执行用户逻辑,待处理完毕后再返回。

HAL库将这一过程封装成了三个关键环节:

  • 定时器句柄初始化(HAL_TIM_Base_Init
  • 时基单元配置(TIM_MasterConfigTypeDef
  • 中断使能与回调函数(HAL_TIM_IRQHandler / HAL_TIM_PeriodElapsedCallback

理解这三个环节,就能轻松驾驭任何STM32型号的定时器中断。

二、HAL库定时器中断配置步骤

1. 使能定时器时钟

使用CubeMX或直接操作RCC寄存器,使能对应定时器的时钟。例如TIM2挂在APB1总线上:

__HAL_RCC_TIM2_CLK_ENABLE();

若使用CubeMX,勾选TIM2后系统自动生成此句,无需手动添加。

2. 初始化时基参数

时基单元决定定时器计数频率和溢出周期。常用参数如下:

  • Prescaler(预分频):将定时器时钟分频,例如APB1 Timer Clock为84MHz,设Prescaler为8399则计数频率为10kHz。
  • Period(自动重装载值):计数达到该值产生更新事件,例如设Period为4999则定时周期为500ms。
  • CounterMode:通常用向上计数模式(TIM_COUNTERMODE_UP)。
  • ClockDivision:一般设为TIM_CLOCKDIVISION_DIV1

使用HAL库编写:

TIM_HandleTypeDef htim2;

void MX_TIM2_Init(void)
{
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 8399;         // 84MHz / 8400 = 10kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 4999;            // 10kHz / 5000 = 2Hz,即500ms中断一次
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
    {
        Error_Handler();
    }
}

3. 使能更新中断并配置NVIC

初始化完成后,需要使能定时器更新中断,并在NVIC中设置中断优先级。注意优先级分组需在主函数中提前配置(HAL_NVIC_SetPriorityGrouping)。

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{
    if (htim_base->Instance == TIM2)
    {
        __HAL_RCC_TIM2_CLK_ENABLE();  // 若已在外部使能可省略

        // 设置中断优先级,抢占优先级1,子优先级0
        HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);
        // 使能TIM2全局中断
        HAL_NVIC_EnableIRQ(TIM2_IRQn);
    }
}

在CubeMX生成的代码中,HAL_TIM_Base_MspInit会自动被调用,无需手动调用。但需确保其内部已包含NVIC配置。

4. 编写中断服务函数(IRQHandler)

每个定时器中断源都有固定的中断函数名,如TIM2对应TIM2_IRQHandler。在中断服务函数中必须调用HAL库的HAL_TIM_IRQHandler,它会自动判断中断类型并调用对应的回调函数。

void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

5. 重写回调函数

HAL库使用弱定义的HAL_TIM_PeriodElapsedCallback,用户需要在自己的代码中重新实现该函数。注意此函数对所有使用HAL_TIM_IRQHandler的定时器共用,因此需要通过句柄判断具体是哪个定时器。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        // 用户代码:翻转LED或执行周期任务
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

6. 启动定时器中断

在主函数初始化完成后,调用以下函数启动定时器:

HAL_TIM_Base_Start_IT(&htim2);

_IT后缀表示中断模式启动,区别于阻塞模式HAL_TIM_Base_Start。此后定时器开始计数,每次溢出进入中断。

三、完整代码示例(TIM2 产生500ms周期中断)

/* main.c 精简示例 */

TIM_HandleTypeDef htim2;

void Error_Handler(void)
{
    while(1) {}
}

void MX_TIM2_Init(void)
{
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 8399;
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 4999;
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
    {
        Error_Handler();
    }
}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{
    if (htim_base->Instance == TIM2)
    {
        __HAL_RCC_TIM2_CLK_ENABLE();
        HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);
        HAL_NVIC_EnableIRQ(TIM2_IRQn);
    }
}

void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转板载LED
    }
}

int main(void)
{
    HAL_Init();
    // 初始化系统时钟(如72MHz或84MHz)
    SystemClock_Config();
    // 初始化LED引脚(略)
    MX_GPIO_Init();
    // 初始化定时器
    MX_TIM2_Init();
    // 启动定时器中断
    HAL_TIM_Base_Start_IT(&htim2);
    
    while(1)
    {
        // 主循环可以执行其他低优先级任务
    }
}

四、注意事项与常见问题

  1. 时钟源频率:不同STM32系列APB1定时器时钟可能不同,务必根据实际主频计算Prescaler和Period。例如F103默认72MHz,F407若APB1分频为2则定时器时钟仍为84MHz。

  2. 中断服务函数命名:一定不要随意修改TIMx_IRQHandler函数名,否则中断无法正确链接。

  3. 回调函数中避免长时间操作:中断回调函数应尽量精简,不要使用HAL_Delay或阻塞式串口打印,否则会拖慢系统响应。需要大工作量时可置标志位,在主循环中处理。

  4. AutoReloadPreload:建议使能预装载,这样修改Period时不会立即生效,而是等本次溢出后才更新,避免周期跳动。

  5. MPU与Cache(Cortex-M7):若使用F7/H7系列,需确保定时器相关寄存器访问未受MPU限制,并注意D-Cache对DMA等的作用,但普通中断不受影响。

  6. 多定时器共用回调HAL_TIM_PeriodElapsedCallback是所有定时器更新中断的统一回调,务必通过htim->Instance区分,否则多个定时器会相互干扰。

  7. NVIC优先级设置:若系统使用FreeRTOS,建议将定时器中断优先级设置为低于内核最高优级(如5~15),避免破坏临界区。

五、总结

使用HAL库配置STM32定时器中断的流程可以浓缩为:时钟使能→时基初始化→NVIC配置→IRQHandler内调用HAL_TIM_IRQHandler→重写PeriodElapsedCallback→启动_IT模式。掌握这套流程,就能灵活生成精确的毫秒级、微秒级定时中断,为按键扫描、传感器采样、通信超时处理等场景提供可靠的时基基础。实践时多注意时钟树和中断优先级这两个细节,就能避免大部分低级错误。