6. 外部中断
6.1 什么是中断
在微处理器或微控制器的世界中,中断是一种特殊的事件,它会打断和暂时挂起当前正在执行的程序,以便处理一个特定的状况或者事件。例如按下按钮、到达定时器时间或收到序列口数据。中断是一个非常重要的计算机系统概念,它通过异步的方式对这些特定情况做出响应。这里举一个例子,比如我们正在敲代码,突然有一个电话打过来,这时我 们停止敲代码转而去接电话,然后在电话聊完事情之后继续敲代码。这里面的电话就相当于一个中断,打断我们当前做的事情,接电话聊事情就相当于中断需要去执行的事情,也就是中断服务程序。
中断可以分为硬件中断和软件中断两种类型。
硬件中断通常由外部设备的物理事件引发,如按下按钮、达到定时器的时间、或数据到达串行端口。当这些事件发生时,微处理器会立即暂停其当前的任务,并跳转到一个预先定义的中断服务程序(ISR)来响应该事件。
软件中断则是由软件指令引发,通常用于更复杂的处理任务。像操作系统的系统调用就使用了软件中断。
6.2 外部中断介绍
在上一章节,我们在做按键实验时,虽然能实现读取 GPIO口输入功能,但代码是一直在检测IO输入口的变化,如果我们后续加入了大量的代码,就需要花费很长的时间才能轮询到按键检测部分,因此效率不高。特别是在一些特定的场合,比如某个按键,可能 1 天才按下一次去执行相关功能,这样我们就浪费大量时间来实时检测按键的情况。为了解决这样的问题,我们引入外部中断概念。顾名思义,就是当按键被按下(产生中断)时,才去执行相关功能。这大大节省了 CPU 的资源,因此中断在实际项目中应用非常普遍。
外部中断是硬件中断的一种,它由微控制器外部的事件引发。微控制器的某些引脚被设计为对特定事件的发生做出响应,例如按钮的按压、传感器的信号改变等。这些指定的引脚通常被称为“外部中断引脚”。
在发生外部中断事件时,当前在执行的程序会被立即停止,然后跳转到对应的中断服务程序(ISR)进行处理。处理完毕后,程序会返回到被中断的地方继续执行。
对于嵌入式系统、实时系统来说,外部中断的使用是非常重要的,能帮助系统对外部事件进行即时响应,大大提高了系统的效率和实时性。ESP32S3开发板提供了许多引脚作为可用的外部中断引脚,可以通过配置这些引脚来进行外部中断实验。
ESP32S3 的外部中断有上升沿触发、下降沿触发、低电平触发、高电平触发模式。上升沿和下降沿触发如下:
当引脚设置为外部中断引脚后,检测到引脚出现设置的触发模式时,就会进入中断回调函数中执行相应程序;
6.3 外部中断的作用和优势
ESP32S3的外部中断功能在Arduino开发中具有以下作用和优势:
- 实时响应外部事件:ESP32S3的外部中断功能可以让你的Arduino在检测到外部事件触发时立即作出响应。这些外部事件可以是来自传感器、按钮、开关、接收到的信号等等。通过外部中断,就可以实时地捕捉到这些事件并执行相应的操作,而无需频繁地轮询或等待。
- 节省计算资源:外部中断允许你将处理外部事件的任务转移给芯片的硬件,从而节省了处理器的计算资源。相比于软件轮询方式,外部中断可以降低对处理器的负担,使其可以更有效地利用其它资源进行更复杂的任务。
- 精确的事件捕捉:ESP32S3的外部中断功能能够以非常精确的方式捕捉外部事件的触发。你可以通过配置中断触发方式(如上升沿、下降沿、任意电平、低电平保持、高电平保持等)来适应不同的外部事件,并在事件发生时立即中断当前程序的执行,转而执行中断服务函数(ISR)。
- 高优先级处理:外部中断可以设置为高优先级处理,优先于当前正在执行的程序。这对于需要立即响应的重要事件非常有用,如紧急通知、传感器检测等。当外部事件触发时,处理器将立即转移到中断服务函数执行,确保及时、准确地处理相关操作,避免对处理程序的延迟。
- 多路中断处理:ESP32S3支持多路外部中断,你可以将多个外部事件与不同的中断引脚相连,从而实现对多个事件的并行处理。这使得你可以在一个Arduino上处理多个传感器、开关等外部事件,提高系统的灵活性和扩展性。
总之,ESP32S3的外部中断功能给的Arduino项目提供了实时响应、节省计算资源、精确事件捕捉、高优先级处理和多路中断处理等优势。它为我们提供了更加灵活、高效的方式来处理外部事件,并帮助构建更强大、可靠的应用。
6.4 使用外部中断流程
使用外部中断流程一般是如下步骤:
配置引脚
因外部中断实际上还是读取引脚的状态,所以需要将引脚设置为输入或启用上拉电阻输入模式;在 setup()
函数中,使用 pinMode()
函数将引脚配置为输入引脚。
关于 pinMode() 函数的说明见LED章节。
int interruptPin = 0; //中断引脚 GPIO0
void setup() {
//设置引脚GPIO0为输入模式
pinMode(interruptPin, INPUT);
}
2
3
4
5
将引脚绑定中断服务程序并设置触发中断方式
在 setup()
函数中,使用 attachInterrupt()
函数将中断服务程序绑定到引脚上。可以选择触发中断的方式,例如上升沿、下降沿或状态变化:
int interruptPin = 0;
void setup() {
// 省略其他代码...
// 配置中断引脚为GPIO0,外部中断回调函数为 ISR(), 中断下降沿触发
attachInterrupt(digitalPinToInterrupt(interruptPin), ISR, FALLING);
}
2
3
4
5
6
其中,attachInterrupt()
函数是Arduino API中的一部分,用于将一个函数(中断服务程序ISR)与外部中断引脚绑定起来。当外部中断引脚满足指定的条件(例如引脚电平变化)时,相应的中断服务程序将自动被调用。这个函数最常用在处理实时操作和中断事件。 函数原型如下:
attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);
其中 pin
是要配置为中断引脚的GPIO引脚编号。ISR
是要绑定的中断服务程序的函数名或指针。 mode
是中断触发条件,即满足何种条件时触发中断。常见的触发条件有上升沿、下降沿、状态变化等。相关参数如下:
CHANGE
:触发条件为引脚电平变化,即从低电平到高电平或从高电平到低电平都会触发中断。RISING
:触发条件为引脚从低电平变为高电平时触发中断。FALLING
:触发条件为引脚从高电平变为低电平时触发中断。LOW
:触发条件为引脚保持低电平时触发中断。HIGH
:触发条件为引脚保持高电平时触发中断。
注意,attachInterrupt()
函数只适用于 digitalPinToInterrupt()
所支持的GPIO引脚,而不是所有的GPIO引脚都能用于外部中断。此外,在中断服务函数进行中断处理时,一定要避免使用占用 CPU 大量时间的操作(例如延时函数),以确保中断响应速度和精度。
attachInterrupt()
函数有两种参数传递方式,可以传递函数指针或者函数名。如果是函数名,可以直接传递函数名,不需要加上括号。如果函数名被加上了括号,那就相当于调用该函数,传递的则是函数的返回值。如果使用函数指针,那么需要在函数名前加上&
符号。
下面是一个 attachInterrupt()
的例子:
void button_ISR() {
// 处理按键中断事件
}
void setup() {
// 将GPIO 5配置为输入引脚
pinMode(5, INPUT);
// 将接在 GPIO5 上的按键 绑定到上升沿触发的 中断服务程序 button_ISR
attachInterrupt(digitalPinToInterrupt(5), button_ISR, RISING);
}
void loop() {
// 在主循环中处理其他任务
}
2
3
4
5
6
7
8
9
10
11
12
13
14
其中,将GPIO5配置为输入引脚,然后将接在GPIO5上的按键绑定到上升沿触发的中断服务程序。当按键被按下时,按键的引脚变为高电平,按钮引脚的电平变化将触发上升沿中断,中断服务程序 button_ISR()
便会执行。
处理中断事件
根据上面的示例中使用的中断回调函数 button_ISR()
进行说明。 在 Arduino 中使用中断需要注意一下几点:
a. 尽量保证中断程序内容少
b. 避免在中断处理函数中使用阻塞函数(如 delay()
),使用非阻塞的延迟方法来处理需要延迟的操作(micros() 函数
),以保证中断的正常执行和系统的稳定性。这是因为 delay()
函数会阻塞整个系统,包括中断的正常执行。当中断触发时,处理函数应该尽快执行完毕,以确保及时响应并避免中断积压;
c. 与主程序共享的变量要加上 volatile 关键字;
d. 在 Arduino 中使用中断时,应尽量避免在中断处理函数中使用 Serial
串口对象的打印函数。当在中断处理函数中使用 Serial
打印函数时,会导致以下问题:
- 时间延迟:
Serial
打印函数通常是比较耗时的操作,它会阻塞中断的执行时间,导致中断响应的延迟。这可能会导致在中断期间丢失其他重要的中断事件或导致系统不稳定。 - 缓冲区溢出:
Serial
对象在内部使用一个缓冲区来存储要发送的数据。如果在中断处理函数中频繁调用Serial
打印函数,可能会导致缓冲区溢出,造成数据丢失或不可预测的行为。
为了避免这些问题,建议在中断处理函数中尽量避免使用 Serial
打印函数。如果需要在中断处理函数中输出调试信息,可以使用其他方式,如设置标志位,在主循环中检查标志位并进行打印。
❓volatile 是什么?
volatile
是一个关键字,用于告诉编译器它所声明的变量可能会被程序以外的因素所改变。它的作用是确保定义为 volatile
的变量在被访问时,每次都从内存中重新读取,而不是使用缓存值,从而保证程序的正确性。
volatile
声明的变量,可以被程序中断服务例程(ISR)、多线程任务或类似操作所修改。对于这些需求,正常的变量定义方式被编译器优化为内存读写最少的方式,因此对于一些变量(例如一个外设寄存器),编译器可能会使用缓存来提高程序效率。这种情况下,如果不使用 volatile
关键字声明变量,那么当变量值被外部因素改变时,程序可能会使用已经缓存的变量值而不是最新的值,从而导致程序出错。
6.5 硬件连接与准备
以开发板上板载的BOOT键作为案例。 其中,BOOT键接到了ESP32S3芯片的GPIO0上,LED接到了GPIO48引脚。我们需要将按键引脚GPIO0配置为外部中断引脚,LED引脚GPIO48配置为输出模式。通过按键控制LED亮灭。
6.6 外部中断验证
#define BUTTON 0
#define LED 48
//LED状态
bool led_flag = false;
//按键状态(中断回调函数与主程序共享的变量要加上 volatile 关键字)
volatile bool flag = false;
//中断回调函数
void ISR()
{
flag = true;
}
void setup()
{
//设置KEY引脚(0) 为上拉输入模式
pinMode(BUTTON, INPUT_PULLUP);
//设置LED引脚(48) 为输出模式
pinMode(LED, OUTPUT);
// 配置中断引脚为GPIO0,外部中断回调函数为 ISR(), 中断下降沿触发
attachInterrupt(digitalPinToInterrupt(BUTTON), ISR, FALLING);
}
void loop()
{
//当按键按下时会产生一个下降沿,进入回调函数 ISR(),而ISR()中只是将flag = true
//故当flag == true 时,说明按键按下
if ( flag == true )
{
//延时200毫秒
delay(200);
//LED状态取反
digitalWrite(LED, led_flag=!led_flag);
// 重置中断标志位
flag = false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
6.7 外部中断效果
按下按键灯亮,再按下按键灯灭,如此反复。