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只能输入,不能输出 |
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);
}
}
这段代码的核心逻辑:
- 检测到电平从高变低(下降沿),说明可能有人按了按键
- 等20ms,再读一次电平
- 如果还是低电平,说明不是抖动,是真的按下了
- 翻转LED状态
本章知识结构
我把这一章的核心逻辑画成了一张图,方便你理解GPIO操作的完整流程:
这张图把GPIO操作分成了清晰的路径:配置→选择模式→执行操作→循环。不管是控制LED还是读取按键,都走这个流程。
常见问题与避坑
最后,分享几个我实际踩过的坑:
- 引脚不够用?别打GPIO6-11的主意,那是Flash的命根子。我见过有人硬要用,结果程序下载完就跑不起来。
- LED不亮?先检查极性。LED长脚是正极,接GPIO;短脚是负极,接GND。反了不会烧,但不亮。
- 按键误触发?大概率是没做消抖。别偷懒,20ms延时花不了多少CPU时间。
- GPIO34-39不能输出?这是硬件限制,只能做输入。选引脚前看一眼数据手册。
好了,这一章的内容就到这。你先把LED闪烁跑起来,再试试按键控制。动手做一遍,比看十遍都管用。