🧠 什么是中断?(生活化比喻)
想象一下,你正在家里专心看书(这代表单片机的主程序 main 函数在正常运行)。
突然,电话响了(这就是一个"中断信号")。你不得不停下书本,去接电话(执行"中断服务函数"),和对方聊完后挂断电话,然后你回到刚才看的地方继续看书(返回主程序继续执行)。
这就是中断的核心逻辑:打断 -> 处理 -> 返回。
如果不使用中断,你就得每隔几秒钟放下书本去检查一下电话有没有响(这叫"轮询")。这样效率极低,而且你根本看不进去书。而中断让单片机具备了"一心二用"的能力。
⚙️ STM32 中断系统的三大核心组件
在 STM32 内部,处理中断主要靠三个部分协作:
- 中断源 (Source):谁在捣乱?
- 比如按键被按下、定时器时间到了、串口收到了数据。
- EXTI (External Interrupt/Event Controller):外部中断控制器
- 相当于家里的"分机线"。如果你有多个电话(比如客厅电话 PA0、卧室电话 PB0),EXTI 负责把它们接到同一条主线路上。
- NVIC (Nested Vectored Interrupt Controller):嵌套向量中断控制器
- 相当于你的"优先级处理大脑"。如果电话响了(高优先级),你必须立刻停下看书;如果门铃也响了(低优先级),你得等打完电话再去开门。
💻 手把手实战案例:按键控制 LED(标准库版)
我们来写一个简单的代码,实现:主程序让 LED 闪烁,同时随时响应按键按下,一旦按下 LED 状态翻转。
第一步:配置步骤(思维导图)
- 初始化外设:初始化 LED 引脚(输出)、按键引脚(输入)。
- 配置 EXTI:告诉单片机"我要监控这个按键引脚的变化"。
- 配置 NVIC:给这个中断分配一个优先级。
- 编写中断服务函数:写好"接电话"时要做的事。
第二步:代码实现
include "stm32f10x.h"
// 1. 定义 LED 和按键的 GPIO
define LED_PIN GPIO_Pin_5
define LED_PORT GPIOA
define KEY_PIN GPIO_Pin_0
define KEY_PORT GPIOA
void GPIO_Config(void);
void EXTI0_Config(void);
int main(void)
{
// 初始化硬件
GPIO_Config();
EXTI0_Config();
while (1)
{
// 主程序:LED 慢闪,模拟在做其他工作
GPIO_SetBits(LED_PORT, LED_PIN); // 灯灭
Delay(0x0FFFFF); // 延时
GPIO_ResetBits(LED_PORT, LED_PIN); // 灯亮
Delay(0x0FFFFF); // 延时
// 如果没有中断,按键必须在这里才能被检测到,响应很慢!
}
}
// 2. 配置 GPIO
void GPIO_Config(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
// 配置 LED 为推挽输出
GPIO_InitStructure.GPIO_Pin = LED_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED_PORT, &GPIO_InitStructure);
// 配置按键为上拉输入
GPIO_InitStructure.GPIO_Pin = KEY_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(KEY_PORT, &GPIO_InitStructure);
}
// 3. 配置外部中断
void EXTI0_Config(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 连接 EXTI Line0 到 PA0 引脚
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0; // 监控第 0 号线路
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发(按键按下通常是从高变低)
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 4. 配置 NVIC 优先级
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 设置优先级分组
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; // 选择中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级最高
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
// 5. 中断服务函数 (ISR) - 这就是"接电话"的过程
// 注意:函数名是固定的,必须这么写,由启动文件定义
void EXTI0_IRQHandler(void)
{
// 1. 必须先判断是不是这个中断发生的(防止误判)
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 2. 在这里写你的处理逻辑
// 比如:翻转 LED 状态
GPIO_WriteBit(LED_PORT, LED_PIN,
(BitAction)(1 - GPIO_ReadOutputDataBit(LED_PORT, LED_PIN)));
// 3. 清除中断标志位 (非常重要!如果不清理,CPU 会认为还在响铃,一直进中断)
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
// 简单的延时函数
void Delay(__IO uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
🔍 代码关键点分析
- 下降沿触发 (EXTI_Trigger_Falling)
- 按键未按下时是高电平,按下瞬间变成低电平。这个从"高"到"低"的变化叫下降沿。我们只在这个瞬间触发一次中断,避免按住不放导致反复触发多次。
- 清除标志位 (EXTI_ClearITPendingBit)
- 这是新手最容易忘记的一步!就像电话挂断后,电话机上的红灯必须熄灭。如果不手动清除,单片机会以为电话还在响,从而卡死在中断里出不来。
- NVIC 优先级
- 如果你有多个中断(比如同时有按键和定时器),优先级高的会先处理。STM32 支持中断嵌套(高优先级打断低优先级)。
📌 总结
- 轮询 (Polling):主动去查,"按键按了吗?按了吗?" ------ 效率低,实时性差。
- 中断 (Interrupt):被动通知,"叮咚!按键按了!" ------ 效率高,实时性强。