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分两步:
- 设置中断优先级:在NVIC->IP寄存器里,配置TIM2的中断优先级。
- 使能中断通道:在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响应,一目了然。
3.9 常见问题与避坑
- 定时器不工作? 先检查CR1寄存器的CEN位有没有置1。我见过有人配完所有寄存器,唯独忘了启动计数器。
- 中断频率不对? 用公式反算一下:(PSC+1)×(ARR+1)是不是等于时钟频率/目标频率。我曾经把PSC和ARR的值搞反了,结果中断频率差了1000倍。
- 进不了中断? 检查NVIC的使能位和优先级分组。有时候优先级分组配置不对,低优先级的中断会被屏蔽。
- 中断响应太慢? 如果中断服务函数里做的事情太多,会影响下一次中断的响应。建议中断里只做标记,具体处理放到主循环里。
总结一下: 定时器中断配置,说白了就是“开时钟、设分频、定周期、开中断、配NVIC”这五步。每一步都有对应的寄存器,配对了就能跑起来。我建议你拿到开发板后,先照着上面的代码敲一遍,然后用示波器或者逻辑分析仪抓一下引脚电平变化,亲眼看到1ms的方波,比什么都管用。