3、ESP32基础外设控制:GPIO数字输出控制LED,模拟输入读取电位器,PWM输出控制LED亮度,使用millis()实现非阻塞延时

好,咱们进入第三节课。这节课的内容,说白了就是ESP32最基础、最常用的几个外设操作。你想想看,不管多复杂的智能音箱,最终都要去控制硬件——要么点亮一个指示灯,要么读取一个传感器数值,要么调节一个电机的转速。

我个人习惯,每接触一个新平台,第一件事就是玩转它的GPIO。这就像学编程先学“Hello World”一样,是基本功。今天咱们就把ESP32的三大基础操作:数字输出、模拟输入、PWM输出,一次性讲透。另外,我还会教你一个非常实用的编程技巧——用millis()实现非阻塞延时。这个技巧,能让你从“卡死”的循环中解放出来。

3.1 GPIO数字输出:点亮你的第一盏LED

先来个最简单的。GPIO数字输出,就是让某个引脚输出高电平(3.3V)或低电平(0V)。在Arduino框架下,用digitalWrite()函数就能搞定。

我在项目中遇到过一个问题:有同学直接把LED正极接在GPIO引脚上,负极接地,结果LED不亮。为什么?因为ESP32的GPIO驱动能力有限,直接驱动LED可能会电流不足。正确的做法是:LED正极接GPIO,负极串联一个220Ω电阻再接地。这样电流大约在10mA左右,既安全又明亮。

核心要点:数字输出只有两种状态——HIGH(高电平)和LOW(低电平)。控制LED亮灭,本质就是控制引脚电平高低。

代码示例:

// 定义LED引脚
const int ledPin = 2;  // ESP32开发板上通常GPIO2连接了板载LED

void setup() {
  pinMode(ledPin, OUTPUT);  // 设置为输出模式
}

void loop() {
  digitalWrite(ledPin, HIGH);  // 点亮LED
  delay(1000);                 // 延时1秒
  digitalWrite(ledPin, LOW);   // 熄灭LED
  delay(1000);                 // 延时1秒
}

嗯,这里要注意:pinMode()必须在setup()中调用,否则引脚默认是输入模式,输出不了高电平。我曾经见过有人把pinMode写在loop()里,结果LED一闪一闪的,但频率完全不对——因为每次循环都在重新配置引脚。

3.2 模拟输入:读取电位器的旋转角度

接下来是模拟输入。ESP32的ADC(模数转换器)可以把0~3.3V的模拟电压,转换成0~4095的数字值。电位器就是一个典型的模拟传感器,旋转它,输出电压会线性变化。

你想想看,智能音箱的音量旋钮,本质上就是一个电位器。读取它的值,就能知道用户想要多大音量。

代码示例:

const int potPin = 34;  // ADC引脚,ESP32的ADC推荐使用GPIO34-39

void setup() {
  Serial.begin(115200);
  pinMode(potPin, INPUT);  // 设置为输入模式
}

void loop() {
  int potValue = analogRead(potPin);  // 读取模拟值(0~4095)
  Serial.println(potValue);
  delay(100);
}
小技巧:ESP32的ADC在低电压段(0~0.1V)和非线性区(3.0~3.3V)精度较差。如果你需要高精度测量,建议使用外部ADC芯片,比如ADS1115。我在做电池电压监测项目时,就吃过这个亏——直接读ESP32的ADC,电压低于0.1V时数据完全不准。

这里有个坑:ESP32的ADC引脚,默认是12位分辨率(0~4095)。但有些开发板可能被配置成了10位(0~1023)。如果你发现读取的最大值只有1023,记得在setup()里加上一句:analogReadResolution(12);

3.3 PWM输出:用呼吸灯效果控制LED亮度

PWM(脉冲宽度调制),说白了就是通过快速开关引脚,让LED看起来有不同的亮度。占空比越高,LED越亮。ESP32的PWM比Arduino Uno灵活得多——它可以在任意GPIO上输出PWM,频率和分辨率都可调。

我个人习惯,做呼吸灯效果时,PWM频率设在5000Hz左右,分辨率8位(0~255)。这样人眼看不到闪烁,而且亮度变化平滑。

代码示例:

const int ledPin = 2;
const int freq = 5000;      // PWM频率 5kHz
const int ledChannel = 0;   // PWM通道(共16个,0~15)
const int resolution = 8;   // 分辨率 8位(0~255)

void setup() {
  ledcSetup(ledChannel, freq, resolution);  // 配置PWM通道
  ledcAttachPin(ledPin, ledChannel);        // 将通道绑定到引脚
}

void loop() {
  // 逐渐变亮
  for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {
    ledcWrite(ledChannel, dutyCycle);
    delay(10);
  }
  // 逐渐变暗
  for (int dutyCycle = 255; dutyCycle >= 0; dutyCycle--) {
    ledcWrite(ledChannel, dutyCycle);
    delay(10);
  }
}
注意:ESP32的PWM是使用LEDC控制器实现的,不是简单的analogWrite()。你必须先ledcSetup()配置通道,再ledcAttachPin()绑定引脚。别搞混了顺序,否则PWM不会输出。

我曾经在调试一个智能灯带项目时,发现PWM输出频率太低,导致灯带闪烁。后来把频率从1000Hz调到10000Hz,问题就解决了。所以,如果你遇到LED闪烁,先检查PWM频率是否够高。

3.4 非阻塞延时:用millis()解放你的CPU

前面几个例子,我都用了delay()。但delay()有个致命问题:它会让CPU完全停下来,什么都干不了。你想想看,如果智能音箱在播放音乐时,因为一个delay(1000)而卡住1秒,用户体验得多糟糕。

解决方案就是用millis()函数。它返回的是ESP32上电以来经过的毫秒数。我们可以通过记录上次执行的时间,来判断是否该执行某个动作。这样CPU就能在等待期间去做其他事情。

代码示例:实现一个非阻塞的LED闪烁:

const int ledPin = 2;
unsigned long previousMillis = 0;  // 上次切换时间
const long interval = 500;         // 闪烁间隔 500ms
int ledState = LOW;

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  unsigned long currentMillis = millis();  // 获取当前时间

  // 检查是否到了切换时间
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;  // 更新上次时间

    // 切换LED状态
    ledState = (ledState == LOW) ? HIGH : LOW;
    digitalWrite(ledPin, ledState);
  }

  // 在这里可以执行其他任务,比如读取传感器、处理网络数据等
  // 不会因为LED闪烁而被阻塞
}
核心思想:millis()做时间管理,而不是用delay()阻塞。这样你的程序就能同时处理多个任务,实现“伪多任务”。

嗯,这里有个细节要注意:millis()返回的是unsigned long类型,最大值是49天左右。如果你的设备需要连续运行超过49天,需要额外处理溢出问题。不过对于大多数智能音箱项目,这个时间足够长了。

我个人习惯,在写任何需要延时的代码时,第一反应就是用millis()而不是delay()。这已经成为我的肌肉记忆了。你也要养成这个习惯,否则后面做多任务处理时会很痛苦。

3.5 综合实战:电位器控制LED亮度(非阻塞版)

最后,咱们把今天学的所有知识串起来:用电位器控制LED亮度,并且使用非阻塞延时来保持系统响应。

代码示例:

const int potPin = 34;
const int ledPin = 2;
const int freq = 5000;
const int ledChannel = 0;
const int resolution = 8;

unsigned long previousMillis = 0;
const long interval = 50;  // 每50ms读取一次电位器

void setup() {
  Serial.begin(115200);
  pinMode(potPin, INPUT);
  ledcSetup(ledChannel, freq, resolution);
  ledcAttachPin(ledPin, ledChannel);
}

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;

    int potValue = analogRead(potPin);          // 读取0~4095
    int dutyCycle = map(potValue, 0, 4095, 0, 255);  // 映射到0~255
    ledcWrite(ledChannel, dutyCycle);

    Serial.print("Potentiometer: ");
    Serial.print(potValue);
    Serial.print(" -> Duty Cycle: ");
    Serial.println(dutyCycle);
  }

  // 这里可以添加其他任务,比如检测按键、处理WiFi连接等
}
扩展思路:你可以把电位器换成光敏电阻,实现“光控灯”;或者换成温度传感器,实现“温控风扇”。原理都是一样的——读取模拟值,映射到PWM输出。

好了,这节课的内容就到这里。总结一下:

  • 数字输出:用digitalWrite()控制LED亮灭,记得加限流电阻。
  • 模拟输入:用analogRead()读取电位器,注意ADC精度问题。
  • PWM输出:用LEDC控制器实现呼吸灯效果,频率和分辨率可调。
  • 非阻塞延时:用millis()替代delay(),让程序同时处理多个任务。

下一节课,我们会把这些基础外设和WiFi连接结合起来,做一个真正的智能音箱雏形。到时候,你就可以用手机控制LED的亮灭了。敬请期待!