
注:本系列为个人学习 ESP32 的记录,主要通过再记录的方式学习,参考和搬运转载极客侠 GeeksMan 的文章。
著作权归极客侠 GeeksMan 所有,基于 GPL 3.0 协议。原文链接:https://docs.geeksman.com/esp32/Arduino/01.esp32-arduino-intro.html
之前我们学习了 ESP32 的按键控制,当时通过查询 GPIO 输入电平来判断按键状态,这种方法占用 CPU 资源,效率不高。本节课我们学习外部中断,通过外部中断实现按键控制 LED。
在单片机中,中断是指当 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() 函数会阻塞整个系统,包括中断的正常执行。当中断触发时,处理函数应该尽快执行完毕,以确保及时响应并避免中断积压;Serial 对象的打印函数。当在中断处理函数中使用 Serial 打印函数时,会导致以下问题:
Serial 打印函数通常是比较耗时的操作,它会阻塞中断的执行时间,导致中断响应的延迟。这可能会导致在中断期间丢失其他重要的中断事件或导致系统不稳定。Serial 对象在内部使用一个缓冲区来存储要发送的数据。如果在中断处理函数中频繁调用 Serial 打印函数,可能会导致缓冲区溢出,造成数据丢失或不可预测的行为。为了避免这些问题,建议在中断处理函数中尽量避免使用 Serial 打印函数。如果需要在中断处理函数中输出调试信息,可以使用其他方式,如设置标志位,在主循环中检查标志位并进行打印,代码如下:
#define BUTTON 14
// 定义可以在外部中断函数中使用的变量
volatile bool flag = false;
// 定义外部中断函数
void handle_interrupt() {
flag = true;
number += 10000;
}
void setup() {
Serial.begin(9600);
pinMode(BUTTON, INPUT_PULLDOWN);
// 配置中断引脚
attachInterrupt(digitalPinToInterrupt(BUTTON), handle_interrupt, FALLING);
}
void loop() {
if (flag) {
Serial.println("外部中断触发");
flag = false;
}
}
因此,我们使用外部中断点灯的代码可以这么写:
#define BUTTON 14
#define LED 2
//定义标记为false
volatile bool flag = false;
//定义标记函数
void ISR() {
flag = true;
}
void setup() {
//因为按钮接高电平,使用设置按钮输出模式为pulldown
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 的官方文档和相关库的文档以获取更详细的信息和使用示例。
LED 的正极接开发板的 D2、D4 引脚,并串联一个电阻,负极接 GND,如下图:

注意
一定要接电阻,不然会由于电流过大,烧坏 LED。
在 ESP32 Arduino 开发环境中,可以使用以下几个库函数来配置和操作硬件定时器(Timer):
void timerBegin(timer_num_t timer_num, uint32_t divider, bool count_up):初始化硬件定时器,参数说明:timer_num:定时器编号,可选值为 0-3 等。divider:定时器的分频系数,用于设置定时器的时钟频率。较大的分频系数将降低定时器的时钟频率。可以根据需要选择合适的值,一般设置为 80 即可;count_up:指定定时器是否为向上计数模式。设置为 true 表示向上计数,设置为 false 表示向下计数。timerAttachInterrupt(hw_timer_t *timer, void (*isr)(void *), void *arg, int intr_type):用于将中断处理函数与特定的定时器关联起来,参数含义如下:timer;定时器指针;isr: 中断处理函数。arg: 传递给中断处理函数的参数。intr_type: 中断类型,可选值为 ture(边沿触发)或 false(电平触发)。 2.timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload):用于设置定时器的计数值,即定时器触发的时间间隔,参数含义如下:
timer:定时器指针;alarm_value: 定时器的计数值,即触发时间间隔;autoreload: 是否自动重载计数值,可选值为 true(自动重载)或 false(单次触发)。timerAlarmEnable(hw_timer_t *timer):用于启动定时器,使其开始计数;timerAlarmDisable(hw_timer_t *timer):用于禁用定时器,停止计数;timerGetAutoReload(hw_timer_t *timer):获取定时器是否自动重新加载;timerAlarmRead(hw_timer_t *timer):获取定时器计数器报警值;timerStart(hw_timer_t *timer):计数器开始计数;timerStop(hw_timer_t *timer):计数器停止计数;timerRestart(hw_timer_t *timer):计数器重新开始计数,从 0 开始;timerStarted(hw_timer_t *timer):计数器是否开始计数。以上是一些常用的硬件定时器相关的库函数,你可以根据自己的需求和定时器的特性,调用适当的函数来配置和操作硬件定时器。请参考 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 毫秒周期性执行;注意
不建议使用 Ticker 回调函数来阻塞 IO 操作(网络、串口、文件);可以在 Ticker 回调函数中设置一个标记,在 loop 函数中检测这个标记;
#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() {
}
系列导航:返回目录索引 | 上一篇:09. SPI 驱动 OLED 液晶屏(下):U8G2 库与按键菜单 | 下一篇:11. 舵机实验
Article Navigation System © 2025
欢迎来到我的留言板,留下你的足迹,与我分享你的想法和感受。
点击文本框会有惊喜哦`(。•̀ᴗ-)✧