2. 定时器基础原理:STM32定时器分类与时基单元

定时器这东西,说白了就是单片机的“心跳”。我刚开始做嵌入式那会儿,总觉得定时器不就是计个数嘛,有啥好研究的?后来踩了几个坑才明白——定时器玩不转,PWM、输入捕获、编码器接口这些高级功能根本别想碰。今天咱们就把STM32定时器的老底儿翻出来看看。

2.1 STM32定时器家族:三类定时器

STM32的定时器分三类:基本定时器、通用定时器、高级定时器。它们不是简单的“低配中配高配”关系,而是各有各的专长。

类型 代表外设 时基单元 核心能力 典型应用
基本定时器 TIM6、TIM7 16位预分频器 + 16位计数器 + 16位自动重装载 仅基础定时、触发DAC 系统滴答、周期性中断
通用定时器 TIM2~TIM5 16位预分频器 + 16位计数器 + 16位自动重装载 定时、PWM、输入捕获、输出比较 电机控制、信号测量
高级定时器 TIM1、TIM8 16位预分频器 + 16位计数器 + 16位自动重装载 + 重复计数器 全部通用功能 + 互补PWM + 死区插入 + 刹车功能 三相电机、数字电源

我的经验:项目里90%的场景用通用定时器就够了。高级定时器那些死区插入、刹车功能,做电机驱动时才用得上。别一上来就开高级定时器,寄存器多到让你怀疑人生。

2.2 时基单元结构:定时器的“心脏”

不管哪类定时器,核心都是时基单元。它由三个寄存器组成:预分频器(PSC)、计数器(CNT)、自动重装载寄存器(ARR)。这三个家伙配合工作,决定了定时器的“心跳节奏”。

STM32定时器时基单元结构 CK_PSC 时钟源 PSC 预分频器 16位可编程 CNT 计数器 向上/向下/中央对齐 ARR 自动重装载 溢出值设定 溢出事件 工作流程: CK_PSC 时钟 → PSC 分频 → 计数器 CNT 每来一个脉冲 +1 当 CNT == ARR 时 → 产生溢出事件 → CNT 清零重新计数 溢出事件可触发中断、DMA、更新定时器输出

2.3 预分频器(PSC)详解

预分频器的作用,就是把输入的时钟频率降下来。STM32的PSC是16位的,取值范围0~65535。注意,实际分频系数是 PSC + 1。

计算公式:

计数器时钟频率 = 定时器时钟频率 / (PSC + 1)
定时周期 = (ARR + 1) / 计数器时钟频率

举个例子。假设定时器时钟是72MHz,我想要一个1ms的定时中断:

// 目标:1ms = 0.001s
// 72MHz = 72,000,000 Hz
// 先分频到 72,000 Hz(即每秒钟计数72000次)
// PSC = 72,000,000 / 72,000 - 1 = 999
// 然后 ARR = 72,000 * 0.001 - 1 = 71

// 寄存器配置
TIM2->PSC = 999;    // 预分频值
TIM2->ARR = 71;     // 自动重装载值
TIM2->EGR = TIM_EGR_UG;  // 立即更新寄存器

避坑指南:我曾经在项目里直接给PSC写72000,心想“分频到1Hz多好”。结果计数器根本不走——PSC最大只能到65535!记住,16位寄存器上限是65535,别超了。

2.4 自动重装载寄存器(ARR)详解

ARR决定了定时器的溢出周期。计数器从0开始,每来一个时钟脉冲加1,加到ARR的值时,就产生溢出事件,然后计数器清零重新开始。

ARR也是16位的,范围0~65535。但要注意,实际计数值是ARR + 1。为什么?因为计数器是从0开始计数的,0也算一个数。

ARR值 实际计数次数 说明
0 1 每个时钟脉冲都溢出,频率最高
99 100 常用做100分频
65535 65536 最大定时周期

重要提醒:ARR寄存器有“影子寄存器”机制。你写入ARR的值不会立即生效,要等到当前计数周期结束才更新。如果想立即生效,必须设置UG位(更新生成位)。我见过有人调试半天计数器不按预期跑,结果就是忘了这茬。

2.5 计数器模式:向上、向下、中央对齐

通用定时器和高级定时器的计数器有三种工作模式:

  • 向上计数模式:从0加到ARR,溢出后回0。最常用。
  • 向下计数模式:从ARR减到0,溢出后回到ARR。某些特殊场景用。
  • 中央对齐模式:从0加到ARR,再从ARR减到0,来回跑。PWM生成时常用。

我个人习惯,做周期性定时中断就用向上计数模式,简单直接。做PWM时用中央对齐模式,波形更对称。

2.6 定时器初始化流程(代码实战)

说了这么多理论,咱们直接上手配寄存器。下面是一个完整的定时器初始化流程:

void TIM2_Init(void)
{
    // 1. 开启定时器时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

    // 2. 设置预分频器
    TIM2->PSC = 7199;   // 72MHz / (7199+1) = 10kHz

    // 3. 设置自动重装载值
    TIM2->ARR = 9999;   // 10kHz / (9999+1) = 1Hz = 1秒

    // 4. 立即更新寄存器(影子寄存器生效)
    TIM2->EGR |= TIM_EGR_UG;

    // 5. 清除更新标志位
    TIM2->SR &= ~TIM_SR_UIF;

    // 6. 使能更新中断
    TIM2->DIER |= TIM_DIER_UIE;

    // 7. 配置NVIC(中断优先级)
    NVIC_SetPriority(TIM2_IRQn, 2);
    NVIC_EnableIRQ(TIM2_IRQn);

    // 8. 启动定时器
    TIM2->CR1 |= TIM_CR1_CEN;
}

// 中断服务函数
void TIM2_IRQHandler(void)
{
    if(TIM2->SR & TIM_SR_UIF)
    {
        TIM2->SR &= ~TIM_SR_UIF;  // 清除中断标志
        // 用户代码:每1秒执行一次
    }
}

我的习惯:初始化时先配PSC和ARR,再触发UG位更新,最后才开中断和使能定时器。顺序搞反了的话,可能会在配置完成前就触发一次意外中断。嗯,这坑我踩过。

2.7 常见问题与避坑

  • 定时器不工作:检查时钟有没有开。RCC那一步最容易忘。
  • 时间算不准:确认APB1/APB2的时钟分频系数。STM32的定时器时钟可能和系统时钟不一样。
  • 中断不触发:检查NVIC使能了没,中断标志清了没。
  • 第一次溢出时间不对:初始化后手动触发UG位,强制更新影子寄存器。

我曾经遇到过一个诡异的问题:定时器中断偶尔会丢。查了两天才发现,中断服务函数里处理时间太长,下一次中断来了还没退出。解决办法是中断里只设标志位,主循环里处理业务逻辑。