中断,是嵌入式中的一个重要概念,是一种由硬件设备或软件程序主动触发的、异步的事件通知机制。

简单做一个类比:

  1. 你坐在办公室里按部就班写工作报告(对应:CPU 按顺序执行普通任务 / 程序,比如 ESP32 中app_main里的循环、普通任务)。
  2. 突然,你的办公电话响了(对应:硬件 / 软件触发了中断,比如 ESP32 中 GPIO11 电平下降、串口收到数据)。
  3. 立刻放下手中的报告,暂停当前的写作进度(对应:CPU 暂停当前正在执行的任务,保存当前执行状态)。
  4. 拿起电话接打,处理这个紧急事情(对应:CPU 跳转到预先定义的「中断服务程序(ISR)」,执行中断处理逻辑)。
  5. 电话挂断后,你回到办公桌前,从刚才暂停的地方继续写报告(对应:ISR 执行完成,CPU 恢复之前暂停的任务状态,继续执行原程序)。

这个场景中,“电话响” 就是中断请求,“接电话” 就是中断处理,整个过程的核心就是「暂停当前任务→处理紧急事件→恢复原任务」。

那么,在ESP32-S3中,要如何完成这一过程呢?

一、新建工程

新建一个名为“Interrupt”的工程,这里不再赘述。

二、添加头文件

分别完善CMake.txt与main.c文件:

idf_component_register(SRCS "main.c"
                    PRIV_REQUIRES driver freertos
                    INCLUDE_DIRS ".")
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
 
void app_main(void)
{
 
}

编译确认无误。

三、初始化中断引脚

在ESP32-S3中,中断是由ISR统一调配。由于大部分中断需要通过引脚的电平改变来触发,所以我们需要先初始化一个中断引脚:

#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void gpio_init(void)
{
    gpio_config_t io_config = {
        .pin_bit_mask = (1ULL << GPIO_NUM_11),  
        .mode = GPIO_MODE_INPUT,                
        .pull_up_en = GPIO_PULLUP_ENABLE,       
        .pull_down_en = GPIO_PULLDOWN_DISABLE,  
        .intr_type = GPIO_INTR_NEGEDGE,         
    };
    gpio_config(&io_config);  

    gpio_install_isr_service(0); 
}

void app_main(void)
{
    gpio_init();
}

io_config中,我们设置了一个GPIO11为输入模式,同时使能了它的上拉电阻,禁用了它的下拉电阻,这部分与前面相同。

不同在最后一行,.inter_type此时不是DISABLE,而是GPIO_INTR_NEGEDGE(下降沿触发)。这一栏的可选参数有:

  • GPIO_INTR_DISABLE(禁用中断)

  • GPIO_INTR_POSEDGE(上升沿触发) 

  • GPIO_INTR_NEGEDGE(下降沿触发)

  • GPIO_INTR_ANYEDGE(任意边沿触发)

  • GPIO_INTR_LOW_LEVEL(低电平触发)

  • GPIO_INTR_HIGH_LEVEL(高电平触发)

什么是上升沿和下降沿?

简单来说,电平的变化是有一个过程的,由低到高的过程被称为上升沿,由高到低的过程被成为下降沿。

这里,我们的按键是连接GPIO11和GND的,所以当按键按下时,将产生一个下降沿,按键松开时,将产生一个上升沿。

这里,我们选择下降沿触发。

gpio_install_isr_service函数是ESP32的全局中断初始化函数,只需要知道在整个程序中,要使用中断就必须执行一次这个代码即可。这里我们参数填0,代表使用默认配置。

四、编写业务代码

#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

volatile bool gpio_interrupt_flag = false;

void IRAM_ATTR gpio_isr_handler(void* arg)
{
    gpio_interrupt_flag = true;
}

void gpio_init(void)
{
    gpio_config_t io_config = {
        .pin_bit_mask = (1ULL << GPIO_NUM_11),  
        .mode = GPIO_MODE_INPUT,                
        .pull_up_en = GPIO_PULLUP_ENABLE,       
        .pull_down_en = GPIO_PULLDOWN_DISABLE,  
        .intr_type = GPIO_INTR_NEGEDGE,         
    };
    gpio_config(&io_config);  

    gpio_install_isr_service(0); 
    gpio_isr_handler_add(GPIO_NUM_11, gpio_isr_handler, NULL);  
}

void app_main(void)
{

    gpio_init();

    while(1)
    {
        if(gpio_interrupt_flag == true)
        {
            printf("基础中断触发!(GPIO%d)\n", GPIO_NUM_11);
            
            gpio_interrupt_flag = false;
            
            vTaskDelay(pdMS_TO_TICKS(200));
        }
        
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

gpio_isr_handler_add函数,作用是将初始化好的GPIO中断引脚绑定一个中断处理函数,当满足中断条件时,CPU中断当前任务,进入中断处理函数进行处理。它需要三个参数:

  1. 已经被正确初始化的中断引脚,这里我们填GPIO11。
  2. 编写好的中断服务函数,函数格式必须符合void (*gpio_isr_t)(void*)(即无返回值、参数为void*),且建议用IRAM_ATTR修饰。

  3. 传递给 ISR 函数的自定义参数(可选),一般用来区分多个GPIO引脚触发同一个中断服务函数的,若无需传递参数,填NULL即可。

需要注意的是,该函数注册的是「专属 ISR」—— 一个 ISR 函数可以绑定多个 GPIO 引脚(共享),但一个 GPIO 引脚只能绑定一个 ISR 函数(专属)。

接下来,我们看终端服务函数。使用IRAM_ATTR进行修饰,强制编译到内部快速 RAM,保证中断微秒级快速响应,避免 Flash 读取延迟导致中断丢失。

在这里,我们定义了一个全局变量叫做gpio_interrupt_flag,用于标记中断是否被触发。使用volatile进行修饰,告诉编译器该变量可能在ISR 中被修改,禁止编译器优化该变量的读取(避免普通代码读取到缓存的旧值)。

在主函数中,我们通过轮询标志位,当检测到标志位发生改变,即输出一条信息。

下面是当前代码的程序执行流程图:

五、编译烧录,查看现象

编译烧录成功后,点击底部栏打开监视器,初次打开会输出一大堆绿色的东西,这是ESP32的初始化信息,此时我们按下按钮,可以看到正常输出了一句话:

六、一些小疑问

是的,正如我们最开始举的例子,我们希望在电话打进来之前,可以做一些自己的工作。但是目前的主函数,每循环一次就要读取一次标志位的值,这样会极大的浪费性能。能不能直接把输出信息的代码放进中断处理函数这样做会导致什么样的后果

且听下回分解。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐