2. 定时器核心寄存器:TIMx_CR1、TIMx_SR、TIMx_CNT、TIMx_PSC、TIMx_ARR 深度解析

说实话,我刚接触STM32定时器那会儿,看着寄存器手册里密密麻麻的位定义,头都大了。后来调了几个项目才明白——真正核心的寄存器,其实就五个。把这五个搞透了,定时器这块你就通了八成。

今天咱们就一个一个掰开揉碎了讲。我会结合我实际调试中踩过的坑,帮你把这些寄存器的脾气摸清楚。

核心观点: 定时器的本质就是一个计数器,在时钟节拍下不断累加,到了设定值就复位或翻转。五个寄存器各司其职,构成了这个“计数-比较-事件”的闭环。
定时器核心寄存器关系图 时钟源 TIMx_PSC TIMx_CNT TIMx_ARR 时钟输入 分频后时钟 计数值 TIMx_CR1 TIMx_SR 更新事件/中断 使能/配置 更新标志 触发 💡 工作流程: 时钟 → PSC分频 → CNT计数 → CNT与ARR比较 → 相等时产生更新事件 → SR置位 → 触发中断或DMA CR1控制整个流程的启停、计数方向、时钟源选择等。

2.1 TIMx_CR1 — 控制寄存器,定时器的“总开关”

CR1 寄存器,说白了就是定时器的控制面板。你想想看,一个定时器要不要启动、怎么计数、要不要自动重装,全由它说了算。

我个人习惯,初始化定时器时第一个动的就是 CR1。它的几个关键位,我列个表给你看:

位段 名称 作用 我常用的配置
位0 CEN 计数器使能,1启动,0停止 最后才置1,先配好其他寄存器
位4 DIR 计数方向:0向上,1向下 向上计数用得最多
位7 ARPE 自动重装载预装载使能 建议置1,防ARR突变
位5~6 CMS 中央对齐模式选择 PWM用边沿对齐时设00
我的小技巧: 写CR1时,我习惯用“读-改-写”的方式,而不是直接赋值。比如 TIM2->CR1 |= TIM_CR1_CEN; 这样不会误清其他位。有一次我直接写了 TIM2->CR1 = 0x01;,结果把默认的时钟分频配置给冲掉了,查了半天才发现。

这里有个坑,ARPE 位。我曾经在一个电机控制项目里,没开 ARPE,直接在运行中改了 ARR 值。结果计数器没等到当前周期结束就用了新值,PWM 波形直接乱掉,电机抖得像筛子。开了 ARPE 后,新 ARR 值会在当前周期结束时才加载,平滑过渡。

2.2 TIMx_SR — 状态寄存器,定时器的“信号灯”

SR 寄存器,就是告诉你定时器当前发生了什么事件。最常用的就是位0——UIF(更新中断标志)。

计数器从0走到ARR,或者从ARR走回0,UIF 就会置1。这时候如果你开了中断,就会进中断服务函数。如果你用轮询方式,就得手动清这个标志。

⚠️ 千万注意: 清 UIF 标志要用 TIMx->SR = ~TIM_SR_UIF; 或者 TIMx->SR = 0;?不对!正确做法是写0来清除对应位。我见过有人写 TIM2->SR &= ~TIM_SR_UIF;,这在某些STM32型号上会出问题。标准做法是:TIM2->SR = ~TIM_SR_UIF; 或者直接用库函数 __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);

SR 寄存器里还有其他标志位,比如 CC1IF(捕获/比较标志)、TIF(触发标志)。调试的时候,我经常在中断服务函数里先读一下 SR,看看到底是哪个事件触发的,再决定处理逻辑。

// 我常用的中断入口调试代码
void TIM2_IRQHandler(void)
{
    uint32_t flags = TIM2->SR;  // 先读SR,保存现场
    if(flags & TIM_SR_UIF) {
        // 更新事件处理
        TIM2->SR = ~TIM_SR_UIF;  // 清标志
        // 你的业务代码...
    }
    if(flags & TIM_SR_CC1IF) {
        // 捕获/比较事件处理
        TIM2->SR = ~TIM_SR_CC1IF;
        // ...
    }
}

2.3 TIMx_CNT — 计数器寄存器,定时器的“里程表”

CNT 就是当前计数值。它一直在跑,从0到ARR,或者从ARR到0,取决于你设的计数方向。

这个寄存器有两个用途:

  • 读它: 获取当前计数值,用于测量时间间隔或频率
  • 写它: 强行改变计数值,实现相位调整或同步

我记得有一次做超声波测距,需要精确测量回波脉冲宽度。我在上升沿捕获时读 CNT,下降沿捕获时再读一次,差值就是脉宽。这个用法在输入捕获模式下特别常见。

注意: 读 CNT 的时候,如果定时器时钟很快(比如72MHz),两次读之间可能已经跑了好几个数。对于高精度测量,建议用 DMA 或者两次读取取平均值。我一般会连续读三次,取中间值,能滤掉大部分毛刺。

2.4 TIMx_PSC — 预分频器,定时器的“减速齿轮”

PSC 寄存器,就是给时钟降频用的。定时器的时钟源通常是系统时钟(比如72MHz),太快了,直接计数的话,ARR 得设很大才能得到想要的周期。PSC 就是用来把时钟分频的。

公式很简单:

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

举个例子,72MHz 时钟,PSC 设 71,那么计数器时钟就是 72M / (71+1) = 1MHz,也就是每1微秒计一次数。

这里有个容易犯的错:PSC 是 16 位的,最大值 65535。如果你需要更低频,就得用更大的 PSC,或者考虑用定时器级联。我做过一个 RTC 补偿的项目,需要 1Hz 的时钟,PSC 设 7199,ARR 设 9999,刚好 1秒中断一次。

我的配置习惯: 先确定你需要的计数器时钟频率,再反推 PSC。比如我要 1MHz 的计数时钟,系统时钟 72MHz,那 PSC = 72M / 1M - 1 = 71。这样 ARR 的值就能直接对应微秒数,调试时心里有数。

2.5 TIMx_ARR — 自动重装载寄存器,定时器的“终点线”

ARR 决定了计数器的最大值。计数器从0走到ARR,然后归零(向上计数模式),或者从ARR走到0(向下计数模式)。

定时周期计算公式:

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

举个例子:72MHz 时钟,PSC=71(1MHz计数时钟),ARR=999,那么定时周期 = (999+1) * 1us = 1ms,也就是 1kHz 的更新频率。

ARR 还有一个重要用途——决定 PWM 的周期。在 PWM 模式下,ARR 决定了 PWM 的频率,而 CCR(捕获/比较寄存器)决定了占空比。

⚠️ 避坑指南: 我曾经在运行中修改 ARR 来改变 PWM 频率,结果没开 ARPE(预装载),计数器直接用了新 ARR,导致当前周期被截断或延长,波形出现毛刺。后来我养成了习惯:只要在运行中改 ARR,一定先开 ARPE。

2.6 五个寄存器的协同工作

这五个寄存器不是孤立的,它们配合起来才能干活。我画个流程图帮你理解:

  1. 配置阶段: 先设 PSC 和 ARR,确定计数时钟和周期。再配 CR1,选择计数方向、预装载使能等。
  2. 启动阶段: CR1 的 CEN 位置1,计数器开始跑。CNT 从0开始累加。
  3. 运行阶段: CNT 每来一个时钟脉冲就加1。当 CNT == ARR 时,产生更新事件。
  4. 事件处理: SR 的 UIF 位置1。如果开了中断,CPU 会去执行中断服务函数。如果没开中断,可以轮询 SR 来判断。
  5. 循环: 计数器归零,重新开始计数。如果 ARPE 开了,新 ARR 值会在此时加载。

你看,整个流程就是一个闭环。我刚开始学的时候,总觉得寄存器太多记不住。后来发现,只要抓住“时钟→分频→计数→比较→事件”这条主线,其他都是细节。

调试口诀: 频率不对查 PSC,周期不对查 ARR,不计数了查 CR1 的 CEN,没中断了查 SR 的 UIF 有没有清。按这个思路排查,90% 的问题都能快速定位。

2.7 实战:快速配置一个1ms定时器

说了这么多,咱们来点实际的。下面是我常用的 1ms 定时器配置代码,用的是 TIM2,系统时钟 72MHz:

void TIM2_1ms_Init(void)
{
    // 1. 开启定时器时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    
    // 2. 配置 PSC:72MHz / (71+1) = 1MHz,即1us计数一次
    TIM2->PSC = 71;
    
    // 3. 配置 ARR:1MHz / (999+1) = 1kHz,即1ms中断一次
    TIM2->ARR = 999;
    
    // 4. 配置 CR1:向上计数,使能自动重装载预装载
    TIM2->CR1 |= TIM_CR1_ARPE;
    
    // 5. 清更新标志,防止刚使能就进中断
    TIM2->SR = ~TIM_SR_UIF;
    
    // 6. 使能更新中断
    TIM2->DIER |= TIM_DIER_UIE;
    
    // 7. 配置NVIC(略,根据你的中断号配置)
    NVIC_EnableIRQ(TIM2_IRQn);
    NVIC_SetPriority(TIM2_IRQn, 1);
    
    // 8. 最后启动定时器
    TIM2->CR1 |= TIM_CR1_CEN;
}

这段代码我用了好几年,基本没出过问题。唯一要注意的是步骤顺序——PSC 和 ARR 一定要在 CR1 使能之前配好,否则计数器可能用默认值跑一段时间,产生意外的中断。

调试小技巧: 如果你不确定定时器有没有正常工作,可以在中断服务函数里翻转一个 GPIO,用示波器看波形。频率对不对,一眼就知道。我调试时经常用这个办法,比看寄存器快多了。