第1章:定时器中断——让芯片学会“闹钟”

各位同学好,我是你们的嵌入式讲师。今天咱们聊聊定时器中断。说实话,我刚开始学STM32时,觉得定时器就是个计数器,没啥了不起。直到有一次做电机控制项目,发现没有定时器中断,整个系统就像没装闹钟的人——要么睡过头,要么频繁看表,效率极低。

定时器中断,说白了就是让芯片内部有个“闹钟”。时间到了,它自动提醒CPU:“嘿,该干活了!”CPU放下手头的事,跑去执行中断服务函数,完事再回来继续。这种机制,让我们的程序能精确控制时间,又不浪费CPU资源。

核心公式:T = (PSC+1) × (ARR+1) / Tclk

这个公式,我建议你刻在脑子里。后面所有定时器配置,都离不开它。

1.1 定时器溢出中断配置流程

配置定时器中断,其实就五步。我习惯按这个顺序来,不容易漏。

  1. 使能定时器时钟——没电,啥也干不了
  2. 配置时基单元——设置PSC和ARR,决定定时多久
  3. 使能更新中断——告诉定时器:溢出时喊我一声
  4. 配置NVIC——设置中断优先级,决定谁先响应
  5. 启动定时器——开始计时

嗯,这里要注意:很多人会忘记第3步。我刚开始也犯过这错,配置了半天,中断就是不触发。查了两小时,发现中断使能位没置1。你说冤不冤?

1.2 中断优先级设置

STM32的中断优先级,分抢占优先级和子优先级。抢占优先级高的,可以打断低的。子优先级相同的情况下,谁先来谁先处理。

我个人习惯:关键任务(比如电机堵转保护)给最高抢占优先级,普通任务(比如LED闪烁)给低优先级。你想想看,如果电机烧了才去处理LED闪烁,那画面太美我不敢看。

优先级分组 抢占优先级位数 子优先级位数 应用场景
NVIC_PriorityGroup_0 0 4 所有中断平等
NVIC_PriorityGroup_1 1 3 简单系统
NVIC_PriorityGroup_2 2 2 常用配置
NVIC_PriorityGroup_3 3 1 复杂系统
NVIC_PriorityGroup_4 4 0 强抢占需求

我曾经踩过的坑:中断优先级分组,整个工程只能设置一次!如果你在多个地方调用NVIC_PriorityGroupConfig,后调用的会覆盖前面的。我建议在main函数一开始就设置好,后面别再动。

1.3 中断服务函数编写

中断服务函数,名字是固定的。比如TIM2的中断服务函数叫TIM2_IRQHandler。你写错名字,编译器不会报错,但中断永远不会执行。嗯,我当年就因为这个,对着屏幕发呆了一下午。

标准写法是这样的:

void TIM2_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        // 这里放你的业务代码
        // 比如:LED翻转、数据采集等
        
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);  // 清标志位,别忘了!
    }
}

看到那个清标志位了吗?我强调一下:不清标志位,中断会一直触发。就像闹钟响了你不按掉,它会一直响。系统就卡死在中断里,其他事都干不了。

1.4 定时周期计算

回到那个核心公式:T = (PSC+1) × (ARR+1) / Tclk

举个例子:STM32F103默认系统时钟72MHz,我想产生1ms定时。

  • Tclk = 72MHz = 72,000,000 Hz
  • 目标T = 0.001秒
  • 我习惯先设PSC=71,这样计数器时钟 = 72M / (71+1) = 1MHz,即1us计数一次
  • 那么ARR = 0.001s × 1MHz - 1 = 999

验证一下:T = (71+1) × (999+1) / 72,000,000 = 72 × 1000 / 72,000,000 = 0.001秒。完美!

小技巧:我一般先定PSC,让计数器时钟变成整数。比如1MHz、100kHz这种,后面ARR算起来方便,也容易调试。

1.5 实战:1ms系统滴答定时器实现

系统滴答定时器,说白了就是系统的“心跳”。很多实时操作系统都靠它来调度任务。咱们今天用TIM2来实现一个简单的1ms滴答。

完整代码:

// 定时器初始化
void TIM2_Init(void)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    
    // 1. 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    
    // 2. 配置时基
    TIM_TimeBaseStructure.TIM_Prescaler = 71;        // PSC = 71
    TIM_TimeBaseStructure.TIM_Period = 999;          // ARR = 999
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
    
    // 3. 使能更新中断
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    
    // 4. 配置NVIC
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    // 5. 启动定时器
    TIM_Cmd(TIM2, ENABLE);
}

// 全局变量,用于计数
volatile uint32_t SysTickCounter = 0;

// 中断服务函数
void TIM2_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        SysTickCounter++;  // 每1ms加1
        
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

// 主函数
int main(void)
{
    TIM2_Init();
    
    while(1)
    {
        // 每1000ms(1秒)翻转一次LED
        if(SysTickCounter >= 1000)
        {
            SysTickCounter = 0;
            GPIO_ToggleBits(GPIOB, GPIO_Pin_0);  // 假设PB0接了LED
        }
    }
}

这段代码,我建议你亲手敲一遍。别复制粘贴,敲的过程中你会注意到很多细节。比如那个volatile关键字,不加的话,编译器可能优化掉SysTickCounter的读取,导致判断永远不成立。

核心要点回顾:

  • 定时器中断 = 硬件闹钟,精确又省CPU
  • 配置流程:时钟→时基→中断使能→NVIC→启动
  • 公式T = (PSC+1)×(ARR+1)/Tclk,先定PSC再算ARR
  • 中断服务函数里,一定要清标志位
  • 全局变量加volatile,防止编译器优化

好了,这一章的内容就到这。定时器中断是嵌入式开发的基石,后面讲PWM、输入捕获、编码器模式,都离不开它。建议你拿块开发板,把1ms滴答跑起来,用示波器或者逻辑分析仪看看波形,感受一下“精确到微秒”是什么体验。


公众号:蓝海资料掘金营,微信deep3321