mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3
934 字
3 分钟
STM32外部中断EXTI详解与旋转编码器实战
2026-06-18

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 00 位4 位无抢占
Group 11 位3 位2 级抢占
Group 22 位2 位4 级抢占(常用)
Group 33 位1 位8 级抢占
Group 44 位0 位16 级抢占

优先级规则#

  1. 抢占优先级高的可以打断抢占优先级低
  2. 抢占优先级相同时,比较子优先级
  3. 子优先级相同时,比较硬件中断号
抢占优先级 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 线函数名中断通道
EXTI0EXTI0_IRQHandlerEXTI0_IRQn
EXTI1EXTI1_IRQHandlerEXTI1_IRQn
EXTI2EXTI2_IRQHandlerEXTI2_IRQn
EXTI3EXTI3_IRQHandlerEXTI3_IRQn
EXTI4EXTI4_IRQHandlerEXTI4_IRQn
EXTI5~9EXTI9_5_IRQHandlerEXTI9_5_IRQn
EXTI15~10EXTI15_10_IRQHandlerEXTI15_10_IRQn

注意:EXTI59 共用一个中断函数,EXTI1015 共用一个中断函数,需要在函数内判断具体是哪条线触发。

void EXTI9_5_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line5) == SET) { /* 处理 EXTI5 */ }
if (EXTI_GetITStatus(EXTI_Line6) == SET) { /* 处理 EXTI6 */ }
// ...
}

常见问题#

中断不触发#

检查清单

  1. GPIO 时钟是否开启?
  2. AFIO 时钟是否开启?
  3. AFIO 映射是否正确?
  4. EXTI 线是否使能?
  5. NVIC 通道是否使能?
  6. 中断服务函数名是否正确?

中断频繁触发#

原因:信号抖动导致多次触发

解决

// 在中断中加入简单的消抖逻辑
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 外部中断配置五步走:

  1. 开时钟 — GPIO + AFIO
  2. 配 GPIO — 输入模式
  3. 配 AFIO — 引脚映射到 EXTI 线
  4. 配 EXTI — 触发方式
  5. 配 NVIC — 优先级

关键要点

  • 同一编号的 EXTI 线只能映射一个端口
  • 中断服务函数名必须与启动文件中的向量表一致
  • 必须清除中断标志位,否则会反复触发中断
  • 旋转编码器利用两路信号的相位差判断方向
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

STM32外部中断EXTI详解与旋转编码器实战
https://mizuki.mysqil.com/posts/stm32-exti-interrupt/
作者
まつざか ゆき
发布于
2026-06-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时