一、定时器中断的基本概念
定时器工作模式
在STM32中,定时器中断通常用于以下场景:
-
定时更新中断:计数器溢出时触发
-
捕获/比较中断:输入捕获或输出比较匹配时触发
TIM2特性概览(以STM32F103为例)
-
16位向上/向下自动重载计数器
-
16位可编程预分频器
-
4个独立通道(输入捕获/输出比较/PWM)
-
支持增量编码器接口
-
支持触发输入作为外部时钟
二、完整配置步骤
步骤1:使能定时器时钟
所有外设使用前必须先使能时钟:
cs
// STM32F1系列
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// STM32F4系列
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
__DSB(); // 数据同步屏障,确保时钟稳定
时钟总线说明:
-
APB1:低速外设总线(最高36MHz/84MHz,取决于系列)
-
APB2:高速外设总线
-
TIM2挂在APB1总线上
步骤2:配置定时器基本参数
2.1 计算定时周期
定时器时钟频率公式:
定时器时钟 = APB1总线时钟 / 预分频系数
中断频率 = 定时器时钟 / (自动重载值 + 1)
2.2 配置预分频器和自动重载值
cs
// 假设系统时钟为72MHz,APB1时钟为36MHz
// 目标:产生1ms中断(1000Hz)
// 方法1:直接计算配置
void TIM2_Configuration(void)
{
// 配置预分频器
// 预分频值 = 定时器时钟 / 目标频率 - 1
// 36MHz / 1000Hz = 36000
// 预分频器设为35999(从0开始计数)
TIM2->PSC = 35999; // 预分频值
// 配置自动重载值
// 如果需要1ms中断,ARR设为999(1000次计数)
TIM2->ARR = 999; // 自动重载值
// 或者更简单的计算:
// 1ms中断 = 1kHz频率
// 计数频率 = 36MHz / (PSC+1) = 1kHz
// 所以 PSC = 36000-1 = 35999
}
2.3 更通用的配置函数
cs
// 通用定时器配置函数
// 输入参数:中断频率(Hz)
void TIM2_Init(uint32_t frequency)
{
uint32_t tim_clock;
uint32_t psc, arr;
// 获取TIM2时钟频率
// 注意:APB1预分频器可能影响定时器时钟
// STM32中,如果APB1预分频系数≠1,定时器时钟=APB1时钟×2
// 假设系统时钟72MHz,APB1分频系数为2
// 则APB1时钟=36MHz,TIM2时钟=72MHz
tim_clock = 72000000; // 72MHz
// 计算预分频器和自动重载值
// 原则:ARR尽可能大,PSC尽可能小,提高分辨率
arr = 1000 - 1; // 固定ARR为1000
psc = (tim_clock / frequency / (arr + 1)) - 1;
TIM2->PSC = psc;
TIM2->ARR = arr;
}
步骤3:配置中断相关寄存器
3.1 使能更新中断
// 使能更新中断
TIM2->DIER |= TIM_DIER_UIE; // 更新中断使能
3.2 中断优先级配置(NVIC)
cs
// 配置NVIC
NVIC_EnableIRQ(TIM2_IRQn); // 使能TIM2中断
NVIC_SetPriority(TIM2_IRQn, 1); // 设置中断优先级
中断优先级说明:
-
STM32使用4位优先级分组
-
优先级数值越小,优先级越高
-
建议:
-
紧急中断:0-1
-
普通中断:2-3
-
低优先级:>3
-
步骤4:启动定时器
cs
// 方法1:使用一个更新事件启动计数器
TIM2->EGR |= TIM_EGR_UG; // 产生更新事件,预装载值生效
TIM2->CR1 |= TIM_CR1_CEN; // 使能计数器
// 方法2:直接启动
TIM2->CR1 |= TIM_CR1_CEN; // 计数器使能
步骤5:编写中断服务函数
cs
// TIM2全局中断服务函数
void TIM2_IRQHandler(void)
{
// 1. 检查中断标志位
if (TIM2->SR & TIM_SR_UIF)
{
// 2. 清除中断标志位(重要!)
TIM2->SR &= ~TIM_SR_UIF;
// 3. 执行中断处理代码
// 例如:翻转LED
GPIOA->ODR ^= (1 << 5); // 翻转PA5
// 4. 可以添加其他功能
static uint32_t counter = 0;
counter++;
if (counter >= 1000) // 1秒处理
{
counter = 0;
// 执行1秒任务
}
}
// 检查其他中断标志位(如需要)
if (TIM2->SR & TIM_SR_CC1IF) // 通道1捕获/比较中断
{
TIM2->SR &= ~TIM_SR_CC1IF;
// 处理通道1中断
}
}
三、完整示例代码
cs
#include "stm32f1xx.h"
// LED GPIO配置
void LED_Init(void)
{
// 使能GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 配置PA5为推挽输出
GPIOA->CRL &= ~(GPIO_CRL_MODE5 | GPIO_CRL_CNF5);
GPIOA->CRL |= GPIO_CRL_MODE5_0; // 输出模式,最大速度10MHz
}
// TIM2初始化:1ms中断
void TIM2_Init_1ms(void)
{
// 1. 使能TIM2时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 2. 配置定时器参数
// 系统时钟72MHz,APB1分频系数为2
// APB1时钟=36MHz,TIM2时钟=72MHz
TIM2->PSC = 7200 - 1; // 分频到10kHz
TIM2->ARR = 10 - 1; // 1ms中断 (10kHz/10 = 1kHz)
// 3. 配置中断
TIM2->DIER |= TIM_DIER_UIE; // 使能更新中断
// 4. 配置NVIC
NVIC_EnableIRQ(TIM2_IRQn);
NVIC_SetPriority(TIM2_IRQn, 2); // 中等优先级
// 5. 启动定时器
TIM2->CR1 |= TIM_CR1_CEN; // 使能计数器
}
// TIM2中断服务函数
void TIM2_IRQHandler(void)
{
static uint32_t ms_counter = 0;
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF; // 清除中断标志
ms_counter++;
// 每500ms翻转LED
if (ms_counter >= 500)
{
ms_counter = 0;
GPIOA->ODR ^= (1 << 5); // 翻转PA5
}
}
}
int main(void)
{
// 初始化系统时钟(假设已配置为72MHz)
SystemInit();
// 初始化LED
LED_Init();
// 初始化定时器
TIM2_Init_1ms();
// 主循环
while(1)
{
// 主程序可以执行其他任务
// 定时器中断在后台运行
__WFI(); // 等待中断,进入低功耗模式
}
return 0;
}
四、高级配置技巧
1. 精确微秒延时
cs
// 使用TIM2实现微秒级延时
void TIM2_Delay_us(uint32_t us)
{
// 配置TIM2为1MHz计数频率
// 假设TIM2时钟=72MHz
TIM2->PSC = 72 - 1; // 72MHz/72 = 1MHz
TIM2->ARR = us - 1; // 设置延时时间
// 清空计数器
TIM2->CNT = 0;
// 启动单次模式
TIM2->CR1 |= TIM_CR1_OPM; // 单脉冲模式
TIM2->CR1 |= TIM_CR1_CEN; // 启动计数器
// 等待计数器停止
while (TIM2->CR1 & TIM_CR1_CEN);
}
2. PWM输出配置
cs
void TIM2_PWM_Init(uint32_t duty_cycle)
{
// 配置TIM2通道1为PWM输出(PA0)
// 1. 使能GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 2. 配置PA0为复用推挽输出
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
GPIOA->CRL |= GPIO_CRL_MODE0_1 | GPIO_CRL_CNF0_1; // 复用输出
// 3. 配置TIM2
TIM2->PSC = 7200 - 1; // 10kHz PWM频率
TIM2->ARR = 1000 - 1; // 1000级分辨率
// 4. 配置PWM模式
// 通道1输出比较配置
TIM2->CCMR1 &= ~TIM_CCMR1_OC1M;
TIM2->CCMR1 |= (6 << 4); // PWM模式1
// 5. 设置占空比
TIM2->CCR1 = duty_cycle; // 占空比数值
// 6. 使能输出
TIM2->CCER |= TIM_CCER_CC1E;
// 7. 启动定时器
TIM2->CR1 |= TIM_CR1_CEN;
}
3. 输入捕获配置(测量脉冲宽度)
cs
void TIM2_InputCapture_Init(void)
{
// 配置TIM2通道1为输入捕获(PA0)
// 1. 配置GPIO
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
GPIOA->CRL |= GPIO_CRL_CNF0_0; // 浮空输入
// 2. 配置TIM2
TIM2->PSC = 72 - 1; // 1MHz计数频率,1μs分辨率
// 3. 配置输入捕获
TIM2->CCMR1 &= ~TIM_CCMR1_CC1S;
TIM2->CCMR1 |= (1 << 0); // CC1通道配置为输入,IC1映射到TI1
// 4. 配置捕获边沿
TIM2->CCER &= ~TIM_CCER_CC1P; // 上升沿捕获
// 5. 使能捕获中断
TIM2->DIER |= TIM_DIER_CC1IE;
NVIC_EnableIRQ(TIM2_IRQn);
// 6. 启动定时器
TIM2->CR1 |= TIM_CR1_CEN;
}
// 中断中处理捕获
void TIM2_IRQHandler(void)
{
static uint16_t capture1 = 0, capture2 = 0;
static uint8_t capture_count = 0;
uint16_t pulse_width;
if (TIM2->SR & TIM_SR_CC1IF) // 捕获中断
{
if (capture_count == 0)
{
capture1 = TIM2->CCR1;
capture_count = 1;
// 改为下降沿捕获
TIM2->CCER |= TIM_CCER_CC1P;
}
else if (capture_count == 1)
{
capture2 = TIM2->CCR1;
capture_count = 0;
// 计算脉冲宽度(μs)
pulse_width = capture2 - capture1;
// 改回上升沿捕获
TIM2->CCER &= ~TIM_CCER_CC1P;
}
TIM2->SR &= ~TIM_SR_CC1IF;
}
}
五、常见问题与调试技巧
1. 定时器不工作的检查清单
cs
void Debug_TIM2_Status(void)
{
// 检查时钟是否使能
if (!(RCC->APB1ENR & RCC_APB1ENR_TIM2EN))
{
// 时钟未使能
}
// 检查定时器是否使能
if (!(TIM2->CR1 & TIM_CR1_CEN))
{
// 定时器未启动
}
// 检查中断是否使能
if (!(TIM2->DIER & TIM_DIER_UIE))
{
// 更新中断未使能
}
// 检查NVIC配置
if (!(NVIC->ISER[0] & (1 << TIM2_IRQn)))
{
// NVIC中断未使能
}
// 读取当前计数器值
uint32_t cnt_val = TIM2->CNT;
// 检查中断标志
if (TIM2->SR & TIM_SR_UIF)
{
// 有未处理的中断
}
}
2. 中断标志未清除的后果
-
中断会连续触发,导致系统卡死
-
必须在中服函数开始时清除标志位
3. 计算精度优化
cs
// 使用宏定义提高可读性
#define SYSTEM_CLOCK 72000000UL
#define APB1_CLOCK (SYSTEM_CLOCK / 2)
#define TIM2_CLOCK (APB1_CLOCK * 2) // APB1分频系数≠1时
// 计算定时参数
#define TIM2_PRESCALER 35999UL
#define TIM2_PERIOD 999UL
#define TIM2_FREQUENCY (TIM2_CLOCK / (TIM2_PRESCALER + 1) / (TIM2_PERIOD + 1))
六、不同STM32系列的差异
STM32F1 vs STM32F4/F7
cs
// 条件编译处理差异
#if defined(STM32F1)
// F1系列
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 35999;
TIM2->ARR = 999;
#elif defined(STM32F4) || defined(STM32F7)
// F4/F7系列
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// F4系列支持32位定时器(TIM2和TIM5)
TIM2->PSC = 8399; // 不同时钟配置
TIM2->ARR = 9999;
// 自动重载预装载使能
TIM2->CR1 |= TIM_CR1_ARPE;
#endif
中断向量表差异
cs
// STM32F1: 中断服务函数名固定
void TIM2_IRQHandler(void) __attribute__((interrupt));
// STM32F4/F7: 使用CMSIS标准
void TIM2_IRQHandler(void)
{
// CMSIS函数操作
if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE))
{
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
// 处理中断
}
}
总结
配置STM32定时器中断需要遵循以下关键步骤:
-
时钟使能:确保TIM2时钟已开启
-
参数计算:根据需求计算PSC和ARR值
-
中断配置:使能更新中断和NVIC
-
标志管理:正确清除中断标志
-
启动定时器:最后使能计数器
掌握定时器中断的底层寄存器操作,不仅能让你写出更高效的代码,还能在复杂应用中灵活应对各种定时需求。建议在实际项目中多练习,从简单的定时闪烁LED开始,逐步尝试PWM输出、输入捕获等高级功能。
通过本文的学习,你应该能够独立配置STM32的定时器中断。下一步可以探索多个定时器协同工作、定时器级联等高级应用场景。