STM32 进阶封神之路(十):外设定时器全解析 —— 基本定时器 + 通用定时器实战(PWM + 中断)

0 阅读15分钟

STM32 进阶封神之路(十):外设定时器全解析 —— 基本定时器 + 通用定时器实战(PWM + 中断)

上一篇我们吃透了内核级的 SysTick 定时器,这一篇聚焦 STM32 的外设定时器—— 作为 SysTick 的补充与拓展,外设定时器(TIM1~TIM14)功能更强大,支持 PWM 输出、脉冲计数、输入捕获等复杂场景,是电机控制、灯光调光、信号测量的核心工具。

本文基于实战资料,从 STM32 定时器分类、基本定时器原理,到通用定时器中断与 PWM 输出实战,手把手带你掌握外设定时器的开发逻辑,让你从 “内核定时” 升级到 “外设精准控制”!

一、STM32 定时器核心认知:分类与应用场景

STM32F103 系列搭载了 14 个外设定时器,按功能可分为基本定时器、通用定时器、高级定时器三类,不同类型适配不同场景,新手需先明确选型逻辑。

1. 定时器分类与核心区别

表格

定时器类型包含型号(STM32F103)核心功能计数器位数时钟源典型应用场景
基本定时器TIM6、TIM7定时中断、触发 DAC16 位内部时钟(APB1)精准延时、周期性数据采集
通用定时器TIM2~TIM5定时中断、PWM 输出、输入捕获、脉冲计数16 位内部时钟 / 外部时钟 / 编码器LED 调光、电机调速、信号频率测量
高级定时器TIM1、TIM8通用定时器所有功能 + 死区控制、互补 PWM16 位内部时钟 / 外部时钟三相电机控制、大功率设备驱动

2. 核心选型原则

  • 仅需定时中断:优先选基本定时器(TIM6/TIM7),配置最简单,占用资源少;
  • 需 PWM 输出 / 输入捕获:选通用定时器(TIM2~TIM5),功能均衡,适配大部分场景;
  • 需互补 PWM / 死区控制:选高级定时器(TIM1/TIM8),工业级电机控制首选;
  • 多定时器并发:合理分配定时器(如 TIM6 做定时中断,TIM3 做 PWM 输出),避免功能冲突。

3. 定时器时钟源与分频逻辑

外设定时器的时钟源来自 APB 总线,核心时钟路径如下:

plaintext

系统时钟(72MHz)→ AHB总线时钟(72MHz)→ APB1总线时钟(36MHz)/ APB2总线时钟(72MHz)→ 定时器时钟
  • 基本定时器(TIM6/TIM7)、通用定时器(TIM2~TIM5)挂载在 APB1 总线,默认时钟 36MHz;
  • 高级定时器(TIM1/TIM8)挂载在 APB2 总线,默认时钟 72MHz;
  • 时钟分频:若 APB 总线预分频系数为 1,定时器时钟 = APB 总线时钟;若预分频系数 > 1,定时器时钟 = 2×APB 总线时钟(STM32F103 默认 APB1 预分频系数 = 2,故 TIM2~TIM7 时钟 = 72MHz)。

二、基本定时器深度解析:TIM6/TIM7 实战(定时中断)

基本定时器(TIM6、TIM7)是外设定时器中最简化的类型,仅支持定时中断和 DAC 触发,核心用于替代 SysTick 实现精准定时,释放内核资源。

1. 基本定时器核心架构

基本定时器的架构极简,核心由 “时钟源→预分频器→计数器→自动重装载寄存器” 组成:

plaintext

定时器时钟 → 预分频器(PSC)→ 16位向上计数器(CNT)→ 自动重装载寄存器(ARR)→ 计数到0 → 触发中断(可选)→ 自动重装计数
  • 预分频器(PSC):将定时器时钟分频,降低计数频率(如 72MHz→1MHz);
  • 自动重装载寄存器(ARR):存储计数器的最大值,计数到该值后复位;
  • 计数器(CNT):从 0 开始向上计数,达到 ARR 值后触发中断并复位。

2. 定时周期计算(核心公式)

基本定时器的定时周期由 “预分频系数(PSC)” 和 “自动重装载值(ARR)” 决定,公式如下:

plaintext

定时周期 T = (PSC + 1) × (ARR + 1) / 定时器时钟频率 f_clk
  • 公式说明:

    • PSC+1:预分频器为 16 位寄存器(0~65535),分频系数 = PSC+1(如 PSC=71,分频系数 = 72);
    • ARR+1:计数器从 0 计数到 ARR,共经历 ARR+1 个时钟周期;
    • f_clk:定时器时钟频率(TIM6/TIM7 默认 72MHz)。
实战计算示例

需求:实现 1ms 定时中断(TIM6),计算 PSC 和 ARR 值:

  • 已知:f_clk=72MHz,T=1ms=0.001s;
  • 推导:(PSC+1)×(ARR+1) = T×f_clk = 0.001×72×10^6 = 72000;
  • 选型:取 PSC=71(分频系数 = 72,72MHz/72=1MHz),则 ARR+1=72000/72=1000 → ARR=999;
  • 结论:PSC=71,ARR=999,定时周期 = (71+1)×(999+1)/72MHz=72×1000/72000000=0.001s=1ms。

3. 基本定时器实战:TIM6 定时中断(库函数 + 寄存器)

硬件需求
  • LED:PB0(推挽输出),通过 TIM6 中断控制 LED1Hz 闪烁(每隔 500ms 翻转一次)。
库函数版实现

c

运行

#include "stm32f10x.h"

// 全局变量:记录中断次数(500次中断=500ms)
uint16_t tim6_interrupt_cnt = 0;

// TIM6中断服务函数
void TIM6_IRQHandler(void) {
    // 检查中断标志位
    if(TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) {
        tim6_interrupt_cnt++;
        // 500次中断=500ms,翻转LED
        if(tim6_interrupt_cnt >= 500) {
            GPIO_WriteBit(GPIOB, GPIO_Pin_0, 
                         (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0)));
            tim6_interrupt_cnt = 0;
        }
        // 清除中断标志位
        TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
    }
}

// 基本定时器TIM6初始化(1ms中断)
void TIM6_Init(void) {
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    // 1. 使能TIM6时钟(APB1总线)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

    // 2. 配置定时器基本参数
    TIM_TimeBaseStruct.TIM_Prescaler = 71;          // 预分频系数=72(71+1)
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
    TIM_TimeBaseStruct.TIM_Period = 999;           // 自动重装载值=999
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频系数=1
    TIM_TimeBaseStruct.TIM_RepetitionCounter = 0;   // 重复计数器=0(基本定时器无此功能)
    TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStruct);

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

    // 4. 配置NVIC中断优先级
    NVIC_InitStruct.NVIC_IRQChannel = TIM6_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;        // 响应优先级0
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 5. 启动TIM6
    TIM_Cmd(TIM6, ENABLE);
}

// GPIO初始化(PB0推挽输出)
void GPIO_Init_Config(void) {
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    GPIO_SetBits(GPIOB, GPIO_Pin_0); // 初始熄灭
}

// 主函数
int main(void) {
    GPIO_Init_Config();
    TIM6_Init();

    while(1) {
        // 主函数无需操作,中断自动触发
    }
}
寄存器版实现(底层逻辑)

c

运行

#include "stm32f10x.h"

uint16_t tim6_interrupt_cnt = 0;

void TIM6_IRQHandler(void) {
    if(TIM6->SR & (1<<0)) { // 检查更新中断标志位(bit0)
        tim6_interrupt_cnt++;
        if(tim6_interrupt_cnt >= 500) {
            GPIOB->ODR ^= (1<<0); // 翻转PB0
            tim6_interrupt_cnt = 0;
        }
        TIM6->SR &= ~(1<<0); // 清除中断标志位
    }
}

void TIM6_Init(void) {
    // 1. 使能TIM6时钟
    RCC->APB1ENR |= (1<<4); // TIM6时钟使能位(bit4)

    // 2. 配置预分频器和自动重装载值
    TIM6->PSC = 71;  // 预分频系数=72
    TIM6->ARR = 999; // 自动重装载值=999

    // 3. 使能更新中断
    TIM6->DIER |= (1<<0); // 使能更新中断(bit0)

    // 4. 配置NVIC
    NVIC->IP[17] = 0x40; // TIM6中断优先级(抢占2,响应0)
    NVIC->ISER[0] |= (1<<17); // 使能TIM6中断(中断编号17)

    // 5. 启动TIM6
    TIM6->CR1 |= (1<<0); // 使能计数器(bit0)
}

// GPIO初始化同库函数版
void GPIO_Init_Config(void) {
    RCC->APB2ENR |= (1<<3); // 使能GPIOB时钟
    GPIOB->CRL &= ~(0x0F<<0);
    GPIOB->CRL |= (0x03<<0); // PB0推挽输出
    GPIOB->ODR |= (1<<0);
}

int main(void) {
    GPIO_Init_Config();
    TIM6_Init();

    while(1);
}

三、通用定时器深度解析:TIM3 PWM 输出实战

通用定时器(TIM2~TIM5)是最常用的外设定时器,支持 PWM 输出功能 —— 通过控制输出电平的高低占空比,实现 LED 调光、电机调速等场景,核心是 “PWM 模式配置” 和 “占空比调节”。

1. PWM 核心概念

  • PWM(脉冲宽度调制):通过周期性的方波信号,控制高电平的占空比(高电平时间 / 周期),实现模拟电压输出;

  • 占空比:高电平时间占一个周期的比例(如 50% 占空比 = 高电平 50ms + 低电平 50ms,周期 100ms);

  • PWM 模式:STM32 通用定时器支持两种 PWM 模式(PWM 模式 1、PWM 模式 2),核心区别在于计数器与比较值匹配时的电平状态:

    • PWM 模式 1:计数器 < 比较值(CCR)时输出高电平,≥比较值时输出低电平;
    • PWM 模式 2:计数器 < 比较值时输出低电平,≥比较值时输出高电平。

2. PWM 周期与占空比计算

(1)PWM 周期计算

与基本定时器定时周期公式一致:

plaintext

PWM周期 T = (PSC + 1) × (ARR + 1) / 定时器时钟频率 f_clk
PWM频率 f = 1 / T
(2)占空比计算

占空比由 “比较值(CCR)” 决定:

plaintext

占空比 = (CCR + 1) / (ARR + 1) × 100%
  • CCR:比较寄存器(16 位,0~ARR),用于设定高 / 低电平的切换阈值。
实战计算示例

需求:实现 1kHz PWM 信号(周期 1ms),占空比 50%(TIM3 通道 1,PA6):

  • 已知:f_clk=72MHz,f=1kHz→T=1ms;
  • 推导:(PSC+1)×(ARR+1)=72000(同基本定时器);
  • 选型:PSC=71(分频系数 72),ARR=999(周期 1ms);
  • 占空比 50%:CCR=(ARR+1)×50% -1=1000×50% -1=499;
  • 结论:PSC=71,ARR=999,CCR=499,PWM 频率 1kHz,占空比 50%。

3. 通用定时器实战:TIM3 PWM 输出(LED 调光)

硬件连接
  • LED:PA6(TIM3_CH1)→ 1KΩ 限流电阻→ GND(PA6 为 TIM3 通道 1 的复用输出引脚)。
库函数版实现

c

运行

#include "stm32f10x.h"

// TIM3 PWM初始化(1kHz,占空比可调节)
void TIM3_PWM_Init(uint16_t arr, uint16_t psc) {
    GPIO_InitTypeDef GPIO_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    TIM_OCInitTypeDef TIM_OCInitStruct;

    // 1. 使能TIM3和GPIOA时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置PA6为复用推挽输出(TIM3_CH1)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 配置TIM3基本参数
    TIM_TimeBaseStruct.TIM_Prescaler = psc;
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStruct.TIM_Period = arr;
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct);

    // 4. 配置PWM模式
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 使能输出
    TIM_OCInitStruct.TIM_Pulse = 0; // 初始占空比0%(CCR=0)
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性高
    TIM_OC1Init(TIM3, &TIM_OCInitStruct); // 配置通道1

    // 5. 使能PWM预装载
    TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能自动重装载预装载

    // 6. 启动TIM3
    TIM_Cmd(TIM3, ENABLE);
}

// 调节PWM占空比(0~100%)
void TIM3_PWM_SetDuty(uint8_t duty) {
    if(duty > 100) duty = 100;
    // CCR = (ARR+1) × duty% -1
    TIM_SetCompare1(TIM3, (1000 * duty) / 100 - 1);
}

// 主函数:LED呼吸灯(占空比0~100%循环)
int main(void) {
    uint8_t duty = 0;
    uint8_t step = 1;

    // 初始化TIM3 PWM(1kHz,PSC=71,ARR=999)
    TIM3_PWM_Init(999, 71);

    while(1) {
        // 占空比递增
        if(duty >= 100) step = -1;
        // 占空比递减
        if(duty <= 0) step = 1;
        duty += step;

        TIM3_PWM_SetDuty(duty);
        // 延时控制呼吸速度
        for(uint32_t i=0; i<100000; i++);
    }
}

4. 核心代码解析

  • GPIO_Mode_AF_PP:PA6 需配置为复用推挽输出,才能作为 TIM3_CH1 的 PWM 输出引脚;
  • TIM_OCMode_PWM1:选择 PWM 模式 1,高电平占空比由 CCR 控制;
  • TIM_SetCompare1:动态修改比较值 CCR,实现占空比调节(呼吸灯核心);
  • 预装载使能:TIM_OC1PreloadConfigTIM_ARRPreloadConfig使能后,CCR 和 ARR 的修改需等待下一个周期生效,避免 PWM 波形畸变。

四、通用定时器实战:TIM4 定时中断 + 按键控制 PWM 占空比

结合定时中断和 PWM 输出,实现 “按键控制 LED 呼吸灯速度”——TIM4 定时中断(10ms)扫描按键,动态修改 PWM 占空比的变化步长。

硬件连接

  • 按键:PB1(上拉输入)→ GND;
  • LED:PA6(TIM3_CH1)→ 1KΩ 限流电阻→ GND。

代码实现

c

运行

#include "stm32f10x.h"

uint8_t duty = 0;
uint8_t step = 1;
uint16_t key_scan_cnt = 0;
uint8_t key_flag = 0;

// TIM4定时中断(10ms):按键扫描
void TIM4_IRQHandler(void) {
    if(TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) {
        key_scan_cnt++;
        // 100ms扫描一次按键(10次中断)
        if(key_scan_cnt >= 10) {
            if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) {
                key_flag = 1; // 按键按下标志
            }
            key_scan_cnt = 0;
        }
        TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
    }
}

// TIM4初始化(10ms中断)
void TIM4_Init(void) {
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);

    TIM_TimeBaseStruct.TIM_Prescaler = 719; // 分频系数720(72MHz/720=100kHz)
    TIM_TimeBaseStruct.TIM_Period = 999;    // 周期= (719+1)*(999+1)/72MHz=10ms
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStruct);

    TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);

    NVIC_InitStruct.NVIC_IRQChannel = TIM4_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    TIM_Cmd(TIM4, ENABLE);
}

// 按键处理:切换呼吸灯速度(step=1/3/5)
void Key_Process(void) {
    if(key_flag == 1) {
        delay_ms(20); // 消抖
        if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) {
            if(step == 1) step = 3;
            else if(step == 3) step = 5;
            else step = 1;
            while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); // 等待释放
        }
        key_flag = 0;
    }
}

// TIM3 PWM初始化、占空比调节函数同前序代码
void TIM3_PWM_Init(uint16_t arr, uint16_t psc);
void TIM3_PWM_SetDuty(uint8_t duty);

int main(void) {
    GPIO_InitTypeDef GPIO_InitStruct;

    // 按键PB1初始化(上拉输入)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    TIM3_PWM_Init(999, 71);
    TIM4_Init();

    while(1) {
        Key_Process();

        // 调节占空比
        if(duty >= 100) step = -step;
        if(duty <= 0) step = -step;
        duty += step;

        TIM3_PWM_SetDuty(duty);
        for(uint32_t i=0; i<50000; i++);
    }
}

五、定时器配置避坑指南(10 + 高频错误)

1. 时钟使能错误→定时器无响应

  • 现象:TIM3 初始化后无 PWM 输出,TIM6 中断未触发;
  • 原因:基本 / 通用定时器挂载在 APB1 总线,需调用RCC_APB1PeriphClockCmd,而非 APB2;
  • 解决:按定时器挂载总线正确使能时钟(TIM1/TIM8→APB2,其余外设定时器→APB1)。

2. GPIO 模式配置错误→PWM 无输出

  • 现象:TIM3_CH1 配置后 PA6 无电平变化;
  • 原因:未将 GPIO 配置为复用推挽输出(GPIO_Mode_AF_PP),仍为普通输出模式;
  • 解决:PWM 输出引脚必须配置为复用推挽输出,才能输出定时器生成的 PWM 信号。

3. 预分频系数 / ARR 值计算错误→周期异常

  • 现象:1ms 定时实际为 10ms,PWM 频率不符合预期;
  • 原因:公式记忆错误(遗漏 + 1),如 PSC=72 误算为分频系数 72(实际为 73);
  • 解决:严格按公式计算,确保(PSC+1)×(ARR+1)/f_clk等于目标周期。

4. 未清除中断标志位→中断重复触发

  • 现象:TIM6 中断服务函数反复执行,主函数无法正常运行;
  • 原因:ISR 中未调用TIM_ClearITPendingBit清除中断标志位;
  • 解决:中断服务函数中必须清除对应中断标志位,否则定时器会持续请求中断。

5. CCR 值超出 ARR 范围→占空比异常

  • 现象:设置占空比 100% 时 LED 未全亮,占空比 0% 时未熄灭;
  • 原因:CCR 值大于 ARR(如 ARR=999,CCR=1000),超出比较寄存器范围;
  • 解决:CCR 值必须在 0~ARR 之间,占空比 100% 时 CCR=ARR,0% 时 CCR=0。

六、总结:外设定时器核心要点与进阶方向

1. 核心要点回顾

  • 外设定时器分三类:基本定时器(定时中断)、通用定时器(PWM + 中断 + 捕获)、高级定时器(互补 PWM);
  • 定时周期公式:T=(PSC+1)×(ARR+1)/f_clk,PWM 占空比公式:占空比=(CCR+1)/(ARR+1)×100%
  • 基本定时器实战:TIM6/TIM7 实现精准定时中断,释放 SysTick 资源;
  • 通用定时器实战:TIM3/TIM4 实现 PWM 输出(LED 调光)和定时中断(按键扫描),功能灵活;
  • 避坑核心:时钟使能、GPIO 复用模式、中断标志位清除、参数计算正确性。

2. 进阶学习方向

  • 输入捕获:用 TIM2/TIM5 实现信号频率、周期测量(如红外传感器信号解码);
  • 编码器接口:用通用定时器的编码器模式,实现电机转速测量;
  • 高级定时器:TIM1/TIM8 实现互补 PWM 和死区控制,适配工业电机驱动;
  • 定时器同步:多定时器联动(如 TIM1 触发 TIM3),实现复杂时序控制。

外设定时器是 STM32 开发的核心工具,从简单的定时中断到复杂的 PWM 调速,掌握后能应对大部分嵌入式控制场景。下一篇我们将学习 ADC 采集与 DAC 输出,实现模拟信号与数字信号的转换,进一步拓展 STM32 的应用边界!