ESP32学习笔记之GPIO
GPIO 是什么
-
全称:General Purpose Input/Output,通用输入输出接口
-
作用:MCU 用来接收外部信号(输入)或控制外部器件(输出)的引脚
详情请见 GPIO工作原理以及核心
https://blog.csdn.net/2301_81636338/article/details/159833657?spm=1011.2124.3001.6209
ESP-IDF 中 GPIO 常用 API
ESP-IDF 提供了丰富的 GPIO 操作函数,以下是开发中最常用的几个:
| API 函数 | 描述 | 示例 |
|---|---|---|
gpio_config() |
统一配置 GPIO 的模式、上下拉、中断类型等,可同时配置多个引脚。 | gpio_config_t io_conf = {.pin_bit_mask = 1ULL<<2, .mode = GPIO_MODE_OUTPUT};gpio_config(&io_conf); |
gpio_set_level() |
设置输出引脚电平(0 或 1)。 | gpio_set_level(GPIO_NUM_2, 1); |
gpio_get_level() |
读取输入引脚电平。 | int level = gpio_get_level(GPIO_NUM_0); |
gpio_reset_pin() |
将指定 GPIO 恢复为默认状态(通常为输入、无上下拉、无中断)。 | gpio_reset_pin(GPIO_NUM_2); |
gpio_install_isr_service() |
安装 GPIO 中断服务(全局一次),允许为多个引脚注册中断处理函数。 | gpio_install_isr_service(0); |
gpio_isr_handler_add() |
为指定引脚添加中断处理函数,可传入用户参数。 | gpio_isr_handler_add(BUTTON_GPIO, isr_handler, (void*)BUTTON_GPIO); |
gpio_intr_enable()/disable() |
启用/禁用指定引脚的中断。 | gpio_intr_enable(BUTTON_GPIO); |
ESP32的程序
1. 点亮 LED
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_err.h"
#include <stdbool.h>
#define LED_GPIO GPIO_NUM_2
#define BLINK_PERIOD_MS 1000
static const char *TAG = "gpio_led";
void app_main(void)
{
gpio_config_t io_conf = {
.pin_bit_mask = 1ULL << LED_GPIO,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
bool led_on = false;
while (1) {
led_on = !led_on;
ESP_ERROR_CHECK(gpio_set_level(LED_GPIO, led_on));
ESP_LOGI(TAG, "LED -> %s", led_on ? "ON" : "OFF");
vTaskDelay(pdMS_TO_TICKS(BLINK_PERIOD_MS));
}
}
灯闪烁
2. GPIO 读取按键
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_err.h"
#define LED_GPIO GPIO_NUM_2
#define BUTTON_GPIO GPIO_NUM_0
static const char *TAG = "gpio_key_led";
void app_main(void)
{
gpio_config_t led_conf = {
.pin_bit_mask = 1ULL << LED_GPIO,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&led_conf));
gpio_config_t button_conf = {
.pin_bit_mask = 1ULL << BUTTON_GPIO,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&button_conf));
while (1) {
int button_level = gpio_get_level(BUTTON_GPIO);
if (button_level == 0) {
gpio_set_level(LED_GPIO, 1);
ESP_LOGI(TAG, "button pressed, LED ON");
} else {
gpio_set_level(LED_GPIO, 0);
ESP_LOGI(TAG, "button released, LED OFF");
}
vTaskDelay(pdMS_TO_TICKS(50));
}
按键+灯0
3. 按一次按键翻转一次 LED
/*GPIO 输入 + 消抖 + 状态翻转 */
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_err.h"
#include <stdbool.h>
#define LED_GPIO GPIO_NUM_2
#define BUTTON_GPIO GPIO_NUM_0
#define DEBOUNCE_MS 20
static const char *TAG = "gpio_toggle";
void app_main(void)
{
gpio_config_t led_conf = {
.pin_bit_mask = 1ULL << LED_GPIO,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&led_conf));
gpio_config_t button_conf = {
.pin_bit_mask = 1ULL << BUTTON_GPIO,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&button_conf));
bool led_on = false;
int last_level = 1;
gpio_set_level(LED_GPIO, led_on);
while (1) {
int current_level = gpio_get_level(BUTTON_GPIO);
if (last_level == 1 && current_level == 0) {
vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));
current_level = gpio_get_level(BUTTON_GPIO);
if (current_level == 0) {
led_on = !led_on;
gpio_set_level(LED_GPIO, led_on);
ESP_LOGI(TAG, "button pressed, LED -> %s", led_on ? "ON" : "OFF");
while (gpio_get_level(BUTTON_GPIO) == 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
last_level = current_level;
vTaskDelay(pdMS_TO_TICKS(10));
}
}
按键+灯
ESP32 按键中断的完整理解
GPIO 中断一般流程
-
配置 GPIO:设置为输入模式,配置上拉/下拉,选择中断触发类型(上升沿、下降沿、任意沿、低电平、高电平)。
-
安装 ISR 服务:调用
gpio_install_isr_service(),全局只需一次。 -
注册中断处理函数:使用
gpio_isr_handler_add()为指定引脚绑定 ISR。 -
在 ISR 中通知任务:ISR 只做最短的工作,通常通过队列、信号量或事件标志将事件传递给任务。
-
任务中处理业务逻辑:包括消抖、状态翻转、响应动作等。
典型流程:GPIO 配置 → 安装 ISR 服务 → 注册 ISR → ISR 发消息 → 任务消抖并处理。
为什么 ISR 不直接做复杂逻辑
-
实时性要求:ISR 执行时间过长会阻塞其他中断,影响系统响应。
-
资源受限:中断上下文可能禁止调度、不能阻塞、不能使用某些非可重入函数。
-
避免竞争:复杂逻辑可能涉及共享资源,在 ISR 中处理容易引入竞态条件。
-
硬件限制:某些外设(如日志输出、动态内存分配)在 ISR 中不安全。
ESP32 按键中断的标准代码结构
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_err.h"
#include <stdbool.h>
#define LED_GPIO GPIO_NUM_2
#define BUTTON_GPIO GPIO_NUM_0
#define DEBOUNCE_MS 20
static const char *TAG = "gpio_isr";
static QueueHandle_t gpio_evt_queue = NULL;
static void IRAM_ATTR button_isr_handler(void *arg)
{
uint32_t gpio_num = (uint32_t)arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
void app_main(void)
{
gpio_config_t led_conf = {
.pin_bit_mask = 1ULL << LED_GPIO,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&led_conf));
gpio_config_t button_conf = {
.pin_bit_mask = 1ULL << BUTTON_GPIO,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
ESP_ERROR_CHECK(gpio_config(&button_conf));
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
ESP_ERROR_CHECK(gpio_install_isr_service(0));
ESP_ERROR_CHECK(gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, (void *)BUTTON_GPIO));
bool led_on = false;
gpio_set_level(LED_GPIO, led_on);
uint32_t io_num;
while (1) {
if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));
if (gpio_get_level(BUTTON_GPIO) == 0) {
led_on = !led_on;
gpio_set_level(LED_GPIO, led_on);
ESP_LOGI(TAG, "GPIO %lu pressed, LED -> %s",
(unsigned long)io_num,
led_on ? "ON" : "OFF");
while (gpio_get_level(BUTTON_GPIO) == 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
}
}
代码逐行理解
1. GPIO 配置
.mode = GPIO_MODE_INPUT:设置为输入模式。
.pull_up_en = GPIO_PULLUP_ENABLE:启用内部上拉电阻,使引脚默认高电平。
.intr_type = GPIO_INTR_NEGEDGE:下降沿触发中断,对应按键按下瞬间(高→低)。
2. 创建队列
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
队列用于 ISR 和主任务之间的安全通信。
每个消息存放一个
uint32_t值,即触发中断的 GPIO 编号。
3. 安装中断服务
gpio_install_isr_service(0);
全局只需调用一次,启用 GPIO 中断底层机制。
参数
0表示使用默认中断分配标志。
4. 注册 ISR
gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, (void *)BUTTON_GPIO);
为
BUTTON_GPIO绑定中断处理函数button_isr_handler,并传入引脚号作为参数。
5. ISR 发消息
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
在 ISR 中调用,将引脚号发送到队列。
FromISR版本是线程安全的,不会阻塞。
6. 任务中处理
if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) { // 收到事件后处理 }
portMAX_DELAY表示无限等待,直到收到消息。收到消息后,先消抖再确认按键状态,然后翻转 LED 并等待释放。
7. 消抖
vTaskDelay(pdMS_TO_TICKS(20)); if (gpio_get_level(BUTTON_GPIO) == 0)
-
延时 20ms 避开抖动期,再读取引脚电平,若仍为低则确认是有效按下。
打总结
1. GPIO 是什么?
GPIO 是芯片上可编程的通用数字输入输出引脚,可通过寄存器配置为输入、输出或复用功能,用于与外部设备(如按键、LED、传感器)交互。
2. 输入脚为什么要上拉或下拉?
为了防止引脚悬空导致电平不确定和干扰。上拉/下拉提供稳定的默认电平,确保无外部信号时逻辑状态已知。
3. 推挽输出和开漏输出的区别?
-
推挽:内部有互补 MOSFET,能主动输出高电平和低电平,驱动能力强,适合点对点信号。
-
开漏:内部只有下拉管,只能主动拉低,高电平需外部上拉电阻;支持线与逻辑,适合多设备共享总线(如 I2C)。
4. 边沿触发和电平触发的区别?
-
边沿触发:在信号电平变化瞬间(上升沿/下降沿)触发中断,适用于检测瞬间动作(如按键按下)。
-
电平触发:当信号维持在特定电平时持续触发,适用于需要响应持续状态的场景(如低电平报警)。
5. 按键为什么常用下降沿触发?
因为按键通常采用上拉输入,按下时引脚从高电平跳变到低电平,下降沿正好对应按下的瞬间,可准确捕获动作。
6. 为什么按键中断必须消抖?
机械按键在按下和释放时会产生多次快速的电平跳变(抖动),若不消抖,一次物理按下可能触发多次中断,导致误动作。软件消抖可滤除这些虚假跳变。
7. ESP32 按键中断如何实现?
-
配置 GPIO 为输入,启用内部上拉,设置下降沿触发。
-
安装 ISR 服务,注册中断处理函数。
-
在 ISR 中通过队列发送事件。
-
在任务中接收队列,进行消抖并处理业务逻辑(如翻转 LED)。
8. 为什么 ISR 里不要做复杂逻辑?
ISR 需尽量短小,避免阻塞和长时间运行,以保证系统实时性和其他中断的及时响应。复杂逻辑应放在任务中处理,通过 IPC 机制(队列、信号量)与 ISR 通信。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)