文档类型:ESP-IDF 通用 API 教程,不绑定单一开发板。
板级例程(引脚、menuconfig、烧录)见 热门例程详解 中Korvo例程详解-*/P4C5例程详解-*。
目录
简介
GPIO(通用输入/输出端口)是 ESP32 系列芯片最基础也是最常用的外设之一。通过 GPIO,您可以控制 LED 灯的亮灭、读取按钮状态、控制继电器等。本教程将详细介绍如何在 ESP-IDF 框架下使用 GPIO 功能。
ESP32 系列芯片拥有丰富的 GPIO 资源,但需要注意的是,不同型号的 ESP32 芯片 GPIO 数量和分布可能有所不同。例如:
- ESP32:拥有 34 个 GPIO 引脚(GPIO0 ~ GPIO39)
- ESP32-S2:拥有 43 个 GPIO 引脚(GPIO0 ~ GPIO46)
- ESP32-S3:拥有 43 个 GPIO 引脚(GPIO0 ~ GPIO46)
- ESP32-C3:拥有 22 个 GPIO 引脚(GPIO0 ~ GPIO21)
GPIO 基础概念
在开始使用 ESP-IDF 的 GPIO 功能前,我们需要了解一些基本概念:
1. GPIO 模式
ESP-IDF 中 GPIO 有以下几种工作模式:
- 输入模式:用于读取外部信号
- 输出模式:用于控制外部设备
- 输入/输出模式:可以同时作为输入和输出
- 开漏模式:一种特殊的输出模式,适用于某些通信协议
2. 上拉/下拉电阻
- 上拉电阻:当没有外部信号输入时,将引脚电平拉高到高电平
- 下拉电阻:当没有外部信号输入时,将引脚电平拉低到低电平
3. GPIO 电平
- 高电平:通常为 3.3V
- 低电平:通常为 0V
ESP-IDF 中的 GPIO 配置
1. 包含必要的头文件
#include "driver/gpio.h"
2. GPIO 配置步骤
在 ESP-IDF 中配置 GPIO 通常遵循以下步骤:
- 配置 GPIO 参数
- 设置 GPIO 工作模式
- 安装 GPIO 驱动(如果需要中断功能)
- 读取或设置 GPIO 电平
3. GPIO 配置示例
// 定义 GPIO 引脚号
#define LED_PIN 2 // 假设 LED 连接到 GPIO2
// 配置 GPIO
gpio_config_t io_conf = {};
// 设置为输出模式
io_conf.mode = GPIO_MODE_OUTPUT;
// 禁用中断
io_conf.intr_type = GPIO_INTR_DISABLE;
// 设置引脚号
io_conf.pin_bit_mask = (1ULL << LED_PIN);
// 禁用上拉
io_conf.pull_up_en = 0;
// 禁用下拉
io_conf.pull_down_en = 0;
// 应用配置
gpio_config(&io_conf);
GPIO 基本操作
1. 设置 GPIO 输出电平
// 设置 GPIO 为高电平
gpio_set_level(LED_PIN, 1);
// 设置 GPIO 为低电平
gpio_set_level(LED_PIN, 0);
2. 读取 GPIO 输入电平
// 定义按钮引脚
#define BUTTON_PIN 4 // 假设按钮连接到 GPIO4
// 配置 GPIO 为输入模式
gpio_config_t io_conf = {};
io_conf.mode = GPIO_MODE_INPUT;
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.pin_bit_mask = (1ULL << BUTTON_PIN);
io_conf.pull_up_en = 1; // 启用内部上拉电阻
gpio_config(&io_conf);
// 读取按钮状态
int button_state = gpio_get_level(BUTTON_PIN);
3. 切换 GPIO 电平
// 切换 LED 状态
gpio_set_level(LED_PIN, !gpio_get_level(LED_PIN));
GPIO 中断处理
ESP-IDF 支持 GPIO 中断功能,可以在 GPIO 电平变化时触发中断处理函数。
1. 中断类型
ESP-IDF 支持以下几种中断类型:
GPIO_INTR_DISABLE:禁用中断GPIO_INTR_POSEDGE:上升沿触发GPIO_INTR_NEGEDGE:下降沿触发GPIO_INTR_ANYEDGE:任意边沿触发GPIO_INTR_LOW_LEVEL:低电平触发GPIO_INTR_HIGH_LEVEL:高电平触发
2. 配置 GPIO 中断
#define BUTTON_PIN 4
// 中断服务处理函数
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
uint32_t gpio_num = (uint32_t) arg;
// 可以在这里发送事件到任务队列
// 注意:中断处理函数应该尽量简短
}
void app_main(void)
{
// 配置 GPIO
gpio_config_t io_conf = {};
io_conf.mode = GPIO_MODE_INPUT;
io_conf.intr_type = GPIO_INTR_NEGEDGE; // 下降沿触发
io_conf.pin_bit_mask = (1ULL << BUTTON_PIN);
io_conf.pull_up_en = 1;
gpio_config(&io_conf);
// 安装 GPIO 中断服务
gpio_install_isr_service(0);
// 添加中断处理函数
gpio_isr_handler_add(BUTTON_PIN, gpio_isr_handler, (void*) BUTTON_PIN);
// 主程序循环
while(1) {
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
3. 使用事件队列处理中断
在实际应用中,通常不会在中断处理函数中执行复杂操作,而是通过事件队列将事件传递给主任务处理。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#define BUTTON_PIN 4
// 定义一个队列句柄
static xQueueHandle gpio_evt_queue = NULL;
// 中断服务处理函数
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
uint32_t gpio_num = (uint32_t) arg;
// 发送 GPIO 号到队列
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
// 任务处理函数
static void gpio_task_example(void* arg)
{
uint32_t io_num;
while(1) {
// 等待队列消息
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
printf("GPIO[%d] 中断触发\n", io_num);
// 在这里处理按钮按下事件
}
}
}
void app_main(void)
{
// 创建队列接收按钮事件
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
// 创建任务处理按钮事件
xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);
// 配置 GPIO
gpio_config_t io_conf = {};
io_conf.mode = GPIO_MODE_INPUT;
io_conf.intr_type = GPIO_INTR_NEGEDGE;
io_conf.pin_bit_mask = (1ULL << BUTTON_PIN);
io_conf.pull_up_en = 1;
gpio_config(&io_conf);
// 安装 GPIO 中断服务
gpio_install_isr_service(0);
// 添加中断处理函数
gpio_isr_handler_add(BUTTON_PIN, gpio_isr_handler, (void*) BUTTON_PIN);
}
实用示例
示例1:LED 闪烁
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
// 定义 LED 引脚
#define LED_PIN 2
void app_main(void)
{
// 配置 GPIO
gpio_config_t io_conf = {};
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.pin_bit_mask = (1ULL << LED_PIN);
io_conf.pull_up_en = 0;
io_conf.pull_down_en = 0;
gpio_config(&io_conf);
printf("LED 闪烁示例\n");
while(1) {
// 设置 LED 为高电平(点亮)
gpio_set_level(LED_PIN, 1);
printf("LED 开\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
// 设置 LED 为低电平(熄灭)
gpio_set_level(LED_PIN, 0);
printf("LED 关\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
示例2:按钮控制 LED
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
// 定义 GPIO 引脚
#define LED_PIN 2
#define BUTTON_PIN 4
void app_main(void)
{
// 配置 LED GPIO
gpio_config_t led_conf = {};
led_conf.mode = GPIO_MODE_OUTPUT;
led_conf.intr_type = GPIO_INTR_DISABLE;
led_conf.pin_bit_mask = (1ULL << LED_PIN);
led_conf.pull_up_en = 0;
led_conf.pull_down_en = 0;
gpio_config(&led_conf);
// 配置按钮 GPIO
gpio_config_t button_conf = {};
button_conf.mode = GPIO_MODE_INPUT;
button_conf.intr_type = GPIO_INTR_DISABLE;
button_conf.pin_bit_mask = (1ULL << BUTTON_PIN);
button_conf.pull_up_en = 1; // 启用内部上拉电阻
gpio_config(&button_conf);
printf("按钮控制 LED 示例\n");
// 初始状态,LED 关闭
gpio_set_level(LED_PIN, 0);
int button_state = 1; // 初始状态(未按下)
int last_button_state = 1;
while(1) {
// 读取按钮状态
button_state = gpio_get_level(BUTTON_PIN);
// 检测按钮状态变化(按下时为低电平)
if (last_button_state == 1 && button_state == 0) {
// 按钮被按下,切换 LED 状态
int led_state = gpio_get_level(LED_PIN);
gpio_set_level(LED_PIN, !led_state);
printf("按钮按下,LED 状态切换为: %s\n", !led_state ? "关" : "开");
}
// 保存当前按钮状态
last_button_state = button_state;
// 短暂延时,避免按钮抖动
vTaskDelay(20 / portTICK_PERIOD_MS);
}
}
示例3:多个 LED 控制
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
// 定义多个 LED 引脚
#define LED_NUM 3
const int led_pins[LED_NUM] = {2, 4, 5}; // 假设有 3 个 LED 分别连接到 GPIO2, GPIO4, GPIO5
void app_main(void)
{
// 配置多个 LED GPIO
for (int i = 0; i < LED_NUM; i++) {
gpio_config_t io_conf = {};
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.pin_bit_mask = (1ULL << led_pins[i]);
io_conf.pull_up_en = 0;
io_conf.pull_down_en = 0;
gpio_config(&io_conf);
// 初始状态,所有 LED 关闭
gpio_set_level(led_pins[i], 0);
}
printf("多 LED 控制示例\n");
int current_led = 0;
while(1) {
// 关闭所有 LED
for (int i = 0; i < LED_NUM; i++) {
gpio_set_level(led_pins[i], 0);
}
// 点亮当前 LED
gpio_set_level(led_pins[current_led], 1);
printf("LED %d 开启\n", current_led);
// 更新下一个要点亮的 LED
current_led = (current_led + 1) % LED_NUM;
// 延时
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
常见问题与解决方案
1. GPIO 引脚选择注意事项
ESP32 的某些 GPIO 引脚在启动时有特殊功能,使用时需要注意:
- GPIO0:启动模式选择引脚,启动时如果为低电平,ESP32 将进入下载模式
- GPIO2:部分板子上连接了板载 LED
- GPIO12:部分 ESP32 模组上,启动时如果为高电平,会影响闪存启动
- GPIO34-39:仅支持输入模式,不支持输出模式
2. 按钮抖动问题
机械按钮按下或释放时,可能会产生多次电平变化,称为"抖动"。解决方法:
- 硬件去抖:使用电容等元件进行硬件去抖
- 软件去抖:
- 延时法:检测到按钮状态变化后,延时一段时间再读取
- 多次采样法:连续多次读取按钮状态,确保状态稳定
// 软件去抖示例
bool is_button_pressed(uint32_t pin)
{
int stable_count = 0;
bool current_state = gpio_get_level(pin) == 0; // 假设按下为低电平
// 连续采样 5 次,确保状态稳定
for (int i = 0; i < 5; i++) {
if (gpio_get_level(pin) == 0) {
stable_count++;
}
vTaskDelay(1 / portTICK_PERIOD_MS);
}
return stable_count >= 3; // 如果大部分采样为按下状态,则认为按钮被按下
}
3. 中断处理注意事项
- 中断处理函数应尽量简短,避免执行耗时操作
- 中断处理函数中不应调用会导致任务切换的 API
- 使用
IRAM_ATTR属性确保中断处理函数放在 IRAM 中,提高响应速度 - 对于需要处理的复杂操作,应使用队列将事件传递给任务处理
4. GPIO 电流限制
ESP32 的 GPIO 引脚有电流限制,单个引脚最大输出电流约为 40mA。如果需要驱动大电流负载(如高亮度 LED、继电器等),应使用三极管或 MOSFET 等元件进行放大。
总结
本教程介绍了 ESP-IDF 中 GPIO 的基本使用方法,包括配置、基本操作、中断处理以及实用示例。通过掌握这些基础知识,您可以开始构建各种基于 ESP32 的项目。
在后续的教程中,我们将介绍更多 ESP-IDF 的功能,如 I2C、SPI、UART 等通信协议的使用方法。