GPIO 基础:数字输入输出,LED 闪烁与按键检测实战
各位同学,欢迎来到第二章。上一章我们搭好了开发环境,今天终于可以动手了。
GPIO,全称 General Purpose Input Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。你可以让它们输出高电平或低电平,也可以读取外部信号是高还是低。
我个人习惯把 GPIO 比作芯片的「手脚」——输出就是「动手」,输入就是「感知」。今天我们就让 ESP32 动动手、感知一下世界。
- 理解 GPIO 的工作模式(输入 vs 输出)
- 掌握 ESP32 的 GPIO 配置与操作 API
- 完成 LED 闪烁实验(输出)
- 完成按键检测实验(输入 + 消抖)
2.1 GPIO 的基本概念
先看一张图,把 GPIO 的内部结构理清楚。
从图上可以看到,一个 GPIO 引脚内部同时连接着输出通路和输入通路。你写寄存器,它就输出;你读寄存器,它就输入。但同一时刻只能选一种模式。
2.2 硬件准备
做实验前,先把东西准备好。我列个清单:
| 元件 | 数量 | 说明 |
|---|---|---|
| ESP32 开发板 | 1 块 | 推荐 ESP32-DevKitC 或 NodeMCU-32S |
| LED(任意颜色) | 1 个 | 工作电压 2V,电流 10-20mA |
| 电阻 220Ω | 1 个 | LED 限流用 |
| 轻触按键 | 1 个 | 四脚或两脚均可 |
| 电阻 10kΩ | 1 个 | 按键上拉用(也可用内部上拉) |
| 杜邦线 | 若干 | 公对母、公对公 |
| 面包板 | 1 块 | 方便搭电路 |
接线方式很简单:
- LED 实验:GPIO2 → 220Ω 电阻 → LED 阳极 → LED 阴极 → GND
- 按键实验:GPIO0 → 按键一端 → 按键另一端 → GND;GPIO0 同时接 10kΩ 电阻到 3.3V
2.3 LED 闪烁:GPIO 输出实战
先来个经典的「Hello World」——让 LED 闪烁。代码不长,但每个函数都有讲究。
#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)
{
// 1. 配置 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);
// 2. 主循环:闪烁
while (1) {
gpio_set_level(LED_GPIO, 1); // 点亮
vTaskDelay(500 / portTICK_PERIOD_MS);
gpio_set_level(LED_GPIO, 0); // 熄灭
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
这里我重点讲几个地方:
- gpio_config_t 结构体:ESP-IDF 统一用这个结构体配置 GPIO,比逐个寄存器操作清晰得多。
- pin_bit_mask:用位掩码指定要配置的引脚。1ULL << LED_GPIO 表示第 2 位为 1。
- vTaskDelay:FreeRTOS 的延时函数,单位是 tick。portTICK_PERIOD_MS 把毫秒转成 tick。
2.4 按键检测:GPIO 输入实战
输出搞定了,接下来做输入——检测按键是否按下。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define BUTTON_GPIO GPIO_NUM_0
void app_main(void)
{
// 1. 配置 GPIO 为输入模式,启用内部上拉
gpio_config_t io_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
};
gpio_config(&io_conf);
int last_state = 1; // 上一次按键状态,默认高电平
while (1) {
int current_state = gpio_get_level(BUTTON_GPIO);
// 检测下降沿:从高电平变为低电平(按键按下)
if (last_state == 1 && current_state == 0) {
printf("按键按下!\n");
}
last_state = current_state;
vTaskDelay(10 / portTICK_PERIOD_MS); // 10ms 采样一次
}
}
这里有个关键点:按键按下时 GPIO 读到低电平,松开时读到高电平。为什么?因为外部接了上拉电阻到 3.3V,按键按下把引脚拉到 GND。
- 轮询方式:主循环不断读取 GPIO 电平,适合简单场景。
- 中断方式:配置 GPIO 中断,按键触发时自动调用回调函数,效率更高。
2.5 按键消抖:避坑指南
你可能会发现,上面的代码有时会误判——按一次却打印好几条「按键按下」。这就是抖动问题。
机械按键在按下和松开的瞬间,触点会弹跳几次,产生多个脉冲。如果不处理,一次按键会被当成多次。
我曾经在一个项目里吃过这个亏。当时做智能家居面板,按键控制灯光。测试时发现按一次灯闪了好几下,排查了半天才发现是没做消抖。后来老老实实加了消抖逻辑。
消抖方法有两种:
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 软件延时消抖 | 检测到电平变化后,延时 20-50ms 再读一次 | 实现简单,无需额外硬件 | 阻塞延时浪费 CPU |
| 硬件消抖 | 在按键两端并联电容(0.1μF) | 不占用 CPU,响应快 | 增加元件成本 |
| 状态机消抖 | 连续多次采样,状态稳定后才确认 | 非阻塞,可靠 | 代码稍复杂 |
我个人推荐状态机消抖,兼顾可靠性和效率。下面给个示例:
#define DEBOUNCE_TIME_MS 30 // 消抖时间 30ms
#define SAMPLE_INTERVAL_MS 5 // 采样间隔 5ms
void app_main(void)
{
gpio_config_t io_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
};
gpio_config(&io_conf);
int stable_state = 1; // 稳定后的状态
int temp_state = 1; // 临时状态
int count = 0; // 连续采样计数
int required = DEBOUNCE_TIME_MS / SAMPLE_INTERVAL_MS; // 需要连续采样次数
while (1) {
int level = gpio_get_level(BUTTON_GPIO);
if (level == temp_state) {
count++;
if (count >= required) {
// 状态稳定,更新
if (stable_state != temp_state) {
stable_state = temp_state;
if (stable_state == 0) {
printf("按键按下(已消抖)\n");
}
}
count = required; // 防止溢出
}
} else {
// 电平变化,重置计数
temp_state = level;
count = 0;
}
vTaskDelay(SAMPLE_INTERVAL_MS / portTICK_PERIOD_MS);
}
}
2.6 常见问题与排查
做实验时可能会遇到各种问题,我把常见的列出来:
- LED 不亮:检查接线极性(LED 长脚接正极),检查 GPIO 号是否正确,检查代码是否调用了 gpio_config。
- LED 亮度异常:限流电阻阻值不对。220Ω 对应约 10mA,如果太暗可以换成 100Ω,但别低于 50Ω。
- 按键检测不到:确认上拉电阻接好,或者启用内部上拉。用万用表量一下引脚电压。
- 按键误触发:没做消抖,或者消抖时间太短。
- 程序下载后没反应:检查串口监视器波特率是否设为 115200,检查是否按了开发板上的 EN 复位键。
2.7 本章小结
今天我们完成了两件事:让 LED 闪烁(输出),检测按键(输入)。看起来简单,但 GPIO 是所有外设驱动的基础。后面讲 I2C、SPI、UART,本质上都是在操作 GPIO,只是加了协议层。
嗯,这里要提醒一句:别觉得简单就跳过动手环节。我见过太多人看完教程觉得懂了,真到写代码时连 gpio_config 的参数都记不全。一定要自己敲一遍代码,跑通实验。