ESP32学习笔记之GPIO

一、GPIO 是什么

  • 全称 :General Purpose Input/Output,通用输入输出接口

  • 作用:MCU 用来接收外部信号(输入)或控制外部器件(输出)的引脚


二、GPIO 的四大核心模式

模式 说明
输入模式 MCU 读取引脚电平(高/低),用于检测按键、传感器信号等
输出模式 MCU 控制引脚输出高/低电平,用于驱动 LED、蜂鸣器等
复用模式 引脚作为片上外设的功能脚(如 UART TX、SPI SCK、PWM 输出等)
模拟模式 引脚直接连接到 ADC 等,用于模拟信号采集

三、输入模式详解

1. 引脚不能悬空

  • 原因 :悬空时电平受外界干扰不稳定,导致读数错误。

  • 解决:通过上拉电阻(接 VCC)或下拉电阻(接 GND)提供确定的默认电平。

2. 内部结构

  • 施密特触发器 :具有迟滞特性,抑制噪声和缓慢变化,增强抗干扰。

  • 输入缓冲器:高输入阻抗,隔离外部信号与内部逻辑,防止过载。

3. 上拉输入与下拉输入

  • 上拉输入:默认高电平,按键接 GND → 按下为低。

  • 下拉输入:默认低电平,按键接 VCC → 按下为高。

4. 按键消抖方法

延时消抖,定时器消抖,状态机消抖

5. 上拉 vs 下拉对比表

特性 上拉 (Pull-up) 下拉 (Pull-down)
连接方式 电阻接引脚与 VCC 电阻接引脚与 GND
默认电平 高电平 (1) 低电平 (0)
主要作用 防止悬空,提供默认高 开漏总线必须上拉 防止悬空,提供默认低
典型应用 按键接 GND、I2C 总线、复位引脚 按键接 VCC、使能引脚默认禁止
常见按键接法 按键接 GND,GPIO 上拉 → 松开读 1,按下读 0 按键接 VCC,GPIO 下拉 → 松开读 0,按下读 1

四、输出模式详解

推挽 vs 开漏对比表

特性 推挽输出 (Push-Pull) 开漏输出 (Open-Drain)
输出高电平方式 内部直接接 VCC,主动推高 高阻态,由外部上拉电阻拉高
输出低电平方式 内部直接接 GND,主动拉低 内部接 GND,主动拉低
是否需要上拉 不需要 必须(除非内部有可配置上拉)
典型应用 普通 LED、SPI、UART、片选信号 I2C、多设备中断、不同电压域通信

为什么 I2C 使用开漏?

  • 所有设备只能拉低,高电平由上拉电阻统一提供,总线空闲时为高。

  • 支持多主机仲裁(通过拉低总线竞争)和时钟同步。


五、高阻态

  • 引脚既不输出高电平,也不输出低电平,对外部电路呈高阻抗,类似"断开"。

  • 引脚电平由外部电路决定(如上拉电阻、其他输出等)。

开漏输出 在"输出高"时,实际就是进入高阻态 ,靠上拉电阻将总线拉高。


六、其他常见模式

  • 复用功能:引脚作为 UART、SPI、PWM 等外设接口,需配置为对应复用模式(推挽/开漏取决于外设要求)。

  • 模拟输入:用于 ADC 采样,数字部分关闭,引脚直接连接模拟前端。

  • 中断触发:可配置为上升沿、下降沿、高/低电平触发,用于响应外部事件(需配合 EXTI 和 NVIC)。


八、寄存器视角(以 STM32 为例)

  • ODR:输出数据寄存器

  • BSRR:原子操作寄存器

BSRR vs ODR:BSRR 允许原子操作(写 1 对应位 set,写 1 对应位+16 reset),无需读-改-写,更安全高效;ODR 需要读-改-写,可能被中断打断。


九、GPIO 中断基础

  • EXTI:外部中断/事件控制器,负责检测引脚上的信号变化,产生中断请求。

  • NVIC:嵌套向量中断控制器,负责管理中断优先级和分发中断给 CPU 处理。

  • 工作流程:引脚电平变化 → EXTI 检测到触发条件 → EXTI 向 NVIC 发送中断信号 → NVIC 根据优先级调度,执行对应的中断服务函数 (ISR)。


十、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

cs 复制代码
#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 读取按键

cs 复制代码
  #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

cs 复制代码
/*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 中断一般流程

  1. 配置 GPIO:设置为输入模式,配置上拉/下拉,选择中断触发类型(上升沿、下降沿、任意沿、低电平、高电平)。

  2. 安装 ISR 服务 :调用 gpio_install_isr_service(),全局只需一次。

  3. 注册中断处理函数 :使用 gpio_isr_handler_add() 为指定引脚绑定 ISR。

  4. 在 ISR 中通知任务:ISR 只做最短的工作,通常通过队列、信号量或事件标志将事件传递给任务。

  5. 任务中处理业务逻辑:包括消抖、状态翻转、响应动作等。

典型流程:GPIO 配置 → 安装 ISR 服务 → 注册 ISR → ISR 发消息 → 任务消抖并处理。

为什么 ISR 不直接做复杂逻辑

  • 实时性要求:ISR 执行时间过长会阻塞其他中断,影响系统响应。

  • 资源受限:中断上下文可能禁止调度、不能阻塞、不能使用某些非可重入函数。

  • 避免竞争:复杂逻辑可能涉及共享资源,在 ISR 中处理容易引入竞态条件。

  • 硬件限制:某些外设(如日志输出、动态内存分配)在 ISR 中不安全。


十三、ESP32 按键中断的标准代码结构

cs 复制代码
  #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 通信。

相关推荐
Flittly2 小时前
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(10)Team Protocols (团队协议)
笔记·python·ai·ai编程
for_ever_love__2 小时前
Objecgtive-C学习实例对象,类对象, 元类对象与 isa指针
c语言·学习·ios
惶了个恐2 小时前
嵌入式硬件第二弹——51单片机(2)
单片机·嵌入式硬件·51单片机
小嘚2 小时前
2026零散记忆
学习
智算菩萨2 小时前
【How Far Are We From AGI】4 AGI的“生理系统“——从算法架构到算力基座的工程革命
论文阅读·人工智能·深度学习·算法·ai·架构·agi
福赖2 小时前
《算法:生产车间》
算法
problc2 小时前
在 OpenClaw 里一句话记账:消费说出来,账单自动进乖猫记账 App
开发语言·python
疯狂成瘾者2 小时前
Redis 实用学习清单
redis·学习
橙露2 小时前
Vue3 自定义指令:实战封装全局常用工具指令
开发语言