3. 定时器中断配置实战:寄存器级配置步骤

好,咱们今天来点真格的。上一章我们把定时器的基本原理讲清楚了,这一章直接上手——怎么通过寄存器配置,让定时器产生1ms的中断。说白了,就是让芯片内部的那个计数器,每1ms“滴答”一下,然后跳进我们的中断服务函数里干活。

我个人习惯,做任何外设驱动,第一件事不是看代码,而是看时钟树。你想想看,定时器本身也是数字电路,没时钟它动不了。所以第一步,永远是时钟使能。

3.1 配置总览:五步走流程

我总结了一个五步走的流程,每次配定时器中断我都按这个来,基本没出过问题。你可以把它当成一个检查清单。

步骤 操作 关键寄存器 说明
1 时钟使能 RCC->APB1/APB2ENR 让定时器模块脱离复位状态,获得时钟
2 预分频配置 TIMx->PSC 将系统时钟分频到想要的计数频率
3 重装载值配置 TIMx->ARR 设定计数周期,决定中断间隔
4 中断使能 TIMx->DIER 允许定时器产生更新中断
5 NVIC配置 NVIC->ISER/IP 将中断从外设路由到CPU内核

核心公式: 中断频率 = 定时器时钟 / ((PSC + 1) × (ARR + 1))

要得到1ms中断,即频率1000Hz,代入公式反推PSC和ARR即可。

3.2 第一步:时钟使能——别让定时器“睡着”

我刚开始学的时候,经常忘了开时钟,结果寄存器怎么配都没反应。后来养成了习惯:配任何外设,先看RCC(Reset and Clock Control)模块。

以STM32F103为例,定时器TIM2挂在APB1总线上。对应的使能位在RCC->APB1ENR寄存器的第0位。

// 时钟使能:打开TIM2的时钟
RCC->APB1ENR |= (1 << 0);   // 位0置1,TIM2时钟使能

小提示: 不同定时器挂在不同总线上。TIM1、TIM8在APB2上,TIM2~TIM7在APB1上。配之前一定查数据手册,别搞混了。我曾经见过有人把TIM1的使能位写到了APB1ENR里,查了半天没反应。

3.3 第二步:预分频器配置——把时钟降下来

系统时钟通常是72MHz(或者别的频率),直接拿这个去计数,1ms要计72000个数,虽然也能算,但精度和灵活性不够。所以我们先用预分频器(PSC)把时钟降一降。

预分频器的值,就是分频系数减1。比如我想把72MHz分成72kHz,分频系数就是1000,PSC就写999。

// 预分频配置:72MHz / (999 + 1) = 72kHz
// 也就是计数器每1/72000秒加1
TIM2->PSC = 999;   // 分频系数 = PSC + 1 = 1000

这里有个坑,我提醒一下:PSC寄存器是16位的,最大值65535。如果你的时钟太高,分频系数超过这个数,就得考虑两级分频或者用更长的定时器。

3.4 第三步:重装载值配置——定下1ms的“闹钟”

现在计数器以72kHz的频率跑,也就是每13.89微秒加1。要得到1ms中断,需要计72个数。所以重装载寄存器ARR设为71(从0开始计数)。

// 重装载值配置:72kHz / (71 + 1) = 1000Hz = 1ms
TIM2->ARR = 71;    // 自动重装载值

注意: ARR的值决定了定时器的精度。如果ARR太小,中断频率高但CPU负担重;如果太大,频率低但精度可能受影响。1ms是一个比较折中的选择,适合大多数应用场景。

3.5 第四步:中断使能——让定时器学会“喊人”

定时器自己计数归零,不会主动通知CPU。我们需要在DIER(DMA/Interrupt Enable Register)寄存器里,把更新中断使能位(UIE)打开。这样每次计数器溢出(更新事件),定时器就会向NVIC发送中断请求。

// 使能更新中断
TIM2->DIER |= (1 << 0);   // 位0:UIE,更新中断使能

嗯,这里要注意:DIER里还有其他中断位,比如捕获比较中断、触发中断等。我们目前只用到更新中断,别多开,省得中断处理函数里一堆乱七八糟的事情要处理。

3.6 第五步:NVIC配置——让CPU“听见”中断

定时器已经把中断请求发出去了,但CPU理不理它,还得看NVIC(嵌套向量中断控制器)的脸色。NVIC就像是一个总闸开关,外设的中断信号得先经过它,才能到达CPU。

配置NVIC分两步:

  1. 设置中断优先级:在NVIC->IP寄存器里,配置TIM2的中断优先级。
  2. 使能中断通道:在NVIC->ISER寄存器里,把TIM2对应的中断位打开。
// 设置TIM2中断优先级(优先级数值越小,优先级越高)
NVIC->IP[TIM2_IRQn] = 0x01;   // 抢占优先级0,子优先级1(取决于分组配置)

// 使能TIM2中断通道
NVIC->ISER[TIM2_IRQn >> 5] |= (1 << (TIM2_IRQn & 0x1F));

个人经验: 优先级分组我一般用NVIC_PriorityGroup_2(2位抢占,2位子优先级)。这样有4级抢占优先级,足够大多数项目用了。优先级别设太高(数值小),否则可能把系统滴答定时器(SysTick)给抢了,导致系统时钟紊乱。

3.7 完整代码示例:1ms定时中断

把上面五步串起来,再加上一个简单的中断服务函数,就是一个完整的1ms定时中断驱动。

#include "stm32f10x.h"

// 全局变量:1ms递增的计数器
volatile uint32_t g_tick_ms = 0;

// 定时器2初始化:配置为1ms中断
void TIM2_Init_1ms(void)
{
    // 1. 时钟使能
    RCC->APB1ENR |= (1 << 0);   // TIM2时钟使能

    // 2. 预分频配置:72MHz / 1000 = 72kHz
    TIM2->PSC = 999;

    // 3. 重装载值配置:72kHz / 72 = 1000Hz = 1ms
    TIM2->ARR = 71;

    // 4. 中断使能
    TIM2->DIER |= (1 << 0);     // 更新中断使能

    // 5. NVIC配置
    NVIC->IP[TIM2_IRQn] = 0x01; // 设置优先级
    NVIC->ISER[TIM2_IRQn >> 5] |= (1 << (TIM2_IRQn & 0x1F));

    // 6. 启动定时器(CEN位置1)
    TIM2->CR1 |= (1 << 0);      // 使能计数器
}

// 定时器2中断服务函数
void TIM2_IRQHandler(void)
{
    // 检查更新中断标志位
    if (TIM2->SR & (1 << 0))
    {
        // 清除中断标志位(必须!否则会一直进中断)
        TIM2->SR &= ~(1 << 0);

        // 用户代码:每1ms执行一次
        g_tick_ms++;
    }
}

避坑指南: 我曾经在项目里忘了清中断标志位,结果程序一进中断就出不来了,整个系统卡死。后来花了半天才找到原因。记住:中断服务函数里,第一件事检查标志,第二件事清标志,第三件事干正事。顺序别搞反。

3.8 知识体系图:定时器中断配置流程

下面这张图把整个配置流程串起来了,从时钟源头到CPU响应,一目了然。

定时器中断配置流程 定时器中断配置流程(从时钟到CPU) ① 时钟源(72MHz) ② RCC时钟使能 ③ PSC预分频(÷1000) ④ ARR重装载(=71) ⑤ DIER中断使能 ⑥ NVIC配置(CPU响应) 关键参数 时钟:72MHz → 72kHz 周期:72kHz / 72 = 1kHz 中断间隔:1ms 避坑提醒 • 别忘了清SR标志位 • 优先级别设太高

3.9 常见问题与避坑

  • 定时器不工作? 先检查CR1寄存器的CEN位有没有置1。我见过有人配完所有寄存器,唯独忘了启动计数器。
  • 中断频率不对? 用公式反算一下:(PSC+1)×(ARR+1)是不是等于时钟频率/目标频率。我曾经把PSC和ARR的值搞反了,结果中断频率差了1000倍。
  • 进不了中断? 检查NVIC的使能位和优先级分组。有时候优先级分组配置不对,低优先级的中断会被屏蔽。
  • 中断响应太慢? 如果中断服务函数里做的事情太多,会影响下一次中断的响应。建议中断里只做标记,具体处理放到主循环里。

总结一下: 定时器中断配置,说白了就是“开时钟、设分频、定周期、开中断、配NVIC”这五步。每一步都有对应的寄存器,配对了就能跑起来。我建议你拿到开发板后,先照着上面的代码敲一遍,然后用示波器或者逻辑分析仪抓一下引脚电平变化,亲眼看到1ms的方波,比什么都管用。