Skip to content

第二章:GPIO 与 LED 控制

2.1 GPIO 基础概念

ESP32-S3 GPIO 特性

GPIO 数量:45 个(GPIO0 ~ GPIO48,部分编号不存在)
电压:3.3V(不能直接接 5V 信号!)
最大电流:单个 GPIO 约 40mA,总电流建议 < 1200mA
内部上下拉:可软件配置(约 45kΩ)
特殊 GPIO:
  GPIO0        → Strapping 引脚(影响启动模式)
  GPIO3        → Strapping 引脚
  GPIO45/46    → Strapping 引脚
  GPIO19/20    → USB D-/D+(使用 USB 时不能复用)
  GPIO26~32    → 连接外部 Flash(N16 版本占用)
  GPIO33~37    → 连接外部 PSRAM(R8 版本占用)

GPIO 工作模式

输出模式:
  GPIO_MODE_OUTPUT          普通推挽输出
  GPIO_MODE_OUTPUT_OD       开漏输出(需外部上拉)

输入模式:
  GPIO_MODE_INPUT           普通输入
  GPIO_MODE_INPUT_OUTPUT    输入+输出(可读回输出值)

上下拉配置:
  GPIO_PULLUP_ONLY          内部上拉
  GPIO_PULLDOWN_ONLY        内部下拉
  GPIO_PULLUP_PULLDOWN      同时上下拉(不常用)
  GPIO_FLOATING             浮空(不推荐用于输入)

2.2 LED 控制

硬件连接

ESP32-S3 开发板
                    330Ω
GPIO2 ──────────┤├──────── LED 阳极(+)
                              LED 阴极(-) ── GND

注意:
- 限流电阻必须加!保护 GPIO 和 LED
- 3.3V / 330Ω ≈ 10mA,亮度适中
- 部分开发板板载 LED 已连接好(查看原理图)

基础 GPIO 控制

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

#define LED_PIN     GPIO_NUM_2    // 根据你的板子修改
#define BLINK_MS    500           // 闪烁间隔

static const char *TAG = "LED";

void app_main(void)
{
    // 1. 配置 GPIO
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << LED_PIN),  // 选择引脚(位掩码)
        .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);

    ESP_LOGI(TAG, "LED 初始化完成,GPIO%d", LED_PIN);

    // 2. 闪烁循环
    while (1) {
        gpio_set_level(LED_PIN, 1);          // 高电平 → LED 亮
        ESP_LOGI(TAG, "LED ON");
        vTaskDelay(pdMS_TO_TICKS(BLINK_MS));

        gpio_set_level(LED_PIN, 0);          // 低电平 → LED 灭
        ESP_LOGI(TAG, "LED OFF");
        vTaskDelay(pdMS_TO_TICKS(BLINK_MS));
    }
}

简化写法(单引脚快速配置)

c
// 快速配置输出引脚(不需要中断和上下拉时)
gpio_reset_pin(LED_PIN);
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
gpio_set_level(LED_PIN, 0);

2.3 按键输入

硬件连接(两种方式)

方式1:外部上拉(推荐)
3.3V ──┤10kΩ├── GPIO4 ──── 按键 ──── GND
按下:GPIO = 0(低电平有效)

方式2:内部上拉(省元件)
GPIO4(配置内部上拉)──── 按键 ──── GND
按下:GPIO = 0(低电平有效)

按键读取(轮询方式)

c
#include "driver/gpio.h"

#define BTN_PIN     GPIO_NUM_0    // BOOT 按键通常在 GPIO0
#define LED_PIN     GPIO_NUM_2

void app_main(void)
{
    // 配置 LED 输出
    gpio_reset_pin(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);

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

    while (1) {
        int btn_state = gpio_get_level(BTN_PIN);

        // 低电平有效(按下 = 0)
        if (btn_state == 0) {
            gpio_set_level(LED_PIN, 1);   // 按下亮
        } else {
            gpio_set_level(LED_PIN, 0);   // 松开灭
        }

        vTaskDelay(pdMS_TO_TICKS(10));    // 10ms 轮询间隔
    }
}

2.4 按键消抖

为什么需要消抖?

理想按键波形:
GPIO ─────┐          ┌─────
          └──────────┘

实际按键波形(有抖动):
GPIO ─────┐ ┌┐┌┐    ┌┐┌─────
          └─┘└┘└────┘└┘
          ↑抖动区域(约 5~20ms)

软件消抖实现

c
#include "esp_timer.h"   // 高精度定时器

#define DEBOUNCE_MS  20  // 消抖时间

typedef struct {
    gpio_num_t pin;
    int        last_stable;    // 上次稳定状态
    int        last_reading;   // 上次读取值
    int64_t    last_change_us; // 上次变化时间(微秒)
} button_t;

// 初始化按键
void button_init(button_t *btn, gpio_num_t pin)
{
    btn->pin = pin;
    btn->last_stable = 1;      // 默认高电平(上拉)
    btn->last_reading = 1;
    btn->last_change_us = 0;

    gpio_config_t cfg = {
        .pin_bit_mask = (1ULL << pin),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    gpio_config(&cfg);
}

// 读取消抖后的按键状态
// 返回:1=松开,0=按下,-1=状态未稳定
int button_read(button_t *btn)
{
    int reading = gpio_get_level(btn->pin);

    if (reading != btn->last_reading) {
        // 状态发生变化,记录时间
        btn->last_change_us = esp_timer_get_time();
        btn->last_reading = reading;
    }

    // 检查是否超过消抖时间
    int64_t elapsed = esp_timer_get_time() - btn->last_change_us;
    if (elapsed > (DEBOUNCE_MS * 1000)) {
        // 状态已稳定
        btn->last_stable = reading;
        return reading;
    }

    return -1;  // 还在抖动中
}

// 检测按键按下事件(边沿检测)
bool button_pressed(button_t *btn)
{
    static int prev_stable = 1;
    int stable = button_read(btn);

    if (stable == -1) return false;  // 抖动中

    bool pressed = (prev_stable == 1) && (stable == 0);
    prev_stable = stable;
    return pressed;
}

2.5 多 LED 控制与位操作

控制多个 LED

c
#define LED1    GPIO_NUM_2
#define LED2    GPIO_NUM_4
#define LED3    GPIO_NUM_5
#define LED4    GPIO_NUM_6

// 一次配置多个引脚(使用位掩码)
void leds_init(void)
{
    gpio_config_t cfg = {
        // 同时配置 4 个引脚
        .pin_bit_mask = (1ULL << LED1) | (1ULL << LED2) |
                        (1ULL << LED3) | (1ULL << LED4),
        .mode         = GPIO_MODE_OUTPUT,
        .pull_up_en   = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    gpio_config(&cfg);
}

// 用一个字节控制 4 个 LED(低 4 位)
void leds_set(uint8_t pattern)
{
    gpio_set_level(LED1, (pattern >> 0) & 1);
    gpio_set_level(LED2, (pattern >> 1) & 1);
    gpio_set_level(LED3, (pattern >> 2) & 1);
    gpio_set_level(LED4, (pattern >> 3) & 1);
}

void app_main(void)
{
    leds_init();

    // 流水灯
    uint8_t patterns[] = {0x01, 0x02, 0x04, 0x08,
                          0x08, 0x04, 0x02, 0x01};
    int idx = 0;

    while (1) {
        leds_set(patterns[idx]);
        idx = (idx + 1) % 8;
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

2.6 GPIO 中断(预览,第三章详讲)

c
// 中断服务函数(ISR)必须放在 IRAM 中
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
    uint32_t gpio_num = (uint32_t)arg;
    // ISR 中不能用 ESP_LOGI!只能用队列通知主任务
    // 详见第三章
}

// 配置中断
gpio_config_t cfg = {
    .pin_bit_mask = (1ULL << BTN_PIN),
    .mode         = GPIO_MODE_INPUT,
    .pull_up_en   = GPIO_PULLUP_ENABLE,
    .intr_type    = GPIO_INTR_NEGEDGE,  // 下降沿触发
};
gpio_config(&cfg);

gpio_install_isr_service(0);
gpio_isr_handler_add(BTN_PIN, gpio_isr_handler, (void*)BTN_PIN);

📝 第二章练习题

练习 2-1:LED 闪烁(基础)

目标:掌握 GPIO 输出配置

任务:实现 LED 以不同频率闪烁:

  • 前 5 秒:快闪(200ms 间隔)
  • 后 5 秒:慢闪(1000ms 间隔)
  • 循环往复

要求

  • 使用 esp_timer_get_time() 获取运行时间
  • 打印当前模式到串口

练习 2-2:按键控制 LED(基础)

目标:掌握 GPIO 输入读取

任务

  • 按键按下:LED 亮
  • 按键松开:LED 灭
  • 每次按下时,串口打印 "按键按下,次数: N"

要求:实现软件消抖(至少 20ms)


练习 2-3:按键切换模式(进阶)

目标:边沿检测 + 状态机

任务:用一个按键控制 LED 的三种模式,每次按下切换:

模式0:LED 常灭
模式1:LED 常亮
模式2:LED 快闪(200ms)
模式3:LED 慢闪(1000ms)
→ 再按回模式0

要求

  • 使用枚举定义模式
  • 串口打印当前模式名称
  • 实现消抖

练习 2-4:流水灯(进阶)

目标:多 GPIO 控制 + 位操作

任务:用 4 个 LED 实现以下效果(按键切换效果):

效果1 - 左移流水:○●○○ → ○○●○ → ○○○● → ●○○○
效果2 - 右移流水:○○○● → ○○●○ → ○●○○ → ●○○○
效果3 - 二进制计数:0000 → 0001 → 0010 → ... → 1111
效果4 - 全亮全灭:████ → ○○○○ → ████

要求

  • 用位操作实现 pattern 计算,不要硬编码数组
  • 按键每次按下切换到下一个效果

练习 2-5:SOS 摩尔斯电码(挑战)

目标:时序控制 + 函数封装

任务:用 LED 发出 SOS 摩尔斯电码信号,循环发送

摩尔斯电码规则:
  点(·)  = 亮 200ms
  划(-)  = 亮 600ms
  符号间隔 = 灭 200ms
  字母间隔 = 灭 600ms
  单词间隔 = 灭 1400ms

S = · · ·
O = - - -
S = · · ·

要求

  • 封装 morse_dot()morse_dash()morse_letter_gap()morse_word_gap() 函数
  • 串口同步打印 "· · · --- · · · " 和 "SOS"

练习 2-6:思考题

  1. 为什么 GPIO 输出高电平时 LED 亮,而有些电路是输出低电平时 LED 亮?两种接法各有什么优缺点?

  2. 内部上拉电阻约 45kΩ,外部常用 10kΩ。为什么外部上拉阻值更小?阻值越小越好吗?

  3. 如果同时配置了上拉和下拉(GPIO_PULLUP_PULLDOWN),GPIO 输入会读到什么值?为什么实际中不这样用?

  4. (1ULL << LED_PIN) 中为什么用 1ULL 而不是 1?如果 LED_PIN = 32 会发生什么?


本章小结

知识点掌握程度自评
gpio_config_t 结构体配置⬜⬜⬜⬜⬜
GPIO 输出控制⬜⬜⬜⬜⬜
GPIO 输入读取⬜⬜⬜⬜⬜
软件消抖原理与实现⬜⬜⬜⬜⬜
位掩码操作⬜⬜⬜⬜⬜
边沿检测(状态机)⬜⬜⬜⬜⬜

上一章← 芯片认知与环境搭建下一章中断与定时器 →

个人知识库