4. 中断与NVIC

定时器是 STM32 里最常用的外设之一。说实话,我做了这么多年嵌入式,几乎每个项目都离不开它。从简单的延时,到复杂的电机控制,定时器无处不在。这一章,我们就把它彻底讲透。

4.1 基本定时器原理

基本定时器(如 TIM6、TIM7)是最简单的定时器。它只有一个功能:计数。说白了,就是一个不断累加的计数器,溢出时产生中断或事件。

它的核心结构就三部分:

  • 预分频器(PSC):对时钟源进行分频,降低计数频率
  • 计数器(CNT):实际计数的寄存器
  • 自动重装载寄存器(ARR):设定计数上限

工作流程很简单:时钟经过 PSC 分频后,驱动 CNT 递增。当 CNT 达到 ARR 的值时,计数器清零,同时产生更新事件或中断。

关键公式:定时时间 = (ARR + 1) × (PSC + 1) / 定时器时钟频率

举个例子:72MHz 时钟,PSC=7199,ARR=999,则定时时间 = 1000 × 7200 / 72,000,000 = 0.1s = 100ms

我的习惯:配置定时器时,我一般先确定 PSC 让计数频率为 1MHz(即 1us 计数一次),然后 ARR 直接写需要的微秒数。这样调试时心里有数,不用反复算。

4.2 通用定时器与 PWM 输出

通用定时器(如 TIM2~TIM5)比基本定时器多了输入捕获、输出比较、PWM 生成等功能。PWM 输出是其中最常用的。

PWM 的核心是输出比较机制。定时器在计数过程中,会不断将 CNT 的值与捕获/比较寄存器(CCR)比较。当 CNT 小于 CCR 时,输出高电平;大于等于 CCR 时,输出低电平。这样就产生了 PWM 波形。

配置 PWM 的步骤:

  1. 使能定时器时钟和 GPIO 时钟
  2. 配置 GPIO 为复用推挽输出
  3. 设置定时器的 PSC 和 ARR 确定频率
  4. 配置输出比较通道为 PWM 模式 1 或 2
  5. 设置 CCR 确定占空比
  6. 使能定时器
// PWM 输出配置示例(TIM2 通道 1,PA0)
void PWM_Init(void) {
    // 1. 使能时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    
    // 2. 配置 GPIO
    GPIOA->MODER |= GPIO_MODER_MODER0_1;   // 复用功能
    GPIOA->AFR[0] |= 1 << 0;               // AF1 = TIM2_CH1
    
    // 3. 定时器基础配置
    TIM2->PSC = 71;      // 72MHz / 72 = 1MHz
    TIM2->ARR = 999;     // 1MHz / 1000 = 1kHz
    
    // 4. PWM 模式配置
    TIM2->CCMR1 |= (6 << 4);   // 通道1 PWM 模式1
    TIM2->CCER |= TIM_CCER_CC1E; // 使能输出
    
    // 5. 设置占空比 50%
    TIM2->CCR1 = 500;
    
    // 6. 使能定时器
    TIM2->CR1 |= TIM_CR1_CEN;
}

我曾经踩过的坑:配置 PWM 时忘记设置 GPIO 的复用功能,结果输出一直没波形。查了半天才发现 GPIO 模式没改。记住:定时器的输出引脚必须配置为复用推挽输出,不是普通推挽输出。

4.3 输入捕获测量频率

输入捕获用来测量外部信号的频率或脉宽。原理是:当检测到指定边沿(上升沿或下降沿)时,定时器会把当前 CNT 的值锁存到 CCR 寄存器,同时可以触发中断。

测量频率的常用方法有两种:

方法 原理 适用场景
周期测量法 捕获两个相邻上升沿,计算差值得到周期 低频信号(<100kHz)
频率测量法 在固定时间内统计捕获次数 高频信号(>100kHz)

周期测量法的实现步骤:

  1. 配置输入捕获通道,选择上升沿触发
  2. 使能捕获中断
  3. 在中断中读取 CCR 值,计算与前一次捕获的差值
  4. 频率 = 定时器计数频率 / 差值
// 输入捕获中断服务函数
uint32_t prev_capture = 0;
uint32_t frequency = 0;

void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_CC1IF) {
        uint32_t current = TIM2->CCR1;
        uint32_t period = current - prev_capture;
        
        // 处理溢出情况
        if (current < prev_capture) {
            period = current + (TIM2->ARR + 1) - prev_capture;
        }
        
        frequency = 1000000 / period;  // 假设计数频率为 1MHz
        prev_capture = current;
        
        TIM2->SR = ~TIM_SR_CC1IF;  // 清除标志
    }
}

避坑指南:我曾经在测量低频信号时,发现频率值跳变很大。后来发现是计数器溢出了。解决办法是:在捕获中断里同时检查更新中断标志,如果发生了溢出,需要把溢出次数也考虑进去。

4.4 编码器模式使用

编码器模式是通用定时器的一个特殊功能,专门用来读取正交编码器的信号。正交编码器输出两路相位差 90° 的方波(A 相和 B 相),定时器通过检测这两路信号的边沿来判断旋转方向和速度。

编码器模式的工作原理:

  • 定时器同时监视 TI1 和 TI2 两个输入通道
  • 根据两路信号的边沿组合,自动判断方向
  • 计数器在每次边沿变化时递增或递减
  • 方向改变时,计数方向自动反转

STM32 的编码器模式有三种:

模式 计数方式 分辨率
模式 1 只在 TI1 边沿计数 1 倍
模式 2 只在 TI2 边沿计数 1 倍
模式 3 在 TI1 和 TI2 边沿都计数 4 倍
// 编码器模式配置(TIM3,通道1=PA6,通道2=PA7)
void Encoder_Init(void) {
    // 时钟和 GPIO 配置省略...
    
    // 配置编码器模式
    TIM3->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1;  // 模式3,4倍频
    
    // 配置输入滤波和极性
    TIM3->CCMR1 |= (3 << 4);   // IC1F=3,8个采样周期滤波
    TIM3->CCER |= TIM_CCER_CC1P; // 反相,根据编码器手册调整
    
    // 设置计数器范围
    TIM3->ARR = 0xFFFF;  // 16位计数器
    
    // 使能定时器
    TIM3->CR1 |= TIM_CR1_CEN;
    
    // 读取当前计数值
    int16_t position = (int16_t)TIM3->CNT;
}

关键点:编码器模式下,计数器是自动双向计数的。你只需要读取 CNT 寄存器就能得到位置信息。方向可以通过读取 CR1 寄存器的 DIR 位来判断。

4.5 定时器中断优先级配置

当多个定时器同时使用时,中断优先级配置就变得很重要。配置不当会导致中断丢失或响应延迟。

NVIC 优先级配置要点:

  • STM32 使用 4 位优先级,分为抢占优先级和子优先级
  • 抢占优先级决定是否可以嵌套中断
  • 子优先级用于同抢占优先级下的中断排队
  • 数值越小,优先级越高
// 中断优先级配置示例
void Timer_IRQ_Priority_Config(void) {
    // 设置优先级分组:3位抢占,1位子优先级
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3);
    
    // 配置 TIM2 中断(高优先级)
    NVIC_InitTypeDef nvic;
    nvic.NVIC_IRQChannel = TIM2_IRQn;
    nvic.NVIC_IRQChannelPreemptionPriority = 1;
    nvic.NVIC_IRQChannelSubPriority = 0;
    nvic.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvic);
    
    // 配置 TIM3 中断(低优先级)
    nvic.NVIC_IRQChannel = TIM3_IRQn;
    nvic.NVIC_IRQChannelPreemptionPriority = 2;
    nvic.NVIC_IRQChannelSubPriority = 0;
    nvic.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvic);
}

我曾经犯过的错:在一个电机控制项目里,我把编码器中断和 PWM 中断设成了相同优先级。结果在高速运转时,两个中断互相干扰,导致位置数据丢失。后来我把编码器中断优先级调高了一级,问题就解决了。记住:对实时性要求高的中断,优先级一定要高。

4.6 定时器知识体系总览

下面这张图总结了本章的核心内容,帮你理清思路:

STM32 定时器知识体系 定时器系统 基本定时器 通用定时器 高级定时器 时基计数 更新中断 PWM 输出 输入捕获 编码器模式 输出比较 互补 PWM 刹车功能 死区插入 本章重点:基本定时器 → PWM → 输入捕获 → 编码器模式 中断优先级配置贯穿所有定时器使用场景

我的建议:学习定时器最好的方法是动手。先拿一个基本定时器做延时中断,再试试 PWM 输出,然后用输入捕获测个频率。一步步来,别急着把所有功能都堆上去。等你把每个功能都调通了,再考虑组合使用。