2. 定时器核心寄存器:TIMx_CR1、TIMx_SR、TIMx_CNT、TIMx_PSC、TIMx_ARR 深度解析
说实话,我刚接触STM32定时器那会儿,看着寄存器手册里密密麻麻的位定义,头都大了。后来调了几个项目才明白——真正核心的寄存器,其实就五个。把这五个搞透了,定时器这块你就通了八成。
今天咱们就一个一个掰开揉碎了讲。我会结合我实际调试中踩过的坑,帮你把这些寄存器的脾气摸清楚。
2.1 TIMx_CR1 — 控制寄存器,定时器的“总开关”
CR1 寄存器,说白了就是定时器的控制面板。你想想看,一个定时器要不要启动、怎么计数、要不要自动重装,全由它说了算。
我个人习惯,初始化定时器时第一个动的就是 CR1。它的几个关键位,我列个表给你看:
| 位段 | 名称 | 作用 | 我常用的配置 |
|---|---|---|---|
| 位0 | CEN | 计数器使能,1启动,0停止 | 最后才置1,先配好其他寄存器 |
| 位4 | DIR | 计数方向:0向上,1向下 | 向上计数用得最多 |
| 位7 | ARPE | 自动重装载预装载使能 | 建议置1,防ARR突变 |
| 位5~6 | CMS | 中央对齐模式选择 | PWM用边沿对齐时设00 |
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。这时候如果你开了中断,就会进中断服务函数。如果你用轮询方式,就得手动清这个标志。
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,下降沿捕获时再读一次,差值就是脉宽。这个用法在输入捕获模式下特别常见。
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秒中断一次。
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(捕获/比较寄存器)决定了占空比。
2.6 五个寄存器的协同工作
这五个寄存器不是孤立的,它们配合起来才能干活。我画个流程图帮你理解:
- 配置阶段: 先设 PSC 和 ARR,确定计数时钟和周期。再配 CR1,选择计数方向、预装载使能等。
- 启动阶段: CR1 的 CEN 位置1,计数器开始跑。CNT 从0开始累加。
- 运行阶段: CNT 每来一个时钟脉冲就加1。当 CNT == ARR 时,产生更新事件。
- 事件处理: SR 的 UIF 位置1。如果开了中断,CPU 会去执行中断服务函数。如果没开中断,可以轮询 SR 来判断。
- 循环: 计数器归零,重新开始计数。如果 ARPE 开了,新 ARR 值会在此时加载。
你看,整个流程就是一个闭环。我刚开始学的时候,总觉得寄存器太多记不住。后来发现,只要抓住“时钟→分频→计数→比较→事件”这条主线,其他都是细节。
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 使能之前配好,否则计数器可能用默认值跑一段时间,产生意外的中断。