中断原理是掌握 STM32 外设编程的关键知识点。
中断的核心概念(通俗理解)
中断的本质是:外设 / 外部事件主动打断 CPU 的正常执行流程,请求 CPU 优先处理自己的紧急事务,处理完成后 CPU 恢复原流程。
中断优先级
STM32 的中断优先级由两部分组成,共 4 位(可配置为 0-15 级,数值越小优先级越高):
- 抢占优先级(Preemption Priority) :高抢占优先级的中断可以打断正在执行的低抢占优先级中断("抢占" 特性)。
- 响应优先级(Sub Priority/Secondary Priority) :仅当两个中断抢占优先级相同 时,响应优先级决定谁先执行;响应优先级高的先执行,但无法打断对方。
中断优先级分组可以使用NVIC_PriorityGroupConfig函数,
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
函数参数说明
参数 NVIC_PriorityGroup 是枚举类型(实际为宏定义),对应 5 种分组方式,取值如下
|----------------------|---------|---------|
| 分组编号 | 抢占优先级位数 | 响应优先级位数 |
| NVIC_PriorityGroup_0 | 0 | 4 |
| NVIC_PriorityGroup_1 | 1 | 3 |
| NVIC_PriorityGroup_2 | 2 | 2 |
| NVIC_PriorityGroup_3 | 3 | 1 |
| NVIC_PriorityGroup_4 | 4 | 0 |
简单代码示例(外部中断)
cs
#include "stm32f10x.h"
// 初始化PA0为外部中断输入
void EXTI0_Init(void)
{
// 定义3个"配置结构体":就像3张表格,分别填GPIO、EXTI、NVIC的设置
GPIO_InitTypeDef GPIO_InitStruct;
EXTI_InitTypeDef EXTI_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
// 1. 使能时钟(GPIOA、AFIO:复用功能时钟,外部中断需要)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置PA0为上拉输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
// 上拉输入,把设置写入ST按键按下,PA0 和 GND 连通,变成低电平
GPIO_Init(GPIOA, &GPIO_InitStruct); // 把设置写入STM32的寄存器,生效
// 3. 将PA0映射到EXTI0(外部中断线0)
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// 4. 配置EXTI0中断参数
EXTI_InitStruct.EXTI_Line = EXTI_Line0; // 中断线0
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式(而非事件模式)
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发(按键按下)
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 使能中断线0
EXTI_Init(&EXTI_InitStruct);
// 5. 配置NVIC(中断优先级+使能)
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn; // 外部中断0的IRQ通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 响应优先级0
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能该通道
NVIC_Init(&NVIC_InitStruct);
}
// 6. 中断服务函数(必须用固定名称)
void EXTI0_IRQHandler(void)
{
// 检查中断标志位,避免误触发
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 这里写中断处理逻辑(比如翻转LED)
GPIO_WriteBit(GPIOC, GPIO_Pin_13, !GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));
// 清除中断标志位(关键!否则会一直触发中断)
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
int main(void)
{
// 初始化LED(假设PC13接LED)
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);
EXTI0_Init(); // 初始化外部中断
while(1)
{
// 主程序常规工作(比如循环打印)
// 中断会打断这个循环,处理完自动返回
}
}
这段代码是让 STM32 的 PA0 引脚(接按键)按下时,触发中断,自动翻转 PC13 引脚(接 LED)的电平(亮 / 灭),主程序不用一直盯着按键,效率更高。
整个代码分 3 部分:
- 初始化函数
EXTI0_Init():给硬件 "做设置",告诉 STM32:PA0 是中断引脚、触发条件是按键按下、这个中断的优先级是多少; - 中断服务函数
EXTI0_IRQHandler():中断发生时(按键按下),STM32 自动执行的代码(核心是翻转 LED); - 主函数
main():程序入口,初始化 LED 和中断后,主程序进入空循环(模拟常规工作)。
STM32 有 16 条外部中断线(EXTI0~EXTI15),但引脚有上百个,需要手动把 "PA0 引脚" 和 "EXTI0 中断线" 绑定,这样 PA0 的电平变化才能触发中断。
cs
// 4. 配置EXTI0中断规则
EXTI_InitStruct.EXTI_Line = EXTI_Line0; // 选中中断线0
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 模式:中断
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 触发条件:下降沿(高→低,对应按键按下)
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 使能这条中断线
EXTI_Init(&EXTI_InitStruct); // 写入寄存器,生效
触发条件解释:Falling(下降沿)= 按键按下(PA0 从高电平变低电平);如果是Rising(上升沿)= 按键松开;Rising_Falling= 按下 / 松开都触发。
cs
// 5. 配置NVIC(中断控制器):给中断设优先级+开启
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn; // 选中"外部中断0"这个通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1(数字越小优先级越高)
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 响应优先级0
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 开启这个中断通道
NVIC_Init(&NVIC_InitStruct); // 写入寄存器,生效
}
NVIC 是 "中断管家":告诉管家 "外部中断 0 可以用,优先级是 1(比较高)",这样管家收到 PA0 的中断请求后,会通知 CPU 处理。
cs
void EXTI0_IRQHandler(void) // 函数名固定!不能改,改了CPU找不到
{
// 先检查:是不是EXTI0中断触发的(避免误触发)
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 中断处理逻辑:翻转PC13的电平(LED亮/灭切换)
GPIO_WriteBit(GPIOC, GPIO_Pin_13, !GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));
// 关键中的关键:清除中断标志位!
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
EXTI_GetITStatus:查 "中断标志位",如果是RESET(0),说明不是 EXTI0 触发的,直接跳过;GPIO_WriteBit(..., !GPIO_ReadOutputDataBit(...)):先读 PC13 当前电平(0 或 1),!是 "取反"(0 变 1,1 变 0),再写回 PC13------ 实现 LED 翻转;EXTI_ClearITPendingBit:清除中断标志位!如果不清除,NVIC 会认为 "中断还没处理完",会反复触发这个函数,导致 LED 疯狂闪、程序卡死。
cs
int main(void)
{
// 先初始化LED(PC13):配置为推挽输出
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 给GPIOC开时钟
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; // 选中PC13
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出(能驱动LED)
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 输出速度(新手随便设,不影响)
GPIO_Init(GPIOC, &GPIO_InitStruct); // 生效
EXTI0_Init(); // 调用上面的函数,初始化外部中断
while(1) // 主程序循环(模拟常规工作)
{
// 这里可以写主程序要做的事,比如:
// 读取传感器、控制电机、打印数据等
// 即使在循环里,按键按下也会触发中断,打断循环去翻转LED
}
}
while(1)是死循环:主程序会一直在这里执行常规任务,中断是 "插队" 的 ------ 按键按下时,CPU 暂停循环,去执行EXTI0_IRQHandler,执行完再回到循环继续。
执行流程
- 程序启动,先执行
main():初始化 LED→初始化中断→进入while(1)空循环; - 你没按按键:CPU 一直在
while(1)里 "空转",PA0 是高电平,无中断; - 你按下按键:PA0 从高→低(下降沿),触发 EXTI0 中断请求;
- NVIC(中断管家)检查:这个中断已开启、优先级够高→通知 CPU;
- CPU 暂停
while(1),自动跳转到EXTI0_IRQHandler执行:- 检查中断标志位→翻转 LED→清除标志位;
- 服务函数执行完,CPU 回到
while(1),继续空转; - 你松开按键:PA0 从低→高(上升沿),但我们只设了下降沿触发,所以无反应。
- 中断代码的核心是3 个配置 + 1 个服务函数:GPIO 引脚配置→EXTI 中断线配置→NVIC 优先级配置→中断服务函数(处理逻辑 + 清标志位);
- 中断服务函数名必须固定 (如
EXTI0_IRQHandler),且一定要清除中断标志位,否则会卡死; - 中断的本质是 "插队":主程序正常运行,按键按下时 CPU 临时去处理 LED 翻转,处理完回来继续。
代码关键原理对应:
GPIO_EXTILineConfig:把外设(PA0)和中断线(EXTI0)关联,触发源和中断请求绑定;NVIC_Init:配置中断优先级并使能 NVIC 通道,满足 "响应条件";EXTI0_IRQHandler:中断服务函数,是 CPU 处理中断的核心逻辑;EXTI_ClearITPendingBit:清除中断挂起标志,否则 NVIC 会认为中断未处理,反复触发。