STM32TIM定时器
TIM 定时器(Timer)在 STM32 里是一个非常核心的外设,STM32 的 TIM 不只是“闹钟”,它还能做信号测量、波形产生、事件计数和外设同步,是一个全能的时间事件处理器。
1.整体信号流
外部脉冲 (PA0) → GPIO → ETR 外部时钟 → 外部时钟模式2 → PSC → CNT → ARR → 中断控制器 → NVIC → 中断函数
图中每个模块的详细解析 + 对应代码
GPIO 输入
位置:左下角绿色框 "GPIO"(连接到 ETR 外部时钟)
硬件功能:
将外部引脚 PA0 接收到的电平信号送入 TIM2 的 ETR 引脚
ETR = External Trigger
PA0 内部有上拉电阻,空闲时是高电平,外部脉冲会产生高低变化
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // ETR 引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
上拉防止悬空
TIM2 的 ETR 功能固定映射在 PA0,所以必须开 GPIOA 时钟
ETR 外部时钟模块
硬件功能:
接收外部脉冲信号
可以设定极性(上升沿 / 下降沿)和滤波
生成一个定时器时钟 CK_PSC(送到 PSC)
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF, // 不分频TIM_ExtTRGPolarity_NonInverted, // 上升沿有效0x00 // 不滤波
);
为什么模式 2:
模式 1 会把 ETR 当作触发输入(需要触发选择器)
模式 2 是直接用 ETR 作为时钟源,每个脉冲都推动 CNT +1,非常适合做外部计数
时钟模式选择器(黄色模块)
位置:黄色框(内部时钟模式 / 外部时钟模式1 / 模式2 / 编码器模式)
硬件功能:
决定 CNT 的驱动时钟从哪里来
TIM_ETRClockMode2Config 把模式选择器切到了 “外部时钟模式 2”
这样内部时钟就被断开,CK_CNT 直接来自 ETR
PSC 预分频器
硬件功能:
对 CK_CNT 再做一次整数分频
输出 PSCCK(预分频后的时钟)驱动 CNT
TIM_TimeBaseInitStruct.TIM_Prescaler = 1 - 1; // 0
效果:
PSC=0 → 不分频 → 每个外部脉冲直接推动 CNT +1
补充:
如果 PSC=9,就会外部脉冲 10 次 CNT 才加 1
CNT 计数器 & ARR 自动重装器
硬件功能:
CNT:记录当前计数值
ARR:最大计数值(溢出点)
TIM_TimeBaseInitStruct.TIM_Period = 10 - 1; // ARR=9
效果:
CNT 从 0 数到 9 → 触发更新事件(溢出)→ CNT 清零
用途:
用 ARR 控制溢出频率
外部脉冲计数中 ARR 越小,中断触发越频繁
中断输出控制
硬件功能:
根据事件(溢出、捕获、比较等)产生中断请求
TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 清除初始溢出标志
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 开溢出中断
作用:
确保中断只在真正溢出时产生
Update 事件就是 CNT 溢出或软件触发
NVIC
硬件功能:
管理所有中断优先级、屏蔽、触发
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
作用:
设置中断优先级(可以和其他外设中断协调)
开启 TIM2 中断入口
中断服务函数
硬件功能:
NVIC 调用 TIM2_IRQHandler
程序员在里面写需要做的事
void TIM2_IRQHandler(void)
{if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){Num++; // 记录溢出次数TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
效果:
每 10 个外部脉冲 → Num++
最终总脉冲数 = Num*10 + TIM_GetCounter(TIM2)
代码和图的对应关系
图中模块 | 代码配置位置 |
---|---|
RCC 内部时钟 | RCC_APB1PeriphClockCmd / RCC_APB2PeriphClockCmd |
ETR 外部时钟 | GPIO_Init(GPIOA,...) + TIM_ETRClockMode2Config |
PSC 预分频器 | TIM_TimeBaseInitStruct.TIM_Prescaler = 1-1; |
ARR 自动重装器 | TIM_TimeBaseInitStruct.TIM_Period = 10-1; |
CNT 计数器 | TIM_GetCounter(TIM2) |
中断输出控制 | TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE) |
NVIC | NVIC_Init(...) |
中断执行 | TIM2_IRQHandler |
总体代码实现
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"uint16_t Num;int main(void)
{OLED_Init();Timer_Init();OLED_ShowString(1,1,"Num:");OLED_ShowString(2,1,"CNT:");while(1){OLED_ShowNum(1,5,Num,5);OLED_ShowNum(2,5,Timer_GetCounter(),5);}
}
#include "stm32f10x.h" // Device header
extern uint16_t Num;void Timer_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00);TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//配置 TIM2 的时间基准(即每计满多少次产生中断)TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;TIM_TimeBaseInitStruct.TIM_Period=10-1;TIM_TimeBaseInitStruct.TIM_Prescaler=1-1;TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);TIM_ClearFlag(TIM2,TIM_FLAG_Update);TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_Init(&NVIC_InitStructure);TIM_Cmd(TIM2,ENABLE);
}uint16_t Timer_GetCounter(void)
{return TIM_GetCounter(TIM2);
}void TIM2_IRQHandler(void)
{if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET){Num++;TIM_ClearITPendingBit(TIM2,TIM_IT_Update);}
}
关键词(术语)
CK_CNT / CK_INT:定时器的输入时钟(内部时钟或外部脉冲经过选择器到来的信号)。
PSC(TIMx_PSC):预分频寄存器,设置分频值 N,实际分频比为 N+1。
CK_PSC:PSC 处理后的时钟(即 PSC 的输出,驱动 CNT 增加)。
CNT(TIMx_CNT):计数器寄存器,实际累加的值。
ARR(TIMx_ARR):自动重装寄存器,CNT 达到 ARR 时发生溢出(更新事件 UEV)。
ARPE(Auto-reload preload enable):ARR 的“预装/缓冲”开关(1=有预装,0=无预装)。
UEV(Update Event):更新事件(通常由 CNT 溢出产生),会把 shadow → active、产生 UIF 标志、中断等。
UG(在 EGR 寄存器里写 TIM_EGR_UG):软件强制产生一次 UEV,立刻将 shadow 寄存器写入 active(常用来同步载入 PSC/ARR)。
2.预分频器时序
PSC 的数值 N 表示分频比 = N + 1,也就是说外部/内部输入每来 (N+1) 个 CK_CNT 脉冲,PSC 输出一个 CK_PSC 脉冲,CNT 才会 +1
计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)
PSC 的“应用时机”(buffering)
PSC 写入不是立即改变工作分频,写入 PSC 寄存器后,新的 PSC 值 要等到下一个 UE(update event)才真正加载到定时器的工作分频器里(即 PSC 有影子/缓冲机制)。如果你希望立即生效,可以在写 PSC 后强制产生一次 UG(写 EGR 的 UG 位),这样会马上把写入值载入。
时序小例子
假设外部脉冲序列(CK_CNT)为: P1 P2 P3 P4 P5 ...
若 PSC = 0(N=0) → division = 1:
每个脉冲都直接传给 CNT,CNT 增加:CNT在P1、P2、P3... 处分别 +1。
若 PSC = 3(N=3) → division = 4:
PSC 内部累加 4 次脉冲后才产生一次 CK_PSC。举例:
P1 → PSC count=1(不增CNT)
P2 → PSC count=2
P3 → PSC count=3
P4 → PSC count=4 → PSC 溢出 → 产生一个 CK_PSC → CNT 增 1(此时CNT++发生在P4的时刻)
因此 CNT 的上升点只在每 4 个输入脉冲处发生。
计数器时序
计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)
定时器时钟源经过两级分频后,最终决定 CNT(计数器)多久溢出一次:
第一步:时钟源 CK_PSC
来自内部时钟(CK_INT)或外部时钟(ETR、ITRx等)。
CK_PSC 表示 进入预分频器 PSC 之前的时钟频率。
第二步:预分频器(PSC)分频
PSC 的值是 0~65535,实际分频系数是 (PSC + 1)。
分频后的时钟叫 CK_CNT
第三步:自动重装器(ARR)决定计数周期
CNT 从 0 计数到 ARR,然后溢出并产生更新事件(中断)。
CNT 的计数周期是 (ARR + 1) 个 CK_CNT 周期。
3.无预装(ARPE=0) vs 有预装(ARPE=1)
这是经常会搞混的部分——差别在于你写入 TIMx_ARR(或 CCRx、PSC 等)后,什么时候“成为生效值”。
ARPE = 0(无预装)——写入立即生效
写 TIMx_ARR = new 会 立即改变活动(active)ARR(不进 shadow)。定时器在下一次计数边沿(或下一次判断时)就以新 ARR 进行比较/溢出判断。
副作用:如果当前 CNT 值已经 ≥ 新 ARR,可能会在很短的下一个时钟边沿就触发一次更新事件(UEV),这个细节在参考手册的时序图中有展示(写 ARR 后可能马上导致溢出/UEV)。所以写 ARR 时要小心,可能瞬间会产生一次 UIF/中断。
ARPE = 1(有预装)——写入先进 shadow,等待 UEV 再生效
写 TIMx_ARR = new 会把 new 存入预装寄存器(shadow),不影响当前活动的 ARR。只有当 TIM 产生下一次 UEV(通常是 CNT 溢出/下次重启,或你写 EGR 的 UG 强制产生 UEV)时,shadow 的值才会传给 active ARR,开始生效。参考手册有对应的“shadow → active 在 UEV 时传输”的时序图。
可视化对比(简化时序)
假设当前活动 ARR = 9(旧值),CNT 现在为 5,外部脉冲继续来:
ARPE = 0(立即生效):
时刻 t0: CNT=5, 写入 ARR = 6(立即生效)
下一个脉冲到 t1 -> CNT变为6,检测 CNT==ARR -> 发生溢出/UEV(很快就触发)
→ 所以写入后在下一个计数边沿就可能产生 UEV。
ARPE = 1(缓冲):
时刻 t0: CNT=5, 写入 ARR = 6(写入shadow,active仍为9)
CNT 继续计数到 9 -> 在 CNT 到达 9 溢出 -> 触发 UEV
在该 UEV 上,shadow(=6) 被加载到 active ARR
从下一周期开始使用 ARR=6