第1章:定时器中断——让芯片学会“闹钟”
各位同学好,我是你们的嵌入式讲师。今天咱们聊聊定时器中断。说实话,我刚开始学STM32时,觉得定时器就是个计数器,没啥了不起。直到有一次做电机控制项目,发现没有定时器中断,整个系统就像没装闹钟的人——要么睡过头,要么频繁看表,效率极低。
定时器中断,说白了就是让芯片内部有个“闹钟”。时间到了,它自动提醒CPU:“嘿,该干活了!”CPU放下手头的事,跑去执行中断服务函数,完事再回来继续。这种机制,让我们的程序能精确控制时间,又不浪费CPU资源。
核心公式:T = (PSC+1) × (ARR+1) / Tclk
这个公式,我建议你刻在脑子里。后面所有定时器配置,都离不开它。
1.1 定时器溢出中断配置流程
配置定时器中断,其实就五步。我习惯按这个顺序来,不容易漏。
- 使能定时器时钟——没电,啥也干不了
- 配置时基单元——设置PSC和ARR,决定定时多久
- 使能更新中断——告诉定时器:溢出时喊我一声
- 配置NVIC——设置中断优先级,决定谁先响应
- 启动定时器——开始计时
嗯,这里要注意:很多人会忘记第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