第4章 定时器(TIM)基础:从工作原理到呼吸灯实战

定时器这东西,可以说是STM32里最常用的外设之一了。我刚开始做嵌入式那会儿,总觉得定时器就是用来计个时、定个闹钟,后来才发现它的玩法远比想象中丰富。PWM输出、输入捕获、编码器接口……很多高级功能都离不开它。

这一章,咱们就把定时器的底裤扒干净。从工作原理讲起,再到HAL库的封装,最后用两个实战案例——定时中断和呼吸灯,让你彻底搞懂它。

本章核心目标:

  • 理解STM32定时器的分层结构与计数模式
  • 掌握HAL库定时器相关函数的使用套路
  • 独立实现定时中断与PWM呼吸灯两个项目
定时器(TIM)知识体系 STM32定时器家族 基本定时器(TIM6/7) 通用定时器(TIM2~5) 高级定时器(TIM1/8) 仅时基 + 中断 时基 + PWM + 捕获 全部功能 + 刹车/互补 PSC预分频器 → CNT计数器 ARR自动重装载 CCR捕获/比较寄存器 向上计数模式 向下计数模式 中央对齐模式 核心公式:定时频率 = 时钟源 / (PSC + 1) / (ARR + 1)

4.1 定时器的工作原理——其实没那么玄乎

说白了,定时器就是一个会自己加1的计数器。它内部有一个CNT寄存器,每来一个时钟脉冲就加1。当CNT的值达到我们设定的ARR(自动重装载寄存器)时,就会触发一个事件——要么产生中断,要么翻转输出电平。

那这个时钟脉冲从哪来?两个途径:

  • 内部时钟(CK_INT):来自APB总线,通常是几十MHz
  • 外部时钟(TIx/ETR):从GPIO引脚输入,可以接编码器或外部信号

这里有个关键器件叫预分频器(PSC)。你想想看,如果直接拿72MHz去计数,那1秒钟要数7200万次,ARR得设到7200万才能得到1秒的周期——这显然不现实。PSC的作用就是把时钟频率降下来。

经验公式:

定时器溢出频率 = 时钟源频率 / (PSC + 1) / (ARR + 1)

举个例子:72MHz时钟,PSC=7199,ARR=999,那么溢出频率 = 72M / 7200 / 1000 = 10Hz,也就是100ms中断一次。

我在项目中遇到过一个问题:用TIM2做1ms定时,算出来的PSC和ARR明明没错,但实际跑起来就是不对。后来查了半天,发现是APB1的时钟树配置问题——TIM2挂在APB1上,如果APB1分频系数不是1,那么定时器的时钟会翻倍。嗯,这里要注意,查数据手册的时钟树是基本功。

4.2 三种计数模式,你都得会

计数模式 工作方式 典型应用
向上计数 CNT从0递增到ARR,然后归零重新开始 普通定时中断、周期性任务
向下计数 CNT从ARR递减到0,然后跳回ARR重新开始 某些特殊时序控制
中央对齐 CNT先递增到ARR,再递减到0,如此反复 对称PWM、电机控制

我个人习惯,90%的场景都用向上计数模式。向下计数用得少,中央对齐模式主要是在做电机FOC控制时才会用到。初学者先把向上计数玩熟就够了。

4.3 HAL库定时器函数——三板斧

HAL库把定时器的操作封装得很规整。我总结了一套"三板斧"套路,你照着来就行:

  1. 初始化:配置PSC、ARR、计数模式
  2. 启动:启动计数,可选是否开启中断
  3. 回调:在中断回调函数里写你的业务逻辑

核心函数就这几个:

// 1. 初始化定时器句柄
TIM_HandleTypeDef htim2;

// 2. 配置时基单元
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7199;         // PSC预分频
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htim2.Init.Period = 999;             // ARR自动重装载值
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);

// 3. 启动定时器(中断模式)
HAL_TIM_Base_Start_IT(&htim2);

// 4. 中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM2)
    {
        // 你的业务代码,比如翻转LED
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

我曾经踩过的坑:

回调函数的名字不能写错!HAL_TIM_PeriodElapsedCallback 是弱定义,你重写的时候必须一字不差。我见过有人写成 PeriodElapsed_Callback 或者 PeriodElapsedCallback_TIM2,结果中断触发了但回调不执行——因为编译器找不到匹配的函数。

另外,启动函数有三种变体,区别在于是否开启中断和DMA:

函数 说明
HAL_TIM_Base_Start 纯软件启动,不产生中断
HAL_TIM_Base_Start_IT 启动并开启中断
HAL_TIM_Base_Start_DMA 启动并配合DMA传输

4.4 实战一:定时中断——让LED以精确间隔闪烁

咱们来写一个完整的例子。目标:用TIM2产生1Hz的中断,让板载LED以1秒为周期闪烁。

步骤1:CubeMX配置

  • 选择TIM2,时钟源选Internal Clock
  • Prescaler设为7199(72MHz / 7200 = 10kHz)
  • Counter Period设为4999(10kHz / 5000 = 2Hz)
  • NVIC Settings里使能TIM2全局中断

步骤2:生成代码,添加用户逻辑

// main.c 中的 MX_TIM2_Init 函数由CubeMX自动生成
// 我们只需要在 main() 中启动定时器

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();

    // 启动定时器中断
    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        // 主循环可以干别的事
        // 中断里已经处理了LED翻转
    }
}

// 在 stm32f1xx_it.c 或用户文件中添加回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM2)
    {
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 假设LED在PB0
    }
}

小技巧:

如果你想让中断频率更精确,可以用定时器时钟预分频。比如APB1时钟是36MHz,但TIM2的时钟可能是72MHz(因为APB1预分频系数不为1时,定时器时钟会翻倍)。我一般会在初始化前用 HAL_RCC_GetPCLK1Freq() 确认一下实际时钟频率。

4.5 实战二:PWM输出与呼吸灯——让LED学会呼吸

PWM的全称是脉冲宽度调制。说白了,就是在一个周期内,让高电平的时间(占空比)可调。人眼有视觉暂留效应,只要频率够高(通常1kHz以上),看到的就不是闪烁,而是亮度变化。

呼吸灯的原理就更简单了:让占空比从0%逐渐增加到100%,再从100%逐渐减小到0%,如此循环。LED就会像呼吸一样,由暗变亮再变暗。

步骤1:CubeMX配置PWM通道

  • 选择TIM3,通道1(假设对应PA6)
  • 时钟源Internal Clock,PSC=71,ARR=999(得到1kHz的PWM频率)
  • Channel1模式选PWM Generation CH1
  • NVIC里不用开中断(PWM不需要中断)

步骤2:代码实现呼吸效果

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

// 呼吸灯主循环
uint16_t duty = 0;
int8_t direction = 1;

while (1)
{
    // 设置占空比
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);

    // 延时10ms(可以用HAL_Delay,但实际项目中建议用定时器)
    HAL_Delay(10);

    // 更新占空比方向
    duty += direction * 5;
    if(duty >= 1000) direction = -1;
    if(duty == 0)    direction = 1;
}

关键点解析:

  • ARR=999,所以占空比范围是0~999,对应0%~100%
  • 每次步进5,步进间隔10ms,一个完整呼吸周期约4秒
  • __HAL_TIM_SET_COMPARE 是宏,直接操作CCR寄存器,效率很高

我在项目中用过这个思路做呼吸式状态指示灯——设备正常运行时LED缓慢呼吸,故障时快速闪烁。用户一看就明白设备状态,连说明书都不用看。

注意:

PWM频率不能太低。低于50Hz人眼就能看到闪烁,低于20Hz甚至会让人不舒服。我一般设1kHz~10kHz,既平滑又听不到线圈啸叫(如果用蜂鸣器或电机的话)。

4.6 定时器配置速查表

为了方便你快速上手,我把常用配置整理成了一张表。假设系统时钟72MHz:

目标功能 PSC ARR 实际频率/周期
1ms定时中断 71 999 1kHz / 1ms
10ms定时中断 719 999 100Hz / 10ms
1s定时中断 7199 9999 1Hz / 1s
1kHz PWM(呼吸灯) 71 999 1kHz
10kHz PWM(电机) 71 99 10kHz

你可能会问:为什么PSC和ARR要这么搭配?其实核心就一句话——先降频到合适范围,再精确计数。PSC负责粗调,ARR负责细调。两者配合,就能得到你想要的任何频率。

好了,定时器的基础内容就这些。从工作原理到HAL库函数,再到两个实战项目,这一套走下来,你应该能自己上手写定时器相关的代码了。记住我那句老话:先算频率,再配参数,最后写回调。顺序别搞反,能省不少调试时间。