外部中断
在单片机中,中断是指当 CPU 在正常处理主程序时,突然发生了另一件事件 A(中断发生)需要 CPU 去处理,这时 CPU 就会暂停处理主程序(中断响应),转而去处理事件 A(中断服务)。当事件 A 处理完以后,再回到主程序原来中断的地方继续执行主程序(中断返回)。这一整个过程称为中断。
例如,当你正在洗衣时,突然手机响了(中断发生),你暂时中断洗衣的工作,转去接电话(中断响应和中断服务),待你接完后,再回来继续洗衣(中断返回),这一过程就是中断。
当中断过程 A 中,发生了另一个中断级别更高的中断事件 B,则 CPU 又会中断当前的 A 转而去处理 B,完毕后再回到 A 的断点继续处理。这称为中断的嵌套。
中断的嵌套涉及到中断的优先级问题,优先级高的中断就可以在打断优先级低的中断执行。
中断可以根据中断源分为 硬件中断
和 软件中断
:
硬件中断
:也被称为外部中断,硬件中断响应外部硬件事件而发生。例如,当检测到触摸时会发生触摸中断,而当 GPIO 引脚的状态发生变化时会发生 GPIO 中断。GPIO 中断和触摸中断属于这一类;软件中断
:当触发软件事件(例如定时器溢出)时,会发生这种类型的中断。定时器中断是软件中断的一个例子。
前面我们在做按键控制实验时,虽然能实现 IO 口输入功能,但代码是一直在检测 IO 输入口的变化,因此效率不高,特别是在一些特定的场合,比如某个按键,可能 1 天才按下一次去执行相关功能,这样我们就浪费大量时间来实时检测按键的情况。
为了解决这样的问题,我们引入外部中断概念,顾名思义,就是当按键被按下(产生中断)时,才去执行相关功能。这大大节省了 CPU 的资源,因此中断在实际项目中应用非常普遍。
ESP32 的外部中断有上升沿、下降沿、低电平、高电平触发模式。上升沿和下降沿触发如下:
若将按键对应 IO 配置为下降沿触发,当按键按下后即触发中断,然后在中断回调函数内执行对应的功能。
电路设计
程序设计
Arduino 中的外部中断配置函数 attachInterrupt(digitalPinToInterrupt(pin), ISR, mode)
包括 3 个参数:
pin
:GPIO 端口号;ISR
:中断服务程序,没有参数与返回值的函数;mode
:中断触发的方式,支持以下触发方式:LOW
低电平触发HIGH
高电平触发RISING
上升沿触发FALLING
下降沿触发CHANGE
电平变化触发
在 Arduino 中使用中断需要注意一下几点:
- 尽量保证中断程序内容少
- 避免在中断处理函数中使用阻塞函数(如
delay()
),使用非阻塞的延迟方法来处理需要延迟的操作(micros() 函数
),以保证中断的正常执行和系统的稳定性。这是因为delay()
函数会阻塞整个系统,包括中断的正常执行。当中断触发时,处理函数应该尽快执行完毕,以确保及时响应并避免中断积压; - 与主程序共享的变量要加上 volatile 关键字;
- 在 Arduino 中使用中断时,应尽量避免在中断处理函数中使用
Serial
对象的打印函数。当在中断处理函数中使用Serial
打印函数时,会导致以下问题: - 时间延迟:
Serial
打印函数通常是比较耗时的操作,它会阻塞中断的执行时间,导致中断响应的延迟。这可能会导致在中断期间丢失其他重要的中断事件或导致系统不稳定。 - 缓冲区溢出:
Serial
对象在内部使用一个缓冲区来存储要发送的数据。如果在中断处理函数中频繁调用Serial
打印函数,可能会导致缓冲区溢出,造成数据丢失或不可预测的行为。
#define BUTTON 14
#define LED 2
volatile bool flag = false;
void ISR() {
flag = true;
}
void setup() {
pinMode(BUTTON, INPUT_PULLDOWN);
pinMode(LED, OUTPUT);
// 配置中断引脚
attachInterrupt(digitalPinToInterrupt(BUTTON), ISR, FALLING);
}
void loop() {
if (flag) {
digitalWrite(LED, HIGH);
delay(2000);
digitalWrite(LED, LOW);
// 重置中断标志位
flag = false;
}
}
定时器中断
定时器,顾名思义就是用来计时的,我们常常会设置计时或闹钟,然后时间到了就告诉我们要做什么。ESP32 也是这样,通过定时器可以完成各种预设好的任务。ESP32 定时器到达指定时间后也会产生中断,然后在回调函数内执行所需功能,这个和外部中断类似。
在 Arduino 中操控 ESP32 时,有 硬件定时器
和 软件定时器
两种类型的定时器可供选择。它们具有不同的工作原理和用途。
硬件定时器
是 ESP32 芯片上的内置计时器,它们是专门设计用于定时和计时任务的硬件模块。硬件定时器可以通过设置特定的寄存器来配置和控制,通常具有更高的精确度和稳定性。它们不受软件的影响,可以在后台独立运行,不会受到其他代码的干扰。硬件定时器适用于需要高精度和实时性的定时任务,例如 PWM 输出、捕获输入脉冲等。
软件定时器
是通过编写代码在 Arduino 中模拟实现的定时器。它们不依赖于硬件模块,而是使用计数器变量来实现定时功能。软件定时器是基于延时循环的原理,在特定的时间间隔内执行特定的任务。但是,使用软件定时器时需要注意,它们可能会受到其他代码的影响而产生误差,特别是当涉及到需要精确时间控制的应用时,如通信协议处理、高速数据采集等。
硬件定时器和软件定时器各有优劣,具体选择取决于你的应用需求。如果需要高精度、实时性和稳定性,建议使用硬件定时器。如果时间精度要求不高,或者只需要基本的定时功能,可以使用软件定时器来简化代码编写。
需要注意的是,ESP32 具有 4 个硬件定时器,具体使用哪个定时器取决于你的需求和硬件资源的可用性。请参考 ESP32 的官方文档和相关库的文档以获取更详细的信息和使用示例。
电路设计
程序设计
硬件定时器
使用硬件定时器的基本步骤如下:
- 初始化定时器:使用
timerBegin()
函数初始化所需的硬件定时器; - 注册中断处理函数:使用
timerAttachInterrupt()
函数将中断处理函数与定时器关联起来; - 设置定时器模式:使用
timerAlarmWrite()
,设置触发一次,还是周期性触发; - 启动定时器:使用
timerAlarmEnable()
函数启动定时器,使其开始计数。
#define LED 2
#define LED_ONCE 4
hw_timer_t *timer = NULL;
hw_timer_t *timer_once=NULL;
// 定时器中断处理函数
void timer_interrupt(){
digitalWrite(LED, !digitalRead(LED));
}
void timer_once_interrupt() {
digitalWrite(LED_ONCE, !digitalRead(LED_ONCE));
}
void setup() {
pinMode(LED, OUTPUT);
pinMode(LED_ONCE, OUTPUT);
// 初始化定时器
timer = timerBegin(0,80,true);
timer_once = timerBegin(1, 80, true);
// 配置定时器
timerAttachInterrupt(timer,timer_interrupt,true);
timerAttachInterrupt(timer_once, timer_once_interrupt, true);
// 定时模式,单位us,只触发一次
timerAlarmWrite(timer,1000000,true);
timerAlarmWrite(timer_once, 3000000, false);
// 启动定时器
timerAlarmEnable(timer);
timerAlarmEnable(timer_once);
}
void loop() {
}
软件定时器
使用软件计时器的时候,我们需要用到 ESP32 内置的库 Ticker
,Ticker
是 ESP32 Arduino 内置的一个定时器库,这个库用于规定时间后调用函数。
接着我们来看看 Ticker
库的一些方法
detach()
:停止 Ticker;active()
:Ticker 是否激活状态,True 表示启用;once(n, callback,arg)
:n 秒后只执行一次 callback 函数,arg 表示回调函数的参数(不写表示没有);once_ms(n, callback,arg)
:n 毫秒后只执行一次 callback 函数,arg 表示回调函数的参数(不写表示没有);attach(n, callback, arg)
:每隔 n 秒周期性执行;attach_ms(n, callback, arg)
:每隔 n 毫秒周期性执行;
#include <Ticker.h>
#define LED 4
#define LED_ONCE 2
// 定义定时器对象
Ticker timer;
Ticker timer_once;
// 定义定时器中断回调函数
void toggle(int pin) {
digitalWrite(pin, !digitalRead(pin));
}
void setup() {
pinMode(LED, OUTPUT);
pinMode(LED_ONCE, OUTPUT);
// 配置周期性定时器
timer.attach(0.5, toggle, LED);
// 配置一次性定时器
timer_once.once(3, toggle, LED_ONCE);
}
void loop() {
}