ESP-C3入门24. 使用中断监控GPIO口
- 一、ESP IDF中断概念
- 1. 一些关键概念:
- 2. GPIO 触发方式
- 二、ESP32 IDF 使用中断来处理 GPIO 输入的步骤
- 1. 引入 ESP32 IDF 头文件:
- 2. 配置 GPIO 引脚:
- 3. 定义中断处理函数:
- 4. 初始化 GPIO 引脚
- 5. 处理中断事件:
- 6. 清除中断:
- 三、中断与任务间传值
- 1. 消息队列
- 2. 函数 `xQueueSendFromISR`
- 四、完整示例
一、ESP IDF中断概念
在 ESP32 IDF(Espressif IoT Development Framework)中,中断是一种用于异步事件处理的重要机制。中断允许微控制器或微处理器在执行主线程代码的同时响应外部事件,而不必等待事件的发生。ESP32 IDF 使用中断来处理各种事件,包括外部硬件触发的事件、定时器事件、通信事件等。
1. 一些关键概念:
-
中断源:
- 中断源是指可以触发中断的事件或条件,例如外部硬件引脚的状态变化、定时器溢出、外部设备的通信完成等。
- ESP32 支持多个不同的中断源,每个中断源都有一个唯一的标识符。
-
中断处理函数:
- 中断处理函数是一个特殊的函数,用于处理特定中断源触发的事件。每个中断源通常都有一个与之关联的中断处理函数。
- 中断处理函数是独立于主程序的,当中断发生时,控制权会立即转移到相应的中断处理函数中执行。
- 在 ESP32 IDF 中,中断处理函数通常是一个带有
IRAM_ATTR
属性修饰符的函数,以确保它存储在内部 RAM 中,以减少延迟。
-
中断控制器:
- ESP32 中有一个中断控制器(Interrupt Controller),负责管理和分发中断请求。它可以将中断请求分派给相应的中断处理函数。
- 中断控制器允许启用或禁用特定的中断源,以及设置中断优先级。
-
中断优先级:
- 中断可以有不同的优先级,允许指定哪个中断应该在发生多个中断时首先得到处理。
- ESP32 支持优先级抢占,即当一个高优先级的中断正在执行时,可以打断低优先级的中断处理函数。
-
中断屏蔽:
- 中断屏蔽是一种机制,可以在需要时禁止或启用中断。这对于防止在关键代码部分中断发生非常有用。
- ESP32 具有中断屏蔽和解除中断屏蔽的指令,允许在代码中显式地控制中断的启用和禁用。
-
中断服务例程:
- 中断服务例程(Interrupt Service Routine,ISR)是指中断处理函数的一般术语,它是用于响应中断的特殊函数。
-
中断处理流程:
- 当中断源触发时,ESP32 中断控制器将控制权转移到相应的中断处理函数。
- 中断处理函数执行完毕后,控制权返回到主程序。
- 可以在中断处理函数中执行与中断源相关的操作,例如读取传感器数据、处理通信数据、更新定时器计数器等。
2. GPIO 触发方式
函数 gpio_set_intr_type(GPIO_PIN, gpio_int_type_t);
用来设置触发方式, gpio_int_type_t是个枚举值,有以下常量:
- GPIO_INTR_POSEDGE:上升沿触发,电平由低变高时触发中断。
- GPIO_INTR_NEGEDGE:下降沿触发,电平由高变低时触发中断。
- GPIO_INTR_ANYEDGE:任何边沿触发,电平变化时都触发中断。
- GPIO_INTR_LOW_LEVEL:低电平触发,只有当电平为低电平时触发中断。
- GPIO_INTR_HIGH_LEVEL:高电平触发,只有当电平为高电平时触发中断。
二、ESP32 IDF 使用中断来处理 GPIO 输入的步骤
1. 引入 ESP32 IDF 头文件:
这些头文件包含了中断和 GPIO 相关的定义和函数。
#include "freertos/FreeRTOS.h"#include "freertos/task.h"#include "driver/gpio.h"
2. 配置 GPIO 引脚:
在代码中配置要使用的 GPIO 引脚,设置其模式为输入,并为该引脚分配一个中断号。
#define GPIO_PIN GPIO_NUM_2 // 选择要使用的 GPIO 引脚#define GPIO_INTR GPIO_INTR_POSEDGE // 触发中断的边沿(上升沿触发)
3. 定义中断处理函数:
创建一个用于处理 GPIO 中断的函数。该函数应该符合以下形式:
void IRAM_ATTR gpio_isr_handler(void* arg) {// 处理中断事件的代码}
这里的 IRAM_ATTR
属性用于将中断处理函数存储在内部 RAM 中,以减少延迟。
4. 初始化 GPIO 引脚
在 app_main()
函数或其他入口函数中,初始化 GPIO 引脚,将它们配置为输入模式,并设置中断处理程序。你还需要创建一个用于安装中断服务的句柄。
void app_main() {// 配置 GPIO 引脚gpio_pad_select_gpio(GPIO_PIN);gpio_set_direction(GPIO_PIN, GPIO_MODE_INPUT);gpio_set_intr_type(GPIO_PIN, GPIO_INTR);// 创建中断句柄gpio_install_isr_service(0);// 注册中断处理函数gpio_isr_handler_add(GPIO_PIN, gpio_isr_handler, NULL);// 主任务继续执行其他操作while (1) {// 在这里执行其他任务vTaskDelay(1000 / portTICK_RATE_MS);}}
5. 处理中断事件:
在中断处理函数 gpio_isr_handler
中,编写处理 GPIO 中断事件的代码。你可以在函数内部执行你需要的操作,例如读取传感器数据、更新状态等。
6. 清除中断:
当完成中断事件的处理后,你可以清除中断标志以准备接收下一个中断事件。
gpio_intr_enable(GPIO_PIN);
当 GPIO 引脚上的电平发生上升沿(或可以根据配置是下降沿)时,中断处理函数 gpio_isr_handler
将会执行,程序可以在其中执行特定的操作。这使得程序能够实时响应 GPIO 引脚状态的变化,例如传感器的触发、按钮的按下等事件。
中断处理程序应该尽量保持简短和高效,因为中断会在主程序中断执行时立即执行,而且不能执行阻塞操作。
如果需要更复杂的处理,可以使用中断来触发任务,以便在任务中执行耗时的操作。
三、中断与任务间传值
1. 消息队列
在中断处理函数中直接执行业务逻辑,极容易造成程序崩溃。
可以使用队列在中断处理函数、任务之间传递信息,以避免程序可能因为竞态条件而崩溃或产生不可预测的结果。
具体操作是:
-
定义 QueueHandle_t 队列
-
中断处理函数
gpio_isr_handler
中,当 GPIO 中断被触发时,它会将触发中断的 GPIO 引脚号发送到队列gpioEventQueue
中,以便任务能够获取该信息。 -
任务函数
gpio_task
中,通过调用xQueueReceive
函数,任务会从队列gpioEventQueue
中等待并获取触发中断的 GPIO 引脚号。一旦获取到引脚号,任务就会执行相应的操作,例如打印 GPIO 的电平状态。
这种机制确保了中断处理函数和任务之间的同步和通信。
2. 函数 xQueueSendFromISR
该函数作用是在中断中安全地向消息队列传值,其原型是:
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken);
参数:
- xQueue:目标队列的句柄,指示要发送数据的队列。
- pvItemToQueue:要发送的数据的指针。
- pxHigherPriorityTaskWoken:一个指向 BaseType_t 变量的指针,用于指示是否唤醒了一个更高优先级的任务。
返回值:
- 如果成功将数据发送到队列,则返回 pdPASS。
- 如果队列已满或其他错误发生,则返回 errQUEUE_FULL。
四、完整示例
下面的示例获取GPIO口的电平值,在任务处理中加了 50ms 防抖动逻辑。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "esp_log.h"static const char* TAG = "main";
#define GPIO_PIN GPIO_NUM_2 // 选择要使用的 GPIO 引脚// 防抖动延迟时间(以毫秒为单位)
#define DEBOUNCE_DELAY_MS 50static volatile bool gpio_int_triggered = false;
QueueHandle_t xQueue;static void IRAM_ATTR gpio_isr_handler(void* arg) {gpio_int_triggered = true;portYIELD_FROM_ISR(); // 唤醒任务以处理中断
}static void gpio_task(void *arg) {uint32_t ioNum;while (1) {if (gpio_int_triggered) {int value = gpio_get_level(GPIO_PIN);vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_DELAY_MS)); // 防抖动延迟if (gpio_get_level(GPIO_PIN) == value) {ioNum = (uint32_t)GPIO_PIN;gpio_int_triggered = false;int gpio_value = gpio_get_level(ioNum);ESP_LOGI(TAG, "GPIO[%lu] 触发中断,电平:%d", ioNum, gpio_value);}}vTaskDelay(10 / portTICK_PERIOD_MS); // 短暂延迟以降低 CPU 负载}
}void app_main(void) {// 配置 GPIO 引脚gpio_set_direction(GPIO_PIN, GPIO_MODE_INPUT);// 任何边沿触发,电平变化时都触发中断gpio_set_intr_type(GPIO_PIN, GPIO_INTR_ANYEDGE);// 创建中断句柄gpio_install_isr_service(0);// 注册中断处理函数gpio_isr_handler_add(GPIO_PIN, gpio_isr_handler, (void*)GPIO_PIN);// 创建任务来处理 GPIO 事件xTaskCreate(gpio_task, "gpio_task", 2048, NULL, 10, NULL);// 主任务继续执行其他操作while (1) {vTaskDelay(1000 / portTICK_PERIOD_MS);}
}