03. 外部中断
一、外部中断简介
外部中断属于硬件中断,由微控制器外部事件触发。微控制器的特定引脚被设计为对特定事件(如按钮按压、传感器信号变化等)作出响应,这些引脚通常称为 “外部中断引脚”。一旦外部中断事件发生,当前程序执行将立即暂停,并跳转到相应的中断服务程序(ISR)进行处理。处理完毕后,程序会恢复执行,从被中断的地方继续。
ESP32 S3 的外部中断具备两种触摸类型:电平触发 和 边沿触发。
- 电平触发:高、低电平触发,要求保持中断的电平状态直到 CPU 响应。
- 边沿触发:上升沿和下降沿触发,这类型的中断一旦触发,CPU 即可响应。
ESP32 S3 支持 六级中断,同时支持中断嵌套,也就是优先级中断可以被高优先级中断打断。优先级的数字越大表明该中断的优先级越高。其中,NMI 中断拥有最高优先级,此类中断已经触发,CPU 必须处理。
在 ESP32 S3 中,中断系统被用于响应各种内部和外部事件。这些中断按照其触发方式和优先级进行分类。
- 中断号:每个中断的唯一标识符,用于在程序中引用和配置特定的中断。
- 类别:中断的来源类型,分为 外部中断 和 内部中断。外部中断 由 外部设备 或 信号触发,如按键、传感器等;内部中断 则由 微控制器内部的硬件事件触发,如定时器溢出、软件中断等。
- 种类:中断的触发方式,包括 电平触发 和 边沿触发。电平触发 是在 输入信号达到特定电平(如高电平或低电平)时触发中断;边沿触发 则是在 输入信号从一种电平状态变化到另一种状态时触发中断。
- 优先级:中断的响应优先级。当多个中断同时发生时,微控制器会根据中断的优先级来决定先处理哪个中断。较高的优先级意味着中断将优先得到处理。
二、外部中断常用函数
2.1、配置GPIO函数
我们可以使用 gpio_config()
函数 配置 GPIO 的模式、上下拉等功能,其函数原型如下所示:
/**
* @brief 设置GPIO配置
*
* @param pGPIOConfig GPIO配置结构体的指针
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig);
形参 pGPIOConfig
为 GPIO 配置结构体指针,它的主要成员如下:
typedef struct
{
uint64_t pin_bit_mask; // 配置引脚位
gpio_mode_t mode; // 设置引脚模式
gpio_pullup_t pull_up_en; // 设置上拉
gpio_pulldown_t pull_down_en; // 设置下拉
gpio_int_type_t intr_type; // 中断配置
} gpio_config_t;
成员 pin_bit_mask
用来 设置的引脚位,我们可以填写的参数格式如下:(1 << x)
,其中 x 为 ESP32 S3 中可用 GPIO。比如我们使用 IO1 引脚,则可以写为:(1ull << GPIO_NUM_1)
。
成员 mode
用来 设置引脚模式,它的可选值如下:
typedef enum
{
GPIO_MODE_DISABLE = GPIO_MODE_DEF_DISABLE, // 失能输入输出模式
GPIO_MODE_INPUT = GPIO_MODE_DEF_INPUT, // 输入模式
GPIO_MODE_OUTPUT = GPIO_MODE_DEF_OUTPUT, // 输出模式
GPIO_MODE_OUTPUT_OD = ((GPIO_MODE_DEF_OUTPUT) | (GPIO_MODE_DEF_OD)), // 输出开漏输出模式
GPIO_MODE_INPUT_OUTPUT_OD = ((GPIO_MODE_DEF_INPUT) | (GPIO_MODE_DEF_OUTPUT) | (GPIO_MODE_DEF_OD)), // 输入输出开漏模式
GPIO_MODE_INPUT_OUTPUT = ((GPIO_MODE_DEF_INPUT) | (GPIO_MODE_DEF_OUTPUT)), // 输入输出模式
} gpio_mode_t;
成员 pull_up_en
用来 配置上拉,它的可选值如下:
typedef enum
{
GPIO_PULLUP_DISABLE = 0x0, // 失能上拉
GPIO_PULLUP_ENABLE = 0x1, // 使能上拉
} gpio_pullup_t;
成员 pull_down_en
用来 配置下拉,它的可选值如下:
typedef enum
{
GPIO_PULLDOWN_DISABLE = 0x0, // 失能下拉
GPIO_PULLDOWN_ENABLE = 0x1, // 使能下拉
} gpio_pulldown_t;
成员 intr_type
用来 配置中断,它的可选值如下:
typedef enum
{
GPIO_INTR_DISABLE = 0, // 失能中断
GPIO_INTR_POSEDGE = 1, // 上升沿
GPIO_INTR_NEGEDGE = 2, // 下降沿
GPIO_INTR_ANYEDGE = 3, // 上升沿和下降沿
GPIO_INTR_LOW_LEVEL = 4, // 输入低电平触发
GPIO_INTR_HIGH_LEVEL = 5, // 输入高电平触发
GPIO_INTR_MAX,
} gpio_int_type_t;
该函数返回 ESP_OK
表示 配置成功,返回 ESP_FAIL
表示 配置失败。
#define ESP_OK 0 // 配置成功
#define ESP_FAIL -1 // 配置失败
2.2、注册中断函数
我们可以使用 gpio_install_isr_service()
函数用来 注册中断服务,它的函数原型如下:
/**
* @brief 注册中断函数
*
* @param intr_alloc_flags 分配中断的标志
* @return esp_err_t ESP_OK注册成功,其它注册失败
*/
esp_err_t gpio_install_isr_service(int intr_alloc_flags);
形参 intr_alloc_flags
是 中断标志位,可选值如下:
#define ESP_INTR_FLAG_LEVEL1 (1<<1) // 使用 Level 1 中断级别。在中断服务程序执行期间禁用同级别的中断
#define ESP_INTR_FLAG_LEVEL2 (1<<2) // 使用 Level 2 中断级别。在中断服务程序执行期间禁用同级别和 Level 1 的中断
#define ESP_INTR_FLAG_LEVEL3 (1<<3) // 使用 Level 3 中断级别。
#define ESP_INTR_FLAG_LEVEL4 (1<<4) // 使用 Level 4 中断级别。
#define ESP_INTR_FLAG_LEVEL5 (1<<5) // 使用 Level 5 中断级别。
#define ESP_INTR_FLAG_LEVEL6 (1<<6) // 使用 Level 6 中断级别。
#define ESP_INTR_FLAG_NMI (1<<7) // 使用 Level 7 中断级别。
#define ESP_INTR_FLAG_SHARED (1<<8) // 中断可以在ISR之间共享
#define ESP_INTR_FLAG_EDGE (1<<9) // 使用边沿触发方式
#define ESP_INTR_FLAG_IRAM (1<<10) // 如果缓存被禁用,可以调用ISR
#define ESP_INTR_FLAG_INTRDISABLED (1<<11) // 禁用此中断后返回
#define ESP_INTR_FLAG_LOWMED (ESP_INTR_FLAG_LEVEL1|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3) // 使用中低水平触发方式
#define ESP_INTR_FLAG_HIGH (ESP_INTR_FLAG_LEVEL4|ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_LEVEL6|ESP_INTR_FLAG_NMI) // 使用高电平触发方式
2.3、设置中断服务函数
我们可以使用 gpio_isr_handler_add()
设置中断服务函数,它的函数原型如下:
/**
* @brief 设置中断服务函数
*
* @param gpio_num GPIO引脚编号
* @param isr_handler 外部中断的回调函数
* @param args 外部中断的回调函数的传入参数
* @return esp_err_t ESP_OK设置成功,其它设置失败
*/
esp_err_t gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void *args);
形参 gpio_num
为 GPIO 引脚号。该参数在 gpio_types.h
文件中枚举 gpio_num_t
有定义。
typedef enum
{
GPIO_NUM_NC = -1, // Use to signal not connected to S/W
GPIO_NUM_0 = 0, // GPIO0, input and output
GPIO_NUM_1 = 1, // GPIO1, input and output
GPIO_NUM_2 = 2, // GPIO2, input and output
GPIO_NUM_3 = 3, // GPIO3, input and output
GPIO_NUM_4 = 4, // GPIO4, input and output
GPIO_NUM_5 = 5, // GPIO5, input and output
GPIO_NUM_6 = 6, // GPIO6, input and output
GPIO_NUM_7 = 7, // GPIO7, input and output
GPIO_NUM_8 = 8, // GPIO8, input and output
GPIO_NUM_9 = 9, // GPIO9, input and output
GPIO_NUM_10 = 10, // GPIO10, input and output
GPIO_NUM_11 = 11, // GPIO11, input and output
GPIO_NUM_12 = 12, // GPIO12, input and output
GPIO_NUM_13 = 13, // GPIO13, input and output
GPIO_NUM_14 = 14, // GPIO14, input and output
GPIO_NUM_15 = 15, // GPIO15, input and output
GPIO_NUM_16 = 16, // GPIO16, input and output
GPIO_NUM_17 = 17, // GPIO17, input and output
GPIO_NUM_18 = 18, // GPIO18, input and output
GPIO_NUM_19 = 19, // GPIO19, input and output
GPIO_NUM_20 = 20, // GPIO20, input and output
GPIO_NUM_21 = 21, // GPIO21, input and output
GPIO_NUM_26 = 26, // GPIO26, input and output
GPIO_NUM_27 = 27, // GPIO27, input and output
GPIO_NUM_28 = 28, // GPIO28, input and output
GPIO_NUM_29 = 29, // GPIO29, input and output
GPIO_NUM_30 = 30, // GPIO30, input and output
GPIO_NUM_31 = 31, // GPIO31, input and output
GPIO_NUM_32 = 32, // GPIO32, input and output
GPIO_NUM_33 = 33, // GPIO33, input and output
GPIO_NUM_34 = 34, // GPIO34, input and output
GPIO_NUM_35 = 35, // GPIO35, input and output
GPIO_NUM_36 = 36, // GPIO36, input and output
GPIO_NUM_37 = 37, // GPIO37, input and output
GPIO_NUM_38 = 38, // GPIO38, input and output
GPIO_NUM_39 = 39, // GPIO39, input and output
GPIO_NUM_40 = 40, // GPIO40, input and output
GPIO_NUM_41 = 41, // GPIO41, input and output
GPIO_NUM_42 = 42, // GPIO42, input and output
GPIO_NUM_43 = 43, // GPIO43, input and output
GPIO_NUM_44 = 44, // GPIO44, input and output
GPIO_NUM_45 = 45, // GPIO45, input and output
GPIO_NUM_46 = 46, // GPIO46, input and output
GPIO_NUM_47 = 47, // GPIO47, input and output
GPIO_NUM_48 = 48, // GPIO48, input and output
GPIO_NUM_MAX,
} gpio_num_t;
形参 isr_handler
指向中断处理函数的函数指针。中断处理函数是一个用户定义的回调函数,将在中断发生时执行。
形参 args
用来 传递给中断处理程序的参数。这是一个指向用户特定数据的指针,可以在中断处理程序中使用。
在设置中断服务函数后,我们需要实现一个中断服务程序的回调函数,在该函数中处理中断响应。中断处理函数需要声明为 IRAM_ATTR
(其中函数名可以随意起名,但是要符合 C 语言标准),以确保其运行在内存中的可执行区域。
// IRAM_ATTR 属性用于将中断处理函数存储在内部 RAM 中,目的在于减少延迟
void IRAM_ATTR gpio_isr_handler(void* arg)
{
// 处理中断响应
}
2.4、开启外部中断函数
我们可以使用 gpio_intr_enable()
函数 开启外部中断,它的函数原型如下:
/**
* @brief 开启外部中断
*
* @param gpio_num GPIO引脚编号
* @return esp_err_t ESP_OK开启成功,其它开始失败
*/
esp_err_t gpio_intr_enable(gpio_num_t gpio_num);
在使用
gpio_intr_enable()
函数之前,开发者需要先通过gpio_install_isr_service()
函数和gpio_isr_handler_add()
函数来安装和注册中断处理程序。
三、实验例程
这里,我们在【components】文件夹下新建一个【peripheral】文件夹,然后再在【peripheral】文件夹下新建一个【inc】文件夹(用来存放头文件)和【src】文件夹(用来存放源文件)。
新建一个 bsp_exit.h
头文件,用来存放变量和函数的声明。
#ifndef __BSP_EXIT_H__
#define __BSP_EXIT_H__
#include "driver/gpio.h"
#include "esp_system.h"
void bsp_exit_init(gpio_num_t gpio_num, uint8_t trigger_edge);
#endif // !__BSP_EXIT_H__
新建一个 bsp_exit.c
源文件,用来存放变量和函数的定义。
#include "bsp_exit.h"
static void bsp_exit_gpio_isr_handler(void *arg);
/**
* @brief 外部中断初始化
*
* @param gpio_num GPIO引脚
* @param trigger_edge 触发边沿
*/
void bsp_exit_init(gpio_num_t gpio_num, uint8_t trigger_edge)
{
gpio_config_t gpio_config_struct = {0};
gpio_config_struct.pin_bit_mask = 1ULL << gpio_num; // 设置引脚
gpio_config_struct.mode = GPIO_MODE_INPUT; // 输入模式
if (trigger_edge)
{
gpio_config_struct.pull_down_en = GPIO_PULLDOWN_ENABLE; // 使用下拉
gpio_config_struct.pull_up_en = GPIO_PULLUP_DISABLE; // 不使用上拉
gpio_config_struct.intr_type = GPIO_INTR_POSEDGE; // 上升沿触发
}
else
{
gpio_config_struct.pull_up_en = GPIO_PULLUP_ENABLE; // 使用上拉
gpio_config_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; // 不使用下拉
gpio_config_struct.intr_type = GPIO_INTR_NEGEDGE; // 下降沿触发
}
gpio_config(&gpio_config_struct); // 配置GPIO
gpio_install_isr_service(ESP_INTR_FLAG_EDGE); // 注册中断服务函数
gpio_isr_handler_add(gpio_num, bsp_exit_gpio_isr_handler, (void *)gpio_num);// 设置GPIO的中断回调函数
gpio_intr_enable(gpio_num); // 使能GPIO模块中断信号
}
/**
* @brief 外部中断服务函数
*
* @param arg 传入的参数
*/
static void IRAM_ATTR bsp_exit_gpio_isr_handler(void *arg)
{
gpio_num_t gpio_num = (gpio_num_t) arg;
if (gpio_num == GPIO_NUM_2)
{
gpio_set_level(GPIO_NUM_1, !gpio_get_level(GPIO_NUM_1)); // 设置GPIO电平
}
}
在【components】文件夹下的【peripheral】文件夹下新建一个 CMakeLists.txt
文件。
# 源文件路径
set(src_dirs src)
# 头文件路径
set(include_dirs inc)
# 设置依赖库
set(requires driver)
# 注册组件到构建系统的函数
idf_component_register(
# 源文件路径
SRC_DIRS ${src_dirs}
# 自定义头文件的路径
INCLUDE_DIRS ${include_dirs}
# 依赖库的路径
REQUIRES ${requires}
)
# 设置特定组件编译选项的函数
# -ffast-math: 允许编译器进行某些可能减少数学运算精度的优化,以提高性能。
# -O3: 这是一个优化级别选项,指示编译器尽可能地进行高级优化以生成更高效的代码。
# -Wno-error=format: 这将编译器关于格式字符串不匹配的警告从错误降级为警告。
# -Wno-format: 这将完全禁用关于格式字符串的警告。
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
修改【main】文件夹下的 main.c
文件。
#include "freertos/FreeRTOS.h"
#include "bsp_exit.h"
#include "led.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
led_init(GPIO_NUM_1);
bsp_exit_init(GPIO_NUM_2, 0);
while (1)
{
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(10));
}
}