前言

在嵌入式开发中,定时器(Timer)的重要性仅次于GPIO。无论是简单的LED闪烁、精确的时间控制、PWM电机驱动,还是复杂的输入捕获,都离不开定时器。

很多初学者(包括曾经的我)在配置定时器时,往往对 PSCARR 这些参数感到困惑:为什么要减1?时钟源到底是多少?

本文将以STM32F1/F4系列为例,从硬件原理到CubeMX配置,再到代码实战,带你彻底搞懂STM32定时器。


一、 定时器的分类与功能

STM32的定时器资源非常丰富,通常分为以下三类。理解分类有助于我们“杀鸡不用牛刀”。

定时器类型 常见编号 主要功能 适用场景
基本定时器 TIM6, TIM7 只有计数功能,无输入输出通道 简单的延时、驱动DAC
通用定时器 TIM2~TIM5 计数、输入捕获、输出比较(PWM)、编码器接口 此时最常用,电机控制、测量脉宽
高级定时器 TIM1, TIM8 包含通用所有功能 + 死区控制、刹车功能 三相无刷电机、复杂电源控制


二、 核心原理:定时器是怎么工作的?

定时器的本质就是一个“只会加法的计数器”。它的工作流程可以概括为:时钟源 -> 分频器 -> 计数器 -> 溢出中断

为了方便理解,我们可以用“水桶接水”的模型来类比:

  1. 时钟源 (CK_INT):相当于水龙头,水流的速度就是频率(比如72MHz)。
  2. 预分频器 (PSC):相当于限流阀。如果PSC=71,表示每来72滴水,只放1滴过去。这样可以让计数更慢、时间更长。
  3. 自动重装载寄存器 (ARR):相当于水桶的深度。设定一个值(比如4999),当水装到这个高度时,水桶就会翻倒。
  4. 计数器 (CNT):相当于当前的水位。水滴一滴滴进来,CNT就从0开始一直加。
  5. 更新中断 (UEV):当水位 (CNT) 达到桶深 (ARR) 时,水桶倒水,并触发一次中断,CNT清零重新开始。

三、 关键公式(敲黑板!)

在配置定时器时,最重要的就是计算 溢出时间(Tout)

  • CK_PSC:定时器的输入时钟频率(注意:通常APB1总线的定时器时钟会倍频,STM32F1的TIM2-TIM7通常为72MHz)。
  • PSC:预分频系数 (Prescaler)。为什么要+1? 因为计算机从0开始计数,写0代表1分频,写71代表72分频。
  • ARR:自动重装载值 (AutoReload Register)。为什么要+1? 同理,计数器从0数到ARR,总共数了ARR+1个数。

举个栗子 🌰

假设系统时钟CK_PSC​=72MHz,我们要实现 500ms (0.5s) 的定时中断:

  1. 确定分频系数 (PSC):为了好算,我们将72MHz分频成10kHz(即1秒数10000个数)。
    • PSC=7200−1=7199PSC=7200−1=7199
    • 此时计数器每秒增加 10000 次,即每个计数周期 0.1ms0.1ms。
  2. 确定重装载值 (ARR):我们需要0.5s,也就是500ms。
    • ARR=500×10−1=4999ARR=500×10−1=4999

结论配置: PSC = 7199ARR = 4999


四、 实战:CubeMX 配置步骤

以STM32F103C8T6为例,配置TIM2实现每1秒翻转一次LED。

1. 时钟树配置

确保系统时钟配置正确(例如HSE外部晶振,倍频到72MHz)。
注意: 查看APB1 Timer Clock的值,这决定了公式中的 CK_PSC

2. 定时器配置

  1. 打开 Timers -> TIM2
  2. Clock Source 选择 Internal Clock
  3. Parameter Settings(参数设置):
    • Prescaler (PSC)7199 (72MHz / 7200 = 10kHz)
    • Counter ModeUp (向上计数)
    • Counter Period (ARR)9999 (10000次计数 = 1秒)
    • auto-reload preloadEnable (使能自动重装载影子寄存器)

3. 中断配置

  1. 切换到 NVIC Settings 标签页。
  2. 勾选 TIM2 global interrupt 后面的 Enabled
  3. (可选) 设置中断优先级。
  4. 点击 GENERATE CODE 生成代码。

五、 代码编写 (HAL库)

生成的代码在 main.c 和 tim.c 中。我们需要做两件事:开启定时器 和 重写中断回调函数

1. 开启定时器

在 main.c 的 main() 函数中,MX_TIM2_Init() 之后,while(1) 之前,添加:

/* USER CODE BEGIN 2 */
// 以中断方式开启定时器2
HAL_TIM_Base_Start_IT(&htim2);
/* USER CODE END 2 */

2. 重写中断回调函数

HAL库处理中断的逻辑是:TIM2_IRQHandler -> HAL_TIM_IRQHandler -> HAL_TIM_PeriodElapsedCallback
我们需要在 main.c 的末尾(或者专门的一个中断处理文件中)重写这个虚函数:

/* USER CODE BEGIN 4 */
/**
 * @brief 定时器周期溢出回调函数
 * @param htim: 定时器句柄
 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    // 判断是否是定时器2触发的中断
    if (htim->Instance == TIM2)
    {
        // 翻转LED (假设LED接在PC13)
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    }
}
/* USER CODE END 4 */

六、 常见问题与避坑指南

  1. 忘了开启定时器: 初始化后一定要调用 HAL_TIM_Base_Start_IT,否则定时器不会跑,中断也不会来。
  2. PSC和ARR忘了减1: 如果你想要分频72,PSC必须写71。如果不减1,时间会有一点点误差,虽然在LED闪烁上看不出来,但在精确计时或通信中是致命的。
  3. 中断没响应: 检查CubeMX里的NVIC有没有勾选;检查 main.c 里是否开启了全局中断 __enable_irq() (HAL库默认开启)。
  4. ARR为0: 如果ARR设为0,定时器刚启动就溢出,可能会导致程序卡死或异常。

七、 总结

STM32的定时器配置看似参数多,其实核心就是**“分频”“重装载”**两个概念。

  • PSC 决定了定时器跳动的快慢。
  • ARR 决定了定时器跳动多少次触发中断。

掌握了这两个参数和计算公式,你就掌握了定时器的基本命脉。接下来可以尝试进阶功能:PWM输出(呼吸灯) 和 输入捕获(测按键时长),原理都是基于此的延伸。

希望这篇文章能帮你彻底理解定时器!如果有疑问,欢迎在评论区留言讨论。

常用配置速查表 (假设时钟 72MHz)
目标效果 第一步:定频率 (PSC) 每一跳时间 第二步:定次数 (ARR) 计算逻辑
500ms中断 7199 (7200分频) 0.1ms 4999 0.1ms×5000=500ms0.1ms×5000=500ms
1s中断 7199 (7200分频) 0.1ms 9999 0.1ms×10000=1s0.1ms×10000=1s
1ms中断 71 (72分频) 1us 999 1us×1000=1ms1us×1000=1ms
PWM周期(20ms) 71 (72分频) 1us 19999 1us×20000=20ms1us×20000=20ms

版权声明: 本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐