3、GPIO基础与LED控制:ESP32的GPIO引脚功能、数字输出控制LED、PWM调光、按键输入检测
好,咱们进入第三章。这一章是动手实践的第一步,也是嵌入式开发最基础、最核心的内容——GPIO。
说白了,GPIO就是芯片和外界打交道的「手脚」。你让它输出高电平,它就输出3.3V;你让它检测输入,它就能知道外面是0V还是3.3V。ESP32有34个GPIO引脚,但不是所有引脚都能随便用。嗯,这里要注意,有些引脚有特殊功能,比如接Flash的、接晶振的,你乱用的话程序可能跑不起来。
3.1 ESP32的GPIO引脚功能概览
ESP32的GPIO引脚,大部分是「多功能复用」的。什么意思?就是同一个引脚,你可以把它配置成普通的数字输入输出,也可以配置成ADC模拟输入、触摸传感器、PWM输出、I2C、SPI、UART等等。
我个人习惯,在设计电路板之前,先把引脚功能表打印出来贴在墙上。因为一旦焊好板子发现引脚冲突,那真是欲哭无泪。
下面这张表,是我整理的最常用的GPIO引脚及其注意事项:
| GPIO编号 | 功能说明 | 注意事项 |
|---|---|---|
| GPIO0 | 数字I/O、ADC2_CH1、触摸 | 上电时拉低会进入下载模式,慎用 |
| GPIO2 | 数字I/O、ADC2_CH2、触摸 | 上电时拉低会影响启动,我踩过这个坑 |
| GPIO4 | 数字I/O、ADC2_CH0、触摸 | 比较安全,常用 |
| GPIO5 | 数字I/O、VSPI_CS | 上电时拉低会输出高电平,注意 |
| GPIO12 | 数字I/O、ADC2_CH5、触摸 | 上电时电平决定Flash电压,别乱动 |
| GPIO13 | 数字I/O、ADC2_CH4、触摸 | 比较安全,常用 |
| GPIO14 | 数字I/O、ADC2_CH6、触摸 | 比较安全,常用 |
| GPIO15 | 数字I/O、ADC2_CH3、触摸 | 上电时拉低会输出配置信息,注意 |
| GPIO16/17 | 数字I/O | 常用于RTC、PSRAM,没问题 |
| GPIO18/19/21/22/23 | 数字I/O | 非常安全,随便用 |
| GPIO25/26/27 | 数字I/O、DAC、ADC | 比较安全,常用 |
| GPIO32/33 | 数字I/O、ADC1 | 非常安全,推荐用于按键 |
| GPIO34/35/36/39 | 仅输入、ADC1 | 不能输出!只能读输入,切记 |
3.2 数字输出控制LED——点亮你的第一盏灯
好了,理论说完了,咱们直接上手。点亮一个LED,是嵌入式界的「Hello World」。你想想看,当那个小灯亮起来的时候,那种成就感是无可替代的。
硬件连接很简单:LED正极接一个220Ω限流电阻,电阻另一端接GPIO2,LED负极接GND。
为什么用220Ω?ESP32的GPIO输出高电平是3.3V,LED正向压降大约2V,电流控制在5~10mA比较安全。220Ω算下来电流大约(3.3-2)/220 ≈ 6mA,刚刚好。
代码怎么写?看下面:
#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_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
while (1) {
// 输出高电平,点亮LED
gpio_set_level(LED_GPIO, 1);
printf("LED ON\n");
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延时1秒
// 输出低电平,熄灭LED
gpio_set_level(LED_GPIO, 0);
printf("LED OFF\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
这段代码逻辑很直白:配置引脚、循环翻转电平。你编译下载后,应该能看到LED以1秒间隔闪烁。
3.3 PWM调光——让LED呼吸起来
单纯的亮灭太单调了。咱们来点高级的——PWM调光。PWM就是脉冲宽度调制,说白了就是快速开关LED,通过调整「开」的时间比例(占空比)来改变亮度。
ESP32的PWM是通过LEDC(LED控制)外设实现的。它支持16个通道,频率和分辨率都可调。
我个人习惯,调光频率设在1kHz左右,分辨率8位(0~255)。频率太低会看到闪烁,频率太高又浪费CPU资源。
来看一个呼吸灯的例子:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"
#define LED_GPIO GPIO_NUM_2
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_DUTY_RES LEDC_TIMER_8_BIT // 8位分辨率
#define LEDC_FREQUENCY 1000 // 1kHz
void app_main(void)
{
// 配置定时器
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
// 配置通道
ledc_channel_config_t channel_conf = {
.gpio_num = LED_GPIO,
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&channel_conf);
int duty = 0;
int step = 5;
while (1) {
// 渐亮
for (duty = 0; duty <= 255; duty += step) {
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
vTaskDelay(10 / portTICK_PERIOD_MS);
}
// 渐暗
for (duty = 255; duty >= 0; duty -= step) {
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
}
这段代码会让LED从暗到亮、再从亮到暗,循环往复,就像呼吸一样自然。
3.4 按键输入检测——让ESP32「听」你的指令
光会输出不行,还得会输入。按键是最简单的输入设备。
硬件连接:按键一端接GPIO32,另一端接GND。同时GPIO32需要接一个10kΩ上拉电阻到3.3V。这样按键没按下时,GPIO32读到高电平;按下时,读到低电平。
为什么需要上拉电阻?因为GPIO在悬空状态时电平不确定,外界一点干扰就会误触发。上拉电阻给它一个确定的默认状态。
ESP32内部有上拉电阻,可以省掉外部电阻。但说实话,我建议外部还是加上,因为内部上拉电阻值大约45kΩ,抗干扰能力不如外部10kΩ强。
来看代码:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define BUTTON_GPIO GPIO_NUM_32
#define LED_GPIO GPIO_NUM_2
void app_main(void)
{
// 配置按键引脚为输入,使能内部上拉
gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);
gpio_set_pull_mode(BUTTON_GPIO, GPIO_PULLUP_ONLY);
// 配置LED引脚为输出
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
int led_state = 0;
int last_button_state = 1; // 上一次按键状态,默认高电平
while (1) {
int button_state = gpio_get_level(BUTTON_GPIO);
// 检测下降沿:从高电平变为低电平
if (last_button_state == 1 && button_state == 0) {
// 延时20ms消抖
vTaskDelay(20 / portTICK_PERIOD_MS);
button_state = gpio_get_level(BUTTON_GPIO);
if (button_state == 0) {
// 确认按键按下,翻转LED状态
led_state = !led_state;
gpio_set_level(LED_GPIO, led_state);
printf("Button pressed! LED state: %d\n", led_state);
}
}
last_button_state = button_state;
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
这里有个关键点——消抖。机械按键在按下和释放的瞬间,会产生多次抖动,如果不处理,一次按下可能会被误判成多次。我用的方法是:检测到下降沿后,等20ms再读一次,如果还是低电平,才确认是有效按下。
3.5 综合实战:按键控制LED亮度
把上面的知识串起来,做一个实用的小项目:按一下按键,LED亮度增加一档,共4档(0%、33%、66%、100%),循环切换。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#define BUTTON_GPIO GPIO_NUM_32
#define LED_GPIO GPIO_NUM_2
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_DUTY_RES LEDC_TIMER_8_BIT
#define LEDC_FREQUENCY 1000
// 4档亮度对应的占空比
const int duty_levels[4] = {0, 85, 170, 255};
void app_main(void)
{
// 配置LEDC
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
ledc_channel_config_t channel_conf = {
.gpio_num = LED_GPIO,
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&channel_conf);
// 配置按键
gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);
gpio_set_pull_mode(BUTTON_GPIO, GPIO_PULLUP_ONLY);
int level_index = 0;
int last_button_state = 1;
while (1) {
int button_state = gpio_get_level(BUTTON_GPIO);
if (last_button_state == 1 && button_state == 0) {
vTaskDelay(20 / portTICK_PERIOD_MS);
button_state = gpio_get_level(BUTTON_GPIO);
if (button_state == 0) {
// 切换到下一档
level_index = (level_index + 1) % 4;
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty_levels[level_index]);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
printf("Brightness level: %d/3\n", level_index);
}
}
last_button_state = button_state;
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
这个项目虽然简单,但包含了嵌入式开发的三个核心要素:输入检测、输出控制、状态管理。你想想看,从点亮一盏灯,到用按键控制灯的亮度,这中间其实已经走过了嵌入式开发的完整流程。
- ESP32的GPIO引脚功能丰富,但要注意特殊引脚的限制
- 数字输出用
gpio_set_level(),控制LED亮灭 - PWM调光用LEDC外设,实现呼吸灯效果
- 按键输入要消抖,20ms延时是常用方法
- 综合起来,就能做出实用的交互控制
下一章,我们会深入ADC和传感器,让ESP32能「感知」模拟世界的温度、光照、声音。准备好了吗?咱们继续。