2、ESP32基础外设:GPIO控制、数字输入输出、按键检测与消抖、PWM呼吸灯

好,咱们正式开始动手了。这一章我带你搞定ESP32最基础、也最常用的几个外设功能。说白了,就是让芯片跟外界“说话”——要么输出信号控制东西,要么读取外部信号判断状态。

我个人习惯,学一个新平台,第一件事就是玩转GPIO。这就像学编程先学“Hello World”一样。ESP32的GPIO功能很强大,但坑也不少。咱们一步步来。

2.1 GPIO基础与引脚布局

ESP32芯片有34个GPIO引脚,编号从GPIO0到GPIO33。但注意,不是所有引脚都能随便用。我刚开始用的时候,就踩过这个坑——随便接了个引脚,结果程序死活跑不起来。

⚠️ 重要提醒: ESP32的某些引脚有特殊功能,比如GPIO6-11连接了Flash芯片,不能用作普通IO。GPIO0、GPIO2、GPIO12等引脚在启动时有特殊作用,使用时要格外小心。

常用的安全引脚有这些:

引脚编号 功能说明 注意事项
GPIO4、GPIO5 普通IO,ADC可用 安全,随便用
GPIO16、GPIO17 普通IO,RTC域 可用于深度睡眠唤醒
GPIO18、GPIO19 普通IO,SPI可用 注意复用功能
GPIO21、GPIO22 I2C专用 做普通IO也可以
GPIO23、GPIO25 普通IO,DAC可用 GPIO25是DAC1
💡 我的经验: 项目初期,我建议只用GPIO4、5、16、17、18、19、21、22、23、25、26、27、32、33这14个引脚。这些引脚基本没有启动限制,用起来最省心。

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默认是浮空输入,所以用之前一定要先设好模式。

🔑 关键点: ESP32的GPIO输出模式有两种: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。按下时引脚接地,读到低电平;松开时引脚悬空,电平不确定。加上内部上拉电阻,松开时引脚就被拉到高电平了。

💡 小技巧: ESP32内部上拉电阻大概45kΩ,下拉电阻也是45kΩ。如果你需要更强的上拉/下拉能力,可以外接10kΩ电阻。我在做低功耗项目时,就喜欢用内部上拉,省一个电阻。

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等任务。定时采样法用状态机思想,不影响其他任务运行。
⚠️ 曾经踩过的坑: 我曾经在一个项目中用了延时消抖,结果按键按下去后,WiFi连接就断了。查了半天才发现是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级亮度
💡 经验之谈: 分辨率越高,亮度变化越细腻。但分辨率高了,能支持的最高频率就会降低。比如16位分辨率下,最高频率只有几kHz。我一般用8位或10位分辨率,频率设在1-5kHz之间,效果已经很好了。

你可能会问,为什么不用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;
  }
}
🔑 核心思路: 这个综合示例展示了嵌入式开发的典型模式——非阻塞状态机。按键检测用定时采样,不阻塞;呼吸灯用变量累加,也不阻塞。整个程序可以随时响应其他任务,比如后面要讲的WiFi连接。

2.7 本章小结

今天咱们把ESP32的GPIO基础玩了个遍。从数字输出控制LED,到数字输入读取按键,再到按键消抖和PWM呼吸灯。这些看似基础,但却是所有IoT项目的基石。

我建议你动手试试:

  1. 先跑通LED闪烁,确认开发板没问题
  2. 接个按键,试试两种消抖方法的效果差异
  3. 调调PWM频率和分辨率,看看对呼吸灯效果的影响
  4. 把综合实战的代码烧进去,感受一下状态机的魅力

下一章咱们要玩点更高级的——ADC模拟量采集和传感器读取。到时候你会看到,这些GPIO基础操作会跟传感器数据采集紧密结合。嗯,做好准备,后面越来越有意思了。


公众号:蓝海资料掘金营,微信deep3321