跳转至

定时器(Timer)

定时器是 STM32 中功能最强大、应用最广泛的外设之一。从简单的延时计数,到 PWM 波形输出驱动电机,再到输入捕获测量脉冲宽度,定时器无处不在。


一、定时器分类

STM32F103 共有 8 个定时器,分为三类:

类型 定时器 位数 总线 主要功能
基本定时器 TIM6, TIM7 16 位 APB1 定时、触发 DAC
通用定时器 TIM2, TIM3, TIM4, TIM5 16 位 APB1 定时、PWM 输出、输入捕获、编码器
高级定时器 TIM1, TIM8 16 位 APB2 通用定时器全部功能 + 互补输出、死区控制、刹车
graph TB
    TIM[STM32 定时器] --> BASIC[基本定时器<br>TIM6/7]
    TIM --> GENERAL[通用定时器<br>TIM2/3/4/5]
    TIM --> ADVANCED[高级定时器<br>TIM1/8]
    BASIC --> B1[定时中断]
    BASIC --> B2[触发 DAC]
    GENERAL --> G1[定时中断]
    GENERAL --> G2[PWM 输出]
    GENERAL --> G3[输入捕获]
    GENERAL --> G4[编码器接口]
    ADVANCED --> A1[通用定时器全部功能]
    ADVANCED --> A2[互补 PWM + 死区]
    ADVANCED --> A3[刹车输入]

二、定时器的核心概念

计数器基本原理

定时器的本质就是一个 自动计数的计数器

  1. 每来一个时钟脉冲,计数器 +1(向上计数时)
  2. 当计数到设定值(自动重装载值 ARR)时,产生一个 更新事件
  3. 计数器清零,重新开始计数

三个关键参数

参数 寄存器 含义
预分频器 PSC(Prescaler) 对输入时钟进行分频,决定计数频率
自动重装载值 ARR(Auto-Reload Register) 计数器的上限值,到达后产生更新事件
计数器 CNT(Counter) 当前计数值

定时时间计算

\[ T_{\text{溢出}} = \frac{(PSC + 1) \times (ARR + 1)}{F_{\text{CLK}}} \]

其中 \(F_{\text{CLK}}\) 是定时器的输入时钟频率。

计算举例

STM32F103 的 TIM2 挂在 APB1 总线上,经过倍频后时钟为 72MHz。

如果我们想让定时器每 1 秒 触发一次中断:

\[ 1\text{s} = \frac{(PSC + 1) \times (ARR + 1)}{72\,000\,000} \]

\(PSC = 7199\), \(ARR = 9999\)

\[ T = \frac{7200 \times 10000}{72\,000\,000} = 1\text{s} \quad ✅ \]

或者 \(PSC = 71\), \(ARR = 999999\)... 但 ARR 是 16 位寄存器(最大 65535),所以这个组合不可行!

PSC 和 ARR 的取值技巧

  • 先用 PSC 把时钟降到一个"好算"的频率(如 10kHz),再用 ARR 设定计数次数
  • 例如 72MHz / (7199+1) = 10kHz,然后 ARR = 9999 → 每 1 秒溢出一次
  • PSC 和 ARR 的值都要在 0~65535 范围内

计数模式

模式 计数方向 溢出条件 典型应用
向上计数 0 → ARR CNT == ARR 时溢出 最常用
向下计数 ARR → 0 CNT == 0 时溢出 较少用
中央对齐 0 → ARR → 0 到达 ARR 和 0 时都溢出 产生对称 PWM

三、定时中断(CubeMX + HAL)

最基本的定时器应用——每隔固定时间触发一次中断。

CubeMX 配置步骤

  1. Timers → TIM2
    • Clock Source: Internal Clock
    • Prescaler: 7200 - 1(即 7199)
    • Counter Period: 5000 - 1(即 4999)
    • Counter Mode: Up
    • auto-reload preload: Enable
  2. NVIC Settings 标签页中勾选 TIM2 global interrupt,设置优先级
  3. 生成代码

代码示例:每 500ms 翻转 LED

CubeMX 会在 tim.c 中生成 MX_TIM2_Init() 函数。你需要做两件事:

1. 在 main.c 中启动定时器中断

/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2);  // 启动 TIM2 并使能更新中断
/* USER CODE END 2 */

2. 实现回调函数

/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)  // 确认是 TIM2
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);  // 翻转 LED
    }
}
/* USER CODE END 4 */

HAL 定时器中断流程

graph LR
    A["TIM2_IRQHandler()<br>(stm32f1xx_it.c)"] --> B["HAL_TIM_IRQHandler()"]
    B --> C["检查 & 清除标志"]
    C --> D["HAL_TIM_PeriodElapsedCallback()<br>(你来实现)"]

你不需要手动清除中断标志位,HAL 已经处理了。


四、PWM 输出

什么是 PWM?

PWM(Pulse Width Modulation,脉冲宽度调制)是一种用数字信号模拟模拟量的技术:

  • 通过高速切换高低电平,控制平均电压
  • 占空比越大,平均电压越高
\[ V_{\text{avg}} = V_{\text{high}} \times \frac{T_{\text{on}}}{T_{\text{period}}} = V_{\text{high}} \times D \]

其中 \(D\) 是占空比(Duty Cycle),范围 0% ~ 100%。

PWM 的实际效果

3.3V 的 GPIO 输出 50% 占空比的 PWM → 等效 1.65V → LED 变暗一半
3.3V 的 GPIO 输出 10% 占空比的 PWM → 等效 0.33V → LED 很暗

PWM 模式

定时器通过比较 CNTCCR(Capture/Compare Register,捕获比较值)来产生 PWM:

PWM 模式 CNT < CCR 时 CNT ≥ CCR 时
PWM 模式 1 有效电平(高) 无效电平(低)
PWM 模式 2 无效电平(低) 有效电平(高)
PWM 模式 1(向上计数):
        ┌──────┐          ┌──────┐
   高   │      │          │      │
        │      │          │      │
   低 ──┘      └──────────┘      └──────
        0    CCR   ARR    0    CCR   ARR

   占空比 = CCR / (ARR + 1)

PWM 关键公式

\[ f_{\text{PWM}} = \frac{F_{\text{CLK}}}{(PSC + 1) \times (ARR + 1)} \]
\[ \text{占空比} = \frac{CCR}{ARR + 1} \times 100\% \]

CubeMX 配置 PWM

以 TIM3 CH1(PA6)输出 1kHz PWM 为例:

  1. Timers → TIM3
    • Clock Source: Internal Clock
    • Channel1: PWM Generation CH1
  2. Parameter Settings
    • Prescaler: 72 - 1(72MHz / 72 = 1MHz 计数频率)
    • Counter Period: 1000 - 1(1MHz / 1000 = 1kHz PWM)
    • Pulse(CCR): 0(初始占空比 0%)
    • Mode: PWM mode 1
    • CH Polarity: High
  3. PA6 会自动映射为 TIM3_CH1
  4. 生成代码

PWM 代码示例:呼吸灯

/* main.c */

/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);  // 启动 PWM 输出
/* USER CODE END 2 */

uint16_t duty = 0;
uint8_t direction = 1;

/* USER CODE BEGIN WHILE */
while (1)
{
    if (direction)
    {
        duty += 5;
        if (duty >= 1000) direction = 0;
    }
    else
    {
        duty -= 5;
        if (duty == 0) direction = 1;
    }

    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);  // 修改占空比
    HAL_Delay(5);
    /* USER CODE END WHILE */
}

PWM 驱动舵机

舵机通常需要 50Hz 的 PWM 信号,通过脉宽控制角度:

脉宽 角度 CCR(ARR=19999 时)
0.5ms 500
1.0ms 45° 1000
1.5ms 90° 1500
2.0ms 135° 2000
2.5ms 180° 2500

CubeMX 配置:PSC = 72-1,ARR = 20000-1 → 50Hz

// 启动 PWM
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

// 设置舵机角度
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1500);  // 90°

五、输入捕获

什么是输入捕获?

输入捕获用于测量外部信号的参数:

  • 脉冲频率:两个上升沿之间的时间
  • 脉冲宽度:一个上升沿到下一个下降沿之间的时间
  • 占空比:高电平时间 / 周期

工作原理

  1. 配置定时器通道为输入捕获模式
  2. 当检测到指定边沿(上升/下降)时,硬件自动将当前 CNT 值锁存到 CCR
  3. 通过两次捕获的 CCR 差值计算时间
信号:  ┌──────┐          ┌──────┐
   高   │      │          │      │
        │      │          │      │
   低 ──┘      └──────────┘      └──────
        ↑      ↑          ↑
      捕获1   捕获2      捕获3
      CCR1    CCR2       CCR3

频率 = F_CNT / (CCR3 - CCR1)
脉宽 = (CCR2 - CCR1) / F_CNT

CubeMX 配置输入捕获

以 TIM3 CH1(PA6)捕获上升沿为例:

  1. Timers → TIM3
    • Clock Source: Internal Clock
    • Channel1: Input Capture direct mode
  2. Parameter Settings
    • Prescaler: 72 - 1(计数频率 1MHz → 1μs 精度)
    • Counter Period: 65535(最大计数范围)
    • Polarity Selection: Rising Edge
    • IC Filter: 15(滤波)
  3. NVIC 中勾选 TIM3 global interrupt
  4. 生成代码

代码示例:测量信号频率

/* main.c */

volatile uint32_t capture1 = 0, capture2 = 0;
volatile uint8_t capture_done = 0;

/* USER CODE BEGIN 2 */
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);  // 启动输入捕获中断
/* USER CODE END 2 */

/* USER CODE BEGIN 4 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
    {
        if (capture_done == 0)
        {
            capture1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            capture_done = 1;
        }
        else
        {
            capture2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            capture_done = 2;  // 两次捕获完成

            // 可选:停止捕获
            HAL_TIM_IC_Stop_IT(&htim3, TIM_CHANNEL_1);
        }
    }
}
/* USER CODE END 4 */

/* 在主循环中计算频率 */
/* USER CODE BEGIN WHILE */
while (1)
{
    if (capture_done == 2)
    {
        uint32_t diff;
        if (capture2 >= capture1)
            diff = capture2 - capture1;
        else
            diff = (65536 + capture2) - capture1;  // 处理溢出

        float freq = 1000000.0f / diff;  // 计数频率 1MHz → 单位是 μs
        capture_done = 0;

        // 重新启动捕获
        HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
    }
    /* USER CODE END WHILE */
}

六、HAL 定时器常用函数速查

函数 功能
HAL_TIM_Base_Start() 启动定时器(无中断)
HAL_TIM_Base_Start_IT() 启动定时器并使能更新中断
HAL_TIM_Base_Stop_IT() 停止定时器并禁止更新中断
HAL_TIM_PWM_Start() 启动 PWM 输出
HAL_TIM_PWM_Stop() 停止 PWM 输出
HAL_TIM_IC_Start_IT() 启动输入捕获(中断模式)
HAL_TIM_IC_Stop_IT() 停止输入捕获
__HAL_TIM_SET_COMPARE() 修改 CCR 值(改变占空比)
__HAL_TIM_SET_AUTORELOAD() 修改 ARR 值
__HAL_TIM_SET_PRESCALER() 修改 PSC 值
HAL_TIM_ReadCapturedValue() 获取捕获值

常用回调函数

回调函数 触发时机
HAL_TIM_PeriodElapsedCallback() 更新事件(定时中断)
HAL_TIM_IC_CaptureCallback() 输入捕获完成
HAL_TIM_PWM_PulseFinishedCallback() PWM 脉冲结束
HAL_TIM_OC_DelayElapsedCallback() 输出比较匹配

七、常见问题

定时器的时钟频率怎么确定?

  • TIM2/3/4/5 挂在 APB1 上。APB1 预分频 ≠ 1 时,定时器时钟 = APB1 × 2 = 36MHz × 2 = 72MHz
  • TIM1/8 挂在 APB2 上,时钟同样为 72MHz
  • 结论:在默认时钟配置下(CubeMX 默认配好),所有定时器的时钟都是 72MHz

PWM 频率和分辨率如何权衡?

PWM 频率 = 72MHz / (PSC+1) / (ARR+1)。ARR 越大,占空比的调节精度(分辨率)越高,但 PWM 频率越低。需要根据应用场景权衡:

  • LED 调光:1kHz 就够,ARR 可以设大些
  • 电机驱动:通常 10~20kHz(避免听到电机的啸叫声)
  • 高速通信:可能需要 MHz 级别

为什么在 CubeMX 中 Preload 建议 Enable?

不使能预装载时,修改 ARR 或 CCR 会立即生效,可能导致在一个 PWM 周期中出现异常波形。使能预装载后,新值会在下一个更新事件时才加载,保证波形完整。

HAL_TIM_Base_Start vs HAL_TIM_Base_Start_IT?

  • HAL_TIM_Base_Start():只启动计数器,不产生中断。用于 PWM 输出、编码器等不需要中断的场景
  • HAL_TIM_Base_Start_IT():启动计数器 + 使能更新中断。用于需要定时中断的场景