在上节中,我们满足了简单的中断函数编写与中断引脚配置。但还记得上一节中留下的一点小疑问吗?能不能直接把输出信息的代码放进中断处理函数?这样做会导致什么样的后果?
接下来,我们来试验一下。
一、将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
- 阻塞API:调用后会让当前任务放弃 CPU 使用权,进入「阻塞状态」,直到满足条件(如超时、信号量可用)才返回,依赖 FreeRTOS 调度器。
- 耗时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)设计的"释放二进制信号量"的函数,它的作用有两个:
- 把gpio_interrupt_sem指向的二进制信号量从 "不可用(计数值 0)" 改为 "可用(计数值 1)",让阻塞等待这个信号量的任务能被唤醒;
- 通过第二个参数&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、创建任务。我们的代码就算是完成了。
这里有一个流程图,大家可以参考一下:

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

