通常在项目中,您希望 ESP32 执行其正常程序,同时持续监视某种事件。一种广泛采用的解决方案是使用中断。
ESP32 中的中断
ESP32 为每个内核提供最多 32 个中断槽。每个中断都有一定的优先级,可以分为两种类型。
硬件中断——这些中断是为了响应外部事件而发生的。例如,GPIO 中断(按下按键时)或触摸中断(检测到触摸时)。
软件中断——这些中断是为了响应软件指令而发生的。例如,简单的定时器中断或看门狗定时器中断(当定时器超时时)。
ESP32 GPIO 中断
在 ESP32 中,我们可以定义一个中断服务例程函数,当 GPIO 引脚改变其逻辑电平时将调用该函数。
ESP32 板上的所有 GPIO 引脚都可以配置为充当中断请求输入。
将中断附加到 GPIO 引脚
在 Arduino IDE 中,我们使用一个名为 的函数attachInterrupt()
来逐个引脚设置中断。语法如下所示。
attachInterrupt(GPIOPin, ISR, Mode);
该函数接受三个参数:
GPIOPin – 将 GPIO 引脚设置为中断引脚,告诉 ESP32 要监控哪个引脚。
ISR – 是每次中断发生时将调用的函数的名称。
模式– 定义何时应触发中断。预定义了五个常量作为有效值:
LOW | 每当引脚为低电平时触发中断 |
HIGH | 每当引脚为高电平时触发中断 |
CHANGE | 每当引脚改变值(从高到低或从低到高)时触发中断 |
FALLING | 当引脚从高电平变为低电平时触发中断 |
RISING | 当引脚从低电平变为高电平时触发中断 |
从 GPIO 引脚分离中断
当您希望 ESP32 不再监控该引脚时,可以调用该detachInterrupt()
函数。语法如下所示。
detachInterrupt(GPIOPin);
中断服务程序
中断服务例程 (ISR) 是每次 GPIO 引脚上发生中断时都会调用的函数。
它的语法如下所示。
void IRAM_ATTR ISR() {
Statements;
}
ESP32 中的 ISR 是特殊类型的函数,它们具有大多数其他函数所没有的一些独特规则。
- ISR 不能有任何参数,并且它们不应该返回任何内容。
- ISR 应尽可能短且快,因为它们会阻止正常的程序执行。
IRAM_ATTR
根据ESP32 文档,它们应该具有该属性。
什么是 IRAM_ATTR?
当我们使用该属性标记一段代码时IRAM_ATTR
,编译后的代码将放置在 ESP32 的内部 RAM (IRAM) 中。否则,代码将保存在 Flash 中。而且 ESP32 上的 Flash 比内部 RAM 慢得多。
如果我们要运行的代码是中断服务例程(ISR),我们通常希望尽快执行它。如果我们必须“等待”ISR 从闪存加载,那么事情可能会出现严重错误。
硬件连接
理论已经够多了!让我们看一个实际的例子。
让我们将一个按钮连接到 ESP32 上的 GPIO#18 (D18)。您不需要对该引脚进行任何上拉,因为我们将在内部将该引脚上拉。
将按钮连接到 ESP32 以实现 GPIO 中断
示例代码:简单中断
下面的草图演示了中断的使用以及编写中断服务程序的正确方法。
该程序监视 GPIO#18 (D18) 的下降沿。换句话说,它会寻找按下按钮时发生的从逻辑高电平到逻辑低电平的电压变化。当这种情况发生时,该函数isr
被调用。此函数中的代码计算按钮被按下的次数。
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
Button button1 = {18, 0, false};
void IRAM_ATTR isr() {
button1.numberKeyPresses++;
button1.pressed = true;
}
void setup() {
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
}
void loop() {
if (button1.pressed) {
Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
}
上传草图后,按 ESP32 上的 EN 按钮并以波特率 115200 打开串行监视器。按下按钮后,您将获得以下输出。
代码说明
在草图的开头,我们创建一个名为 的结构Button
。该结构具有三个成员——引脚编号、按键次数和按下状态。仅供参考,结构是单个名称下不同类型(但逻辑上彼此相关)的变量的集合。
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
然后,我们创建 Button 结构的一个实例,并将引脚编号初始化为18
,按键次数初始化为0
,默认按下状态初始化为false
。
Button button1 = {18, 0, false};
下面的代码是一个中断服务程序。前面提到,ESP32 中的 ISR 必须具有该IRAM_ATTR
属性。
在 ISR 中,我们只需将 KeyPresses 计数器增加 1 并将按钮按下状态设置为 True。
void IRAM_ATTR isr() {
button1.numberKeyPresses += 1;
button1.pressed = true;
}
在代码的设置部分,我们首先初始化与 PC 的串行通信,然后启用 D18 GPIO 引脚的内部上拉。
isr
接下来,我们告诉 ESP32 监视 D18 引脚,并在引脚从高电平变为低电平(即下降沿)时调用中断服务程序。
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
在代码的循环部分,我们简单地检查按钮是否被按下,然后打印到目前为止按键被按下的次数,并将按钮按下状态设置为 false,以便我们可以继续接收中断。
if (button1.pressed) {
Serial.printf("Button 1 has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
管理跳动
中断的一个常见问题是,同一事件经常会多次触发中断。如果您查看上面示例的串行输出,您会注意到即使您只按一次按钮,计数器也会增加几次。
要找出发生这种情况的原因,您必须查看信号。如果您在按下按钮时监控信号分析仪上引脚的电压,您将得到如下信号:
您可能感觉立即发生了接触,但实际上按钮内的机械部件在进入特定状态之前会接触多次。这会导致触发多个中断。
这纯粹是一种被称为“开关弹跳”的机械现象,就像丢球一样——它会弹跳几次,然后最终落地。每一次弹跳都会引起一次中断,从而调动执行一次中断程序。所以就出现了按动一次触发了多次打印的情况。
如何消除开关弹跳的过程称为“去弹跳”。有两种方法可以实现这一目标。
- 通过硬件:通过添加适当的RC滤波器来平滑过渡。
- 通过软件:在触发第一个中断后的短时间内暂时忽略进一步的中断。
示例代码:消除中断抖动
这里重写了上面的草图,以演示如何以编程方式消除中断反跳。在此草图中,我们允许 ISR 在每次按下按钮时仅执行一次,而不是多次执行。
对草图的更改突出显示在绿色的。
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
Button button1 = {18, 0, false};
//建立一个变量用来保存上次调用中断处理程序的时间,如果这个时间小于250毫秒则不再确认按下按钮。
unsigned long button_time = 0;
unsigned long last_button_time = 0;
void IRAM_ATTR isr() {
button_time = millis();
if (button_time - last_button_time > 250)
{
button1.numberKeyPresses++;
button1.pressed = true;
last_button_time = button_time;
}
}
void setup() {
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
}
void loop() {
if (button1.pressed) {
Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
}
让我们再次查看按下按钮时的串行输出。请注意,每次按下按钮只会调用一次 ISR。
代码说明:
此修复之所以有效,是因为每次执行 ISR 时,都会将函数返回的当前时间millis()
与上次调用 ISR 的时间进行比较。
如果在 250ms 之内,ESP32 会忽略中断并立即返回到正在执行的操作。如果没有,它会执行语句中的代码if
,增加计数器并更新last_button_time
变量,以便该函数有一个新值可以在将来触发时进行比较。
这个问题也可以在远程序的基础上,在loop模块中打印以前延迟10毫秒再做一次判断,看是否还是按下的状态,如果是则打印不是则不打印。