3、GPIO与LED控制:从点亮一颗灯到读懂按键
GPIO,说白了就是芯片的「手脚」。你要让芯片对外界做点什么,或者感知外界发生了什么,都得靠它。这一章我们就把ESP32的GPIO玩明白——从输出控制LED,到输入检测按键。嗯,这都是嵌入式开发最基础、也最常用的操作。
- GPIO初始化配置(管脚、模式、上下拉)
- 输出控制:设置高低电平
- LED闪烁实验:延时与状态翻转
- 输入检测:按键消抖与电平读取
3.1 GPIO初始化——别小看这一步
我见过不少新手,上来就直接调用gpio_set_level(),结果灯死活不亮。为什么?因为GPIO默认可能是输入模式,或者管脚复用被占用了。初始化这一步,真的不能省。
在ESP-IDF里,初始化一个GPIO通常分两步:
- 配置结构体:用
gpio_config_t把管脚号、模式、上下拉、中断类型都填好。 - 调用函数:
gpio_config(&io_conf)一次性写入硬件寄存器。
我个人习惯把GPIO初始化封装成一个函数,比如led_init()、button_init(),这样代码结构清晰,后期维护也方便。
3.2 输出控制——让LED亮起来
初始化完成后,控制输出就简单了。两个函数搞定:
gpio_set_level(gpio_num, level):设置高电平(1)或低电平(0)gpio_get_level(gpio_num):读取当前电平(输出模式下也能读)
LED的接法有两种常见方式:
| 接法 | GPIO输出高电平 | GPIO输出低电平 | 注意事项 |
|---|---|---|---|
| LED正极接GPIO,负极接GND | LED亮 | LED灭 | GPIO输出电流有限,记得串限流电阻(约220Ω) |
| LED正极接VCC,负极接GPIO | LED灭 | LED亮 | 这种叫「低电平有效」,逻辑是反的 |
我个人更推荐第一种接法,逻辑直观,代码读起来也顺。你想想看,gpio_set_level(LED_PIN, 1)就是亮,多清楚。
3.3 LED闪烁实验——你的第一个「动态」程序
闪烁的本质就是:亮→延时→灭→延时→循环。代码写起来很简单,但这里有个关键点——延时函数的选择。
ESP-IDF里常用的延时方式:
vTaskDelay(pdMS_TO_TICKS(ms)):FreeRTOS任务延时,会释放CPU,推荐使用esp_rom_delay_us(us):忙等待,精度高但占用CPU,短延时用delay(ms):Arduino风格,ESP-IDF里没有,别搞混了
来看一个完整的LED闪烁示例:
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define LED_GPIO GPIO_NUM_2
void led_init(void) {
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);
}
void app_main(void) {
led_init();
while (1) {
gpio_set_level(LED_GPIO, 1); // 亮
vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
gpio_set_level(LED_GPIO, 0); // 灭
vTaskDelay(pdMS_TO_TICKS(500));
}
}
3.4 GPIO输入与按键检测——从「输出」到「输入」
输出玩明白了,接下来看输入。按键检测是输入最典型的应用。但这里有个大坑——机械抖动。
你按一下按键,实际上在几毫秒内会反复弹跳多次,如果不处理,程序可能一次按下检测到十几次触发。这就是为什么需要「消抖」。
3.4.1 硬件消抖 vs 软件消抖
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 硬件消抖 | 在按键两端并联电容(通常0.1μF) | 不占CPU,响应快 | 增加BOM成本,PCB面积 |
| 软件消抖 | 检测到电平变化后,延时10~20ms再读一次 | 零成本,灵活 | 占用CPU(可用定时器优化) |
我个人习惯用软件消抖,毕竟省成本。但如果你做的是工业级产品,硬件消抖更可靠。
3.4.2 按键检测的完整代码
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define BUTTON_GPIO GPIO_NUM_0
void button_init(void) {
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << BUTTON_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, // 内部上拉,按键接GND
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_conf);
}
int button_read(void) {
if (gpio_get_level(BUTTON_GPIO) == 0) { // 按下为低电平
vTaskDelay(pdMS_TO_TICKS(20)); // 消抖延时
if (gpio_get_level(BUTTON_GPIO) == 0) {
return 1; // 确认按下
}
}
return 0;
}
void app_main(void) {
button_init();
while (1) {
if (button_read()) {
printf("Button pressed!\n");
}
vTaskDelay(pdMS_TO_TICKS(10)); // 主循环不要太快
}
}
3.5 本章知识结构图
下面这张图把GPIO的核心操作串起来了,从初始化到输出控制、输入检测,一目了然。
3.6 避坑指南——我踩过的那些GPIO坑
- 管脚复用冲突:ESP32很多管脚有多个功能,比如UART、SPI、ADC。如果你初始化了GPIO,但外设驱动也占用了同一个管脚,结果就是谁都用不了。我建议在初始化前先调用
gpio_reset_pin()清一下配置。 - 上下拉电阻没配:按键输入如果不配上拉,管脚悬空时电平不确定,读到的值会乱跳。内部上拉虽然方便,但阻值约45kΩ,抗干扰能力不如外部4.7kΩ电阻。工业环境我建议用外部上拉。
- 主循环太快:如果
while(1)里不加延时,CPU会跑满,按键消抖也容易失效。我一般会在主循环末尾加个vTaskDelay(10),既省电又稳定。 - 中断服务函数里别做复杂操作:虽然本章没讲中断,但提前提醒一句——ISR里别调用
printf()、vTaskDelay()这些阻塞函数,否则系统会崩溃。我当年调试时吃过这个亏,查了两天才发现是ISR里打了日志。
GPIO是嵌入式开发的「基本功」。输出控制LED让你看到程序的「存在感」,输入检测按键让你感知外部世界。这两者结合起来,就能做出很多有意思的东西——比如按键控制LED开关、按键切换闪烁频率等等。
下一章我们会把这些基础操作组合起来,做一个完整的项目。嗯,先别急,把这一章的代码亲手跑一遍,感受一下GPIO的「手感」。