4. 定时器基础与PWM输出:SysTick与HAL_Delay原理、通用定时器配置、PWM输出与呼吸灯实现、封装PWM驱动模块

定时器,说白了就是单片机的“心跳”。没有它,你连个精准的延时都做不了。我刚开始用STM32时,总觉得定时器就是用来计数的,后来才发现——它才是整个系统调度的基石。

这一章,咱们从最基础的SysTick讲起,再到通用定时器,最后用PWM做个呼吸灯。嗯,我会把项目中踩过的坑也一并告诉你。

4.1 SysTick与HAL_Delay原理

SysTick是Cortex-M内核自带的一个24位递减计数器。它只有一个任务——每1ms产生一次中断。HAL_Delay()就是靠它实现的。

核心流程:

  • SysTick每1ms减到0,触发中断
  • 中断服务函数中,全局变量 uwTick 加1
  • HAL_Delay(ms) 不断读取 uwTick,直到差值达到ms

我曾经在项目里直接用HAL_Delay做按键消抖,结果系统卡死。为什么?因为HAL_Delay是阻塞的——它占着CPU不放。你想想看,如果中断里调了HAL_Delay,那整个系统就停摆了。

避坑指南:不要在中断服务函数里调用HAL_Delay()。我曾经因为这个让整个电机控制系统“死机”了3天。

4.2 通用定时器配置

通用定时器比SysTick灵活得多。它有多个通道、支持输入捕获、输出比较、PWM生成。我个人习惯用TIM2或TIM3做基础定时,因为它们挂在APB1上,时钟配置相对简单。

配置步骤其实就四步:

  1. 使能时钟:__HAL_RCC_TIMx_CLK_ENABLE()
  2. 设置分频与重装值:PSC和ARR
  3. 配置中断:HAL_NVIC_SetPriority() + HAL_NVIC_EnableIRQ()
  4. 启动定时器:HAL_TIM_Base_Start_IT()

这里有个关键公式:

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

举个例子,如果APB1时钟是72MHz,想要1ms中断一次:

PSC = 71;  // 72MHz / (71+1) = 1MHz
ARR = 999; // 1MHz / (999+1) = 1kHz = 1ms

个人经验:PSC和ARR的值尽量用整数,避免浮点运算。我习惯先算PSC让计数器频率为1MHz,再算ARR得到目标周期。

4.3 PWM输出与呼吸灯实现

PWM,说白了就是快速开关信号,通过改变占空比来模拟不同电压。呼吸灯就是PWM占空比从0%慢慢变到100%,再变回来。

配置PWM的步骤:

  1. 初始化定时器为PWM模式
  2. 配置通道:极性、占空比初始值
  3. 启动PWM输出:HAL_TIM_PWM_Start()
  4. 在循环中修改CCR寄存器改变占空比

代码示例:

// 初始化TIM3通道1为PWM输出
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0;  // 初始占空比0%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);

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

// 呼吸灯主循环
while (1) {
    for (int i = 0; i <= 1000; i += 10) {
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, i);
        HAL_Delay(5);
    }
    for (int i = 1000; i >= 0; i -= 10) {
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, i);
        HAL_Delay(5);
    }
}

注意:CCR的值不能超过ARR。如果ARR=999,CCR最大只能设999。我曾经设了1000,结果PWM直接变成常亮——占空比100%了。

4.4 封装PWM驱动模块

裸写PWM配置代码,每个项目都要重复一遍。我习惯把它封装成一个驱动模块,方便复用。

封装思路:

  • pwm_driver.h:对外接口声明
  • pwm_driver.c:具体实现
  • 结构体:保存定时器句柄、通道、ARR值

头文件示例:

#ifndef __PWM_DRIVER_H
#define __PWM_DRIVER_H

#include "stm32f4xx_hal.h"

typedef struct {
    TIM_HandleTypeDef *htim;
    uint32_t channel;
    uint32_t period;  // ARR值
} PWM_Handle_t;

void PWM_Init(PWM_Handle_t *pwm, TIM_HandleTypeDef *htim, 
              uint32_t channel, uint32_t period);
void PWM_SetDuty(PWM_Handle_t *pwm, uint16_t duty);
void PWM_Start(PWM_Handle_t *pwm);
void PWM_Stop(PWM_Handle_t *pwm);

#endif

实现文件关键部分:

void PWM_SetDuty(PWM_Handle_t *pwm, uint16_t duty) {
    if (duty > pwm->period) duty = pwm->period;
    __HAL_TIM_SET_COMPARE(pwm->htim, pwm->channel, duty);
}

封装的好处:换一个定时器或通道,只需要改初始化参数,不用改业务逻辑。我在三个项目里复用了同一套PWM驱动,改的只是引脚和时钟配置。

4.5 知识结构图

下面这张图展示了本章的核心知识脉络:

定时器与PWM知识结构图 定时器与PWM知识结构 定时器系统 SysTick 通用定时器 PWM输出 HAL_Delay阻塞延时 PSC + ARR配置 中断回调函数 CCR占空比控制 呼吸灯实现 + PWM驱动模块封装 核心:定时器是系统调度的基础,PWM是定时器最常用的输出形式 封装驱动模块可提高代码复用性,减少重复配置工作

4.6 常见问题与避坑

问题 原因 解决方法
HAL_Delay不准确 SysTick优先级被其他中断抢占 提高SysTick中断优先级
PWM输出无波形 GPIO未配置为复用功能 检查GPIO_AF配置
呼吸灯闪烁不均匀 CCR变化步长太大 减小步长,增加延时
定时器中断不触发 未使能NVIC中断 调用HAL_NVIC_EnableIRQ()

本章小结:

  • SysTick是1ms心跳,HAL_Delay是阻塞的,别在中断里用
  • 通用定时器配置记住公式:频率 = 时钟 / (PSC+1) / (ARR+1)
  • PWM占空比由CCR控制,CCR不能超过ARR
  • 封装驱动模块,让代码可复用、可维护

嗯,定时器这部分内容就到这里。我个人觉得,把SysTick和通用定时器搞明白,后面学高级定时器、输入捕获、编码器接口都会轻松很多。呼吸灯虽然简单,但它背后涉及的PWM原理和驱动封装思路,是嵌入式开发里非常实用的基本功。