定时器应用
Author:余生
一、硬件连接
二、新建文件
如图:
三、代码编写
Timer.h
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
#endif
2
3
4
5
6
Timer.c
#include "stm32f10x.h" // Device header
void Timer_Init(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 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_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);
}
// void TIM2_IRQHandler(void)
// {
// if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
// {
// // 这里写你想做的事,比如:LED1_Turn();
// TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// }
// }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
函数: Timer_Init()
详解
第 1 步:打开 TIM2 的电源
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
类比:
“我要用定时器 TIM2 了,请给它通电!”
注意:TIM2 属于 APB1 总线(低速外设),和 GPIO 不一样,所以要开 APB1 的时钟。
第 2 步:使用内部时钟
TIM_InternalClockConfig(TIM2);
意思是:
“TIM2 你用自己内部的时钟来计数,别等外部信号。”
默认就是内部时钟,这句其实可以省略,但写上更清晰。
第 3 步:配置定时器的 “计数规则”
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
这是一个 “配置表”,我们要填几个关键参数:
填表开始
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
计数方式:向上计数从 0 开始加,一直加到一个数,然后 “溢出”,产生中断,再回到 0 重新开始。
就像秒表:0 → 1 → 2 → … → 99,然后 “叮!” 归零。
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
预分频器(PSC):把时钟变慢
- STM32 主频是 72MHz
- 我们想让定时器每 1ms 计一次数,就要先 “减速”
7200 - 1
表示:每 7200 个时钟周期,计数器才 + 1
计算:
72MHz / 7200 = 10,000Hz → 每0.1ms计一次数
为什么减 1?因为硬件设计就是 “加 1 后才用”,所以填 7199 实际就是分频 7200。
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
自动重装载值(ARR):数到多少会 “叮”?
- 计数器从 0 开始往上加,加到
10000 - 1 = 9999
时,就 “溢出”,产生中断 - 然后回到 0 重新开始
计算:
每0.1ms加1 → 加10000次 = 1秒
所以:每 1 秒产生一次中断!
为什么减 1?同上,硬件从 0 开始数,数到 9999 刚好是 10000 步。
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
时钟分频,一般用不到,设为 1 就行(不额外分频)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
重复计数器,高级定时器用得多,这里设为 0(不用)
第 4 步:初始化定时器
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
意思是:
“把上面填好的配置表交给 TIM2,开始生效!”
第 5 步:清除中断标志,开启中断
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
2
ClearFlag
:先把 “已经溢出” 的标志清掉(防止一上来就进中断)ITConfig
:允许 “更新中断”(即溢出时触发中断)
就像:先把闹钟铃声关掉,再打开闹钟功能。
第 6 步:配置中断优先级(NVIC)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
设置中断优先级分组:2 位抢占优先级,2 位子优先级
我们现在只用一个中断,知道这句是 “设置中断规则” 就行。
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);
2
3
4
5
意思是:
“TIM2 的中断我要用,优先级设为 2(抢占)和 1(子),请注册到中断系统中”
就像:把闹钟的 “响铃通道” 打开,并设置它响的时候能不能打断别的任务。
第 7 步:启动定时器
TIM_Cmd(TIM2, ENABLE);
启动!开始计数!
从现在开始,TIM2 就开始工作了:每 1 秒 “溢出” 一次,触发中断。
被注释掉的中断服务函数
// void TIM2_IRQHandler(void)
// {
// if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
// {
// // 这里写你想做的事,比如:LED1_Turn();
// TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// }
// }
2
3
4
5
6
7
8
这是 “闹钟响了之后要做的事”
TIM2_IRQHandler
是中断函数名,固定写法TIM_GetITStatus
判断是不是更新中断TIM_ClearITPendingBit
清除中断标志,不然会一直进中断
你必须在这里写上你想执行的任务,比如翻转 LED、刷新屏幕等。
总结
定时器就像一个自动闹钟,
先设置多久响一次(PSC + ARR),
再打开中断,告诉 CPU “到时间叫我”,
最后在
TIM2_IRQHandler
里写 “响了之后做什么”。
本例定时时间计算(重点!)
主频:72MHz
分频:7200 → 72MHz / 7200 = 10kHz → 每0.1ms计一次数
计数:10000次 → 0.1ms × 10000 = 1000ms = 1秒
2
3
所以:每 1 秒产生一次中断
如果你想改成 500ms,就把 Period
改成 5000 - 1
注意
- 定时器中断适合做 “周期性任务”,比
Delay_ms()
更高效 Delay_ms()
会卡住 CPU,而中断是 “后台悄悄数”,CPU 可以干别的事- 中断里不要写太多代码,尽量只做标记(比如设置一个 flag),在主循环里处理
main.c
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "stm32f10x.h" // Device header
uint16_t Num;
int main(void) {
OLED_Init();
Timer_Init();
OLED_ShowString(1, 1, "Num:");
while (1) {
OLED_ShowNum(1, 5, Num, 5);
}
}
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) {
Num++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这个程序是干啥的?
让 OLED 屏幕上的一个数字(Num)每 1 秒自动加 1!
就像一个计时器:0 → 1 → 2 → 3 → … 一直往上数,显示在屏幕上。
程序结构解析
第一步:包含头文件
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
2
3
4
意思是:
“我要用 STM32、延时、OLED 屏幕、定时器功能,请把它们都准备好。”
尤其是 Timer.h
,它包含了你之前写的 Timer_Init()
函数。
第二步:定义一个全局变量
uint16_t Num;
这是一个 “计数器”,用来存当前的数字,初始值是 0。
uint16_t
表示无符号 16 位整数(0 ~ 65535)- 每过 1 秒,它就会 +1
第三步:主函数 main()
1. 初始化
OLED_Init();
Timer_Init();
2
- 初始化 OLED 屏幕(点亮)
- 初始化定时器 TIM2(设好 1 秒中断)
就像:打开显示器 + 设置好闹钟
2. 显示提示文字
OLED_ShowString(1, 1, "Num:");
在第 1 行第 1 列显示:
Num:
提示用户:后面这个数字是计数值。
3. 进入主循环
while (1)
{
OLED_ShowNum(1, 5, Num, 5);
}
2
3
4
意思是:
“一直重复做一件事:把当前的
Num
值显示在屏幕第 1 行第 5 列,显示 5 位数”
注意:这里没有 Delay_ms()
,因为 OLED 刷新不需要延时,CPU 跑得很快。
第四步:中断函数 TIM2_IRQHandler
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Num ++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
2
3
4
5
6
7
8
这是 “闹钟响了之后要做的事”:
- 判断是不是 TIM2 的 “溢出中断”(也就是 1 秒到了)
- 如果是,就把
Num
加 1 - 清除中断标志,准备下一次中断
关键点:这个函数是 “自动被调用” 的,不需要你在 main()
里写。
主循环 vs 中断:谁在干活?
主循环(while (1)) | 中断函数(TIM2_IRQHandler) |
---|---|
不停刷新屏幕,显示当前 Num 的值 | 每隔 1 秒自动执行一次,让 Num +1 |
负责 “显示” | 负责 “计时 + 计数” |
CPU 大部分时间在这里 | 只在 1 秒到时 “插进来” 执行一下 |
类比:
你一边看电视(主循环),
一边等着闹钟响(中断)。
闹钟一响,你就按一下计数器按钮(Num++),
然后继续看电视。
屏幕显示效果
Num: 0
1 秒后:
Num: 1
再 1 秒后:
Num: 2
…… 一直往上数,直到 65535 后回到 0(因为是 16 位)
注意
- 中断里不要放延时,否则会卡住整个系统
- 中断里不要频繁刷新 OLED,因为 OLED 刷新较慢,会影响其他任务
- 正确做法:中断只做 “标记” 或 “加减”,主循环负责 “显示”