ESP32-S3开发教程五-按键中断2(使用FreeRTOS)

在上节中,我们满足了简单的中断函数编写与中断引脚配置。但还记得上一节中留下的一点小疑问吗?能不能直接把输出信息的代码放进中断处理函数?这样做会导致什么样的后果?

接下来,我们来试验一下。

一、将printf()直接移植到中断函数里:

cpp 复制代码
#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)
{
    printf("基础中断触发!(GPIO%d)\n", GPIO_NUM_11);
}

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();
}

这样的写法,在编译器里是不会报错的,甚至编译烧录都不会出现任何异常,但当我们按下按钮查看监视器时,会发现,芯片并没有正常输出,而是按钮按下一次就重启一次:

这是为什么呢?

这是因为,printf()本身属于**"阻塞 / 耗时 API"**,咱们一个一个解释。

二、阻塞/耗时API

  1. 阻塞API:调用后会让当前任务放弃 CPU 使用权,进入「阻塞状态」,直到满足条件(如超时、信号量可用)才返回,依赖 FreeRTOS 调度器。
  2. 耗时API:执行时间长且不确定(毫秒级及以上),会占用 CPU 大量时间,导致中断响应超时或其他紧急事件被阻塞。

这两类API,都是不能直接用在中断函数里的,否则就会出现刚才一直重启的情况。

这里列举了大部分常用的阻塞/耗时API:

2.1 FreeRTOS 核心阻塞 API

2.2 ESP32 外设驱动耗时 / 阻塞 API

2.3 系统 / 文件操作耗时 API

三、正确做法

了解了为什么不能直接将printf()函数放入中断函数中,现在我们重新编写一段逻辑代码,使其能完成我们的要求:

cpp 复制代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

SemaphoreHandle_t gpio_interrupt_sem = NULL;

void IRAM_ATTR gpio_isr_handler(void* arg)
{
    // 给信号量发信号,通知任务处理(ISR版本的信号量API)
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(gpio_interrupt_sem, &xHigherPriorityTaskWoken);
    
    // 如果需要切换任务,触发上下文切换
    if(xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

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 interrupt_process_task(void *arg)
{
    while(1) {
        // 等待信号量(阻塞直到中断触发)
        if(xSemaphoreTake(gpio_interrupt_sem, portMAX_DELAY) == pdTRUE) {
            // 安全打印中断触发信息
            printf("GPIO interrupt triggered! (GPIO%d)\n", GPIO_NUM_11);
            
            // 可选:添加短延时,避免按钮抖动导致多次触发
            vTaskDelay(pdMS_TO_TICKS(200));
        }
    }
}

void app_main(void)
{
    gpio_interrupt_sem = xSemaphoreCreateBinary();
    gpio_init();
    xTaskCreate(
        interrupt_process_task,   // 任务函数
        "interrupt_process",      // 任务名
        2048,                     // 栈大小
        NULL,                     // 任务参数
        1,                        // 优先级
        NULL                      // 任务句柄
    );
}

在这里,我们使用了FreeRTOS中的一个概念:信号量。何为信号?就是一个让不同任务之间相互通讯的信息。

首先来看这一句:

cpp 复制代码
SemaphoreHandle_t gpio_interrupt_sem = NULL;

此处,我们定义了一个信号量的"句柄",初始化为NULL。

接下来看到app_main()中,我们将信号明确为二进制信号量:

cpp 复制代码
gpio_interrupt_sem = xSemaphoreCreateBinary();

二进制信号量只有两种状态:可用(有信号)和不可用(无信号)。

接下来,来看新的中断函数:

cpp 复制代码
void IRAM_ATTR gpio_isr_handler(void* arg)
{
    // 给信号量发信号,通知任务处理(ISR版本的信号量API)
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(gpio_interrupt_sem, &xHigherPriorityTaskWoken);
    
    // 如果需要切换任务,触发上下文切换
    if(xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

首先,我们用BaseType_t定义了一个变量,用于标记是否因释放信号量唤醒了更高优先级的任务,若为pdTRUE,则触发上下文切换,让中断处理任务立即执行。这里我们不需要,所以给pdFALSE就可以了。

xSemaphoreGiveFromISR()函数是FreeRTOS专为中断服务程序(ISR)设计的"释放二进制信号量"的函数,它的作用有两个:

  1. 把gpio_interrupt_sem指向的二进制信号量从 "不可用(计数值 0)" 改为 "可用(计数值 1)",让阻塞等待这个信号量的任务能被唤醒;
  2. 通过第二个参数&xHigherPriorityTaskWoken,让函数自动标记 "是否因为释放信号量,唤醒了一个比当前 CPU 正在执行的任务优先级更高的任务",为后续快速切换任务做准备。

执行完这个函数后,gpio_interrupt_sem指向的二进制信号量从"不可用(计数值 0)" 改为 "可用(计数值 1)",让阻塞等待这个信号量的任务能被唤醒。

最后的if判断是用来判断是否有优先级更高的任务被唤醒,用来切换上下文的。这里我们不需要。

接着来看中断处理任务:

cpp 复制代码
void interrupt_process_task(void *arg)
{
    while(1) {
        // 等待信号量(阻塞直到中断触发)
        if(xSemaphoreTake(gpio_interrupt_sem, portMAX_DELAY) == pdTRUE) {
            // 安全打印中断触发信息
            printf("GPIO interrupt triggered! (GPIO%d)\n", GPIO_NUM_11);
            
            // 可选:添加短延时,避免按钮抖动导致多次触发
            vTaskDelay(pdMS_TO_TICKS(200));
        }
    }
}

我们在这里写了一个死循环,用于一直等待信号量。

xSemaphoreTake()函数是用来等待信号量发生变化的。第一个参数表明等待的是句柄为gpio_interrupt_sem的信号量,第二个参数表面此任务会无线等待,永不超时。

在这里补充一下其他常用值:

  • pdMS_TO_TICKS(1000):等待 1 秒(1000 毫秒),超时后返回pdFALSE,任务继续执行(不会卡死);
  • 0:不阻塞,立即检查信号量 ------ 有则获取(返回 pdTRUE),无则直接返回 pdFALSE,任务不等待。
  • portMAX_DELAY:是 FreeRTOS 定义的宏(值为0xffffffff),表示「无限阻塞等待」------ 任务会一直等,直到获取到信号量为止,永不超时。

此函数只会有两个可能的返回值,一个是pdTRUE,成功获取信号量,证明信号量是1(可用状态),执行完成后,此函数会把信号量变为0(不可用)。另一个情况是pdFALSE,获取失败,可能原因有超时或信号量无效。

这里的判断,当信号量从无效变为有效时,执行if里面的语句,同时将信号量变为无效。

在if里,执行我们原来的打印和延时。

最后在主函数中,创建二值信号量、初始化GPIO、创建任务。我们的代码就算是完成了。

这里有一个流程图,大家可以参考一下:

四、查看现象

编译烧录后,打开串口监视器,可以看到每按下一次按钮,正常输出一句文本:

相关推荐
天天爱吃肉82183 小时前
新能源汽车多测试设备联调与多物理信息融合测试方法及数据价值挖掘
人工智能·嵌入式硬件·机器学习·汽车
小吴同学啊11 小时前
基于N32G457QEL7软件开发的基础准备
单片机·n32g457·国名技术单片机
Struggle to dream12 小时前
STM32对于中断的简单理解
stm32·单片机·嵌入式硬件
来自晴朗的明天15 小时前
23、MCU 上电复位(POR)电路
单片机·嵌入式硬件·硬件工程
戏舟的嵌入式开源笔记16 小时前
基于ESP32(PIO+Arduino)简单上手LVGL9
esp32·嵌入式软件
上海合宙LuatOS16 小时前
LuatOS核心库API——【fft 】 快速傅里叶变换
java·前端·人工智能·单片机·嵌入式硬件·物联网·机器学习
嵌入式科普18 小时前
一、为什么RA6T2是数字电源与伺服的理想MCU
单片机·瑞萨·数字电源·ra6t2
小灰灰搞电子20 小时前
ESP32 使用ESP-IDF 建立WiFi热点(AP模式)并使用TCP客户端通信源码分享
tcp/ip·esp32·esp-idf
余生皆假期-1 天前
无感观测的锁相环 (PLL) 原理与实现方式
单片机·嵌入式硬件