5. 外部中断
5.1 什么是中断
在微处理器或微控制器的世界中,中断是一种特殊的事件,它会打断和暂时挂起当前正在执行的程序,以便处理一个特定的状况或者事件。例如按下按钮、到达定时器时间或收到序列口数据。中断是一个非常重要的计算机系统概念,它通过异步的方式对这些特定情况做出响应。这里举一个例子,比如我们正在敲代码,突然有一个电话打过来,这时我 们停止敲代码转而去接电话,然后在电话聊完事情之后继续敲代码。这里面的电话就相当于一个中断,打断我们当前做的事情,接电话聊事情就相当于中断需要去执行的事情,也就是中断服务程序。
中断可以分为硬件中断和软件中断两种类型。
硬件中断通常由外部设备的物理事件引发,如按下按钮、达到定时器的时间、或数据到达串行端口。当这些事件发生时,微处理器会立即暂停其当前的任务,并跳转到一个预先定义的中断服务程序(ISR)来响应该事件。
软件中断则是由软件指令引发,通常用于更复杂的处理任务。像操作系统的系统调用就使用了软件中断。
5.2 外部中断
在上一章节,我们在做按键实验时,虽然能实现读取 GPIO口输入功能,但代码是一直在检测IO输入口的变化,如果我们后续加入了大量的代码,就需要花费很长的时间才能轮询到按键检测部分,因此效率不高。特别是在一些特定的场合,比如某个按键,可能 1 天才按下一次去执行相关功能,这样我们就浪费大量时间来实时检测按键的情况。为了解决这样的问题,我们引入外部中断概念,顾名思义,就是当按键被按下(产生中断)时,才去执行相关功能。这大大节省了 CPU 的资源,因此中断在实际项目中应用非常普遍。
外部中断是硬件中断的一种,它由微控制器外部的事件引发。微控制器的某些引脚被设计为对特定事件的发生做出响应,例如按钮的按压、传感器的信号改变等。这些指定的引脚通常被称为“外部中断引脚”。
在发生外部中断事件时,当前的程序执行会被立即停止,并跳转到对应的中断服务程序(ISR)进行处理。处理完毕后,程序会返回到被中断的地方继续执行。
对于嵌入式系统、实时系统来说,外部中断的使用是非常重要的,能帮助系统对外部事件进行即时响应,大大提高了系统的效率和实时性。ESP32S3开发板提供了许多引脚作为可用的外部中断引脚,可以通过配置这些引脚来进行外部中断实验。
ESP32 的外部中断有上升沿、下降沿、低电平、高电平触发模式。上升沿和下降沿触发如下:
当引脚设置为外部中断引脚后,当检测到引脚出现设置的触发模式时,就会进入中断回调函数中执行相应程序;
5.3 外部中断的作用和优势
ESP32S3的外部中断功能在开发中具有以下作用和优势:
- 实时响应外部事件:ESP32S3的外部中断功能可以让你在检测到外部事件触发时立即作出响应。这些外部事件可以是来自传感器、按钮、开关、接收到的信号等等。通过外部中断,就可以实时地捕捉到这些事件并执行相应的操作,而无需频繁地轮询或等待。
- 节省计算资源:外部中断允许你将处理外部事件的任务转移给芯片的硬件,从而节省了处理器的计算资源。相比于软件轮询方式,外部中断可以降低对处理器的负担,使其可以更有效地利用其它资源进行更复杂的任务。
- 精确的事件捕捉:ESP32S3的外部中断功能能够以非常精确的方式捕捉外部事件的触发。你可以通过配置中断触发方式(如上升沿、下降沿、任意电平、低电平保持、高电平保持等)来适应不同的外部事件,并在事件发生时立即中断当前程序的执行,转而执行中断服务函数。
- 高优先级处理:外部中断可以设置为高优先级处理,优先于当前正在执行的程序。这对于需要立即响应的重要事件非常有用,如紧急通知、传感器检测等。当外部事件触发时,处理器将立即转移到中断服务函数执行,确保及时、准确地处理相关操作,避免对处理程序的延迟。
- 多路中断处理:ESP32S3支持多路外部中断,你可以将多个外部事件与不同的中断引脚相连,从而实现对多个事件的并行处理。这使得你可以处理多个传感器、开关等外部事件,提高系统的灵活性和扩展性。
总之,ESP32S3的外部中断功能提供了实时响应、节省计算资源、精确事件捕捉、高优先级处理和多路中断处理等优势。它为我们提供了更加灵活、高效的方式来处理外部事件,并帮助构建更强大、可靠的应用。
5.4 使用外部中断流程
使用 ESP32S3 外部中断,可以使 ESP32S3 在某个特定事件发生时自动中断程序,并立即处理该事件。下面是在 ESP-IDF 环境中,使用外部中断的基本流程:
- 配置引脚和中断 在代码中导入相应的头文件,以便使用ESP-IDF提供的GPIO功能。通常情况下,需要导入<driver/gpio.h>头文件。
#include <driver/gpio.h>
在 ESP32S3 上设置引脚作为外部中断输入,使其能够检测外部事件的触发。
//配置按键
gpio_config_t io_conf = {};
//设置为 下降沿中断
io_conf.intr_type = GPIO_INTR_NEGEDGE;
//设置为 GPIO0 引脚
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
//设置为 输入模式
io_conf.mode = GPIO_MODE_INPUT;
//设置为 使能上拉电阻
io_conf.pull_up_en = 1;
//设置为 禁止下拉电阻
io_conf.pull_down_en = 0;
//将以上配置设置到引脚上
gpio_config(&io_conf);
2
3
4
5
6
7
8
9
10
11
12
13
14
- 设置中断处理函数 实现一个中断服务程序的回调函数,在函数中处理中断响应。(其中函数名可以随意起名,但是要符合C语言标准)中断处理函数需要声明为
IRAM_ATTR
,以确保其运行在内存中的可执行区域。
void IRAM_ATTR gpio_isr_handler(void* arg) {
// 处理中断响应
}
2
3
- 配置中断 使用
gpio_install_isr_service()
函数安装 GPIO 中断服务程序,并使用gpio_isr_handler_add()
函数为指定的 GPIO 引脚分配中断处理程序:
gpio_install_isr_service(ESP_INTR_FLAG_EDGE); // 安装 GPIO 中断服务程序
gpio_isr_handler_add(GPIO_NUM_21, gpio_isr_handler, NULL); // 分配中断处理程序
2
gpio_install_isr_service()
函数的原型:
void gpio_install_isr_service(esp_intr_alloc_flag_t flags);
该函数会安装 GPIO 中断服务程序,并根据指定的标志位参数 flags,配置中断服务程序的行为。
- flags 可以使用以下标志位:
- ESP_INTR_FLAG_LEVEL1: 使用 Level 1 中断级别。在中断服务程序执行期间禁用同级别的中断。
- ESP_INTR_FLAG_LEVEL2: 使用 Level 2 中断级别。在中断服务程序执行期间禁用同级别和 Level 1 的中断。
- ESP_INTR_FLAG_EDGE: 使用边沿触发方式。使能 GPIO 边沿触发中断。
- ESP_INTR_FLAG_LOWMED: 设置中断优先级为低等。
- ESP_INTR_FLAG_HIGH: 设置中断优先级为高等。 示例代码中使用了 ESP_INTR_FLAG_EDGE 标志位,表示使能 GPIO 边沿触发中断。你可以根据你的实际需求选择适合的标志位。
什么是边沿触发?
📌 边沿触发是一种触发信号的方式,它依据信号发生变化的边沿来触发相应的操作。边沿可以是上升沿(rising edge)或下降沿(falling edge)。 在上升沿触发中,信号从低电平变为高电平时触发操作。上升沿触发通常用于检测信号的变化,表示某个事件的开始。 在下降沿触发中,信号从高电平变为低电平时触发操作。下降沿触发通常用于检测信号的变化,表示某个事件的结束或状态的转换。 边沿触发可以在数字电路、中断控制、输入输出接口等应用中使用。例如,当一个开关从关到开的瞬间,可以使用上升沿触发来检测开关状态的变化并执行相应的操作。同样地,当一个脉冲信号的脉冲宽度结束时,可以使用下降沿触发来检测脉冲信号的变化并进行相应的处理。
gpio_isr_handler_add()
函数的原型:
esp_err_t gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void* args);
该函数接受以下参数:
gpio_num:GPIO
引脚号,指定要分配中断处理程序的 GPIO 引脚。isr_handler
:指向中断处理函数的函数指针。中断处理函数是一个用户定义的回调函数,将在中断发生时执行。args
:传递给中断处理程序的参数。这是一个指向用户特定数据的指针,可以在中断处理程序中使用。
使用gpio_isr_handler_add()
函数,你可以为特定的 GPIO 引脚分配自定义的中断处理程序。当指定的 GPIO 引脚触发中断时,分配的中断处理程序将被调用,并执行预定义的操作。
注意,在使用该函数之前,你需要先调用gpio_install_isr_service()
函数来安装 GPIO 中断服务程序。
- 开启外部中断
//使能GPIO模块中断信号
gpio_intr_enable(KEY_PIN);
2
gpio_intr_enable()
是 ESP32 中关于 GPIO 中断的一个函数,用于启用指定 GPIO 号对应的 IO 口的中断。函数原型为:
void gpio_intr_enable(gpio_num_t gpio_num)
其中 gpio_num
表示要启用中断的 GPIO 号。 在使用 gpio_intr_enable()
函数之前,您需要先通过 gpio_install_isr_service()
函数和 gpio_isr_handler_add()
函数来安装和注册中断处理程序。
5.5 硬件连接与准备
以开发板上板载的BOOT键作为案例。
其中,BOOT键接到了ESP32S3芯片的GPIO0上,LED接到了GPIO48引脚。我们需要将按键引脚GPIO0配置为外部中断引脚,LED引脚GPIO48配置为输出模式。通过按键控制LED亮灭。
5.6 外部中断验证
在main.c中编写如下代码:(按键代码bsp_key.h与LED代码bsp_led.h请参考LED章节和按键点灯章节)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "esp_system.h"
#include "esp_log.h"
#include "bsp_led.h"
#include "bsp_key.h"
//中断服务函数
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
static int cnt = 0;
cnt++;
//当中断被触发时,LED状态变化
gpio_set_level(LED_PIN, cnt % 2);
}
void app_main(void)
{
gpio_config_t io_conf = {};
//配置LED
LedGpioConfig();
//配置按键
//下降沿中断
io_conf.intr_type = GPIO_INTR_NEGEDGE;
//设置GPIO0的输入寄存器
io_conf.pin_bit_mask = ( 1 << KEY_PIN);
//输入模式
io_conf.mode = GPIO_MODE_INPUT;
//使能上拉模式
io_conf.pull_up_en = 1;
io_conf.pull_down_en = 0;
gpio_config(&io_conf);
//注册中断服务
gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
//设置GPIO的中断服务函数
gpio_isr_handler_add(KEY_PIN, gpio_isr_handler, (void*)NULL);
//使能GPIO模块中断信号
gpio_intr_enable(KEY_PIN);
int time = 0;
while(1)
{
printf("time: %d\n", time++);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
下载上述代码后,按下按键可以看到LED的状态变化。按下按键灯亮,再按下按键灯灭,如此反复。
但是使用以上方法,有一个问题。ESP-IDF下载的代码是基于FreeRTOS运行的,如果在中断服务函数中,加入printf,延时等函数,会出现复位现象。
在中断处理函数中,最好避免使用延时函数(如 vTaskDelay())或阻塞调用的原因是,中断处理函数应该尽量快速地执行完毕,以确保及时处理中断事件。延时函数和阻塞调用会导致中断处理函数长时间占用 CPU,影响其他任务的执行和系统的响应性能。此外,中断处理函数通常在中断上下文中执行,与任务上下文有一些区别,某些阻塞函数可能无法在中断上下文中使用。
在中断处理函数中避免使用标准输出函数(如 printf())是因为标准输出函数通常是阻塞的,会导致中断处理函数阻塞并延长执行时间。而中断处理函数应该尽量短暂,只完成必要的操作,以确保系统的及时响应。另外,中断处理函数通常在中断上下文中执行,标准输出函数可能无法正确地工作。
基于以上问题,可以使用另一种外部中断的方式。创建一个FreeRTOS的任务,通过队列的方式,检测按键的状态。代码如下:运行之后LED以1秒作为间隔进行闪烁,当按下按键时就会触发中断,触发中断就会发出队列信号,按键检测任务中,
就会输出。#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#define GPIO_OUTPUT_PIN_SEL (1ULL<<GPIO_NUM_48)
#define GPIO_INPUT_PIN_SEL (1ULL<<GPIO_NUM_0)
#define ESP_INTR_FLAG_DEFAULT 0
static QueueHandle_t gpio_evt_queue = NULL;
//中断服务函数
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
uint32_t gpio_num = (uint32_t) arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
//按键检测任务
static void gpio_get_key_value_task(void* arg)
{
uint32_t io_num;
for(;;) {
//读取最新的gpio_evt_queue消息
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
printf("GPIO[%"PRIu32"] intr, val: %d\n", io_num, gpio_get_level(io_num));
}
}
}
void app_main(void)
{
gpio_config_t io_conf = {};
//配置LED
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
io_conf.pull_down_en = 0;
io_conf.pull_up_en = 0;
gpio_config(&io_conf);
//配置按键
//下降沿中断
io_conf.intr_type = GPIO_INTR_NEGEDGE;
//设置GPIO0的输入寄存器
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
//输入模式
io_conf.mode = GPIO_MODE_INPUT;
//使能上拉模式
io_conf.pull_up_en = 1;
io_conf.pull_down_en = 0;
gpio_config(&io_conf);
//创建一个队列来处理来自isr的gpio事件
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
//注册中断服务
gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
//设置GPIO的中断服务函数
gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, (void*) GPIO_NUM_0);
//使能GPIO模块中断信号
gpio_intr_enable(GPIO_NUM_0);
//创建一个按键检测任务
xTaskCreate(gpio_get_key_value_task, //任务函数
"gpio_get_key_value_task", //任务名字
2048, //任务堆栈
NULL, //传递给任务函数的参数
10, //任务优先级
NULL //任务句柄
);
int cnt = 0;
while(1) {
printf("cnt: %d\n", cnt++);
vTaskDelay(1000 / portTICK_PERIOD_MS);
gpio_set_level(GPIO_NUM_48, cnt % 2);
}
}
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
以上示例中用到了一些FreeRTOS的知识。FreeRTOS是一个小型,轻量级的实时操作系统(RTOS)。它的设计目标是简单和易用。它提供简单的线程、互斥、信号量和计时器功能,支持多种不同的编程模型,包括预先设定的优先级调度和时间片轮询调度。
特性:
- 小型的内核,使其适合用于嵌入式系统和微控制器。
- 提供了优先级调度实时多任务处理能力。
- 互斥量,信号量,队列以及定时器等丰富的内核对象来帮助处理任务之间的同步和通信。
- 有很详细的在线文档以及大量的样例代码来帮助理解和快速上手。
其具体实现的细节和使用方式可以参考其官方文档或许多流行嵌入式系统开发手册中的相关章节。
总的来说,FreeRTOS 是一个用于嵌入式系统实时任务处理的优秀操作系统,小型易用,并且非常灵活。
上面的示例代码里主要使用了以下几个FreeRTOS相关的函数:
xQueueCreate()
: 创建一个新的队列。参数一定量的可用存储空间和存储空间中每个项的大小(以字节数为单位)xQueueSendFromISR()
: 在中断例程中将一个项目发送到队列。它接受要发送的队列,元素的指针以及用以接收任务是否需要切换的指针。xQueueReceive()
: 从队列中接收一个项目。这个函数从gpio_evt_queue
队列中接收了io_num
。xTaskCreate()
: 创建一个新的任务并且把它添加到就绪任务的清单中。vTaskDelay()
: 让调用它的任务在给定的时间里延迟。 这些都是 FreeRTOS 中常用的函数及其用途,它们负责任务调度、中断服务、以及内存管理等。