4. 定时器基础与应用:SysTick定时器、通用定时器配置、PWM输出、输入捕获、呼吸灯与频率测量

定时器,说白了就是单片机的“心跳”和“节拍器”。

我刚开始学STM32那会儿,总觉得定时器不就是个计数器嘛,有啥好学的?后来做项目才发现,定时器玩不转,很多功能根本做不出来。PWM调不了光、电机控不了速、频率测不准——全得回来补课。

这一章,咱们就把定时器彻底搞明白。从最基础的SysTick,到通用定时器的配置,再到PWM输出和输入捕获。最后用两个实战案例收尾:呼吸灯和频率测量。

本章核心脉络: 理解定时器本质 → 掌握SysTick作为系统心跳 → 学会通用定时器配置 → 实战PWM与输入捕获

4.1 定时器家族:STM32的“时间管理大师”

STM32的定时器分好几类,我习惯把它们分成三个梯队:

类型 定时器 位数 主要用途
基本定时器 TIM6、TIM7 16位 基础定时、触发DAC
通用定时器 TIM2~TIM5 16/32位 PWM、输入捕获、输出比较
高级定时器 TIM1、TIM8 16位 电机控制、互补PWM、刹车功能
SysTick 系统定时器 24位 操作系统心跳、精确延时

你想想看,基本定时器就像个“闹钟”,到点就响。通用定时器就厉害了,它不仅能当闹钟,还能测量外部信号的脉宽、产生可调宽度的方波。高级定时器则是为电机控制量身定做的,带死区插入和刹车功能。

嗯,这里要注意:SysTick是内核自带的,不属于外设定时器。它挂在系统总线上,专门给操作系统提供心跳。咱们做FreeRTOS移植,第一步就是配好SysTick。

4.2 SysTick定时器:FreeRTOS的“心脏起搏器”

SysTick是个24位的递减计数器。它从你设定的重装载值开始,每个系统时钟周期减1,减到0就触发一次中断,然后自动重装。

我个人习惯把SysTick的周期设为1ms。为什么?因为FreeRTOS的时钟节拍默认就是1ms,这样最省事。

配置代码其实很简单:

// SysTick配置函数
// 参数:系统时钟频率(Hz),目标中断频率(Hz)
uint32_t SysTick_Config(uint32_t ticks)
{
    // 判断重装载值是否超出24位范围
    if (ticks > SysTick_LOAD_RELOAD_Msk)  
        return 1;
    
    // 设置重装载值
    SysTick->LOAD = ticks - 1;
    
    // 设置当前值为0
    SysTick->VAL = 0;
    
    // 使能SysTick中断,使能定时器
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
    
    return 0;
}

// 如果系统时钟是72MHz,要产生1ms中断
// ticks = 72000000 / 1000 = 72000
// SysTick_Config(72000);

我的经验: 在FreeRTOS中,不要直接在SysTick中断里做耗时操作。我曾经犯过这个错——在中断里打印日志,结果系统直接卡死。SysTick中断里只做两件事:递增系统时钟计数、通知FreeRTOS调度器。

4.3 通用定时器配置:从“跑起来”到“玩出花”

通用定时器的配置,我总结了一个“三步走”套路:

  1. 时钟使能——让定时器通电
  2. 参数配置——设置分频、重装载值、计数模式
  3. 中断/事件使能——决定定时器怎么通知你

咱们以TIM3为例,配置一个1秒中断一次:

void TIM3_Config(void)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    
    // 第一步:使能TIM3时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    
    // 第二步:配置定时器参数
    // 预分频器:72MHz / 7200 = 10KHz
    TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1;
    // 自动重装载值:10000 / 10KHz = 1秒
    TIM_TimeBaseStructure.TIM_Period = 10000 - 1;
    // 向上计数模式
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    // 时钟分频(用于滤波,这里不用)
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
    
    // 第三步:使能更新中断
    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
    // 使能定时器
    TIM_Cmd(TIM3, ENABLE);
    
    // 配置NVIC(中断优先级)
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

// 中断服务函数
void TIM3_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
    {
        // 1秒到了,做你想做的事
        // 注意:中断里别做耗时操作!
        
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

避坑指南: 我曾经在配置TIM2时,忘了查它挂在哪个总线上。TIM2挂在APB1上,如果APB1的时钟不是系统时钟的一半,实际频率会翻倍。记住:APB1的定时器时钟 = APB1时钟 × 2(如果APB1分频系数≠1)。这个坑我踩过,你别再踩了。

4.4 PWM输出:让LED“呼吸”起来

PWM,说白了就是“快速开关”。通过调节高电平占一个周期的时间比例(占空比),就能控制LED的亮度、电机的转速。

STM32的通用定时器都带PWM模式。配置起来也不复杂:

void TIM2_PWM_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;
    
    // 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // 配置PA0为复用推挽输出(TIM2_CH1)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 定时器基础配置
    TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;      // 72MHz/72 = 1MHz
    TIM_TimeBaseStructure.TIM_Period = 1000 - 1;       // 1MHz/1000 = 1KHz
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
    
    // PWM模式配置
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;  // PWM模式1
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 500;                // 初始占空比50%
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);
    
    // 使能定时器
    TIM_Cmd(TIM2, ENABLE);
}

// 设置占空比函数
void PWM_SetDuty(uint16_t duty)
{
    TIM_SetCompare1(TIM2, duty);
}

你看,核心就是TIM_Pulse这个值。它和TIM_Period的比值就是占空比。比如Period=1000,Pulse=500,占空比就是50%。

4.5 输入捕获:测量外部信号的“脉搏”

输入捕获,就是测量外部信号的频率或脉宽。原理很简单:当检测到信号的上升沿或下降沿时,定时器会把当前的计数值“捕获”到寄存器里。

我举个例子:测量一个方波的频率。

volatile uint32_t IC_Value1 = 0;
volatile uint32_t IC_Value2 = 0;
volatile uint8_t  IC_Flag = 0;

void TIM4_InputCapture_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_ICInitTypeDef TIM_ICInitStructure;
    
    // 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    
    // 配置PB6为浮空输入(TIM4_CH1)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    // 定时器基础配置
    TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;      // 1MHz计数
    TIM_TimeBaseStructure.TIM_Period = 0xFFFF;         // 最大计数值
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
    
    // 输入捕获配置:捕获上升沿
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0;
    TIM_ICInit(TIM4, &TIM_ICInitStructure);
    
    // 使能捕获中断
    TIM_ITConfig(TIM4, TIM_IT_CC1, ENABLE);
    TIM_Cmd(TIM4, ENABLE);
    
    // NVIC配置
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

void TIM4_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM4, TIM_IT_CC1) != RESET)
    {
        if (IC_Flag == 0)
        {
            // 第一次捕获:记录第一个上升沿
            IC_Value1 = TIM_GetCapture1(TIM4);
            IC_Flag = 1;
            // 改为捕获下降沿
            TIM_OC1PolarityConfig(TIM4, TIM_ICPolarity_Falling);
        }
        else if (IC_Flag == 1)
        {
            // 第二次捕获:记录下降沿(用于测脉宽)
            IC_Value2 = TIM_GetCapture1(TIM4);
            IC_Flag = 2;
            // 改回捕获上升沿
            TIM_OC1PolarityConfig(TIM4, TIM_ICPolarity_Rising);
        }
        else if (IC_Flag == 2)
        {
            // 第三次捕获:记录第二个上升沿(用于测周期)
            uint32_t temp = TIM_GetCapture1(TIM4);
            uint32_t period = temp - IC_Value1;
            // 频率 = 1MHz / period
            // 这里可以计算频率
            
            IC_Value1 = temp;
            IC_Flag = 1;
            TIM_OC1PolarityConfig(TIM4, TIM_ICPolarity_Falling);
        }
        
        TIM_ClearITPendingBit(TIM4, TIM_IT_CC1);
    }
}

我的经验: 输入捕获最怕信号抖动。我建议你开启滤波功能(TIM_ICFilter),设个4-8的滤波值。别设太大,否则会滤掉有效信号。这个值我一般根据信号频率来调,低频信号滤波值设小点,高频信号设大点。

4.6 实战一:呼吸灯——从“亮”到“灭”的优雅渐变

呼吸灯,就是让LED的亮度像呼吸一样缓慢变化。原理很简单:不断改变PWM的占空比,从0%逐渐增加到100%,再从100%逐渐减小到0%。

我习惯用定时器中断来更新占空比:

// 呼吸灯参数
#define BREATHE_PERIOD  20    // 每次更新间隔20ms
#define STEP_VALUE      5     // 每次变化步长

uint16_t breathe_duty = 0;
uint8_t breathe_dir = 1;      // 1:增加,0:减小

// 在定时器中断中调用(假设定时器每20ms中断一次)
void Breathe_Update(void)
{
    if (breathe_dir)
    {
        breathe_duty += STEP_VALUE;
        if (breathe_duty >= 1000)
        {
            breathe_duty = 1000;
            breathe_dir = 0;
        }
    }
    else
    {
        if (breathe_duty < STEP_VALUE)
        {
            breathe_duty = 0;
            breathe_dir = 1;
        }
        else
        {
            breathe_duty -= STEP_VALUE;
        }
    }
    
    PWM_SetDuty(breathe_duty);
}

你想想看,这个渐变过程其实很“人性化”。LED不会突然亮起或熄灭,而是缓缓变化,看起来就很舒服。

4.7 实战二:频率测量——给信号“把脉”

频率测量,说白了就是数一数单位时间内信号跳了多少次。有两种方法:

方法 原理 适用场景 精度
测频法 固定闸门时间,计数脉冲个数 高频信号(>1KHz) 闸门时间越长越准
测周法 测量一个周期的时间 低频信号(<1KHz) 周期越长越准

我一般这样选:信号频率高就用测频法,频率低就用测周法。如果频率范围很宽,那就两种方法结合着用。

测频法的代码实现:

// 测频法:1秒闸门
volatile uint32_t pulse_count = 0;
volatile uint32_t frequency = 0;

// 外部中断服务函数(检测信号上升沿)
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        pulse_count++;
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

// 1秒定时器中断服务函数
void TIM3_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
    {
        frequency = pulse_count;  // 读取1秒内的脉冲数
        pulse_count = 0;          // 清零,开始下一轮计数
        
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

避坑指南: 我曾经用测频法测一个50Hz的信号,闸门时间设了1秒,结果发现频率一直在49-51之间跳动。后来才意识到,闸门时间和信号周期不是整数倍关系,会有±1的计数误差。解决办法:闸门时间设长一点,比如10秒,或者用测周法。

4.8 本章知识体系

下面这张图,是我画的本章节知识结构,帮你理清思路:

定时器基础与应用 · 知识体系 STM32 定时器 SysTick 定时器 通用定时器 PWM 输出 输入捕获 24位递减计数器 FreeRTOS心跳 1ms中断配置 时钟使能 预分频 + 重装载 中断使能 占空比控制 频率 = 1KHz 呼吸灯实战 上升/下降沿捕获 频率测量实战

这张图把本章的知识点串起来了。你从中心出发,沿着四个方向走,就能把定时器的全貌看清楚。

好了,定时器这块的内容就到这儿。代码我都跑过,你直接复制到工程里就能用。遇到问题别慌,先检查时钟配置,再看中断优先级——80%的问题都出在这两个地方。