解码STM32定时器:原理、配置与实战

定时器核心概述

定时器是STM32微控制器中核心的外设之一,核心功能是通过精准的时钟计数实现定时控制、中断触发、波形生成等功能。在嵌入式系统中,定时器的应用场景极为广泛,例如:

  • 定时控制:LED灯周期性翻转、系统延时函数实现、任务调度器时钟基准;
  • 波形生成:通过PWM(脉冲宽度调制)控制电机转速、舵机角度、LED亮度;
  • 信号测量:通过输入捕获功能测量外部信号的脉冲宽度(如红外遥控信号)、频率;
  • 同步控制:多定时器联动实现复杂时序逻辑,或与其他外设(如ADC、DAC)同步工作。

对于STM32F407系列微控制器,内部集成14个定时器,按功能可分为三类,各类定时器的核心差异在于支持的功能和计数位数:

定时器类型 具体型号 计数位数 核心功能
基本定时器 TIM6、TIM7 16位 仅定时功能,可触发DAC转换
通用定时器 TIM2TIM5、TIM9TIM14 TIM2/TIM5为32位,其余16位 定时、输入捕获、输出比较、PWM生成、单脉冲模式
高级定时器 TIM1、TIM8 16位 包含通用定时器所有功能,新增互补输出、死区控制(适配电机驱动)

定时器核心原理

时钟来源与频率计算

定时器的计数时钟源自系统时钟(RCC),需经过APB总线分频后传入,不同总线挂载的定时器,其时钟频率计算规则不同:

image

  • APB1总线:最大频率42MHz,挂载定时器TIM2TIM5、TIM6TIM7、TIM12~TIM14;若APB1分频系数为1(不分频),定时器时钟=APB1频率;若分频系数>1,定时器时钟=APB1频率×2(最终为84MHz);
  • APB2总线:最大频率84MHz,挂载定时器TIM1、TIM8~TIM11;若APB2分频系数为1,定时器时钟=APB2频率;若分频系数>1,定时器时钟=APB2频率×2(最终为168MHz)。

核心结论:STM32F407中,APB1挂载的定时器实际工作频率为84MHz,APB2挂载的为168MHz。

image

定时时间计算

定时器的定时时间由“预分频器(PSC)”和“自动重载寄存器(ARR)”共同决定,核心公式如下:

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

参数说明:

  • PSC(预分频器):16位寄存器,取值范围0~65535,作用是将定时器时钟分频,分频后的频率=定时器时钟/(PSC+1);
  • ARR(自动重载值):16位或32位寄存器(取决于定时器位数),取值范围0~最大值,计数器计数到ARR值后会触发更新事件(溢出/下溢),并重新开始计数。

示例:使用TIM6(APB1挂载,时钟84MHz)定时500ms,计算PSC和ARR值:

已知定时时间=500ms=0.5s,定时器时钟=84MHz=84×10⁶Hz;

代入公式:(PSC+1)×(ARR+1)= 0.5 × 84×10⁶ = 42×10⁶;

可选配置:PSC=8399(分频后频率=84MHz/8400=10kHz),ARR=4999(计数5000次),则(8399+1)×(4999+1)=8400×5000=42×10⁶,刚好满足500ms定时。

计数模式

不同类型定时器支持的计数模式不同,核心模式分为三类:

  • 递增计数(默认):计数器从0开始递增,计数到ARR值后触发更新事件,重新从0开始;
  • 递减计数:计数器从ARR值开始递减,计数到0后触发更新事件,重新从ARR开始;
  • 中心对齐计数:计数器从0递增到ARR-1(上溢),再递减到1(下溢),循环往复,适用于对称PWM生成。

注意:TIM9TIM14仅支持递增计数,TIM2TIM5支持三种模式,高级定时器支持所有模式。

定时器基础配置(定时中断)

以TIM6定时1s实现LED0翻转为例,讲解基础配置流程,核心步骤:开启时钟→配置时基→配置中断→使能定时器→编写中断服务函数。

代码(标准库)

#include "stm32f4xx.h"

/**
 * @brief  TIM6初始化函数,配置1s定时中断
 * @param  无
 * @return 无
 * @note   TIM6挂载APB1总线,时钟84MHz;定时1s配置:PSC=8399,ARR=9999
 *         定时时间=(8399+1)*(9999+1)/84e6 = 8400*10000/84e6 = 1s
 */
void TIM6_Init(void) {
    // 1. 开启TIM6和APB1总线时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

    // 2. 配置定时器时基结构体
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
    TIM_TimeBaseInitStruct.TIM_Prescaler = 8399;          // 预分频器值
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 递增计数模式
    TIM_TimeBaseInitStruct.TIM_Period = 9999;             // 自动重载值
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频因子(不分频)
    TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;     // 重复计数器(仅高级定时器有效)
    TIM_TimeBaseInit(TIM6, &TIM_TimeBaseInitStruct);

    // 3. 配置NVIC中断控制器(允许TIM6中断)
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = TIM6_DAC_IRQn;      // TIM6中断通道
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;       // 响应优先级0
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;          // 使能中断通道
    NVIC_Init(&NVIC_InitStruct);

    // 4. 使能TIM6更新中断
    TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);

    // 5. 使能TIM6计数器
    TIM_Cmd(TIM6, ENABLE);
}

/**
 * @brief  TIM6中断服务函数,处理定时中断事件
 * @param  无
 * @return 无
 * @note   中断触发条件:TIM6计数器计数到ARR值(9999)后产生更新中断
 *         需先判断中断标志位,处理完成后清除标志位
 */
void TIM6_DAC_IRQHandler(void) {
    // 判断是否为TIM6更新中断
    if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) {
        GPIO_ToggleBits(GPIOF, GPIO_Pin_9); // 翻转LED0(假设LED0接PF9)
        TIM_ClearITPendingBit(TIM6, TIM_IT_Update); // 清除中断标志位
    }
}

/**
 * @brief  GPIO初始化函数,配置PF9为输出(控制LED0)
 * @param  无
 * @return 无
 */
void LED_Init(void) {
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); // 开启GPIOF时钟
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(GPIOF, &GPIO_InitStruct);
    GPIO_SetBits(GPIOF, GPIO_Pin_9); // 初始熄灭LED0
}

int main(void) {
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置中断优先级分组2
    LED_Init();          // 初始化LED
    TIM6_Init();         // 初始化TIM6定时中断
    while (1) {
        // 主循环无需处理,中断中完成LED翻转
    }
}

通用定时器进阶功能

通用定时器(如TIM2TIM5、TIM9TIM14)相比基本定时器,新增输入捕获和输出比较功能,核心应用为PWM生成。

image

查表 数据手册中的引脚功能表获取用MCU的某个IO口去控制硬件、用哪个定时器的哪个通道

image

PWM核心原理

PWM(脉冲宽度调制)是通过周期性脉冲信号的高电平占空比调节,实现模拟信号输出的数字技术,核心参数:

  • 频率:单位Hz,指每秒输出的脉冲个数,频率=定时器时钟/[(PSC+1)×(ARR+1)];

  • 占空比:指一个周期内高电平持续时间占总周期的百分比,占空比=(CCR+1)/(ARR+1)×100%(PWM模式1,高电平有效)。

    image

PWM模式分类(通过TIMx_CCMRx寄存器的OCxM位配置):

image

image

  • PWM模式1:递增计数时,CNT<CCR时输出有效电平,CNT≥CCR时输出无效电平;
  • PWM模式2:递增计数时,CNT<CCR时输出无效电平,CNT≥CCR时输出有效电平。

注:CCR(捕获/比较寄存器):16位寄存器,用于设定PWM占空比,取值范围0~ARR。

image

PWM配置实战(舵机控制)

舵机(如SG90)的控制要求:20ms周期(频率50Hz)的PWM信号,高电平时间0.5ms2.5ms对应0°180°角度。以下以TIM9_CH2(PE6引脚)生成舵机控制PWM为例。

硬件接线

  • 舵机红色线:接5V电源(舵机工作电压3~7.2V,开发板5V供电即可);
  • 舵机棕色线:接GND;
  • 舵机黄色线:接TIM9_CH2(PE6引脚,复用功能GPIO_AF_TIM9)。

image

代码

#include "stm32f4xx.h"

/**
  * @brief  延时微秒
  * @param  nus: 待延时时间,单位是微秒
  * @retval 无
  * @note   系统滴答定时器(Systick)采用21MHZ的时钟源
  */
void delay_us(u32 nus)
{
    SysTick->CTRL = 0;              // 关闭定时器
    SysTick->LOAD = nus*21 - 1;     // 设置重载值
    SysTick->VAL  = 0;              // 清除当前值
    SysTick->CTRL = 1;              // 启动定时器
    while ((SysTick->CTRL & 0x00010000)==0);// 等待时间到达
    SysTick->CTRL = 0;              // 关闭定时器
}

/**
  * @brief  延时毫秒
  * @param  nms: 待延时时间,单位是毫秒
  * @retval 无
  * @note   系统滴答定时器(Systick)采用21MHZ的时钟源,该函数存在微小误差
  */
void delay_ms(u32 nms)
{
    while(nms--) 
    {
        SysTick->CTRL = 0;              // 关闭定时器
        SysTick->LOAD = 21*1000 - 1;    // 设置重载值
        SysTick->VAL  = 0;              // 清除当前值
        SysTick->CTRL = 1;              // 启动定时器
        while ((SysTick->CTRL & 0x00010000)==0);// 等待时间到达
        SysTick->CTRL = 0;              // 关闭定时器
    }   
}

/**
  * @brief  配置TIM9输出通道(用于舵机控制)
  * @param  无
  * @retval 无
  */
void TIM_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;
    
    // 1. 开启TIM9的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM9, ENABLE);

    // 2. 开启GPIO端口时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);

    // 3. 配置GPIO引脚
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6 ;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;         // 复用模式
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;    // 100MHz输出速率
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;        // 推挽输出
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP ;        // 上拉电阻
    GPIO_Init(GPIOE, &GPIO_InitStructure); 

    // 4. 选择GPIO引脚要复用的功能(TIM9)
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource6, GPIO_AF_TIM9);

    // 5. 配置定时器的时基(生成20ms周期)
    TIM_TimeBaseStructure.TIM_Prescaler = 168-1;    // TIM9挂载在APB2总线,分频后频率1MHz(1us计数1次)
    TIM_TimeBaseStructure.TIM_Period    = 20000-1;  // 自动重载值,计数20000次对应20ms
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;    // 不分频
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 递增计数模式
    TIM_TimeBaseInit(TIM9, &TIM_TimeBaseStructure);
    
    // 6. 配置TIM的输出比较(PWM模式)
    TIM_OCInitStructure.TIM_OCMode      = TIM_OCMode_PWM1;          // PWM1模式:递增计数时,CNT<CCR通道有效,否则无效
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   // 开启输出比较功能
    TIM_OCInitStructure.TIM_Pulse       = 500;                      // 舵机转动0度,对应0.5ms高电平(1us×500=500us=0.5ms)
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;       // 高电平有效
    TIM_OC2Init(TIM9, &TIM_OCInitStructure);

    // 7. 使能TIM9通道2预装载寄存器(确保CCR值更新后在下一周期生效)
    TIM_OC2PreloadConfig(TIM9, TIM_OCPreload_Enable);
    
    // 8. 使能TIM9自动重载预装载寄存器(确保ARR值更新后同步生效)
    TIM_ARRPreloadConfig(TIM9, ENABLE);

    // 9. 开启定时器
    TIM_Cmd(TIM9, ENABLE);
}

/**
  * @brief  主程序(程序入口)
  * @param  无
  * @retval 无
  */
int main(void)
{
    // 1. 进行NVIC优先级分组:分组4,抢占优先级4位(取值范围0~15)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); 
    
    // 2. 硬件初始化(仅初始化TIM9,用于舵机控制)
    TIM_Config();
    
    /* 无限循环 */
    while (1)
    {
        // 角度为0度
        TIM_SetCompare2(TIM9,500);
        delay_ms(500);
        // 角度为45度
        TIM_SetCompare2(TIM9,1000);
        delay_ms(500);
        // 角度为90度
        TIM_SetCompare2(TIM9,1500);
        delay_ms(500);
        // 角度为135度
        TIM_SetCompare2(TIM9,2000);
        delay_ms(500);
        // 角度为180度
        TIM_SetCompare2(TIM9,2500);
        delay_ms(500); // 补充180度停留延时,使角度切换更均匀
    }
}

PWM实战拓展(LED呼吸灯)

利用PWM占空比渐变实现LED呼吸灯效果,以TIM14_CH1(PF9引脚)为例,配置1kHz频率PWM,通过循环修改CCR值实现亮度渐变。

#include "stm32f4xx.h"
/**
 * @brief  TIM14_CH1(PF9)PWM初始化函数,配置1kHz频率
 * @param  无
 * @return 无
 * @note   TIM14挂载APB1总线,时钟84MHz;1kHz频率配置:PSC=83,ARR=999
 *         频率=84e6/[(83+1)*(999+1)] = 84e6/(84*1000) = 1000Hz
 */
void TIM14_PWM_Init(void) {
    // 1. 开启时钟(TIM14和GPIOF)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);

    // 2. 配置PF9为复用功能(AF9,对应TIM14_CH1)
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(GPIOF, &GPIO_InitStruct);
    GPIO_PinAFConfig(GPIOF, GPIO_PinSource9, GPIO_AF_TIM14);

    // 3. 配置时基
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
    TIM_TimeBaseInitStruct.TIM_Prescaler = 83;
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStruct.TIM_Period = 999;
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM14, &TIM_TimeBaseInitStruct);

    // 4. 配置PWM模式
    TIM_OCInitTypeDef TIM_OCInitStruct;
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStruct.TIM_Pulse = 0; // 初始亮度最低
    TIM_OC1Init(TIM14, &TIM_OCInitStruct);

    // 5. 使能预装载
    TIM_OC1PreloadConfig(TIM14, TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM14, ENABLE);

    // 6. 使能定时器
    TIM_Cmd(TIM14, ENABLE);
}

int main(void) {
    uint16_t ccr_val = 0;
    uint8_t dir = 1; // 亮度变化方向:1=递增,0=递减
    TIM14_PWM_Init();
    while (1) {
        if (dir) {
            ccr_val++;
            if (ccr_val > 999) dir = 0;
        } else {
            ccr_val--;
            if (ccr_val <= 0) dir = 1;
        }
        TIM_SetCompare1(TIM14, ccr_val); // 更新占空比
        delay_ms(5); // 控制渐变速度
    }
}

软件模拟PWM

可以利用定时器基本定时功能,通过编程控制普通 IO 口输出高低电平并配合精准延时,软件模拟生成可调占空比的 PWM 信号;该方法不受硬件定时器通道限制,可用于任意普通输出 IO 口,通用性与可移植性更强

模拟PWM配置(PF9口,50Hz)

int main(void)
{
    // 配置NVIC优先级分组为4(仅抢占优先级,0-15)
    NVIC_SetPriorityGrouping(NVIC_PriorityGroup_4);
    // 初始化按键模块
    PF9_Init();
    
    // 主循环
    while (1)
    {
        GPIO_ResetBits(GPIOF, GPIO_Pin_9); // 低电平
				delay_ms(19);
        GPIO_SetBits(GPIOF, GPIO_Pin_9);   // 高电平
				delay_ms(1);
    }
}

示波器检测结果

image

关键注意事项

  • 时钟使能:所有定时器和对应GPIO口必须先开启时钟,否则配置无效;
  • 复用功能:PWM输出或输入捕获时,GPIO口需配置为复用模式,并正确设置AF值;
  • 中断优先级:多个定时器中断同时使用时,需合理配置抢占优先级和响应优先级,避免中断冲突;
  • 预装载寄存器:PWM模式下,建议使能CCR和ARR的预装载功能,保证波形稳定;
    • 假设你在 STM32 的定时器中生成 PWM,且未开启预装载:
      • ARR(周期)立即修改:如果在计数器计数过程中修改 ARR,当前周期会被强行截断 / 延长,导致这一个 PWM 周期的时长变得不规则(比如原本 1ms 的周期,中途改成 0.8ms,这个周期就会变成 0.8ms,而下一个周期才是正常的 1ms)。
      • CCR(占空比)立即修改:如果在计数器计数过程中修改 CCR,比如计数器已经计数到 500,你突然把 CCR 从 600 改成 400,那么本周期的比较事件会提前触发,导致 PWM 的高 / 低电平时长被 “打断”,出现毛刺或不规则的电平跳变。
  • 硬件 vs 软件PWM:硬件PWM精度高、CPU占用低,适合对时序要求严格的场景;软件PWM通用性强(任意IO口),但精度依赖定时器定时精度,CPU占用略高。
posted @ 2026-01-12 19:00  YouEmbedded  阅读(10)  评论(0)    收藏  举报