Keil uVision5 定时器配置实战指南:从点击到理解,彻底搞懂定时器怎么用
你有没有过这样的经历?在 Keil uVision5 里打开 STM32CubeMX,点了几下“TIM2”,设了个PSC=71、ARR=9999,然后生成代码、编译下载——灯开始闪了,串口也开始发数据了。看起来一切正常,但心里却空落落的:
“这到底发生了什么?为什么是 71?为什么加 1?中断是怎么触发的?回调函数又是谁调的?”
别担心,这不是你一个人的问题。大多数初学者对定时器的理解,都停留在“照着教程点按钮”的阶段。而真正决定你能否胜任复杂嵌入式开发的关键,就在于能不能把“操作”变成“掌控”。
今天我们就来彻底拆解这个过程:不讲套话,不堆术语,带你从零开始,一步步看清楚定时器背后的每一个齿轮是如何咬合运转的。全程基于 Keil uVision5 + STM32 的真实开发流程,适合所有正在查“keil uvision5使用教程”的朋友。
一、定时器不是魔法,它是 MCU 的“心跳引擎”
先问一个问题:你怎么让一个 LED 每 500ms 闪烁一次?
新手做法:
while (1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); HAL_Delay(500); // 等待500ms }看似没问题,但只要你的系统要干别的事(比如读传感器、处理通信),就会发现:CPU 被卡死在 delay 里了!
这就是软件延时的致命缺陷——它靠“空转”来计时,浪费资源不说,还破坏系统的实时性。
而硬件定时器不同。它的本质很简单:
一个能自动数数的寄存器,接到时钟上,每来一个脉冲就+1,数到头就喊一声:“我满了!”
这一声“我满了”,就是更新事件(Update Event),可以用来触发中断、启动 DMA 或产生 PWM 波形。
STM32 上常见的定时器有三类:
| 类型 | 典型代表 | 特点 |
|---|---|---|
| 基本定时器 | TIM6/TIM7 | 结构简单,只能做中断或 DAC 触发 |
| 通用定时器 | TIM2-TIM5 | 功能全面,支持输入捕获、PWM 输出等 |
| 高级定时器 | TIM1/TIM8 | 支持互补输出、死区控制,专为电机设计 |
我们最常用的是通用定时器,因为它灵活、够用、不烧脑。
二、定时器的核心公式:一切精度都来自这里
你要记住唯一重要的公式:
$$
\text{溢出时间} = \frac{(PSC + 1) \times (ARR + 1)}{时钟频率}
$$
别怕数学,我们来一步步解释:
- 时钟频率:假设你的 STM32 主频是 72MHz(APB1 总线)
- PSC(预分频器):相当于给时钟“减速”。比如 PSC=71,就把 72MHz 分频成
$$
\frac{72\,MHz}{71+1} = 1\,MHz
$$
也就是每 1μs 计一次数。 - ARR(自动重装载值):设定计多少次后归零并触发中断。如果 ARR=9999,那就是数一万次。
所以总时间就是:
$$
10000 \times 1\mu s = 10ms
$$
也就是说,每 10ms 定时器会发出一次“我满了”的信号,你可以在这个时候去执行任务。
✅ 小贴士:为什么都要 +1?
因为计数是从 0 开始的。PSC=0 表示不分频;ARR=0 表示数到 0 就溢出,即只计一次。
三、图形化配置实战:STM32CubeMX + Keil uVision5 协同工作流
Keil uVision5 本身并不负责画引脚、配时钟树,但它和 STM32CubeMX 是黄金搭档。现代嵌入式开发的标准姿势是:
STM32CubeMX 配置外设 → 生成初始化代码 → 导出 Keil 工程 → 在 Keil 中写业务逻辑
下面我们手把手走一遍。
步骤 1:选芯片,建工程
打开 STM32CubeMX,新建项目,选择你的 MCU 型号(如 STM32F103C8T6)。这款“蓝色小板”主频最高 72MHz,自带多个通用定时器,非常适合学习。
步骤 2:启用 TIM2
进入 Pinout 视图,找到 TIM2,右键 → Set Mode → Timer → Internal Clock。
注意:如果你不用 TIM2 输出 PWM,就不需要连接任何引脚。它是纯内部功能模块。
步骤 3:设置参数达成 10ms 中断
切换到Configuration标签页,点击 TIM2,进入参数设置界面:
| 参数 | 设置值 | 含义 |
|---|---|---|
| Clock Source | Internal Clock | 使用内部时钟 |
| Prescaler | 71 | 分频系数,使计数频率为 1MHz |
| Counter Mode | Up | 向上计数模式 |
| Counter Period (ARR) | 9999 | 数到 9999 后溢出 |
| Clock Division | None | 不影响本次实验 |
此时你会发现,工具自动计算出了:
- Counter Clock Frequency: 1 MHz
- Autoreload Preload: Enabled(建议开启,防止修改时出错)
步骤 4:打开中断
点击 NVIC Settings 标签页,勾选 “TIM2 global interrupt”。
这样系统就会在启动文件中注册中断向量,并自动生成中断服务程序框架。
步骤 5:生成 Keil 工程
Project Manager 设置如下:
- Application Structure: Simple
- Toolchain / IDE: MDK-ARM V5
- Code Generator: 按外设生成.c/.h文件(推荐)
点击 Generate Code,稍等片刻,就能在指定目录看到完整的 Keil 工程文件(.uvprojx)。
双击打开,你会发现:
-main.c里已经调用了MX_TIM2_Init();
-stm32f1xx_it.c中有了TIM2_IRQHandler();
-tim.c里包含了 HAL 层的初始化逻辑。
一切都准备好了,只差你在中断里“做点事”。
四、中断回调函数:真正的控制中枢
HAL 库为了简化编程,提供了一个非常实用的机制:回调函数(Callback Function)。
当 TIM2 发生更新事件时,底层会自动调用:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)我们要做的,就是在main.c或其他源文件中实现这个函数,并且判断是不是我们关心的那个定时器。
来看一个经典应用场景:多速率任务调度。
场景需求
在一个温控风扇系统中,我们需要:
- 每 10ms 采样一次温度;
- 每 100ms 更新 LCD 显示;
- 每 1s 向串口发送日志。
这些任务节奏不同,但如果都用单独 delay 控制,代码会乱成一团。更好的方式是:用一个 10ms 中断作为“心跳”,统一分配任务。
实现代码
volatile uint32_t ms_counter = 0; // 必须加 volatile! void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) // 确保是 TIM2 触发的 { ms_counter++; // 10ms 执行一次 Read_Temperature(); // 100ms 执行一次 if (ms_counter % 10 == 0) Update_LCD(); // 1s 执行一次 if (ms_counter % 100 == 0) { Send_Status_Log(); ms_counter = 0; // 归零防溢出 } } }关键细节解析
🔹 为什么要加volatile?
volatile uint32_t ms_counter = 0;因为ms_counter被中断和主循环同时访问。如果不加volatile,编译器可能认为这个变量不会变,直接优化掉读取操作,导致逻辑错误。
🔹 为什么不在中断里做复杂运算?
中断上下文要求快进快出。像浮点计算、printf、malloc这些耗时操作都不能放进去,否则会影响其他中断响应。
正确做法是:在中断中置标志位或采集数据,在主循环中处理逻辑。
例如:
volatile uint8_t temp_ready = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { temp_ready = 1; // 只标记“该干活了” } } // 主循环中检查标志 while (1) { if (temp_ready) { Read_Temperature(); Process_Control_Algorithm(); temp_ready = 0; } Other_Tasks(); }这种方式更安全,也更容易调试。
五、避坑指南:那些没人告诉你却必踩的雷
即使配置正确,你也可能会遇到这些问题。提前知道,少走三天弯路。
❌ 坑点 1:ARR 设置超过 65535
通用定时器的 ARR 是 16 位寄存器,最大值为 65535。如果你想实现 1 秒定时,不能设 ARR=999999。
解决办法:
- 提高 PSC,降低计数频率;
- 或者使用更高精度定时器(如 TIM2 支持 32 位模式);
- 更优方案:保持短周期中断(如 1ms),通过计数累计实现长延时。
❌ 坑点 2:误关自动重载(Auto-reload)
确保在 CubeMX 中启用了Autoreload Preload Enable。否则你改了 ARR 值也不会立即生效,调试时一头雾水。
❌ 坑点 3:NVIC 没开,中断不触发
最容易忽略的一点:虽然你在 TIM2 配置里打开了中断,但 NVIC 必须单独勾选!
检查路径:TIM2 Configuration → NVIC Settings → 勾选对应中断。
✅ 秘籍:用 Keil 调试窗口验证定时器状态
在 Keil uVision5 调试模式下,你可以直观查看定时器运行情况:
- Debug → Start/Stop Debug Session
- Peripherals → Core Peripherals → TIM2
- 查看:
- CNT(当前计数值)
- ARR(目标值)
- SR(状态寄存器,UIF=1 表示已触发更新中断)
还可以配合Event Recorder功能,可视化中断发生的时间点,验证是否准时。
六、不止于延时:定时器还能做什么?
你以为定时器只是用来“每隔几毫秒叫一下”?太小看它了。
✔ PWM 输出:无处不在的调光调速
只需启用通道(如 CH1),设置 PWM 模式,就可以输出占空比可调的方波:
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 5000); // 50% 占空比应用包括:
- LED 调光
- 电机调速
- 舵机角度控制
✔ 输入捕获:精准测量脉冲宽度
接一个超声波模块 HC-SR04?用输入捕获模式记录上升沿和下降沿时间差,轻松算出高电平持续时间。
✔ 编码器接口:直接读取旋转编码器
TIM2 支持编码器模式,两根线接入 A/B 相,就能自动识别正反转和脉冲数,省去外部计数逻辑。
✔ 触发 ADC/DAC:构建同步采样系统
让定时器定期触发 ADC 转换,实现等间隔采样,避免手动触发带来的抖动问题。
写在最后:从“会点”到“真懂”,只差一层窗户纸
你看,定时器并没有那么神秘。它的核心逻辑只有三步:
- 分频(PSC)→ 得到合适的计数频率
- 计数(CNT)→ 数到 ARR 归零
- 通知(中断/DMA/PWM)→ 做你想做的事
当你下次再打开 STM32CubeMX 去配置 TIM2 的时候,请不要再机械地点数字。停下来想想:
“我为什么要设 PSC=71?”
“ARR=9999 对应多少时间?”
“这个中断会不会影响别的任务?”
真正的工程师,不是会用工具的人,而是明白工具为何如此设计的人。
而这条路的起点,往往就是搞懂一个定时器该怎么配。
如果你正在学习嵌入式开发,正在查阅“keil uvision5使用教程”,希望这篇文章能帮你跨过那道从“模仿”到“理解”的门槛。
欢迎留言交流你在定时器配置中遇到的难题,我们一起拆解。