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 的步骤:
- 使能定时器时钟和 GPIO 时钟
- 配置 GPIO 为复用推挽输出
- 设置定时器的 PSC 和 ARR 确定频率
- 配置输出比较通道为 PWM 模式 1 或 2
- 设置 CCR 确定占空比
- 使能定时器
// 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) |
周期测量法的实现步骤:
- 配置输入捕获通道,选择上升沿触发
- 使能捕获中断
- 在中断中读取 CCR 值,计算与前一次捕获的差值
- 频率 = 定时器计数频率 / 差值
// 输入捕获中断服务函数
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 定时器知识体系总览
下面这张图总结了本章的核心内容,帮你理清思路:
我的建议:学习定时器最好的方法是动手。先拿一个基本定时器做延时中断,再试试 PWM 输出,然后用输入捕获测个频率。一步步来,别急着把所有功能都堆上去。等你把每个功能都调通了,再考虑组合使用。