3、ESP32 基础外设控制:GPIO 控制 LED 灯、PWM 调光、按键输入检测、中断编程基础
好,咱们开始动手了。前面两章我们把开发环境搭好,也跑了个点灯程序。但说实话,那个点灯只是让你感受一下「我能控制硬件了」的快感。真正的嵌入式开发,你得学会跟 GPIO 打交道——输入、输出、模拟输出、中断响应。这一章,我把这些基础操作一次性讲透。
3.1 GPIO 输出:控制 LED 灯
GPIO 输出是最简单的。你把引脚拉高,灯就亮;拉低,灯就灭。但这里有个坑——你得搞清楚你的 LED 是「高电平点亮」还是「低电平点亮」。我刚开始做项目时,照着网上的电路图焊板子,结果灯死活不亮。查了半天,原来是 LED 正负极接反了,而且那个板子用的是低电平驱动。
来看代码。用 Arduino 框架控制 GPIO 输出,其实就三步:
// 定义引脚
#define LED_PIN 2
void setup() {
// 1. 设置引脚为输出模式
pinMode(LED_PIN, OUTPUT);
}
void loop() {
// 2. 拉高电平,点亮 LED
digitalWrite(LED_PIN, HIGH);
delay(1000);
// 3. 拉低电平,熄灭 LED
digitalWrite(LED_PIN, LOW);
delay(1000);
}
嗯,这里要注意:pinMode() 一定要在 setup() 里调用。我见过有人把它写在 loop() 里,结果每循环一次就重新配置一次引脚,虽然也能工作,但效率极低。
3.2 PWM 调光:让 LED 呼吸起来
数字输出只有 0 和 1,但很多时候我们需要「中间状态」。比如让 LED 半亮,或者让电机转得慢一点。这时候就要用 PWM(脉冲宽度调制)。
说白了,PWM 就是快速地在高电平和低电平之间切换。你想想看,如果切换频率足够快(人眼看不出来),那 LED 的亮度就取决于高电平占整个周期的比例——也就是占空比。
ESP32 的 PWM 比 Arduino Uno 灵活得多。它有独立的 LEDC(LED Control)外设,可以设置频率、分辨率、通道。我个人习惯用 5000 Hz 的频率,13 位的分辨率(0-8191)。
#define LED_PIN 2
#define PWM_CH 0 // PWM 通道
#define PWM_FREQ 5000 // 频率 5KHz
#define PWM_RES 13 // 分辨率 13 位
void setup() {
// 配置 PWM
ledcSetup(PWM_CH, PWM_FREQ, PWM_RES);
// 将引脚绑定到 PWM 通道
ledcAttachPin(LED_PIN, PWM_CH);
}
void loop() {
// 呼吸灯效果:从暗到亮
for (int duty = 0; duty <= 8191; duty++) {
ledcWrite(PWM_CH, duty);
delay(1);
}
// 从亮到暗
for (int duty = 8191; duty >= 0; duty--) {
ledcWrite(PWM_CH, duty);
delay(1);
}
}
为什么用 13 位分辨率?其实 8 位(0-255)也够用,但 13 位能让亮度变化更平滑。我在做智能灯带项目时,客户要求调光不能有「阶梯感」,所以分辨率越高越好。当然,分辨率越高,PWM 频率就得适当降低,否则 ESP32 的时钟跟不上。
3.3 按键输入检测:读取外部信号
输出讲完了,咱们来看看输入。按键是最常见的输入设备。但按键有个烦人的问题——抖动。你按下去的一瞬间,机械触点会弹跳几次,导致电平在短时间内反复变化。如果不处理,程序可能会误判为按了多次。
我曾经在一个门禁项目里吃过这个亏。按键按下一次,门开了又关、关了又开,反复三次。后来加了消抖才解决。
消抖有两种方式:硬件消抖(加电容)和软件消抖(延时判断)。这里我讲软件消抖,因为不需要额外元件。
#define BUTTON_PIN 0 // 注意:ESP32 开发板上的 BOOT 按钮通常接 GPIO 0
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP); // 启用内部上拉电阻
Serial.begin(115200);
}
void loop() {
// 检测按键是否按下(低电平有效)
if (digitalRead(BUTTON_PIN) == LOW) {
delay(50); // 延时 50ms 跳过抖动
if (digitalRead(BUTTON_PIN) == LOW) {
Serial.println("按键按下!");
// 等待按键释放
while (digitalRead(BUTTON_PIN) == LOW);
}
}
}
这里用了 INPUT_PULLUP 模式。什么意思呢?就是 ESP32 内部有一个上拉电阻,默认把引脚拉高到 3.3V。按键按下时,引脚接地,变成低电平。这样你就不用外接上拉电阻了,省一个元件。
3.4 中断编程基础:让 CPU 别傻等
刚才的按键检测有个问题——while 循环一直在轮询,CPU 啥也干不了。这就好比你在门口等着快递员,什么事都做不了。中断就不一样了:你该干嘛干嘛,快递员来了按门铃,你再去开门。
ESP32 支持两种中断:GPIO 中断和定时器中断。这里先讲 GPIO 中断。
#define BUTTON_PIN 0
#define LED_PIN 2
volatile bool ledState = false; // volatile 关键字很重要!
void IRAM_ATTR buttonISR() {
// 中断服务函数:翻转 LED 状态
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
// 配置中断:下降沿触发(按键按下时电平从高变低)
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
}
void loop() {
// 主循环可以干别的事情,比如串口打印
Serial.println("主循环正在运行...");
delay(1000);
}
这里有几个要点:
volatile关键字:告诉编译器,这个变量可能在中断中被修改,不要优化掉。我见过有人忘了加,结果中断里改了值,主循环死活读不到新值。IRAM_ATTR:把中断函数放到 IRAM(指令 RAM)中,保证执行速度。如果不加,中断可能会因为 Flash 读取延迟而响应变慢。- 中断函数要短小精悍:不要在中断里做复杂操作,比如
delay()、Serial.print()。最好只设置标志位,具体处理放到主循环里。
Serial.println(),结果程序频繁崩溃。后来查资料才知道,串口输出依赖中断,在中断里调用串口函数会导致递归中断,栈溢出。所以,中断里只做最基础的操作。
3.5 综合实战:按键控制呼吸灯
把上面的知识串起来,做一个综合实验:按键按下一次,LED 开始呼吸;再按一次,LED 停止呼吸。
#define LED_PIN 2
#define BUTTON_PIN 0
#define PWM_CH 0
#define PWM_FREQ 5000
#define PWM_RES 13
volatile bool breathing = false;
volatile unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;
void IRAM_ATTR buttonISR() {
unsigned long now = millis();
if (now - lastDebounceTime > debounceDelay) {
breathing = !breathing;
lastDebounceTime = now;
}
}
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
ledcSetup(PWM_CH, PWM_FREQ, PWM_RES);
ledcAttachPin(LED_PIN, PWM_CH);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
}
void loop() {
if (breathing) {
for (int duty = 0; duty <= 8191; duty++) {
ledcWrite(PWM_CH, duty);
delay(1);
if (!breathing) break; // 如果按键再次按下,立即退出
}
for (int duty = 8191; duty >= 0; duty--) {
ledcWrite(PWM_CH, duty);
delay(1);
if (!breathing) break;
}
} else {
ledcWrite(PWM_CH, 0); // 关闭 LED
delay(10);
}
}
这个例子把中断、消抖、PWM 都结合起来了。注意看,我在中断里做了软件消抖——用 millis() 判断两次中断的时间间隔。这样既避免了阻塞,又解决了抖动问题。
3.6 本章小结
这一章的内容,说白了就是嵌入式开发的「四则运算」:
- GPIO 输出:用
digitalWrite()控制高低电平 - PWM 调光:用
ledcWrite()实现模拟输出 - 按键输入:用
digitalRead()读取电平,配合消抖 - 中断编程:用
attachInterrupt()实现事件驱动
这些基础操作,在后面的章节里会反复用到。比如下一章讲传感器,就要用 GPIO 输入读取数据;讲电机控制,就要用 PWM 调速。所以,这一章的内容一定要亲手敲一遍代码,别光看。
嗯,我记得有个学员跟我说,他看完这章后,把家里的台灯改成了手机控制的呼吸灯。虽然只是个小项目,但那种「把代码变成现实」的感觉,真的很棒。你也试试?