934 字
3 分钟
STM32外部中断EXTI详解与旋转编码器实战
STM32外部中断EXTI详解与旋转编码器实战
EXTI(External Interrupt)是 STM32 的外部中断/事件控制器,用于检测 GPIO 引脚的电平变化并触发中断。本文以旋转编码器为实战案例,详解 EXTI 的配置与使用。
EXTI 基础概念
什么是 EXTI?
EXTI 可以监测 GPIO 引脚的电平变化,支持:
- 上升沿触发:低电平 → 高电平
- 下降沿触发:高电平 → 低电平
- 双边沿触发:任意电平变化
EXTI 线与 GPIO 映射
STM32F103 有 16 条 EXTI 线(EXTI0 ~ EXTI15),每条线对应一个引脚号:
EXTI0 ← PA0 / PB0 / PC0 / PD0 ...(同一时间只能映射一个端口)EXTI1 ← PA1 / PB1 / PC1 / PD1 ......EXTI15 ← PA15 / PB15 / PC15 / PD15 ...注意:同一编号的 EXTI 线只能映射到一个端口。例如 EXTI0 映射到 PB0 后,就不能同时映射到 PA0。
中断 vs 事件
| 类型 | 说明 | 应用场景 |
|---|---|---|
| 中断 | 触发 CPU 执行中断服务函数 | 需要软件处理的场景 |
| 事件 | 触发硬件外设操作(如 DMA、ADC) | 无需 CPU 干预的场景 |
EXTI 配置流程
标准库配置步骤
1. 开启时钟(GPIO + AFIO)2. 配置 GPIO 为输入模式3. 配置 AFIO 映射(GPIO → EXTI)4. 配置 EXTI 触发方式5. 配置 NVIC 中断优先级6. 编写中断服务函数代码模板
void EXTI_Init(void){ /* 1. 开启时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
/* 2. 配置 GPIO */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 3. 配置 AFIO 映射 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);
/* 4. 配置 EXTI */ EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_Init(&EXTI_InitStructure);
/* 5. 配置 NVIC */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure);}
/* 6. 中断服务函数 */void EXTI0_IRQHandler(void){ if (EXTI_GetITStatus(EXTI_Line0) == SET) { // 中断处理逻辑 EXTI_ClearITPendingBit(EXTI_Line0); // 必须清除标志位 }}NVIC 中断优先级
优先级分组
STM32 使用 4 位优先级,分为抢占优先级和子优先级:
| 分组 | 抢占位 | 子优先级位 | 说明 |
|---|---|---|---|
| Group 0 | 0 位 | 4 位 | 无抢占 |
| Group 1 | 1 位 | 3 位 | 2 级抢占 |
| Group 2 | 2 位 | 2 位 | 4 级抢占(常用) |
| Group 3 | 3 位 | 1 位 | 8 级抢占 |
| Group 4 | 4 位 | 0 位 | 16 级抢占 |
优先级规则
- 抢占优先级高的可以打断抢占优先级低的
- 抢占优先级相同时,比较子优先级
- 子优先级相同时,比较硬件中断号
抢占优先级 1 > 抢占优先级 2(数字越小优先级越高)实战:旋转编码器
硬件原理
旋转编码器输出两路正交信号(A 相和 B 相),相位差 90°:
正转:A 相先于 B 相变化 ┌─┐ ┌─┐ ┌─┐ A ───┘ └───┘ └───┘ └─── ┌─┐ ┌─┐ ┌─┐ B ────┘ └───┘ └───┘ └──
反转:B 相先于 A 相变化 ┌─┐ ┌─┐ ┌─┐ A ────┘ └───┘ └───┘ └── ┌─┐ ┌─┐ ┌─┐ B ───┘ └───┘ └───┘ └───方向判断原理
通过检测一路信号的下降沿时,另一路信号的电平来判断方向:
- A 相下降沿时,B 相为低 → 正转(计数 -1)
- B 相下降沿时,A 相为低 → 反转(计数 +1)
正转时序: A: ──╲____╱──╲____╱── B: ____╱──╲____╱──╲__
A 下降沿时,B = 0 → 正转完整代码
#include "stm32f10x.h"
/* 旋转编码器计数值,正转增加,反转减少 */int16_t Encoder_Count;
/** * @brief 旋转编码器初始化 * @note 使用 PB0、PB1 两个引脚,配置为上拉输入 * 通过外部中断(下降沿触发)检测编码器脉冲 */void Encoder_Init(void){ /* 开启 GPIOB 和 AFIO 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
/* 配置 PB0、PB1 为上拉输入模式 */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 将 PB0、PB1 映射到外部中断线 EXTI0、EXTI1 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
/* 配置 EXTI0 和 EXTI1 为下降沿触发的中断模式 */ EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_Init(&EXTI_InitStructure);
/* 配置 NVIC 分组为 Group2(2位抢占优先级,2位子优先级) */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置 EXTI0 中断:抢占优先级 1,子优先级 1 */ NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure);
/* 配置 EXTI1 中断:抢占优先级 1,子优先级 2 */ NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_Init(&NVIC_InitStructure);}
/** * @brief 获取旋转编码器的计数值 * @note 读取后自动清零,返回值为自上次读取以来的累计变化量 */int16_t Encoder_Get(void){ int16_t Temp; Temp = Encoder_Count; Encoder_Count = 0; return Temp;}
/** * @brief EXTI0 中断服务函数(PB0 下降沿触发) * @note 编码器正转时,PB0 下降沿到来时 PB1 为低电平,计数减 1 */void EXTI0_IRQHandler(void){ if (EXTI_GetITStatus(EXTI_Line0) == SET) { /* PB0 下降沿触发时,检测 PB1 的电平判断方向 */ if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) { Encoder_Count--; } EXTI_ClearITPendingBit(EXTI_Line0); }}
/** * @brief EXTI1 中断服务函数(PB1 下降沿触发) * @note 编码器反转时,PB1 下降沿到来时 PB0 为低电平,计数加 1 */void EXTI1_IRQHandler(void){ if (EXTI_GetITStatus(EXTI_Line1) == SET) { /* PB1 下降沿触发时,检测 PB0 的电平判断方向 */ if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) { Encoder_Count++; } EXTI_ClearITPendingBit(EXTI_Line1); }}使用示例
#include "stm32f10x.h"#include "OLED.h"#include "Encoder.h"
int main(void){ OLED_Init(); Encoder_Init();
int16_t count = 0;
while (1) { count += Encoder_Get(); // 累加编码器变化量 OLED_ShowSignedNum(1, 1, count, 5); }}中断服务函数命名
每个 EXTI 线有固定的中断服务函数名:
| EXTI 线 | 函数名 | 中断通道 |
|---|---|---|
| EXTI0 | EXTI0_IRQHandler | EXTI0_IRQn |
| EXTI1 | EXTI1_IRQHandler | EXTI1_IRQn |
| EXTI2 | EXTI2_IRQHandler | EXTI2_IRQn |
| EXTI3 | EXTI3_IRQHandler | EXTI3_IRQn |
| EXTI4 | EXTI4_IRQHandler | EXTI4_IRQn |
| EXTI5~9 | EXTI9_5_IRQHandler | EXTI9_5_IRQn |
| EXTI15~10 | EXTI15_10_IRQHandler | EXTI15_10_IRQn |
注意:EXTI59 共用一个中断函数,EXTI1015 共用一个中断函数,需要在函数内判断具体是哪条线触发。
void EXTI9_5_IRQHandler(void){ if (EXTI_GetITStatus(EXTI_Line5) == SET) { /* 处理 EXTI5 */ } if (EXTI_GetITStatus(EXTI_Line6) == SET) { /* 处理 EXTI6 */ } // ...}常见问题
中断不触发
检查清单:
- GPIO 时钟是否开启?
- AFIO 时钟是否开启?
- AFIO 映射是否正确?
- EXTI 线是否使能?
- NVIC 通道是否使能?
- 中断服务函数名是否正确?
中断频繁触发
原因:信号抖动导致多次触发
解决:
// 在中断中加入简单的消抖逻辑static uint32_t last_time = 0;uint32_t now = HAL_GetTick();if (now - last_time < 10) return; // 10ms 内忽略last_time = now;中断卡死
原因:忘记清除中断标志位
解决:
// 必须在中断处理完成后清除标志位EXTI_ClearITPendingBit(EXTI_Line0);触发方式选择
| 触发方式 | 宏定义 | 适用场景 |
|---|---|---|
| 上升沿 | EXTI_Trigger_Rising | 按键释放检测 |
| 下降沿 | EXTI_Trigger_Falling | 按键按下检测 |
| 双边沿 | EXTI_Trigger_Rising_Falling | 编码器、频率测量 |
总结
EXTI 外部中断配置五步走:
- 开时钟 — GPIO + AFIO
- 配 GPIO — 输入模式
- 配 AFIO — 引脚映射到 EXTI 线
- 配 EXTI — 触发方式
- 配 NVIC — 优先级
关键要点:
- 同一编号的 EXTI 线只能映射一个端口
- 中断服务函数名必须与启动文件中的向量表一致
- 必须清除中断标志位,否则会反复触发中断
- 旋转编码器利用两路信号的相位差判断方向
分享
如果这篇文章对你有帮助,欢迎分享给更多人!
STM32外部中断EXTI详解与旋转编码器实战
https://mizuki.mysqil.com/posts/stm32-exti-interrupt/ 部分信息可能已经过时





