4. 开发环境搭建与工程模板

定时器,说白了就是单片机的“心跳计数器”。

我刚开始学STM32时,觉得定时器不就是个计时器嘛,有啥好讲的?后来做项目才发现,这玩意儿的水深得很。从简单的延时,到复杂的电机控制、波形生成,全都离不开它。

今天咱们就把定时器家族捋清楚。你想想看,STM32的定时器分三类:基本、通用、高级。它们之间到底差在哪?怎么用HAL库快速上手?呼吸灯又是怎么实现的?

嗯,一个一个来。

三类定时器的核心区别

先看一张对比表,心里有个底:

特性 基本定时器 通用定时器 高级定时器
典型型号 TIM6、TIM7 TIM2~TIM5 TIM1、TIM8
计数方向 仅向上 向上/向下/双向 向上/向下/双向
PWM输出 ❌ 无 ✅ 有(4通道) ✅ 有(6通道+互补)
输入捕获 ❌ 无 ✅ 有 ✅ 有
死区插入 ❌ 无 ❌ 无 ✅ 有
刹车功能 ❌ 无 ❌ 无 ✅ 有
主要用途 基础定时、触发DAC PWM、捕获、编码器 电机控制、逆变器

看到没?基本定时器就是个“光杆司令”,只能计时。通用定时器是“多面手”,PWM、捕获都能干。高级定时器则是“特种兵”,专门伺候电机和电源这类高要求的活儿。

我的经验:做普通LED呼吸灯、舵机控制,用通用定时器就够了。别一上来就开高级定时器,配置复杂不说,还容易踩坑。我曾经在一个项目中误用了高级定时器的互补通道,结果输出波形一直不对,查了两天才发现是死区设置的问题。

定时器的核心工作原理

不管哪类定时器,核心就三样东西:

  • 预分频器(PSC):把时钟源分频,降低计数频率
  • 计数器(CNT):真正干活的,每来一个时钟脉冲就加1或减1
  • 自动重装载寄存器(ARR):计数值到了这个数,就重置或翻转

举个例子:时钟是72MHz,PSC设为71,那计数频率就是72M / (71+1) = 1MHz,也就是1微秒计一次。如果ARR设为999,那定时周期就是1000微秒,也就是1毫秒。

公式很简单:定时时间 = (PSC+1) × (ARR+1) / 时钟频率

小技巧:我习惯先定PSC让计数频率变成1MHz或1kHz,这样ARR直接就是毫秒或微秒数,调试时脑子不用转。比如要50ms中断,PSC=71(72MHz下),ARR=49999,搞定。

HAL_TIM_Base_Start_IT:开启定时器中断

HAL库把定时器封装得挺舒服。用基本定时器做个1ms中断,代码就这几步:

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

void MX_TIM6_Init(void)
{
    htim6.Instance = TIM6;
    htim6.Init.Prescaler = 71;         // 72MHz / (71+1) = 1MHz
    htim6.Init.Period = 999;           // 1MHz / (999+1) = 1kHz = 1ms
    HAL_TIM_Base_Init(&htim6);
}

// 2. 启动中断模式
HAL_TIM_Base_Start_IT(&htim6);

// 3. 中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        // 每1ms执行一次
        // 比如翻转一个LED
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

这里有个细节:HAL_TIM_Base_Start_IT 启动的是中断模式,还有 HAL_TIM_Base_Start(轮询)和 HAL_TIM_Base_Start_DMA(DMA)。我一般只在需要精确时序控制时才用中断,简单延时用HAL_Delay就够了。

注意:中断回调里别做耗时操作!我曾经在回调里加了个HAL_Delay(10),结果系统直接卡死。中断服务函数要短平快,复杂逻辑扔到主循环或任务里处理。

PWM输出:让LED呼吸起来

PWM,说白了就是快速开关信号,通过调节占空比来模拟模拟量。LED呼吸灯就是最经典的例子——占空比从0慢慢变到100%,再变回来。

用通用定时器的PWM模式,配置如下:

// 使用TIM2的通道1(PA0)
TIM_HandleTypeDef htim2;
TIM_OC_InitTypeDef sConfigOC;

void MX_TIM2_PWM_Init(void)
{
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;         // 1MHz
    htim2.Init.Period = 999;           // PWM频率 = 1kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    HAL_TIM_PWM_Init(&htim2);

    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 0;               // 初始占空比0%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
}

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

// 动态调节占空比
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 500);  // 50%占空比

呼吸灯的逻辑就更简单了:

uint16_t pulse = 0;
int8_t direction = 1;

while (1)
{
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse);
    HAL_Delay(5);  // 每5ms调整一次

    pulse += direction * 5;
    if (pulse >= 999) direction = -1;
    if (pulse == 0)   direction = 1;
}

你看,就这么几行代码,LED就能像呼吸一样渐亮渐暗。

避坑指南:我曾经把PWM频率设成了10Hz,结果LED一闪一闪的,跟呼吸完全不像。人眼对50Hz以上的闪烁就不敏感了,建议PWM频率至少100Hz以上。另外,占空比变化步长别太大,否则会看到明显的跳变。

知识结构图:定时器体系一览

下面这张图帮你理清三类定时器的定位和关系:

STM32定时器体系结构 时钟源(72MHz) 预分频器(PSC) 基本定时器 TIM6/TIM7 通用定时器 TIM2~TIM5 高级定时器 TIM1/TIM8 功能: 仅向上计数 无PWM/无捕获 触发DAC 功能: 双向计数 4通道PWM/捕获 编码器接口 功能: 6通道互补PWM 死区插入 刹车功能 基础定时/延时 PWM/呼吸灯/舵机 电机控制/逆变器 选择原则:简单任务用基本定时器,通用场景用通用定时器,电机控制才上高级定时器 别杀鸡用牛刀,也别牛刀杀鸡

总结一下关键点

  • 基本定时器:只有向上计数,不能输出PWM,适合做系统心跳或触发DAC
  • 通用定时器:功能最均衡,PWM、捕获、编码器都能干,日常项目的主力
  • 高级定时器:带死区、刹车、互补输出,专为电机和电源设计
  • HAL_TIM_Base_Start_IT:启动定时器中断,回调里别做耗时操作
  • PWM呼吸灯:调节占空比从0到100%循环,频率100Hz以上,步长别太大

最后说一句:定时器是STM32里最常用的外设之一,也是最容易出问题的。我见过太多人把PWM频率算错、中断优先级配乱、占空比寄存器写错位置。我的建议是——先拿基本定时器跑通中断,再上PWM,最后再碰高级功能。一步一步来,稳得很。