2、ESP32基础外设:GPIO控制、数字输入输出、按键检测与消抖、PWM呼吸灯
好,咱们正式开始动手了。这一章我带你搞定ESP32最基础、也最常用的几个外设功能。说白了,就是让芯片跟外界“说话”——要么输出信号控制东西,要么读取外部信号判断状态。
我个人习惯,学一个新平台,第一件事就是玩转GPIO。这就像学编程先学“Hello World”一样。ESP32的GPIO功能很强大,但坑也不少。咱们一步步来。
2.1 GPIO基础与引脚布局
ESP32芯片有34个GPIO引脚,编号从GPIO0到GPIO33。但注意,不是所有引脚都能随便用。我刚开始用的时候,就踩过这个坑——随便接了个引脚,结果程序死活跑不起来。
常用的安全引脚有这些:
| 引脚编号 | 功能说明 | 注意事项 |
|---|---|---|
| GPIO4、GPIO5 | 普通IO,ADC可用 | 安全,随便用 |
| GPIO16、GPIO17 | 普通IO,RTC域 | 可用于深度睡眠唤醒 |
| GPIO18、GPIO19 | 普通IO,SPI可用 | 注意复用功能 |
| GPIO21、GPIO22 | I2C专用 | 做普通IO也可以 |
| GPIO23、GPIO25 | 普通IO,DAC可用 | GPIO25是DAC1 |
2.2 数字输出:点亮LED
咱们先来个最简单的——控制LED亮灭。ESP32的GPIO输出电平是3.3V,驱动能力大概40mA。直接驱动一个LED没问题,但记得串个电阻限流。
看代码:
// 定义LED引脚
#define LED_PIN 2
void setup() {
// 设置引脚为输出模式
pinMode(LED_PIN, OUTPUT);
}
void loop() {
// 点亮LED
digitalWrite(LED_PIN, HIGH);
delay(1000);
// 熄灭LED
digitalWrite(LED_PIN, LOW);
delay(1000);
}
这段代码很简单,对吧?但我想说的是,pinMode()这个函数其实做了不少事。它不光设置方向,还会配置引脚的上下拉电阻、驱动能力等参数。ESP32默认是浮空输入,所以用之前一定要先设好模式。
OUTPUT(推挽输出)和OUTPUT_OPEN_DRAIN(开漏输出)。推挽输出能同时输出高低电平,开漏输出只能拉低,高电平要靠外部上拉电阻。我一般用推挽输出,除非要做I2C这种需要线与逻辑的场合。
2.3 数字输入:读取按键状态
输出搞定了,咱们看看输入。读取按键状态是嵌入式开发的基本功。ESP32读取数字输入也很简单:
#define BUTTON_PIN 0
void setup() {
Serial.begin(115200);
// 设置输入模式,并启用内部上拉电阻
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
int buttonState = digitalRead(BUTTON_PIN);
if (buttonState == LOW) {
Serial.println("按键按下");
} else {
Serial.println("按键松开");
}
delay(100);
}
这里我用了INPUT_PULLUP模式。为什么?因为按键电路通常是一端接地,另一端接GPIO。按下时引脚接地,读到低电平;松开时引脚悬空,电平不确定。加上内部上拉电阻,松开时引脚就被拉到高电平了。
2.4 按键消抖:别让毛刺坑了你
嗯,这里要注意。直接读按键会有一个大问题——抖动。机械按键按下和松开的瞬间,触点会弹跳几次,产生多个脉冲。如果不处理,一次按键会被误认为多次。
为什么会这样?因为机械触点的物理特性。你想想看,两个金属片碰撞,不可能一下子就稳定接触。这个弹跳时间大概5-20ms。
我分享两种消抖方法:
方法一:软件延时消抖
#define BUTTON_PIN 0
#define DEBOUNCE_TIME 50 // 消抖时间50ms
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
static int lastState = HIGH;
static unsigned long lastDebounceTime = 0;
int reading = digitalRead(BUTTON_PIN);
// 检测到电平变化,记录时间
if (reading != lastState) {
lastDebounceTime = millis();
}
// 如果电平稳定超过消抖时间,才认为有效
if ((millis() - lastDebounceTime) > DEBOUNCE_TIME) {
if (reading == LOW) {
Serial.println("按键按下(已消抖)");
}
}
lastState = reading;
}
方法二:定时采样法
#define BUTTON_PIN 0
#define SAMPLE_INTERVAL 10 // 每10ms采样一次
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
static int stableState = HIGH;
static int lastReading = HIGH;
static unsigned long lastSampleTime = 0;
// 定时采样
if (millis() - lastSampleTime >= SAMPLE_INTERVAL) {
lastSampleTime = millis();
int reading = digitalRead(BUTTON_PIN);
// 连续两次采样相同,才更新状态
if (reading == lastReading && reading != stableState) {
stableState = reading;
if (stableState == LOW) {
Serial.println("按键按下(定时采样消抖)");
}
}
lastReading = reading;
}
// 其他任务...
}
delay()会卡住整个程序,这在做IoT设备时很致命——你还要同时处理WiFi、MQTT等任务。定时采样法用状态机思想,不影响其他任务运行。
delay(50)阻塞了WiFi任务。从那以后,我再也不用阻塞式消抖了。
2.5 PWM呼吸灯:让LED渐变
呼吸灯效果,说白了就是让LED亮度慢慢变化。这需要用到PWM(脉冲宽度调制)。ESP32的LEDC控制器可以输出PWM信号,精度很高。
ESP32的LEDC有8个通道,每个通道可以独立设置频率和占空比。我一般这样配置:
#define LED_PIN 2
#define PWM_CHANNEL 0
#define PWM_FREQ 5000 // 5kHz
#define PWM_RESOLUTION 8 // 8位分辨率,0-255
void setup() {
// 配置LEDC
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
// 将PWM通道绑定到GPIO引脚
ledcAttachPin(LED_PIN, PWM_CHANNEL);
}
void loop() {
// 呼吸灯:从暗到亮
for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(10);
}
// 从亮到暗
for (int dutyCycle = 255; dutyCycle >= 0; dutyCycle--) {
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(10);
}
}
这段代码会让LED像呼吸一样慢慢变亮再慢慢变暗。注意ledcSetup()的三个参数:
- 通道号:0-7,共8个独立通道
- 频率:5kHz对人眼来说足够快,不会看到闪烁
- 分辨率:8位就是0-255共256级亮度
你可能会问,为什么不用analogWrite()?嗯,ESP32的Arduino核心确实支持analogWrite(),但它底层也是调用的LEDC。我个人习惯直接用LEDC API,控制更灵活——比如可以随时改频率、改通道绑定。
2.6 综合实战:按键控制呼吸灯
咱们把今天学的串起来——用按键切换呼吸灯的模式。按一下呼吸,再按一下常亮,再按一下关闭。
#define LED_PIN 2
#define BUTTON_PIN 0
#define PWM_CHANNEL 0
#define PWM_FREQ 5000
#define PWM_RESOLUTION 8
enum Mode { BREATH, ON, OFF };
Mode currentMode = BREATH;
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
ledcAttachPin(LED_PIN, PWM_CHANNEL);
}
void loop() {
static int lastStableState = HIGH;
static int lastReading = HIGH;
static unsigned long lastSampleTime = 0;
// 定时采样消抖
if (millis() - lastSampleTime >= 10) {
lastSampleTime = millis();
int reading = digitalRead(BUTTON_PIN);
if (reading == lastReading && reading != lastStableState) {
lastStableState = reading;
if (lastStableState == LOW) {
// 切换模式
currentMode = (Mode)((currentMode + 1) % 3);
Serial.print("当前模式: ");
Serial.println(currentMode);
}
}
lastReading = reading;
}
// 根据模式控制LED
static int dutyCycle = 0;
static int direction = 1;
switch (currentMode) {
case BREATH:
ledcWrite(PWM_CHANNEL, dutyCycle);
dutyCycle += direction * 5;
if (dutyCycle >= 255 || dutyCycle <= 0) direction = -direction;
delay(20);
break;
case ON:
ledcWrite(PWM_CHANNEL, 255);
break;
case OFF:
ledcWrite(PWM_CHANNEL, 0);
break;
}
}
2.7 本章小结
今天咱们把ESP32的GPIO基础玩了个遍。从数字输出控制LED,到数字输入读取按键,再到按键消抖和PWM呼吸灯。这些看似基础,但却是所有IoT项目的基石。
我建议你动手试试:
- 先跑通LED闪烁,确认开发板没问题
- 接个按键,试试两种消抖方法的效果差异
- 调调PWM频率和分辨率,看看对呼吸灯效果的影响
- 把综合实战的代码烧进去,感受一下状态机的魅力
下一章咱们要玩点更高级的——ADC模拟量采集和传感器读取。到时候你会看到,这些GPIO基础操作会跟传感器数据采集紧密结合。嗯,做好准备,后面越来越有意思了。
公众号:蓝海资料掘金营,微信deep3321