3、定时器中断实验:CubeMX配置定时器、HAL库定时器中断回调函数、实现1ms精准定时

定时器这东西,说白了就是单片机的“心跳计数器”。

我刚开始学STM32时,总觉得定时器中断不就是个延时嘛,有啥好研究的?直到有一次做电机控制项目,发现电机转速忽快忽慢,查了半天才发现是定时器配置出了问题——预分频算错了,导致定时周期漂了将近20%。从那以后,我对定时器的每一个寄存器都格外小心。

这一章,咱们就手把手把1ms定时中断做出来。你跟着我走一遍,以后遇到任何定时需求都能自己搞定。

3.1 定时器中断的核心逻辑

先理清一个概念:定时器中断不是让程序“等”,而是让定时器“自己数数”,数到指定值就通知CPU。

整个过程分三步:

  1. 时钟源:给定时器一个稳定的时钟脉冲(比如72MHz)
  2. 预分频:把时钟频率降到你想要的节奏(比如降到1MHz)
  3. 自动重装载:计数器从0数到设定值,触发中断

举个例子:你让定时器每秒数100万下,然后设定“数到1000就叫我”。那它每1ms就叫你一次。就这么简单。

关键公式:

定时周期 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率

要得到1ms中断:72MHz时钟 → 预分频71(72分频)→ 1MHz → 重装载值999 → 1ms

3.2 CubeMX配置步骤(我习惯的流程)

我个人习惯先把时钟树配好,再配定时器。顺序乱了容易漏配置。

3.2.1 时钟配置

打开CubeMX,先切到Clock Configuration标签页:

  • HSE选择Crystal/Ceramic Resonator
  • PLL源选HSE
  • 主频拉到72MHz(STM32F103最常见配置)
  • APB1定时器时钟自动变成72MHz

嗯,这里要注意:APB1上的定时器(TIM2~TIM7)时钟可能翻倍。如果APB1预分频不是1,定时器时钟 = APB1时钟 × 2。我踩过这个坑,当时配了半天中断频率都不对,查手册才发现这个细节。

3.2.2 定时器参数配置

切到Pinout & Configuration,选TIM2:

参数 值 说明
Clock Source Internal Clock 使用内部时钟源
Prescaler (PSC) 71 72MHz ÷ (71+1) = 1MHz
Counter Mode Up 向上计数模式
Counter Period (ARR) 999 1MHz ÷ (999+1) = 1kHz → 1ms
Auto-reload Preload Enable 使能自动重装载预装载

配完这些,别忘了在NVIC Settings标签页里勾上TIM2全局中断。我见过有人配了定时器但忘了开中断,调试了半天以为代码写错了。

3.3 代码生成与HAL库回调机制

点击Generate Code,生成工程。打开后你会发现,HAL库已经把定时器初始化写好了,在MX_TIM2_Init()里。

启动定时器中断只需要一行:

HAL_TIM_Base_Start_IT(&htim2);

我建议放在main()的初始化部分,就在MX_TIM2_Init()之后。

3.3.1 中断回调函数

HAL库的中断处理分两层:

  • 底层中断函数:TIM2_IRQHandler()——由HAL库自动生成,你不用动
  • 用户回调函数:HAL_TIM_PeriodElapsedCallback()——你在这里写业务逻辑

回调函数长这样:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM2)
    {
        // 1ms到了,在这里做你想做的事
        // 比如翻转一个LED、累加一个计数器
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

我的经验:回调函数里不要做耗时操作。1ms很短,如果你在里面跑个printf或者复杂计算,中断还没退出下一个中断又来了,程序就卡死了。我一般只放标志位或者简单累加。

3.4 验证1ms精度

怎么确认定时真的准?我习惯用逻辑分析仪或者示波器看波形。

在回调函数里翻转一个GPIO:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    static uint16_t cnt = 0;
    if(htim->Instance == TIM2)
    {
        cnt++;
        if(cnt >= 1000)  // 1ms × 1000 = 1s
        {
            cnt = 0;
            HAL_GPIO_TogglePin(TEST_PIN_GPIO_Port, TEST_PIN_Pin);
        }
    }
}

用示波器量TEST_PIN,周期应该是2秒(1秒高1秒低)。如果偏差超过1%,检查时钟配置和预分频值。

我曾经踩过的坑:

  • 预分频值写成了71,但忘了PSC和ARR都是从0开始计数的,实际分频是72,周期是72/72MHz=1μs,不是1ms
  • APB1预分频没注意,定时器时钟变成了36MHz,算出来的周期翻倍
  • 中断优先级没配,被其他高优先级中断一直抢占,定时器回调迟迟进不去

3.5 知识结构总览

下面这张图把整个定时器中断的脉络串起来了,建议你保存下来对照着配:

定时器中断1ms实现流程 时钟源 72MHz 预分频 PSC = 71 → 1MHz 自动重装载 ARR = 999 → 1ms 更新事件 → 触发中断 HAL_TIM_PeriodElapsedCallback() 用户业务逻辑 配置要点 • PSC = 71 不是 72 • ARR = 999 不是 1000 • 别忘了开NVIC中断 • 回调里别做耗时操作

3.6 常见问题与排查思路

配完发现定时不准?别慌,按这个顺序查:

现象 可能原因 排查方法
中断完全不触发 NVIC没使能 / 没调用Start_IT 检查CubeMX NVIC设置,确认代码中有HAL_TIM_Base_Start_IT
周期偏大一倍 APB1预分频导致定时器时钟翻倍 查RCC时钟树,确认APB1分频系数
周期偏小 PSC或ARR算错了 重新代入公式计算,注意+1
程序卡死 回调函数耗时超过1ms 回调里只放标志位,主循环处理

嗯,这一套走下来,1ms定时中断应该稳了。你把这个基础打牢,后面做PWM、输入捕获、ADC定时触发都会轻松很多。