2. 定时器基础原理:STM32定时器分类与时基单元
定时器这东西,说白了就是单片机的“心跳”。我刚开始做嵌入式那会儿,总觉得定时器不就是计个数嘛,有啥好研究的?后来踩了几个坑才明白——定时器玩不转,PWM、输入捕获、编码器接口这些高级功能根本别想碰。今天咱们就把STM32定时器的老底儿翻出来看看。
2.1 STM32定时器家族:三类定时器
STM32的定时器分三类:基本定时器、通用定时器、高级定时器。它们不是简单的“低配中配高配”关系,而是各有各的专长。
| 类型 | 代表外设 | 时基单元 | 核心能力 | 典型应用 |
|---|---|---|---|---|
| 基本定时器 | TIM6、TIM7 | 16位预分频器 + 16位计数器 + 16位自动重装载 | 仅基础定时、触发DAC | 系统滴答、周期性中断 |
| 通用定时器 | TIM2~TIM5 | 16位预分频器 + 16位计数器 + 16位自动重装载 | 定时、PWM、输入捕获、输出比较 | 电机控制、信号测量 |
| 高级定时器 | TIM1、TIM8 | 16位预分频器 + 16位计数器 + 16位自动重装载 + 重复计数器 | 全部通用功能 + 互补PWM + 死区插入 + 刹车功能 | 三相电机、数字电源 |
我的经验:项目里90%的场景用通用定时器就够了。高级定时器那些死区插入、刹车功能,做电机驱动时才用得上。别一上来就开高级定时器,寄存器多到让你怀疑人生。
2.2 时基单元结构:定时器的“心脏”
不管哪类定时器,核心都是时基单元。它由三个寄存器组成:预分频器(PSC)、计数器(CNT)、自动重装载寄存器(ARR)。这三个家伙配合工作,决定了定时器的“心跳节奏”。
2.3 预分频器(PSC)详解
预分频器的作用,就是把输入的时钟频率降下来。STM32的PSC是16位的,取值范围0~65535。注意,实际分频系数是 PSC + 1。
计算公式:
计数器时钟频率 = 定时器时钟频率 / (PSC + 1)
定时周期 = (ARR + 1) / 计数器时钟频率
举个例子。假设定时器时钟是72MHz,我想要一个1ms的定时中断:
// 目标:1ms = 0.001s
// 72MHz = 72,000,000 Hz
// 先分频到 72,000 Hz(即每秒钟计数72000次)
// PSC = 72,000,000 / 72,000 - 1 = 999
// 然后 ARR = 72,000 * 0.001 - 1 = 71
// 寄存器配置
TIM2->PSC = 999; // 预分频值
TIM2->ARR = 71; // 自动重装载值
TIM2->EGR = TIM_EGR_UG; // 立即更新寄存器
避坑指南:我曾经在项目里直接给PSC写72000,心想“分频到1Hz多好”。结果计数器根本不走——PSC最大只能到65535!记住,16位寄存器上限是65535,别超了。
2.4 自动重装载寄存器(ARR)详解
ARR决定了定时器的溢出周期。计数器从0开始,每来一个时钟脉冲加1,加到ARR的值时,就产生溢出事件,然后计数器清零重新开始。
ARR也是16位的,范围0~65535。但要注意,实际计数值是ARR + 1。为什么?因为计数器是从0开始计数的,0也算一个数。
| ARR值 | 实际计数次数 | 说明 |
|---|---|---|
| 0 | 1 | 每个时钟脉冲都溢出,频率最高 |
| 99 | 100 | 常用做100分频 |
| 65535 | 65536 | 最大定时周期 |
重要提醒:ARR寄存器有“影子寄存器”机制。你写入ARR的值不会立即生效,要等到当前计数周期结束才更新。如果想立即生效,必须设置UG位(更新生成位)。我见过有人调试半天计数器不按预期跑,结果就是忘了这茬。
2.5 计数器模式:向上、向下、中央对齐
通用定时器和高级定时器的计数器有三种工作模式:
- 向上计数模式:从0加到ARR,溢出后回0。最常用。
- 向下计数模式:从ARR减到0,溢出后回到ARR。某些特殊场景用。
- 中央对齐模式:从0加到ARR,再从ARR减到0,来回跑。PWM生成时常用。
我个人习惯,做周期性定时中断就用向上计数模式,简单直接。做PWM时用中央对齐模式,波形更对称。
2.6 定时器初始化流程(代码实战)
说了这么多理论,咱们直接上手配寄存器。下面是一个完整的定时器初始化流程:
void TIM2_Init(void)
{
// 1. 开启定时器时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 2. 设置预分频器
TIM2->PSC = 7199; // 72MHz / (7199+1) = 10kHz
// 3. 设置自动重装载值
TIM2->ARR = 9999; // 10kHz / (9999+1) = 1Hz = 1秒
// 4. 立即更新寄存器(影子寄存器生效)
TIM2->EGR |= TIM_EGR_UG;
// 5. 清除更新标志位
TIM2->SR &= ~TIM_SR_UIF;
// 6. 使能更新中断
TIM2->DIER |= TIM_DIER_UIE;
// 7. 配置NVIC(中断优先级)
NVIC_SetPriority(TIM2_IRQn, 2);
NVIC_EnableIRQ(TIM2_IRQn);
// 8. 启动定时器
TIM2->CR1 |= TIM_CR1_CEN;
}
// 中断服务函数
void TIM2_IRQHandler(void)
{
if(TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF; // 清除中断标志
// 用户代码:每1秒执行一次
}
}
我的习惯:初始化时先配PSC和ARR,再触发UG位更新,最后才开中断和使能定时器。顺序搞反了的话,可能会在配置完成前就触发一次意外中断。嗯,这坑我踩过。
2.7 常见问题与避坑
- 定时器不工作:检查时钟有没有开。RCC那一步最容易忘。
- 时间算不准:确认APB1/APB2的时钟分频系数。STM32的定时器时钟可能和系统时钟不一样。
- 中断不触发:检查NVIC使能了没,中断标志清了没。
- 第一次溢出时间不对:初始化后手动触发UG位,强制更新影子寄存器。
我曾经遇到过一个诡异的问题:定时器中断偶尔会丢。查了两天才发现,中断服务函数里处理时间太长,下一次中断来了还没退出。解决办法是中断里只设标志位,主循环里处理业务逻辑。