4. 定时器基本原理与寄存器:通用定时器与高级定时器区别、时基单元寄存器(PSC、ARR、CNT)、定时器初始化流程、实战:寄存器方式实现1ms定时中断
定时器,说白了就是单片机的“心跳节拍器”。没有它,你连个精准的LED闪烁都做不出来。我刚开始学单片机时,总觉得定时器就是个计数器,后来踩了坑才发现——这里面的门道深着呢。
4.1 通用定时器 vs 高级定时器:到底差在哪?
STM32的定时器家族分三档:基本定时器、通用定时器、高级定时器。咱们做PWM和中断,主要跟后两个打交道。
| 特性 | 通用定时器(TIMx) | 高级定时器(TIM1/TIM8) |
|---|---|---|
| 时基单元 | 16位PSC + 16位ARR | 16位PSC + 16位ARR |
| PWM通道数 | 4个独立通道 | 6个通道(含互补输出) |
| 死区插入 | ❌ 不支持 | ✅ 支持(电机控制必备) |
| 刹车功能 | ❌ 不支持 | ✅ 支持(紧急停止) |
| 重复计数器 | ❌ 不支持 | ✅ 支持(RCR寄存器) |
| 中断源 | 更新/捕获/比较 | 更新/捕获/比较/刹车/触发 |
我的经验:做普通PWM呼吸灯、舵机控制,通用定时器完全够用。但如果你要做电机FOC控制、三相逆变器,必须上高级定时器——死区时间不设好,MOS管直接炸给你看。我曾经在无刷电机驱动上偷懒用了通用定时器,结果换相时上下管直通,板子冒烟了……
4.2 时基单元三大核心寄存器
不管什么定时器,核心都是这三个寄存器:PSC(预分频器)、ARR(自动重装载)、CNT(计数器)。它们构成了定时器的“心脏”。
4.2.1 PSC(Prescaler)—— 预分频器
PSC把时钟源频率降下来。比如72MHz的时钟,PSC设为71,就变成1MHz。公式很简单:
定时器时钟频率 = 时钟源频率 / (PSC + 1)
注意:PSC是16位的,范围0~65535。我见过有人把PSC设成0,结果定时器跑得飞快,中断响应不过来——系统直接卡死。
4.2.2 ARR(Auto-Reload Register)—— 自动重装载寄存器
ARR决定了定时器计数的“终点”。CNT从0数到ARR,然后归零,产生更新事件。溢出时间公式:
溢出时间 = (PSC + 1) × (ARR + 1) / 时钟源频率
举个例子:72MHz时钟,PSC=71(得到1MHz),ARR=999,溢出时间就是1ms。
4.2.3 CNT(Counter)—— 计数器
CNT是实际计数的寄存器。你可以随时读它,也能写它。调试时我经常读CNT值来判断定时器是否在跑——如果CNT一直不动,八成是时钟没开。
小技巧:初始化时先写PSC,再写ARR,最后写CNT=0。顺序反了可能导致第一次溢出时间不准。我踩过这个坑,调试了半小时才发现。
4.3 定时器初始化流程(寄存器版)
用寄存器操作定时器,其实就五步。我习惯把这五步写成函数,每次复用。
- 开启定时器时钟 —— RCC->APB1ENR(通用定时器)或RCC->APB2ENR(高级定时器)
- 配置PSC和ARR —— 计算好分频系数和重装载值
- 配置中断 —— 使能更新中断,设置NVIC优先级
- 清除标志位 —— 写SR寄存器,把脏数据清掉
- 启动定时器 —— CR1寄存器的CEN位置1
注意:第4步很多人会忘。上电后SR寄存器可能有随机值,不清掉的话,一使能中断就立刻进中断——我第一次就中招了,还以为代码写错了。
4.4 实战:寄存器方式实现1ms定时中断
咱们用TIM2(通用定时器,挂在APB1上)实现1ms中断。时钟源72MHz,APB1定时器时钟也是72MHz(因为APB1预分频器不为1时,定时器时钟翻倍)。
计算参数:
- 目标:1ms = 0.001s
- 时钟频率:72MHz = 72,000,000 Hz
- PSC = 71 → 定时器时钟 = 72M / (71+1) = 1MHz
- ARR = 999 → 溢出时间 = 1M / (999+1) = 1kHz = 1ms
// 定时器2初始化 —— 1ms中断
void TIM2_Init(void)
{
// 1. 开启TIM2时钟(APB1总线)
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 2. 配置预分频器 PSC = 71
TIM2->PSC = 71 - 1; // 注意:实际写入值 = 目标值 - 1
// 3. 配置自动重装载 ARR = 999
TIM2->ARR = 1000 - 1; // 同样,写入值 = 目标值 - 1
// 4. 清除更新中断标志位
TIM2->SR &= ~TIM_SR_UIF;
// 5. 使能更新中断
TIM2->DIER |= TIM_DIER_UIE;
// 6. 配置NVIC(中断优先级)
NVIC_SetPriority(TIM2_IRQn, 2); // 优先级设为2
NVIC_EnableIRQ(TIM2_IRQn);
// 7. 启动定时器
TIM2->CR1 |= TIM_CR1_CEN;
}
// 中断服务函数
void TIM2_IRQHandler(void)
{
// 检查更新中断标志
if(TIM2->SR & TIM_SR_UIF)
{
// 清除标志位(必须软件清除)
TIM2->SR &= ~TIM_SR_UIF;
// 用户代码:1ms执行一次
// 比如:LED翻转、按键扫描、系统滴答计数
}
}
避坑指南:我曾经在中断里做了一堆浮点运算,结果中断执行时间超过了1ms,导致定时器溢出事件丢失。后来我学乖了——中断里只设标志位,主循环里处理业务。记住:中断服务函数要短小精悍,别在里面磨叽。
4.5 定时器核心结构图
下面这张图帮你理清定时器内部的数据流。从时钟源到PSC分频,再到CNT计数,最后触发中断——一条线走到底。
嗯,这张图把整个流程串起来了。你从左边时钟源进来,经过PSC分频,CNT开始计数,数到ARR的值,比较器触发更新事件——要么进中断,要么更新PWM输出。
4.6 调试与故障定位
定时器出问题,无非这几种情况。我列个清单,你照着排查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 定时器不计数 | 时钟未开启 / CEN位没置1 | 读RCC对应使能位,读CR1的CEN位 |
| 中断不触发 | DIER未使能 / NVIC没配 | 检查DIER的UIE位,检查NVIC优先级 |
| 中断频率不对 | PSC或ARR算错了 | 用逻辑分析仪测引脚翻转频率 |
| 进一次中断后不再进 | 中断里没清标志位 | 确认写了SR &= ~UIF |
| 上电立刻进中断 | SR脏数据没清 | 初始化时先清SR再使能中断 |
调试神器:我习惯在中断里翻转一个GPIO,用示波器看波形。频率对不上?直接读CNT和ARR的值,打印出来看。寄存器调试,说白了就是“读出来,对一下”——别怕麻烦,多读几次就找到规律了。
定时器这块,理解了PSC、ARR、CNT三者的关系,你就掌握了80%。剩下的20%是各种模式配置——PWM模式、输入捕获、编码器模式——但时基单元的原理都一样。先把1ms中断跑通,后面做PWM输出就是水到渠成的事。