ESP-IDF教程2 GPIO - 输入、输出和中断
文章目录
- 1、前提
- 1.1、基础知识
- 1.1.1、GPIO 分类
- 1.1.2、FALSH SPI 模式
- 1.1.3、过滤器
- 1.1.4、外部中断
- 1.2、数据结构
- 1.2.1、GPIO
- 1.2.2、毛刺过滤器
- 1.3、硬件原理图
- 2、示例程序
- 2.1、GPIO 输出 - 点亮 LED 灯
- 2.2、GPIO 输入 - 按键响应
- 2.3、GPIO 外部中断 - 按键响应
- 3、常用函数
- 4、烧录验证
1、前提
1.1、基础知识
1.1.1、GPIO 分类
ESP32 系列芯片按照 GPIO 特殊的使用限制分类,可以将其分为如下几类:
- GPIO PIN
- GPI PIN
- Strapping PIN
- SPI FLASH PIN
- 特殊功能引脚
GPIO 引脚
表示通用的输入输出引脚,无使用限制,可以随意使用。
GPI 引脚
表示仅支持输入的引脚,不具备软件使能的上拉或下拉功能,谨慎使用。
Strapping 引脚
用于 ESP32 的启动配置项,ESP32 启动时需要配置如启动模式、内置 LDO 电压、U0TXD 打印使能等参数,这些参数需要芯片在上电或硬件复位时通过 Strapping 管脚采样其电平并锁存来决定,但当采样结束后 Strapping 引脚将恢复为普通 GPIO ,谨慎使用。
SPI FLASH 引脚
因为大部分 ESP32 芯片封装内部无 FLASH 存储,因此需要依赖外部 FLASH 芯片存储程序和数据,SPI FLASH 引脚就是用于 ESP32 FLASH 通信使用的,共有 4-6 个引脚,不要使用这些引脚。
特殊功能引脚
JTAG 和 UART0 这类专用于某一功能的引脚,除非不使用该特殊功能,否则不建议将其作为普通 GPIO 使用
1.1.2、FALSH SPI 模式
根据通信不同阶段使用的引脚数不同,ESP32 的 FALSH SPI 模式有如下四种:
- QIO
- QOUT
- DIO
- DOUT
四种 FLASH SPI 模式的具体区别如 SPI Flash Modes / Summary 所示,其中 QIO 和 QOUT 需要全部的 6 个 SPI FLASH 引脚,而 DIO 和 DOUT 仅需要其中的 4 个引脚,剩下未使用的引脚可做普通 GPIO 使用。
1.1.3、过滤器
ESP32 某些系列芯片支持 GPIO 过滤器,有如下两种:
- 毛刺过滤器
- 管脚毛刺过滤器(pin_glitch_filter)
- 灵活毛刺过滤器(flex_glitch_filter)
- 迟滞过滤器
管脚毛刺过滤器 pin_glitch_filter
仅能将脉冲宽度窄于 2 个采样时钟的信号剔除掉,采样时钟默认为 80MHz 的
SOC_MOD_CLK_APB
宏,因此过滤宽度 = 2 × (1/80 MHz) = 25 ns 。
灵活毛刺过滤器 flex_glitch_filter
会在
window_width_ns
时间窗口内,对信号采样多次,若某次“高”或“低”持续时间小于window_thres_ns
,则当作毛刺丢弃,该功能仅部分型号支持,比如 ESP32-C6、ESP32-H2 和 ESP32-P4 ,笔者尚未测试。
管脚毛刺过滤器和灵活毛刺过滤器是各自独立的,支持为同一 GPIO 同时启用这两种过滤器。
GPIO 迟滞过滤器
“支持输入引脚的硬件迟滞,可以减少由于输入电压在逻辑 0、1 临界值附近时采样不稳定造成的 GPIO 中断误触”,看起来或许可以作为按键消抖使用,该功能仅部分型号支持,比如 ESP32-C5、ESP32-H2 和 ESP32-P4 ,笔者尚未测试。
1.1.4、外部中断
ESP32 的 GPIO 支持如下类型的几种中断:
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;
与 STM32 逻辑开发环境的不同, ESP-IDF 从上到下已经有很多层,如下所示:
- APP
- Components
- HAL
- FreeRTOS
- FLASH BootLoader
- ROM BootLoader
- SoC
某些情况下,ESP32 的 FALSH 访问会被中断或者锁住,为确保中断服务函数能在任何时候可靠运行,要求中断服务函数满足以下两点要求:
- 纯粹运行在内部 RAM(IRAM)中
- 不访问 FLASH 中的数据或代码
使用属性宏 IRAM_ATTR
来告诉编译器,该函数必须放到 IRAM 中执行,外部中断服务函数模板如下:
static void IRAM_ATTR exti_isr_handler(void* arg) {//...
}
printf
函数存储在 FLASH 中,因此不要在中断中使用,如果非要使用请用以下函数代替:
#include <rom/ets_sys.h>int ets_printf(const char *fmt, ...);
此外,不能在中断中执行能引起上下文切换的函数和中断不安全的函数,建议在中断中利用 FreeRTOS 的队列、事件组等任务间通信机制来传递信息。
中断在分配时有一些标志位 intr_alloc_flags
,这些标志位控制着 ISR 的行为,主要有如下一些标志位:
#define ESP_INTR_FLAG_LEVELx (1<<x) /* 中断优先级,x 从 1 到 6 */
#define ESP_INTR_FLAG_NMI (1<<7) /* 最高中断优先级 7 */
#define ESP_INTR_FLAG_SHARED (1<<8) /* 共享中断 */
#define ESP_INTR_FLAG_EDGE (1<<9) /* 边沿触发中断 */
#define ESP_INTR_FLAG_IRAM (1<<10) /* 将中断处理函数放在 IRAM 中,避免访问 FLASH */
#define ESP_INTR_FLAG_INTRDISABLED (1<<11) /* ISR 安装时默认不使能中断 */#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) /* 高等级优先级 */
使用 GPIO 外部中断时一般只需以下两步即可:
gpio_install_isr_service
gpio_isr_handler_add
1.2、数据结构
1.2.1、GPIO
ESP-IDF 驱动头文件
#include "driver/gpio.h"
GPIO 配置结构体
typedef struct {uint64_t pin_bit_mask; /* GPIO 引脚掩码 */gpio_mode_t mode; /* GPIO 模式 */gpio_pullup_t pull_up_en; /* GPIO 上拉使能 */gpio_pulldown_t pull_down_en; /* GPIO 下拉使能 */gpio_int_type_t intr_type; /* GPIO 中断类型*/
#if SOC_GPIO_SUPPORT_PIN_HYS_FILTERgpio_hys_ctrl_mode_t hys_ctrl_mode; /* GPIO 迟滞控制模式 */
#endif
}gpio_config_t;
1.2.2、毛刺过滤器
ESP-IDF 驱动头文件
#include "driver/gpio_filter.h"
管脚毛刺滤波器 配置结构体
typedef struct {glitch_filter_clock_source_t clk_src; /* 时钟源 */gpio_num_t gpio_num; /* 引脚号 */
}gpio_pin_glitch_filter_config_t;
灵活毛刺滤波器 配置结构体
typedef struct {glitch_filter_clock_source_t clk_src; /* 时钟源 */gpio_num_t gpio_num; /* 引脚号 */uint32_t window_width_ns; /* 采样窗口宽度(ns) */uint32_t window_thres_ns; /* 采样窗口阈值(ns) */
} gpio_flex_glitch_filter_config_t;
1.3、硬件原理图
笔者使用了合宙的 ESP32-C3 开发板,本文仅涉及 KEY 和 LED 两部分硬件,KEY 和 LED 部分的硬件原理图如下图所示:
其中两个 LED 灯 D4 和 D5 分别由 ESP32-C3 的 GPIO12 和 GPIO13 控制,当引脚输出高电平时对应的 LED 灯会被点亮。
ESP32-C3 的 GPIO9 被 10K 的电阻 R4 上拉至高电平,当按键 S2 按下时,GPIO9 被拉低至地,因此可以通过 GPIO9 输入获取按键的状态。
2、示例程序
2.1、GPIO 输出 - 点亮 LED 灯
功能:
- 固定每隔 1s 翻转一次 LED 灯的状态
程序流程:
- 使用
gpio_config()
配置 LED 引脚为 输出模式 - 通过
gpio_set_level()
固定周期设置引脚输出的高低电平
示例程序:
#include "freertos/FreeRTOS.h"
#include "driver/gpio.h"#define LED_GPIO_PIN GPIO_NUM_13/* 以下内容无需修改 */
uint8_t led_pin_level = 0;void led_init(void) {gpio_config_t io_cfg = {.pin_bit_mask = 1 << LED_GPIO_PIN,.mode = GPIO_MODE_OUTPUT,.pull_up_en = 0,.pull_down_en = 0,.intr_type = GPIO_INTR_DISABLE,};gpio_config( & io_cfg);
}void app_main(void) {led_init();while (1) {led_pin_level = !led_pin_level;gpio_set_level(LED_GPIO_PIN, led_pin_level);vTaskDelay(1000);}
}
2.2、GPIO 输入 - 按键响应
功能:
- 按键 KEY 按下翻转 LED 状态
程序流程:
- 使用
gpio_config()
配置 LED 引脚为 输出模式 - 使用
gpio_config()
配置 KEY 引脚为 输入模式 - 通过
gpio_get_level()
获得 KEY 引脚输入的高低电平 - 当 KEY 引脚输入的电平为低电平时表示按键按下,通过
gpio_set_level()
翻转 LED 引脚输出电平
示例程序:
#include "freertos/FreeRTOS.h"
#include "driver/gpio.h"#define KEY_GPIO_PIN GPIO_NUM_9
#define LED_GPIO_PIN GPIO_NUM_13/* 以下内容无需修改 */
uint8_t led_pin_level = 0;void led_init(void) {gpio_config_t io_cfg = {.pin_bit_mask = 1 << LED_GPIO_PIN,.mode = GPIO_MODE_OUTPUT,.pull_up_en = 0,.pull_down_en = 0,.intr_type = GPIO_INTR_DISABLE,};gpio_config( & io_cfg);
}void key_init(void) {gpio_config_t io_cfg = {.pin_bit_mask = 1 << KEY_GPIO_PIN,.mode = GPIO_MODE_INPUT,.pull_up_en = 1,.pull_down_en = 0,.intr_type = GPIO_INTR_DISABLE,};gpio_config( & io_cfg);
}void app_main(void) {led_init();key_init();while (1) {if (gpio_get_level(KEY_GPIO_PIN) == 0) {vTaskDelay(5);if (gpio_get_level(KEY_GPIO_PIN) == 0) {gpio_set_level(LED_GPIO_PIN, led_pin_level);led_pin_level = !led_pin_level;while (!gpio_get_level(KEY_GPIO_PIN)) {};}}vTaskDelay(10);}
}
2.3、GPIO 外部中断 - 按键响应
功能:
- 按键按下翻转 LED 状态
程序流程:
- 使用
gpio_config()
配置 LED 引脚为 输出模式 - 使用
gpio_config()
配置 KEY 引脚为 输入下降沿中断模式 - 调用
gpio_install_isr_service()
和gpio_isr_handler_add()
注册并使能 KEY 引脚中断 - 按键 KEY 按下触发下降沿中断,进入中断服务子程序,发送事件到队列
- 当检测到队列非空时,通过
gpio_set_level()
翻转 LED 灯状态
具体示例程序如下:
#include "freertos/FreeRTOS.h"
#include "driver/gpio.h"#define KEY_GPIO_PIN GPIO_NUM_9
#define LED_GPIO_PIN GPIO_NUM_13/* 以下内容无需修改 */
static uint8_t led_pin_level = 0;
static QueueHandle_t key_event_queue = NULL;static void IRAM_ATTR exti_isr_handler(void * arg) {uint32_t gpio_num = (uint32_t) arg;BaseType_t hp = pdFALSE;xQueueSendFromISR(key_event_queue, & gpio_num, & hp);if (hp) portYIELD_FROM_ISR();
}void led_init(void) {gpio_config_t io_cfg = {.pin_bit_mask = 1 << LED_GPIO_PIN,.mode = GPIO_MODE_OUTPUT,.pull_up_en = 0,.pull_down_en = 0,.intr_type = GPIO_INTR_DISABLE,};gpio_config( & io_cfg);
}void key_init(void) {gpio_config_t io_cfg = {.pin_bit_mask = 1 << KEY_GPIO_PIN,.mode = GPIO_MODE_INPUT,.pull_up_en = 1,.pull_down_en = 0,/* edge */.intr_type = GPIO_INTR_NEGEDGE,};gpio_config( & io_cfg);/* Register a interrupt */gpio_install_isr_service(ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM);/* Add exti callback function to key gpio */gpio_isr_handler_add(KEY_GPIO_PIN, exti_isr_handler, (void * ) KEY_GPIO_PIN);
}static void key_task(void * arg) {uint32_t io_num;while (1) {if (xQueueReceive(key_event_queue, & io_num, portMAX_DELAY)) {// 消抖vTaskDelay(10);if (gpio_get_level(io_num) == 0) {// 抬手检测while (gpio_get_level(io_num) == 0) {vTaskDelay(10);}gpio_set_level(LED_GPIO_PIN, led_pin_level);led_pin_level = !led_pin_level;}}vTaskDelay(10);}// Will not executegpio_uninstall_isr_service();
}void app_main(void) {/* Configure the peripheral */led_init();key_init();/* Create event queue */key_event_queue = xQueueCreate(10, sizeof(uint32_t));xTaskCreate(key_task, "key_task", 2048, NULL, 10, NULL);
}
3、常用函数
/* GPIO 配置函数 */
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig)/* 设置 GPIO 输出电平 */
esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level)/* 获取 GPIO 输入电平 */
int gpio_get_level(gpio_num_t gpio_num);/* 注册 GPIO 中断服务 */
esp_err_t gpio_install_isr_service(int intr_alloc_flags)
/* 添加 GPIO 中断服务函数 */
esp_err_t gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void *args)
/* GPIO 中断使能 */
esp_err_t gpio_intr_enable(gpio_num_t gpio_num)
4、烧录验证
“GPIO 输出 - 点亮 LED 灯” 实验效果如下:
可能由于 GIF 帧率原因 LED ,看起来不是以固定周期闪烁,因此使用逻辑分析仪捕获了 LED 引脚的电平,证明了其确实是 1s 翻转一次 LED 状态,结果如下所示:
“GPIO 输入 - 按键响应” 和 “GPIO 外部中断 - 按键响应” 实验效果一样,如下所示: