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 中断一般流程

  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 按键中断的标准代码结构

  #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 通信。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐