课程目录
3.1 中断引例
想象一个情景,周一早上有节早八。好消息是这是节水课,不收手机,坏消息是老师上课前要点名,不能不去。那么为了我们的身心愉悦,只能去得早点抢个后排。为此,你定了个早上7点的闹钟。周一到了,闹钟响了,你起床洗漱完之后,走在路上,看到路人玩手机,忽然间想起手机没拿,快乐的源泉忘带了。没办法了只能返身去取手机,之后再去教室占座。
3.2 什么是中断
想象到此结束,让我们来剖析一下整个过程,正常来说,我们不会再返回宿舍,而是直接到教室。如果从单片机角度来看,“直接到教室”这个过程,是主进程,“返回宿舍拿手机”就属于是中断任务,“看到路人玩手机,忽然间想起手机没拿”就是中断触发源,当中断被触发,就会打断主进程,转而去执行中断任务,只有在中断执行完之后,才会继续执行主进程。
3.2.1中断概念:
在单片机中,中断是指当 CPU 在正常处理主程序时,突然发生了另一件事件 A(中断发生)需要 CPU 去处理,这时 CPU 就会暂停处理主程序(中断响应),转而去处理事件 A(中断服务)。当事件 A 处理完以后,再回到主程序原来中断的地方继续执行主程序(中断返回)。这一整个过程称为中断。
3.2.2中断的作用和意义:
(1)实时响应:外部中断可以立即暂停当前任务并执行中断程序,因此可以实现对外部事件的实时响应。比如,当按下按钮时,可以立即触发中断处理程序,而不需要在主循环中轮询按钮状态。 (2)支持异步通信:在异步通信协议(如UART、SPI、I2C)中,外部中断可以用于处理接收和发送事件。例如,当接收到数据时,可以通过中断通知CPU读取数据,而不需要持续轮询通信接口的状态。(上课传纸条,加密,小A指代,不用盯着看) (3)节省CPU资源:外部中断的处理由硬件负责,不需要CPU持续监测输入状态。这样可以节省CPU的资源,使得CPU可以处理其他任务,提供更好的系统性能。 (4)增强系统安全性:外部中断可以用于实现安全机制,如硬件看门狗定时器(Watchdog Timer)。当系统出现异常时,看门狗定时器可以触发中断,强制系统复位或执行特定的安全操作,防止系统进入不可控状态。(喂狗)
(5)减少功耗:通过使用外部中断来检测外部事件,可以让系统进入低功耗状态,只有当中断事件发生时才唤醒系统处理。这在对功耗要求较高的应用中尤为重要,可以延长电池寿命或降低能耗。(熊冬眠)
(6)精准触发:外部中断可以通过配置边沿触发方式(上升沿、下降沿或双边沿)以及滤波选项,实现对特定事件的精确触发。这对于需要高精度的事件检测和计时非常有用,如测量脉冲宽度或计数器应用。
3.2.3中断优先级:
3.2.4中断触发源:
硬件中断(外部中断):外部事件或硬件信号触发,如GPIO中断、定时器中断、UART中断等。
- GPIO中断:
- ESP32的GPIO引脚可以配置为触发中断。当引脚状态发生变化(如上升沿、下降沿、高电平、低电平等)时,会触发中断。
- 例如,当某个按钮按下时,GPIO引脚状态从高电平变为低电平,触发中断。
- 硬件定时器中断:
- ESP32有多个硬件定时器,可以配置为在特定时间间隔或计数达到特定值时触发中断。
- 例如,定时器每1秒触发一次中断,用于执行定时任务。
- UART中断:
- 当UART(通用异步收发传输器)接收到数据或发送数据完成时,可以触发中断。
- 例如,当UART接收到一帧数据时,触发中断以便处理接收到的数据。
………………………………………………………省略号……………………………………………………………
软件中断:由软件指令触发的,通常用于实现特定的功能或处理特定的任务。
- 任务调度中断:
- ESP32在使用FreeRTOS作为实时操作系统时,任务调度器会根据任务的优先级和状态触发软件中断,以切换任务。
- 例如,当一个高优先级任务就绪时,调度器会触发软件中断,切换到该任务执行。
- 系统调用中断:
- 在某些情况下,操作系统或库函数会通过软件中断来执行特定的系统调用。
- 例如,当应用程序调用某个系统函数时,可能会触发软件中断以执行底层操作。
- 软件定时器中断:
- ESP32支持软件定时器,可以在特定时间间隔触发中断。
- 例如,软件定时器每100毫秒触发一次中断,用于执行周期性任务。
3.2.5中断处理函数(Interrupt Service Routine,简称ISR ):
要注意中断处理函数的执行时间
- 避免阻塞:在ISR中避免执行可能阻塞的操作,如延时函数(delay();)、复杂的计算或等待资源。
- 快速执行:中断处理函数应该尽可能快速执行。长时间的中断处理会影响系统的响应速度和其他中断的处理。
3.3 GPIO中断
3.3.1 源代码
先贴一段代码:
#include <Arduino.h>
#define BUTTON 12
// 定义可以在外部中断函数内部使用的变量
bool flag = false;
void handle_interrupt()
{
flag = true;
}
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
// 配置中断引脚
attachInterrupt(digitalPinToInterrupt(BUTTON), handle_interrupt, CHANGE);
}
void loop() {
if (flag)
{
// 软件去抖动
delay(50); // 延时50毫秒以消除抖动
if (digitalRead(BUTTON) == LOW) {
flag = false;
}
}
}
3.3.2 代码分析
让我们来逐行分析:
#define BUTTON 14
这行代码定义了一个宏 BUTTON
,其值为 14
。这意味着在代码中使用 BUTTON
的地方都会被替换为 14
,这样做会有个好处,就是当你想改变触发的引脚的时候只需要改变一个地方,而不需要在整个工程里面逐行去修改IO口。
bool flag = false;
- 引脚电平变化时,flag的值会改变。
void handle_interrupt()
{
flag = true;
}
- 当外部中断触发时,这个函数会被自动调用,更改flag的值。
Serial.begin(115200);
- 配置串口波特率为
115200
。
pinMode(BUTTON, INPUT_PULLUP);
- 配置
BUTTON
引脚模式为INPUT_PULL
UP,默认输入高电平。
// 配置中断引脚
attachInterrupt(digitalPinToInterrupt(BUTTON), handle_interrupt, CHANGE);
attachInterrupt
:digitalPinToInterrupt(BUTTON)
:handle_interrupt
:CHANGE
:
指定中断触发模式为 CHANGE
,即当引脚电平发生变化时触发中断。
让我们来详细看一下这个中断触发模式(源代码位于Arduino.h
):
//Interrupt Modes
#define RISING 0x01
#define FALLING 0x02
#define CHANGE 0x03
#define ONLOW 0x04
#define ONHIGH 0x05
#define ONLOW_WE 0x0C
#define ONHIGH_WE 0x0D
RISING
:检测上升沿(从低电平到高电平)。FALLING
:检测下降沿(从高电平到低电平)。

CHANGE
:检测任何电平变化。ONLOW
:检测引脚为低电平。ONHIGH
:检测引脚为高电平。ONLOW_WE
:检测引脚为低电平,并持续触发中断。ONHIGH_WE
:检测引脚为高电平,并持续触发中断。
void loop() {
if (flag) {
// 软件去抖动
delay(50); // 延时50毫秒以消除抖动
if (digitalRead(BUTTON) == HIGH) {
Serial.println("外部中断触发了");
flag = false;
}
}
}
在 loop
函数中检查标志位,进行简单的软件去抖动,确认按钮状态,并打印消息。
为什么我们这样操作,而不是采用如下形式呢?
void handle_interrupt()
{
// 软件去抖动
delay(50); // 延时50毫秒以消除抖动
if (digitalRead(BUTTON) == HIGH) {
Serial.println("外部中断触发了");
}
void loop()
{
}
中断处理函数(ISR)应该尽可能快速执行,以避免影响系统的响应速度和其他中断的处理。delay(50)
会阻塞 ISR 50 毫秒,这会导致系统在这段时间内无法响应其他中断或执行其他任务,很不优雅。
3.3.3 红绿灯源代码
#include <Arduino.h>
#define BUTTON 4
#define RED_LED 15
#define YELLOW_LED 16
#define GREEN_LED 17
// 定义可以在外部中断函数内部使用的变量
bool flag = false;
bool running = false; // 流水灯运行状态
unsigned long previousMillis = 0; // 上一次切换时间
const long interval = 500; // 切换间隔时间(毫秒)
int currentLED = RED_LED; // 当前点亮的LED
void handle_interrupt()
{
flag = true;
}
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
pinMode(RED_LED, OUTPUT);
pinMode(YELLOW_LED, OUTPUT);
pinMode(GREEN_LED, OUTPUT);
// 配置中断引脚
attachInterrupt(digitalPinToInterrupt(BUTTON), handle_interrupt, CHANGE);
}
void loop()
{
if (flag)
{
// 软件去抖动
delay(50); // 延时50毫秒以消除抖动
if (digitalRead(BUTTON) == LOW) {
running = !running; // 切换流水灯的运行状态
flag = false;
}
}
if (running)
{
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval)
{
// 保存上一次切换时间
previousMillis = currentMillis;
// 关闭当前LED
digitalWrite(currentLED, LOW);
// 切换到下一个LED
if (currentLED == RED_LED)
{
currentLED = YELLOW_LED;
}
else if (currentLED == YELLOW_LED)
{
currentLED = GREEN_LED;
}
else
{
currentLED = RED_LED;
}
// 点亮下一个LED
digitalWrite(currentLED, HIGH);
}
}
else
{
// 关闭所有LED
digitalWrite(RED_LED, LOW);
digitalWrite(YELLOW_LED, LOW);
digitalWrite(GREEN_LED, LOW);
}
}
3.3.4 红绿灯代码分析
头文件和宏定义
#include <Arduino.h>
- 包含Arduino的核心库,提供了Arduino的基本功能。
#define BUTTON 4
#define RED_LED 15
#define YELLOW_LED 16
#define GREEN_LED 17
- 定义了按钮和三个LED的引脚编号。
全局变量
bool flag = false;bool running = false; // 流水灯运行状态
unsigned long previousMillis = 0; // 上一次切换时间
const long interval = 500; // 切换间隔时间(毫秒)
int currentLED = RED_LED; // 当前点亮的LED
flag
:用于标志外部中断是否被触发。running
:表示流水灯是否在运行。previousMillis
:记录上一次LED切换的时间。interval
:LED切换的时间间隔,设置为500毫秒。currentLED
:当前点亮的LED引脚。
中断处理函数
void handle_interrupt()
{
flag = true;
}
handle_interrupt()
:外部中断处理函数。当按钮状态发生变化时,flag
被设置为true
,表示中断被触发。
setup()
函数
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
pinMode(RED_LED, OUTPUT);
pinMode(YELLOW_LED, OUTPUT);
pinMode(GREEN_LED, OUTPUT);
// 配置中断引脚
attachInterrupt(digitalPinToInterrupt(BUTTON), handle_interrupt, CHANGE);
}
pinMode(BUTTON, INPUT_PULLUP);
:将按钮引脚设置为输入上拉模式。pinMode(RED_LED, OUTPUT);
:将红色LED引脚设置为输出模式。pinMode(YELLOW_LED, OUTPUT);
:将黄色LED引脚设置为输出模式。pinMode(GREEN_LED, OUTPUT);
:将绿色LED引脚设置为输出模式。attachInterrupt(digitalPinToInterrupt(BUTTON), handle_interrupt, CHANGE);
:将按钮引脚配置为外部中断,触发模式为CHANGE
(电平变化时触发),中断处理函数为handle_interrupt
。
loop()
函数
void loop()
{
if (flag)
{
// 软件去抖动
delay(50); // 延时50毫秒以消除抖动
if (digitalRead(BUTTON) == LOW) {
running = !running; // 切换流水灯的运行状态
flag = false;
}
}
if (flag)
:检查flag
是否为true
,即是否触发了外部中断。delay(50);
:延时50毫秒以消除按钮的抖动。if (digitalRead(BUTTON) == LOW)
:检查按钮是否被按下(低电平)。running = !running;
:切换流水灯的运行状态(启动或停止)。flag = false;
:重置flag
,等待下一次中断。
if (running)
{
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval)
{
// 保存上一次切换时间
previousMillis = currentMillis;
// 关闭当前LED
digitalWrite(currentLED, LOW);
// 切换到下一个LED
if (currentLED == RED_LED)
{
currentLED = YELLOW_LED;
}
else if (currentLED == YELLOW_LED)
{
currentLED = GREEN_LED;
}
else
{
currentLED = RED_LED;
}
// 点亮下一个LED
digitalWrite(currentLED, HIGH);
}
}
else
{
// 关闭所有LED
digitalWrite(RED_LED, LOW);
digitalWrite(YELLOW_LED, LOW);
digitalWrite(GREEN_LED, LOW);
}
}
if (running)
:检查流水灯是否在运行。unsigned long currentMillis = millis();
:获取当前时间。
millis()
是 Arduino 中的一个内置函数,用于返回自程序启动以来经过的毫秒数。这个函数非常有用,特别是在需要进行时间相关的操作时,例如定时器、延迟、时间戳等。
millis()
函数的作用
- 返回自程序启动以来的毫秒数:
millis()
返回一个unsigned long
类型的值,表示自程序启动以来经过的毫秒数。- 这个值会随着时间的推移不断增加,直到达到
unsigned long
(它的存储范围是从 0 到 2^32 – 1,即 0 到 4,294,967,295) 的最大值(大约 49.7 天),然后会从零重新开始。
- 非阻塞延迟:
- 与
delay()
函数不同,millis()
不会阻塞程序的执行。这意味着你可以在等待一段时间的同时执行其他代码。 - 例如,你可以使用
millis()
来实现一个定时器,而不需要使用delay()
函数。
- 与
if (currentMillis - previousMillis >= interval)
:检查是否达到了切换时间间隔。previousMillis = currentMillis;
:更新上一次切换时间。digitalWrite(currentLED, LOW);
:关闭当前点亮的LED。if (currentLED == RED_LED)
:根据当前点亮的LED切换到下一个LED。digitalWrite(currentLED, HIGH);
:点亮下一个LED。else
:如果流水灯未运行,关闭所有LED。
总结
这段代码通过外部中断控制流水灯的启动和停止。当按钮被按下时,流水灯会在红色、黄色和绿色LED之间循环切换,每次切换间隔为500毫秒。如果按钮再次被按下,流水灯将停止。代码中还包含了软件去抖动处理,以避免按钮抖动引起的误触发。
3.4 定时器

定时器就像是一个闹钟,它可以在设定的时间间隔后提醒你做某件事情。在电子设备中,定时器用于在特定的时间间隔后触发某些操作。
物理实体
代码层面
硬件定时器的优缺点:
优点:
- 精确性:硬件定时器通常非常精确,因为它们由专门的硬件电路实现,不受CPU负载的影响。
- 独立性:硬件定时器可以独立于CPU运行,即使CPU忙于其他任务,定时器也能准确计时。
- 实时性:硬件定时器适合需要高实时性的应用,如嵌入式系统和实时操作系统。
- 可靠性:由于不依赖于软件,硬件定时器在系统崩溃或软件错误时仍能工作。
缺点:
- 灵活性:硬件定时器的功能通常比较固定,不如软件定时器灵活。
- 资源限制:硬件定时器的数量和类型可能受限于硬件设计,无法像软件那样容易扩展,就4个。
软件定时器的优缺点:
优点:
- 灵活性:软件定时器可以根据程序的需要灵活地创建、配置和销毁。
- 可编程性:软件定时器可以通过编程实现复杂的定时任务和调度。
- 扩展性:软件定时器理论上没有数量限制,可以根据需要创建多个定时器。
- 成本:软件定时器不需要额外的硬件支持,可以节省成本。
缺点:
- 精确性:软件定时器的精确性受限于操作系统的调度和CPU的负载,可能不如硬件定时器精确。
- 依赖性:软件定时器依赖于操作系统和CPU的正常运行,如果系统崩溃或CPU过载,定时器可能无法正常工作。
- 实时性:软件定时器不适合需要极高实时性的应用,因为它们受操作系统调度的影响。
硬件定时器和软件定时器各有优劣,具体选择取决于你的应用需求。如果需要高精度、实时性和稳定性,建议使用硬件定时器。如果时间精度要求不高,或者只需要基本的定时功能,可以使用软件定时器来简化代码编写。
3.4.1 软件定时器中断
这里我们使用Ticker
,这是一个内置的软件定时器库,它使用 FreeRTOS 的任务调度器来实现定时功能。与硬件定时器不同,Ticker
不需要硬件支持,代码层面,因此可以在任何支持 FreeRTOS 的平台上使用,而且定时器的数量也是没有限制的。
3.4.1.1 源代码
贴个代码
#include <Arduino.h>
#include <Ticker.h>
// 定义定时器对象
Ticker timer;
Ticker timer_once;
// 定义LED引脚
#define LED 16
#define LED_ONCE 17
// 定义定时器中断回调函数
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() {
// 主循环中不需要执行任何操作
}
Ticker timer;
Ticker timer_once;
3.4.1.2 代码分析
- 定义定时器对象:
Ticker timer;
:创建一个Ticker
对象timer
,用于周期性定时器。Ticker timer_once;
:创建一个Ticker
对象timer_once
,用于一次性定时器。
- 定义定时器中断回调函数:
void toggle(int pin)
:定义一个回调函数toggle
,接受一个整数参数pin
。digitalWrite(pin, !digitalRead(pin));
:读取引脚的当前状态,并将其取反后写回引脚,从而切换引脚状态。
timer.attach(0.5, toggle, LED);
:配置timer
,使其每隔0.5
秒触发一次toggle
函数,并将LED
引脚作为参数传递给toggle
函数。timer_once.once(3, toggle, LED_ONCE);
:配置timer_once
,使其在3
秒后触发一次toggle
函数,并将LED_ONCE
引脚作为参数传递给toggle
函数。attach_ms(uint32_t milliseconds, callback_function)
:- 以毫秒为单位设置定时器间隔,并绑定回调函数。
- 示例:
timer.attach_ms(500, onTick);
每隔500毫秒触发一次onTick
函数。
once(float seconds, callback_function)
:- 设置定时器在指定时间后触发一次回调函数,然后停止。
- 示例:
timer.once(2, onTick);
2秒钟后触发一次onTick
函数,然后停止。
once_ms(uint32_t milliseconds, callback_function)
:- 以毫秒为单位设置定时器在指定时间后触发一次回调函数,然后停止。
- 示例:
timer.once_ms(1000, onTick);
1000毫秒后触发一次onTick
函数,然后停止。
detach()
:- 停止
timer
,不再触发onTick
函数 - 示例:
timer.detach();
- 停止
3.4.2 硬件定时器中断
硬件定时器
是 ESP32 芯片上的内置计时器,它们是专门设计用于定时和计时任务的硬件模块,通常具有更高的精确度和稳定性。它们不受软件的影响,可以在后台独立运行,不会受到其他代码的干扰。硬件定时器适用于需要高精度和实时性的定时任务,例如 PWM 输出、捕获输入脉冲等。
需要注意的是,ESP32 有 4 个硬件定时器

时钟频率:
ESP32默认时钟频率是240MHz,可以简单理解成高低电平的方波信号,经过一系列分频操作,最终作为外设的时钟频率,用于定时器的时钟频率默认是80MHz。
预分频器:
预分频器的工作原理非常简单:它接收一个高频的输入时钟信号,并根据预设的分频系数(通常是一个整数),生成一个频率较低的输出时钟信号。例如,如果输入时钟频率为 F_in
,分频系数为 N
,则输出时钟频率 F_out
为:$$Fout=Fin/N$$
系数N我们一般是80,因为80MHz经过预分频器之后变成1MHz,周期就是1us,方便我们统计计算。
自动重装寄存器和计数器:
自动重装寄存器设定一个值a,计数器根据方式向上计数,向下计数。分别从0到a,或者从a到0。然后触发一次定时器中断。

3.4.2.1 源代码
贴个代码
#include <Arduino.h>
// 定义定时器编号和定时器组
#define TIMER_INTERVAL_MS 1000 // 定时器间隔时间(毫秒)
#define TIMER_DIVIDER 80 // 定时器分频器,用于将系统时钟频率转换为定时器时钟频率
#define RED_LED 15 // 红色LED引脚
// 定义可以在中断函数内部使用的变量
bool timerFlag = false;
// 中断处理函数
void IRAM_ATTR onTimer()
{
timerFlag = true;
}
void setup()
{
pinMode(RED_LED, OUTPUT);
// 配置定时器
hw_timer_t * timer = NULL;
timer = timerBegin(0, TIMER_DIVIDER, true); // 使用定时器0,分频器为80,向上计数
timerAttachInterrupt(timer, &onTimer, true); // 将中断处理函数绑定到定时器
timerAlarmWrite(timer, TIMER_INTERVAL_MS * 1000, true); // 设置定时器间隔时间(微秒)
timerAlarmEnable(timer); // 启用定时器
}
void loop()
{
if (timerFlag)
{
timerFlag = false;
digitalWrite(RED_LED, !digitalRead(RED_LED));
}
}
3.4.2.2 代码分析
// 中断处理函数
void onTimer()
{
timerFlag = true;
}
onTimer
函数是中断处理函数,当定时器中断触发时,这个函数会被调用。- 在
onTimer
函数中,将timerFlag
设置为true
,以通知主程序定时器中断已经触发。
void setup()
{
// 配置定时器
hw_timer_t * timer = NULL;
timer = timerBegin(0, TIMER_DIVIDER, true); // 使用定时器0,分频器为80,向上计数
timerAttachInterrupt(timer, &onTimer, true); // 将中断处理函数绑定到定时器
timerAlarmWrite(timer, TIMER_INTERVAL_MS * 1000, true); // 设置定时器间隔时间(微秒)
timerAlarmEnable(timer); // 启用定时器
}
timer
:硬件定时器句柄。timerBegin(0, TIMER_DIVIDER, true);
:初始化定时器 0,分频器为 80,向上计数。timerAttachInterrupt(timer, &onTimer, true);
:将中断处理函数onTimer
绑定到定时器。timerAlarmWrite(timer, TIMER_INTERVAL_MS * 1000, true);
:设置定时器的间隔时间(微秒),并启用自动重载。timerAlarmEnable(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(电平触发)。
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)
:用于禁用定时器,停止计数;
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)
:计数器是否开始计数。