GPIO基础与LED控制:数字输出、GPIO模式配置、LED闪烁实验、按键输入与消抖处理

好,咱们正式开始动手了。上一章我们把ESP32的开发环境搭好,也跑了第一个Hello World。这一章,我们要跟硬件真正打交道——控制GPIO。

GPIO,全称是General-Purpose Input/Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。你可以让它输出高电平或低电平,也可以读取外部信号是0还是1。这是嵌入式开发最基础、也是最核心的操作。

我个人习惯,每接触一款新芯片,第一件事就是点亮一个LED。这就像程序员的"Hello World",能验证硬件、软件、工具链是否都正常。别小看这一步,我在项目里见过太多人,上来就写复杂逻辑,结果连灯都不亮,排查半天发现是GPIO配置错了。

ESP32的GPIO概览

ESP32有34个GPIO引脚,编号从0到39。但要注意,不是所有引脚都能随便用。有些引脚有特殊功能,或者在上电时有特定状态。我整理了一张表,你开发时最好贴在工位上。

GPIO编号 功能说明 注意事项
GPIO0 下载模式选择 低电平时进入下载模式,慎用
GPIO1 UART0 TX 串口输出,调试常用
GPIO2 普通IO 开发板上的板载LED通常接这个
GPIO3 UART0 RX 串口输入
GPIO4-5 普通IO 可安全使用
GPIO6-11 Flash接口 不要用作普通IO,会出问题
GPIO12 JTAG TDI 上电时电平影响Flash电压,小心
GPIO13-19 普通IO 大部分可安全使用
GPIO21-23 普通IO 常用,I2C、SPI也在这
GPIO25-27 普通IO 也可用作DAC输出
GPIO32-39 普通IO GPIO34-39只能输入,不能输出
注意:GPIO6到GPIO11是连接内部Flash的,千万别当普通IO用。我曾经有个同事,为了省引脚把Flash的CS脚当按键输入用,结果程序跑着跑着就崩溃了——Flash访问被干扰了。

GPIO模式配置

在ESP-IDF中,配置GPIO用gpio_set_direction()函数。你需要告诉芯片:这个引脚是输出还是输入,要不要上拉或下拉。

常用的模式有这些:

  • GPIO_MODE_OUTPUT:数字输出模式,驱动LED、蜂鸣器等
  • GPIO_MODE_INPUT:数字输入模式,读取按键、传感器信号
  • GPIO_MODE_INPUT_OUTPUT:双向模式,开漏总线常用
  • GPIO_PULLUP_ONLY:仅使能内部上拉电阻
  • GPIO_PULLDOWN_ONLY:仅使能内部下拉电阻

配置代码很简单,就两行:

// 配置GPIO2为输出模式
gpio_pad_select_gpio(GPIO_NUM_2);
gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);

嗯,这里要注意:gpio_pad_select_gpio()这个函数在较新版本的ESP-IDF中可能被标记为废弃,但很多旧项目还在用。新项目我建议直接用gpio_config()结构体配置,更清晰。

LED闪烁实验

好,咱们直接上手。拿一块ESP32开发板,找一个LED,串一个220Ω的电阻,接到GPIO2和GND之间。为什么是220Ω?因为ESP32的GPIO输出3.3V,LED正向压降约2V,电流控制在5-10mA比较安全。电阻值 = (3.3 - 2) / 0.01 = 130Ω,取220Ω是留点余量。

完整代码长这样:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define LED_GPIO GPIO_NUM_2

void app_main(void)
{
    // 配置GPIO
    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
    };
    gpio_config(&io_conf);

    while (1) {
        // 点亮LED
        gpio_set_level(LED_GPIO, 1);
        vTaskDelay(500 / portTICK_PERIOD_MS);

        // 熄灭LED
        gpio_set_level(LED_GPIO, 0);
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
}

这段代码的逻辑很直白:循环里,先拉高电平,等500ms,再拉低电平,等500ms。LED就会以1Hz的频率闪烁。

小技巧:用gpio_set_level()控制电平,也可以用gpio_get_level()读取当前状态。调试时我经常用后者确认引脚状态对不对。

按键输入与消抖处理

LED能亮了,下一步就是让LED响应按键。按键输入比输出麻烦一点,因为——机械按键会抖动。

你想想看,一个按键按下时,金属触点不是瞬间稳定接触的。它会弹跳几次,持续几毫秒到几十毫秒。如果不处理,一次按下可能被识别成多次。我在一个门禁项目里就吃过这个亏,按键按一次,门开了又关,关了又开,用户以为闹鬼了。

消抖有两种常用方法:

  • 硬件消抖:在按键两端并联一个0.1μF电容,利用电容充放电平滑信号
  • 软件消抖:检测到电平变化后,延时10-20ms再读一次,确认状态稳定

实际项目中,我倾向于软件消抖。因为硬件要加元件,增加成本和PCB面积。而且软件消抖灵活,调整延时参数就行。

下面是一个带消抖的按键控制LED代码:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define LED_GPIO   GPIO_NUM_2
#define KEY_GPIO   GPIO_NUM_0

void app_main(void)
{
    // 配置LED输出
    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
    };
    gpio_config(&led_conf);

    // 配置按键输入,使能内部上拉
    gpio_config_t key_conf = {
        .pin_bit_mask = (1ULL << KEY_GPIO),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&key_conf);

    int led_state = 0;
    int last_key_state = 1;  // 上拉,默认高电平

    while (1) {
        int current_key_state = gpio_get_level(KEY_GPIO);

        // 检测下降沿:按键按下(从高到低)
        if (last_key_state == 1 && current_key_state == 0) {
            // 延时消抖
            vTaskDelay(20 / portTICK_PERIOD_MS);
            current_key_state = gpio_get_level(KEY_GPIO);

            if (current_key_state == 0) {
                // 确认按下,翻转LED
                led_state = !led_state;
                gpio_set_level(LED_GPIO, led_state);
                printf("按键按下,LED状态:%d\n", led_state);
            }
        }

        last_key_state = current_key_state;
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}

这段代码的核心逻辑:

  1. 检测到电平从高变低(下降沿),说明可能有人按了按键
  2. 等20ms,再读一次电平
  3. 如果还是低电平,说明不是抖动,是真的按下了
  4. 翻转LED状态
关键点:消抖延时不是越长越好。太短消不掉抖动,太长会感觉按键反应迟钝。我一般取10-20ms,具体看按键品质。便宜的按键抖动时间长,可能要30ms。

本章知识结构

我把这一章的核心逻辑画成了一张图,方便你理解GPIO操作的完整流程:

GPIO操作核心流程 1. GPIO配置 2. 选择模式:输出 / 输入 输出模式:控制LED gpio_set_level() 控制高低电平 输入模式:读取按键 gpio_get_level() + 软件消抖 while(1) 循环执行 消抖延时 10-20ms

这张图把GPIO操作分成了清晰的路径:配置→选择模式→执行操作→循环。不管是控制LED还是读取按键,都走这个流程。

常见问题与避坑

最后,分享几个我实际踩过的坑:

  • 引脚不够用?别打GPIO6-11的主意,那是Flash的命根子。我见过有人硬要用,结果程序下载完就跑不起来。
  • LED不亮?先检查极性。LED长脚是正极,接GPIO;短脚是负极,接GND。反了不会烧,但不亮。
  • 按键误触发?大概率是没做消抖。别偷懒,20ms延时花不了多少CPU时间。
  • GPIO34-39不能输出?这是硬件限制,只能做输入。选引脚前看一眼数据手册。

好了,这一章的内容就到这。你先把LED闪烁跑起来,再试试按键控制。动手做一遍,比看十遍都管用。