STM32定时器中断配置详解:以TIM2为例

一、定时器中断的基本概念

定时器工作模式

在STM32中,定时器中断通常用于以下场景:

  1. 定时更新中断:计数器溢出时触发

  2. 捕获/比较中断:输入捕获或输出比较匹配时触发

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定时器中断需要遵循以下关键步骤:

  1. 时钟使能:确保TIM2时钟已开启

  2. 参数计算:根据需求计算PSC和ARR值

  3. 中断配置:使能更新中断和NVIC

  4. 标志管理:正确清除中断标志

  5. 启动定时器:最后使能计数器

掌握定时器中断的底层寄存器操作,不仅能让你写出更高效的代码,还能在复杂应用中灵活应对各种定时需求。建议在实际项目中多练习,从简单的定时闪烁LED开始,逐步尝试PWM输出、输入捕获等高级功能。

通过本文的学习,你应该能够独立配置STM32的定时器中断。下一步可以探索多个定时器协同工作、定时器级联等高级应用场景。

相关推荐
leaves falling20 小时前
c语言-扫雷游戏
c语言·单片机·游戏
梁洪飞21 小时前
clk学习
linux·arm开发·嵌入式硬件·arm
浩瀚地学1 天前
【Java】JDK8的一些新特性
java·开发语言·经验分享·笔记·学习
JeffDingAI1 天前
【Datawhale学习笔记】深入大模型架构
笔记·学习
暖阳之下1 天前
学习周报三十一
学习
a不是橘子1 天前
03在Ubuntu中验证PV操作
笔记·ubuntu·操作系统·虚拟机·os·pv操作
tangyal1 天前
渗透笔记1
笔记·网络安全·渗透
eewj1 天前
STM32中FCLK时钟信号的作用
stm32·单片机·嵌入式硬件
淘晶驰AK1 天前
ESP32和STM32哪个更容易学?
stm32·单片机·嵌入式硬件
fanged1 天前
STM32(5)--HAL1(TODO)
笔记