4. 定时器中断:定时器的工作原理、定时器中断的配置、定时器中断实现精确延时
各位同学,今天我们来聊聊单片机开发里最常用的外设之一——定时器。说它常用,是因为几乎每个嵌入式项目都离不开它。我刚开始做项目时,总觉得定时器就是“数数”,后来踩了不少坑才明白,这里面的门道其实挺深的。
4.1 定时器的工作原理
定时器,说白了就是一个会自己加1的计数器。它内部有一个寄存器,每来一个时钟脉冲,这个寄存器的值就加1。当它加到最大值(比如16位定时器就是65535)时,再+1就会溢出,回到0重新开始。
这个“溢出”事件,就是定时器中断的触发源。
核心概念:定时器 = 计数器 + 时钟源 + 自动重装载机制
我画了一张图,帮你理清定时器的工作流程:
定时器的核心公式就一个:
溢出时间 = (ARR + 1) × (PSC + 1) / 定时器时钟频率
其中:
- ARR:自动重装载寄存器,决定计数上限
- PSC:预分频器,把时钟频率降下来
- 定时器时钟频率:通常来自系统时钟或专门的定时器时钟
举个例子,如果系统时钟是72MHz,你想产生1ms的定时中断:
- 先设置PSC=71,把72MHz分频成1MHz(每1us计数一次)
- 再设置ARR=999,计数1000次就是1ms
嗯,这里要注意:预分频器是16位的,最大只能到65535。如果你需要更长的定时,就得用软件计数器再累加。
4.2 定时器中断的配置
配置定时器中断,我习惯按下面这几步来走。每一步都有它的道理,别跳步骤。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 使能定时器时钟 | RCC_APB1PeriphClockCmd() 或类似函数 |
| 2 | 配置预分频器 (PSC) | 决定计数时钟频率 |
| 3 | 配置自动重装载值 (ARR) | 决定溢出周期 |
| 4 | 配置计数模式 | 向上/向下/中央对齐 |
| 5 | 使能更新中断 | TIM_IT_Update |
| 6 | 配置NVIC优先级 | 设置抢占优先级和子优先级 |
| 7 | 使能定时器 | TIM_Cmd() 启动计数 |
| 8 | 编写中断服务函数 | 处理溢出事件 |
下面是一个完整的配置代码示例,基于STM32标准库:
// 定时器中断配置 - 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_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseStructure.TIM_Period = 999; // ARR = 999
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // 高级定时器才用
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 = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 5. 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
// 中断服务函数
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
// 清除中断标志位
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 用户代码:每1ms执行一次
// 比如:软件计数器累加、LED闪烁、ADC采样等
}
}
小技巧:中断服务函数里要尽量少做事。我一般只放一个标志位,真正的处理放在主循环里。否则中断嵌套多了,系统响应会变慢。
4.3 定时器中断实现精确延时
用定时器中断做延时,比软件延时(比如for循环空转)靠谱多了。软件延时受编译器优化、中断干扰影响很大,而定时器是硬件在跑,不受这些因素干扰。
我常用的方法有两种:
方法一:查询标志位法
这种方法适合短延时,不需要中断服务函数:
// 微秒级延时 - 查询方式
void delay_us(uint32_t us)
{
TIM_SetCounter(TIM2, 0); // 计数器清零
TIM_Cmd(TIM2, ENABLE); // 启动定时器
while (TIM_GetCounter(TIM2) < us); // 等待计数到目标值
TIM_Cmd(TIM2, DISABLE); // 停止定时器
}
不过要注意,这种方法会阻塞CPU,不适合在中断里调用。
方法二:中断累加法
这种方法适合长延时,不阻塞CPU:
volatile uint32_t g_tick_ms = 0; // 全局毫秒计数器
// 1ms中断服务函数
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
g_tick_ms++; // 每1ms加1
}
}
// 毫秒级延时函数
void delay_ms(uint32_t ms)
{
uint32_t start = g_tick_ms;
while ((g_tick_ms - start) < ms); // 非阻塞等待
}
我曾经踩过的坑:g_tick_ms这个变量一定要加volatile关键字!否则编译器优化后,while循环里可能永远读不到更新后的值。我有个项目因为这个bug排查了整整两天。
你想想看,用这种方法,主程序可以同时做其他事情,延时到了自然就知道。这就是“非阻塞延时”的思想。
4.4 常见问题与避坑指南
做定时器中断开发,有几个地方特别容易出问题:
- 中断标志位没清除:会导致一直进中断,系统卡死。每次进中断第一件事就是清标志。
- 预分频器配置错误:PSC和ARR都是16位的,别超范围。我见过有人把PSC设成100000,结果定时器根本不工作。
- 中断优先级冲突:多个中断共用优先级时,可能出现响应延迟。建议把定时器中断优先级设高一些。
- 变量类型溢出:g_tick_ms用uint32_t,能跑49天不溢出。如果项目需要长期运行,记得处理溢出情况。
总结一下:定时器中断的核心就是“硬件计数+软件处理”。配置时注意时钟源、预分频、重装载值这三个参数;使用时注意中断服务函数的效率;调试时注意变量声明和标志位清除。掌握了这些,你就能做出精确到微秒级的延时和定时任务。