STM32 定时器实战记录(一)—— 定时中断 vs 外部时钟,TIM 到底怎么用?
一、先说结论
|
用法 |
本质 |
适合场景 |
|---|---|---|
|
定时中断 |
数内部时钟 |
系统节拍、周期性任务 |
|
外部时钟 |
数外部引脚 |
计数、测频、编码器 |
👉 定时器的核心不是“定时”,而是“计数”。
二、为什么要把这两件事放在一起写?
在第一篇里,我用 EXTI 检测红外遮挡;
在第二篇里,我用 TIM2 定时中断 给 EXTI 消抖。
但当时我忽略了一个问题:
如果我只是想“数遮挡次数”,为什么不用 TIM 直接数?
于是这次,我同时试了两种用法:
-
TIM 定时中断:系统心跳
-
TIM 外部时钟:数外部脉冲
三、TIM 定时中断:给系统“心跳”
1️⃣ 配置思路
-
时钟源:内部时钟(APB1)
-
预分频 PSC
-
自动重装 ARR
-
更新中断
-
NVIC
2️⃣ 核心代码
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_Period = 7200 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 10000 - 1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
3️⃣ 中断里只做一件事
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update))
{
Num++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
✅ 不延时
✅ 不刷屏
✅ 不碰硬件
四、TIM 外部时钟:数外部脉冲
1️⃣ 什么是外部时钟模式 2?
TIM 可以不直接数内部时钟,而是:
数某个 GPIO 引脚上的边沿
这正是红外计数的“理想形态”。
2️⃣ 硬件连接

|
TIM |
STM32 |
外部模块 |
|---|---|---|
|
ETR |
PA0 |
红外 DO |
3️⃣ 核心配置思路
-
使用 外部时钟模式 2
-
时钟来自 PA0(TIM2_ETR)
-
开启 GPIOA 时钟
-
配置 PA0 为上拉输入
-
使用最大滤波器,抗抖动
4️⃣ 关键代码
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; //GPIO初始化
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, 0x0F);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //时基单元初始化
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 10 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //中断输出配置
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //NVIC中断分组
NVIC_InitTypeDef NVIC_InitStructure; //NVIC配置
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); //TIM使能
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2); //返回定时器TIM2的CNT
}
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Timer_Init(); //定时中断初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Num:"); //1行1列显示字符串Num:
OLED_ShowString(2, 1, "CNT:"); //2行1列显示字符串CNT:
while (1)
{
OLED_ShowNum(1, 5, Num, 5); //不断刷新显示Num变量
OLED_ShowNum(2, 5, Timer_GetCounter(), 5); //不断刷新显示CNT的值
}
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Num ++; //Num变量自增,用于测试定时中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
✅ TIM2的ETR引脚固定为PA0的硬件限制说明
STM32的TIM2定时器外部触发输入(ETR)引脚硬件绑定在PA0(部分型号可能复用为PA5/PB3,需查阅具体芯片手册)。
✅ 红外遮挡计数原理详解
当红外对管的光路被遮挡时,接收端会产生上升沿/下降沿信号,通过TIM2的ETR引脚触发下列硬件自动流程:
- 信号经输入滤波器(可通过TIMx_SMCR寄存器的ETF[3:0]配置消抖时间)
- 边沿检测器(TIMx_SMCR的ETP位选择上升/下降沿)
- 计数器CNT立即+1(无需CPU干预)
典型应用场景示例:
- 传送带物品计数(每遮挡一次计1件)
- 旋转编码器脉冲采集
✅ 硬件消抖与EXTI方案对比优势
- 硬件消抖:通过TIMx_SMCR寄存器的ETF[3:0]直接配置滤波时钟周期(例如文中设置0x6表示8个时钟周期滤波),消除机械抖动
- 无EXTI需求:ETR信号直接接入定时器硬件单元,比EXTI中断方案减少以下耗时步骤:
- 中断优先级配置
- 上下文保存/恢复
- 软件计数器操作
- 实时性保障:CNT自增动作在检测到有效边沿后立即执行,延迟仅取决于TIM2时钟频率(例如72MHz下延迟<14ns)
五、两种用法对比
|
对比项 |
定时中断 |
外部时钟 |
|---|---|---|
|
时钟来源 |
内部 |
外部 GPIO |
|
是否进 IRQ |
✅ 是 |
❌ 否 |
|
CPU 占用 |
有 |
几乎为零 |
|
适合 |
心跳、任务调度 |
计数、测频 |
|
红外场景 |
消抖 |
直接计数 |
👉 红外计数,最优解是外部时钟,不是 EXTI。
六、我踩过的坑
-
ETR 引脚固定
不是随便一个 GPIO 都能当外部时钟 -
极性配置反了
计数方向完全不对 -
忘了 TIM 仍然是“计数器”
一直把它当“定时器”用
七、给同样在入门的朋友建议
1️⃣ 不要把 TIM 只当成 Delay 的替代品
2️⃣ 学会把 TIM 当“计数器”用
3️⃣ 外部事件能交给硬件,就别麻烦 CPU
4️⃣ 工程里,“少进中断”本身就是一种优化
八、总结一句话
定时中断,是让系统知道“时间到了”;
外部时钟,是让系统知道“发生了几次”。
真正会用 STM32 的人,不是会写 EXTI,而是会用 TIM。
更多推荐


所有评论(0)