2. ESP32基础外设控制:GPIO控制LED、按键输入检测、PWM呼吸灯、ADC模拟量读取

好,咱们进入第二章。这一章我打算带大家把ESP32的四个最基础、也最常用的外设功能过一遍。说白了,就是让这块芯片能「看、听、说、动」起来。你想想看,一个单片机如果连个灯都点不亮,那还谈什么物联网?

我个人习惯,学任何新平台,第一件事就是玩GPIO。为什么?因为这是硬件和软件交互的「握手礼」。你写一行代码,板子上的灯就亮了——这种即时反馈,能让你快速建立信心。好,咱们开始。

2.1 GPIO控制LED:让灯亮起来

ESP32的GPIO引脚,大部分都可以直接输出高低电平。驱动一个LED,电路很简单:一个GPIO口串一个220Ω的限流电阻,再接LED到GND。

嗯,这里要注意:ESP32的GPIO输出能力有限,单个引脚最大输出40mA。你要是想驱动大功率的灯,比如12V的LED灯带,那必须加三极管或者MOS管驱动。我在项目中遇到过有人直接拿GPIO去推继电器,结果烧了引脚——这就是典型的「想当然」错误。

代码示例,用Arduino框架写,最直观:

void setup() {
  pinMode(2, OUTPUT);  // 板载LED通常接GPIO2
}

void loop() {
  digitalWrite(2, HIGH);  // 点亮
  delay(1000);            // 等1秒
  digitalWrite(2, LOW);   // 熄灭
  delay(1000);
}

这段代码跑起来,你会看到板子上的LED一秒一闪。如果你用的是其他引脚,记得查一下原理图。我建议新手先拿板载LED练手,省得接线出错。

小技巧: 如果你想让多个LED同时亮灭,可以用一个字节的位操作来控制。比如 GPIO.out_w1ts = (1 << 2) | (1 << 4); 这样一次操作多个引脚,效率高很多。

2.2 按键输入检测:让芯片「听」你的

按键输入,本质上是检测GPIO的电平变化。但这里有个坑——机械按键按下和松开时,会产生抖动。你想想看,一个按键按下去,实际上会在几毫秒内产生几十次高低电平的跳变。如果不做处理,程序会误判为按了多次。

我曾经在一个工业控制面板项目里,就因为没做消抖,导致按键按一次,屏幕跳了三个菜单。后来被客户投诉,连夜改代码。所以,消抖不是可选项,是必选项。

硬件消抖:加一个0.1μF的电容并联在按键两端。软件消抖:检测到电平变化后,延时20ms再读一次。我一般用软件消抖,省成本:

const int buttonPin = 0;  // 很多开发板用GPIO0作为BOOT按键
int lastState = HIGH;
int currentState;

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);  // 内部上拉,省去外部电阻
  Serial.begin(115200);
}

void loop() {
  currentState = digitalRead(buttonPin);
  if (lastState == HIGH && currentState == LOW) {
    delay(20);  // 消抖
    if (digitalRead(buttonPin) == LOW) {
      Serial.println("按键被按下");
    }
  }
  lastState = currentState;
}
警告: 不要用 delay() 做长时间消抖,它会阻塞整个程序。在正式项目中,建议用定时器或者状态机来做消抖。我后面讲RTOS时会详细说。

2.3 PWM呼吸灯:让LED「活」起来

PWM,脉冲宽度调制。说白了,就是通过快速开关引脚,改变「开」的时间比例,来模拟不同的电压。人眼有视觉暂留效应,只要频率够高(一般1kHz以上),你就看不出闪烁,只看到亮度变化。

ESP32的LEDC外设,专门用来做PWM。它有16个通道,可以独立设置频率和占空比。我习惯把频率设在5kHz,这样既听不到啸叫,又能平滑调光。

呼吸灯的效果,就是让LED从暗到亮,再从亮到暗,循环往复。代码实现:

const int ledPin = 2;
const int freq = 5000;
const int ledChannel = 0;
const int resolution = 8;  // 8位分辨率,0-255

void setup() {
  ledcSetup(ledChannel, freq, resolution);
  ledcAttachPin(ledPin, ledChannel);
}

void loop() {
  for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {
    ledcWrite(ledChannel, dutyCycle);
    delay(5);
  }
  for (int dutyCycle = 255; dutyCycle >= 0; dutyCycle--) {
    ledcWrite(ledChannel, dutyCycle);
    delay(5);
  }
}

你运行一下,会看到LED像呼吸一样慢慢变亮再变暗。这个效果在智能家居的指示灯上很常见。我记得有一次做智能床头灯,客户要求灯光像日出一样慢慢亮起——用的就是PWM加延时控制。

重点: PWM的占空比分辨率越高,亮度变化越细腻。8位分辨率有256级,12位有4096级。但分辨率越高,频率上限就越低。你需要根据实际需求权衡。

2.4 ADC模拟量读取:让芯片「感觉」世界

ADC,模数转换器。ESP32有两个12位的ADC,可以读取0-3.3V的模拟电压,转换成0-4095的数字值。你想想看,温度传感器、光敏电阻、电位器……这些模拟信号,都得靠ADC来「翻译」成数字信号。

但ESP32的ADC有个众所周知的毛病——非线性。尤其是在低电压段(0-0.1V)和高电压段(3.2-3.3V),误差比较大。我曾经做一个电池电压监测项目,发现ADC读出来的值比实际电压低了0.15V。后来查资料才知道,ESP32的ADC需要做校准。

最简单的校准方法:用万用表测出实际电压,然后和ADC读数做对比,建立一个线性映射表。或者直接用官方的ADC校准库 esp_adc_cal

读取模拟值的代码:

const int adcPin = 34;  // ADC1通道6,GPIO34

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);  // 设置12位分辨率
}

void loop() {
  int adcValue = analogRead(adcPin);
  float voltage = adcValue * (3.3 / 4095.0);
  Serial.print("ADC值: ");
  Serial.print(adcValue);
  Serial.print(",电压: ");
  Serial.println(voltage);
  delay(500);
}
经验之谈: 如果你需要高精度的模拟量采集,建议在ADC输入端加一个RC低通滤波器,截止频率设在100Hz左右。这样可以滤掉高频噪声。另外,ADC的参考电压是3.3V,但实际可能略有偏差,最好用万用表测一下VREF引脚的实际电压。

2.5 综合实战:做一个智能感应灯

好,咱们把上面四个知识点串起来,做一个简单的智能感应灯。功能:

  • 用光敏电阻检测环境光(ADC读取)
  • 用按键切换模式(自动/手动)
  • 用PWM控制LED亮度
  • 环境光暗时自动亮灯,亮时自动灭灯

这个项目虽然简单,但涵盖了传感器输入、用户交互、执行器输出三个核心环节。你做完这个,基本上就掌握了ESP32外设控制的基本套路。

代码框架我留给你自己填充,但给你几个提示:

  1. ADC读取光敏电阻电压,阈值设在1.5V左右(根据实际环境调整)
  2. 按键用中断方式检测,避免轮询占用CPU
  3. PWM占空比根据环境光强度线性调节,实现平滑过渡
注意: ESP32的ADC2在Wi-Fi开启时会被占用,所以如果你同时要用Wi-Fi,请使用ADC1的引脚(GPIO32-GPIO39)。这个坑我踩过,调试了一下午才发现是Wi-Fi和ADC冲突。

好了,这一章的内容就到这里。四个基础外设:GPIO输出、按键输入、PWM、ADC。你把这些玩熟了,后面讲传感器、执行器、通信协议,都会轻松很多。下一章,咱们开始讲ESP32的Wi-Fi连接,让这块板子真正「上网」。