引言
在上一篇关于 Systick 定时器的分享中,我们掌握了精确控制时间的方法。但在实际开发中,还有一个更重要的问题需要解决:如何让 CPU 在等待外部事件(如按键按下、传感器数据就绪)时不被 “占用”,从而高效地处理其他任务?这就需要引入嵌入式系统的核心机制之一 ——中断。
想象一下,你正在专注工作时,突然收到一条重要消息。此时,你可以选择放下手中的工作立即处理,也可以继续工作直到完成当前任务。STM32 的中断机制就类似于前者:当外部事件发生时,CPU 会暂时停止当前工作,转而去处理相应的事件,处理完毕后再回到原来的位置继续执行。这种机制大大提高了 CPU 的利用率,让系统能够同时应对多个任务。
中断 VS 轮询:嵌入式开发中的效率之争
在深入了解中断之前,我们先来对比一下两种常见的事件处理方式:轮询和中断。
轮询(Polling)
轮询就像是一个 “不停检查” 的过程:CPU 会周期性地询问外设是否有事件发生。例如,在按键检测中,程序会不断读取按键引脚的电平状态,直到检测到按键按下为止。这种方式的优点是简单直接,但缺点也很明显:CPU 资源被大量浪费在 “等待” 上。如果按键长时间不被按下,CPU 将一直处于 “死等” 状态,无法处理其他任务。
中断(Interrupt)
中断则是一种 “事件驱动” 的机制:外设会在事件发生时主动向 CPU 发送信号,CPU 接收到信号后会立即暂停当前任务,转而去处理相应的事件。处理完成后,CPU 会自动回到原来的位置继续执行。这种方式的优点是CPU 利用率高,可以在等待事件的同时处理其他任务;缺点是实现相对复杂,需要配置中断源、优先级等参数。
适用场景对比
场景 | 轮询 | 中断 |
---|---|---|
响应时间要求 | 适用于短时间等待(ns-us 级) | 适用于长时间等待或随机事件 |
CPU 资源利用 | 低(大量时间浪费在轮询上) | 高(仅在事件发生时响应) |
实现复杂度 | 简单 | 复杂(需要配置中断系统) |
典型应用 | 传感器数据采集(高频) | 按键检测、外部事件触发 |
中断的硬件连接
STM32 的中断系统由多个部分组成,其中硬件连接是基础。外部中断的硬件连接通常涉及以下几个部分:
-
外设信号源:如按键、传感器等,它们会在特定事件发生时产生电平变化。
-
GPIO 引脚:外设信号需要连接到 STM32 的 GPIO 引脚上,以便 CPU 能够检测到信号变化。
-
外部中断控制器(EXTI):负责接收 GPIO 引脚的信号,并将其转换为中断请求。
-
嵌套向量中断控制器(NVIC):负责管理中断优先级、分发中断请求给 CPU。
下面是一个典型的外部中断硬件连接示意图:
外部中断 / 事件控制器 (EXTI)
STM32 的外部中断 / 事件控制器(EXTI)是连接外部信号和 CPU 的桥梁。它可以检测 GPIO 引脚上的电平变化(上升沿、下降沿或双边沿),并根据配置产生中断或事件。
EXTI 核心特性
-
多通道支持:不同型号的 STM32 支持的 EXTI 通道数不同,例如 F103 系列支持 19 个外部中断通道(EXTI0-EXTI19)。
-
灵活的触发方式:每个通道可以独立配置为上升沿触发、下降沿触发或双边沿触发。
-
中断 / 事件分离:可以选择将信号转换为中断请求(用于软件处理)或事件(用于硬件触发)。
-
可屏蔽控制:每个通道都可以独立开启或关闭,方便灵活控制。
EXTI 框图解析
从框图中可以看出,EXTI 的工作流程如下:
-
外部信号通过 GPIO 引脚输入到 EXTI 控制器。
-
边沿检测器检测信号的上升沿或下降沿。
-
根据配置,信号可以触发中断或事件。
-
中断请求会被发送到 NVIC 进行优先级处理,最终由 CPU 响应。
EXTI 配置步骤
配置 EXTI 控制器通常需要以下步骤:
-
指定外部中断源:将 GPIO 引脚映射到对应的 EXTI 通道(如 PE4 映射到 EXTI4)。
-
配置触发方式:设置上升沿触发、下降沿触发或双边沿触发。
-
选择中断 / 事件模式:决定是产生中断请求还是事件。
-
使能中断通道:开启对应 EXTI 通道的中断功能。
-
清除中断标志:在中断处理完成后,需要清除对应的中断标志位,以准备下一次中断。
NVIC:中断优先级的 “指挥官”
NVIC(Nested Vectored Interrupt Controller)是 ARM Cortex-M 内核的一部分,负责管理和调度所有中断请求。它的主要功能包括:
NVIC 核心功能
-
中断优先级管理:为每个中断源分配抢占优先级和响应优先级。
-
中断使能 / 禁用:控制每个中断源是否允许产生中断请求。
-
中断挂起 / 解挂:管理中断请求的挂起状态。
-
中断嵌套处理:支持高优先级中断抢占低优先级中断的执行。
优先级机制详解
在 STM32 中,中断优先级分为两个级别:抢占优先级和响应优先级。
-
抢占优先级:决定一个中断是否可以抢占另一个中断的执行。数值越小,优先级越高。高抢占优先级的中断可以打断低抢占优先级的中断,形成中断嵌套。
-
响应优先级:当两个中断的抢占优先级相同时,响应优先级决定它们的执行顺序。响应优先级高的中断会优先执行,但不能抢占正在执行的同抢占优先级中断。
优先级分组
STM32 允许用户通过配置将 4 位优先级寄存器划分为不同的组,以适应不同的应用需求。F103 系列支持 5 种分组方式:
分组方式 | 抢占优先级位数 | 响应优先级位数 |
---|---|---|
Group 0 | 0 位 | 4 位 |
Group 1 | 1 位 | 3 位 |
Group 2 | 2 位 | 2 位 |
Group 3 | 3 位 | 1 位 |
Group 4 | 4 位 | 0 位 |
通过调用库函数NVIC_PriorityGroupConfig()
可以设置分组方式:
中断处理流程
当 NVIC 向 CPU 发送中断请求后,CPU 会按照以下流程处理中断:
-
保存上下文:CPU 将当前执行的程序上下文(如寄存器值、程序计数器等)保存到栈中,以便在中断处理完成后恢复。
-
跳转到中断服务函数:CPU 根据中断向量表,跳转到对应的中断服务函数(ISR)执行。
-
执行中断服务函数:在中断服务函数中,处理中断事件(如读取传感器数据、控制外设等)。
-
清除中断标志:在中断处理完成后,需要清除对应的中断标志位,以防止重复触发。
-
恢复上下文:从栈中恢复之前保存的程序上下文,继续执行被中断的程序。
中断服务函数命名规则
STM32 的中断服务函数名称由 ST 公司预定义,位于启动文件startup_stm32f10x_hd.s
中。例如:
-
EXTI0_IRQHandler
:处理 EXTI0 中断 -
EXTI4_IRQHandler
:处理 EXTI4 中断 -
TIM2_IRQHandler
:处理 TIM2 定时器中断
开发人员只需编写这些函数的实现代码即可。
代码实战:按键中断控制 LED 和蜂鸣器
下面通过一个完整的示例,演示如何使用外部中断实现按键控制 LED 和蜂鸣器。我们的目标是:
-
KEY0 按下:切换 LED0 状态
-
KEY_UP 按下:切换蜂鸣器状态
// exti.c #include "exti.h" #include "systick.h" #include "led.h" #include "key.h" #include "beep.h" // 外部中断初始化函数 void My_EXTI_Init(void){ // 1. 配置GPIO为输入模式(已在key.c中完成) // 2. 打开AFIO时钟,用于GPIO与EXTI的映射 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 3. 将GPIO引脚映射到对应的EXTI通道 // KEY0 (PE4) -> EXTI4 GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4); // KEY_UP (PA0) -> EXTI0 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); // 4. 配置EXTI控制器 EXTI_InitTypeDef EXTI_Config; // 配置EXTI4 (KEY0) EXTI_Config.EXTI_Line = EXTI_Line4; // 选择EXTI4 EXTI_Config.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式 EXTI_Config.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发(按键按下) EXTI_Config.EXTI_LineCmd = ENABLE; // 使能中断线 EXTI_Init(&EXTI_Config); // 配置EXTI0 (KEY_UP) EXTI_Config.EXTI_Line = EXTI_Line0; // 选择EXTI0 EXTI_Config.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发(按键按下) EXTI_Init(&EXTI_Config); // 5. 配置NVIC控制器,设置中断优先级 NVIC_InitTypeDef NVIC_Config; // 配置EXTI4中断 NVIC_Config.NVIC_IRQChannel = EXTI4_IRQn; // EXTI4中断通道 NVIC_Config.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1 NVIC_Config.NVIC_IRQChannelSubPriority = 2; // 响应优先级2 NVIC_Config.NVIC_IRQChannelCmd = ENABLE; // 使能中断通道 NVIC_Init(&NVIC_Config); // 配置EXTI0中断 NVIC_Config.NVIC_IRQChannel = EXTI0_IRQn; // EXTI0中断通道 NVIC_Config.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1 NVIC_Config.NVIC_IRQChannelSubPriority = 1; // 响应优先级1(高于EXTI4) NVIC_Init(&NVIC_Config); } // EXTI4中断服务函数(KEY0按下) void EXTI4_IRQHandler(void){ // 检查是否是EXTI4中断 if(EXTI_GetITStatus(EXTI_Line4) == SET){ // 消抖处理 delay_ms(10); // 再次确认按键状态 if(KEY0 == 0) LED0 = !LED0; // 切换LED0状态 } // 清除中断标志位 EXTI_ClearITPendingBit(EXTI_Line4); } // EXTI0中断服务函数(KEY_UP按下) void EXTI0_IRQHandler(void){ // 检查是否是EXTI0中断 if(EXTI_GetITStatus(EXTI_Line0) == SET){ // 消抖处理 delay_ms(10); // 再次确认按键状态 if(KEY_UP == 1) BEEP = !BEEP; // 切换蜂鸣器状态 } // 清除中断标志位 EXTI_ClearITPendingBit(EXTI_Line0); }
// main.c #include "stm32f10x.h" #include "led.h" #include "beep.h" #include "system.h" #include "systick.h" #include "key.h" #include "exti.h" int main(void) { // 设置中断优先级分组(2位抢占优先级,2位响应优先级) NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 初始化外设 LED_Init(); // LED初始化 BEEP_Init(); // 蜂鸣器初始化 Systick_Init(); // SysTick定时器初始化 KEY_Init(); // 按键初始化 My_EXTI_Init(); // 外部中断初始化 // 主循环:可以处理其他任务 while(1) { // 主循环可以执行低优先级任务 // 当有按键中断发生时,CPU会自动跳转到对应的中断服务函数 } }
代码解析
-
中断初始化:
My_EXTI_Init()
函数完成了中断的全部配置,包括 GPIO 映射、EXTI 触发方式设置和 NVIC 优先级配置。 -
中断服务函数:
EXTI4_IRQHandler()
和EXTI0_IRQHandler()
分别处理 KEY0 和 KEY_UP 的中断事件,实现了 LED 和蜂鸣器的控制。 -
主循环优化:主循环中不需要轮询按键状态,CPU 可以专注于处理其他任务,大大提高了资源利用率。
总结
通过本文的学习,我们深入理解了 STM32 中断系统的工作原理和配置方法。与轮询相比,中断机制能够显著提高 CPU 的利用率,让系统更加高效地响应外部事件。关键要点总结如下:
-
中断基础知识:了解中断与轮询的区别,掌握中断的硬件连接和工作流程。
-
EXTI 控制器:学会配置外部中断 / 事件控制器,设置触发方式和中断模式。
-
NVIC 优先级:理解抢占优先级和响应优先级的区别,合理配置中断优先级分组。
-
中断处理流程:掌握中断服务函数的编写和中断标志的清除方法。
在实际开发中,中断是实现实时响应和多任务处理的基础。合理使用中断机制,能够让我们的 STM32 应用更加稳定、高效。
最后
作为技术分享者,我始终致力于用通俗易懂的语言和丰富的实例,帮助大家理解复杂的技术概念。但由于知识水平有限,文中难免存在不足之处。如果你发现任何错误或有不同的见解,欢迎在评论区留言讨论!同时,也期待你分享在实际项目中使用中断的经验和技巧,让我们共同进步!