STM32 进阶封神之路(十):外设定时器全解析 —— 基本定时器 + 通用定时器实战(PWM + 中断)
上一篇我们吃透了内核级的 SysTick 定时器,这一篇聚焦 STM32 的外设定时器—— 作为 SysTick 的补充与拓展,外设定时器(TIM1~TIM14)功能更强大,支持 PWM 输出、脉冲计数、输入捕获等复杂场景,是电机控制、灯光调光、信号测量的核心工具。
本文基于实战资料,从 STM32 定时器分类、基本定时器原理,到通用定时器中断与 PWM 输出实战,手把手带你掌握外设定时器的开发逻辑,让你从 “内核定时” 升级到 “外设精准控制”!
一、STM32 定时器核心认知:分类与应用场景
STM32F103 系列搭载了 14 个外设定时器,按功能可分为基本定时器、通用定时器、高级定时器三类,不同类型适配不同场景,新手需先明确选型逻辑。
1. 定时器分类与核心区别
表格
| 定时器类型 | 包含型号(STM32F103) | 核心功能 | 计数器位数 | 时钟源 | 典型应用场景 |
|---|---|---|---|---|---|
| 基本定时器 | TIM6、TIM7 | 定时中断、触发 DAC | 16 位 | 内部时钟(APB1) | 精准延时、周期性数据采集 |
| 通用定时器 | TIM2~TIM5 | 定时中断、PWM 输出、输入捕获、脉冲计数 | 16 位 | 内部时钟 / 外部时钟 / 编码器 | LED 调光、电机调速、信号频率测量 |
| 高级定时器 | TIM1、TIM8 | 通用定时器所有功能 + 死区控制、互补 PWM | 16 位 | 内部时钟 / 外部时钟 | 三相电机控制、大功率设备驱动 |
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_OC1PreloadConfig和TIM_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 的应用边界!