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左右,既安全又明亮。
代码示例:
// 定义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引脚,默认是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);
}
}
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连接等
}
好了,这节课的内容就到这里。总结一下:
- 数字输出:用
digitalWrite()控制LED亮灭,记得加限流电阻。 - 模拟输入:用
analogRead()读取电位器,注意ADC精度问题。 - PWM输出:用LEDC控制器实现呼吸灯效果,频率和分辨率可调。
- 非阻塞延时:用
millis()替代delay(),让程序同时处理多个任务。
下一节课,我们会把这些基础外设和WiFi连接结合起来,做一个真正的智能音箱雏形。到时候,你就可以用手机控制LED的亮灭了。敬请期待!