05-EXTI外部中断
中断系统概念
graph TD
subgraph NORMAL
A["主程序<br/>Task A"] --> B["Task B"]
end
subgraph INTERRUPT
IRQ["中断请求<br/>(硬件事件)"] --> SAVE["保存现场<br/>(自动压栈)"]
SAVE --> SERVICE["中断服务函数<br/>ISR"]
SERVICE --> RESTORE["恢复现场<br/>(出栈)"]
RESTORE --> RETURN["返回主程序"]
end
NORMAL --> IRQ
RETURN --> NORMAL
关键术语
| 术语 |
说明 |
| 中断 |
CPU 暂停当前任务,处理紧急事件 |
| 中断优先级 |
决定多个中断同时触发时的响应顺序 |
| 中断嵌套 |
高优先级中断可以打断低优先级中断服务函数 |
| 向量表 |
存储所有中断函数入口地址的表(位于 0x08000000 起始) |
| 尾链(Tail Chaining) |
Cortex-M3 优化:两个中断连续进出时省去多余出栈入栈 |
STM32 中断架构
| 特性 |
STM32F103 |
| 可屏蔽中断通道 |
68 个 |
| 可编程优先级 |
16 级 (4bit) |
| 固定优先级 |
中断号越小优先级越高 |
| 系统异常 |
8 个 (Reset, NMI, HardFault, MemManage, BusFault, UsageFault, SVCall, PendSV, SysTick) |
| 外部中断 |
EXTI0~EXTI15 (16 条线) |
NVIC 优先级分组
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
| 分组宏 |
抢占位 |
子优先级位 |
抢占级数 |
子优先级数 |
NVIC_PriorityGroup_0 |
0 |
4 |
1 |
16 |
NVIC_PriorityGroup_1 |
1 |
3 |
2 |
8 |
NVIC_PriorityGroup_2 |
2 |
2 |
4 |
4 |
NVIC_PriorityGroup_3 |
3 |
1 |
8 |
2 |
NVIC_PriorityGroup_4 |
4 |
0 |
16 |
1 |
建议:全系统只用一种分组,在 main() 入口配置一次。推荐 NVIC_PriorityGroup_2。
中断抢占规则
- 抢占优先级不同 → 高抢占优先级的先响应,可打断低优先级
- 抢占优先级相同、子优先级不同 → 不能嵌套,等当前 ISR 执行完再响应
- 抢占、子优先级都相同 → 按中断号排队(号小先响应)
EXTI 基本结构
graph TD
subgraph GPIO_PINS
PA0["PA0"]
PB0["PB0"]
PC0["PC0"]
PA1["PA1"]
PB14["PB14"]
PE0["PE0"]
end
subgraph AFIO
AFIO_MUX["AFIO 中断引脚选择"]
end
subgraph EXTI
EXTI0["EXTI0"]
EXTI1["EXTI1"]
EXTI14["EXTI14"]
end
subgraph NVIC
NVIC0["NVIC EXTI0_IRQn"]
NVIC1["NVIC EXTI1_IRQn"]
NVIC9_5["NVIC EXTI9_5_IRQn"]
NVIC15_10["NVIC EXTI15_10_IRQn"]
end
subgraph CORE
CPU["Cortex-M3"]
end
PA0 --> AFIO_MUX --> EXTI0 --> NVIC0 --> CPU
PB0 --> AFIO_MUX --> EXTI0
PC0 --> AFIO_MUX --> EXTI0
PA1 --> AFIO_MUX --> EXTI1 --> NVIC1 --> CPU
PB14 --> AFIO_MUX --> EXTI14 --> NVIC9_5 --> CPU
PE0 --> AFIO_MUX --> EXTI0
中断线分配
| EXTI 线 |
中断号 |
NVIC 通道 |
IRQHandler |
| EXTI0 |
6 |
EXTI0_IRQn |
EXTI0_IRQHandler |
| EXTI1 |
7 |
EXTI1_IRQn |
EXTI1_IRQHandler |
| EXTI2 |
8 |
EXTI2_IRQn |
EXTI2_IRQHandler |
| EXTI3 |
9 |
EXTI3_IRQn |
EXTI3_IRQHandler |
| EXTI4 |
10 |
EXTI4_IRQn |
EXTI4_IRQHandler |
| EXTI5~9 |
23 |
EXTI9_5_IRQn |
EXTI9_5_IRQHandler |
| EXTI10~15 |
40 |
EXTI15_10_IRQn |
EXTI15_10_IRQHandler |
EXTI5-9 共用中断函数 EXTI9_5_IRQHandler,需在函数内判断 EXTI_GetITStatus(EXTI_Line5) ~ EXTI_GetITStatus(EXTI_Line9)。
EXTI10-15 共用 EXTI15_10_IRQHandler,同理。
EXTI 输入线选择
// 将 PB14 连接到 EXTI14
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
API 表格
EXTI 初始化
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
| EXTI_InitTypeDef 成员 |
取值 |
说明 |
EXTI_Line |
EXTI_Line0 ~ EXTI_Line15 |
中断线 |
EXTI_Mode |
EXTI_Mode_Interrupt / EXTI_Mode_Event |
中断模式 / 事件模式 |
EXTI_Trigger |
EXTI_Trigger_Rising / EXTI_Trigger_Falling / EXTI_Trigger_Rising_Falling |
触发方式 |
EXTI_LineCmd |
ENABLE / DISABLE |
使能 |
NVIC 初始化
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
| NVIC_InitTypeDef 成员 |
取值 |
说明 |
NVIC_IRQChannel |
EXTI0_IRQn 等 |
中断通道 |
NVIC_IRQChannelPreemptionPriority |
0~15 (取决于分组) |
抢占优先级 |
NVIC_IRQChannelSubPriority |
0~15 (取决于分组) |
子优先级 |
NVIC_IRQChannelCmd |
ENABLE / DISABLE |
使能 |
中断标志位函数
| 函数 |
原型 |
说明 |
EXTI_GetITStatus |
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line) |
获取中断标志位 |
EXTI_ClearITPendingBit |
void EXTI_ClearITPendingBit(uint32_t EXTI_Line) |
清除中断标志位 |
标准中断函数模板
void EXTIx_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_LineX) != RESET)
{
// 处理中断事件
EXTI_ClearITPendingBit(EXTI_LineX); // 清除标志位
}
}
void EXTI9_5_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) != RESET)
{
// PB14 中断处理
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
完整代码示例
对射式红外传感器计次 (PB14)
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
uint16_t SensorCount = 0;
void Sensor_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 1. 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 2. GPIO 配置 (上拉输入)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 3. AFIO 中断引脚选择
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
// 4. EXTI 配置 (下降沿触发)
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 5. NVIC 配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
uint16_t Sensor_Get(void)
{
return SensorCount;
}
// EXTI15_10 中断服务函数 (PB14 在 EXTI14 上)
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) != RESET)
{
SensorCount++;
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
int main(void)
{
Delay_Init();
OLED_Init();
Sensor_Init();
OLED_ShowString(1, 1, "Sensor Count:");
while (1)
{
OLED_ShowNum(2, 1, Sensor_Get(), 5);
Delay_ms(100);
}
}
旋转编码器 (A相PB12, B相PB13)
graph TD
subgraph ENCODER
A_PHASE["A 相 (PB12)"] -->|"EXTI12<br/>双边缘触发"| MCU
B_PHASE["B 相 (PB13)"] -->|"读取电平<br/>判断方向"| MCU
end
MCU --> COUNT["计数值<br/>+1 / -1"]
#include "stm32f10x.h"
int16_t EncoderCount = 0;
void Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// A相 PB12 — 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// B相 PB13 — 同上
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// AFIO 映射
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource12);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource13);
// EXTI12 — 双边缘触发
EXTI_InitStructure.EXTI_Line = EXTI_Line12;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
int16_t Encoder_Get(void)
{
int16_t temp;
temp = EncoderCount;
EncoderCount = 0; // 读取后清零
return temp;
}
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line12) != RESET)
{
// 判断方向: 读取 B 相电平
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) == 0)
EncoderCount++;
else
EncoderCount--;
EXTI_ClearITPendingBit(EXTI_Line12);
}
}
完整初始化步骤总结
1. RCC_APB2PeriphClockCmd(GPIOx, ENABLE);
2. RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // AFIO 时钟
3. GPIO_Init() — 配置输入模式
4. GPIO_EXTILineConfig() — 选择引脚到 EXTI 线
5. EXTI_Init() — 配置触发方式
6. NVIC_PriorityGroupConfig() — 一次
7. NVIC_Init() — 配置中断优先级
8. 写 ISR 函数, 检查标志位并清除
常见坑点/注意事项
- AFIO 时钟必须开启 —
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE),否则 GPIO_EXTILineConfig 无效
- 同一条 EXTI 线只能对应一个 GPIO — 例如 EXTI0 可以选 PA0/PB0/PC0,但同一时间只能选一个。选了 PB0 后 PA0 不再触发
- EXTI5-9 和 EXTI10-15 共用中断函数 — 必须在 ISR 内判断具体哪条线的标志位
- 清除标志位 —
EXTI_ClearITPendingBit 必须在 ISR 结束前调用,否则一直进中断
- 中断优先级分组只设一次 — 重复调用
NVIC_PriorityGroupConfig 行为未定义
- 中断函数名必须正确 — Startup 文件中已定义弱符号,写错名字不会编译报错但不会进入中断
- 不要在 ISR 中做复杂操作 — 中断应短而快,标志位+主循环处理;延时、打印等在 ISR 中要尽量避免
- 下拉输入与上升沿 — 如果按键接 VCC 按下高电平,用
IPD + EXTI_Trigger_Rising;按键接 GND 用 IPU + EXTI_Trigger_Falling
- 中断同时触发 — 同优先级中断按中断号排队响应,不同级中断按抢占/子优先响应