Appearance
第二章: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:思考题
为什么 GPIO 输出高电平时 LED 亮,而有些电路是输出低电平时 LED 亮?两种接法各有什么优缺点?
内部上拉电阻约 45kΩ,外部常用 10kΩ。为什么外部上拉阻值更小?阻值越小越好吗?
如果同时配置了上拉和下拉(
GPIO_PULLUP_PULLDOWN),GPIO 输入会读到什么值?为什么实际中不这样用?(1ULL << LED_PIN)中为什么用1ULL而不是1?如果 LED_PIN = 32 会发生什么?
本章小结
| 知识点 | 掌握程度自评 |
|---|---|
| gpio_config_t 结构体配置 | ⬜⬜⬜⬜⬜ |
| GPIO 输出控制 | ⬜⬜⬜⬜⬜ |
| GPIO 输入读取 | ⬜⬜⬜⬜⬜ |
| 软件消抖原理与实现 | ⬜⬜⬜⬜⬜ |
| 位掩码操作 | ⬜⬜⬜⬜⬜ |
| 边沿检测(状态机) | ⬜⬜⬜⬜⬜ |
上一章:← 芯片认知与环境搭建下一章:中断与定时器 →