ESP32 第一个程序实战:读懂 Blink、串口调试与按键控制 LED(含去抖)
前置条件:已完成开发环境搭建,Blink 能跑。本文带你逐行读懂每句代码,并完成 L1 阶段收官项目:按键控制 LED。所有实验只需一块开发板 + 一颗按键 + 2 根杜邦线。
一、理解 Arduino 程序骨架
void setup() { // 只在上电/复位时执行一次:做「初始化」
}
void loop() { // setup 结束后无限循环执行:做「主逻辑」
}
类比:setup() 是搬家布置水电,loop() 是之后每天的生活循环。
⚠️ 新手第一坑:loop() 里严禁写 while(1) 和长 delay()——它们会阻塞系统后台任务(Wi-Fi 协议栈、看门狗),导致复位或报 Task watchdog got triggered。保持 loop 快速空转,逻辑用「状态判断」写(见第四节)。
二、实验 1:读懂 Blink 的每一行
#define LED_PIN 2 // 宏定义:板载 LED 接在 GPIO2(多数开发板)
void setup() {
pinMode(LED_PIN, OUTPUT); // 声明 GPIO2 为输出模式(必须!)
}
void loop() {
digitalWrite(LED_PIN, HIGH); // GPIO2 输出 3.3V → LED 亮
delay(500); // 暂停 500 毫秒(1 秒 = 1000)
digitalWrite(LED_PIN, LOW); // GPIO2 输出 0V → LED 灭
delay(500);
}
逐行知识点:
| 语句 | 知识点 |
|---|---|
#define LED_PIN 2 | 编译期替换,不占内存;改引脚只改一处 |
pinMode(pin, OUTPUT) | 未声明的引脚状态不确定,忘记写是新手第二大坑 |
HIGH / LOW | 本质是 1 和 0;digitalWrite(LED_PIN, 1) 完全等价 |
delay(500) | 阻塞式暂停——教学用,实战换非阻塞(第五节) |
💡 你的板子 LED 不闪?部分 S3/C3 板载 LED 不是 GPIO2,查板子丝印或手册,把
2改成对应编号。
三、实验 2:串口——你的「调试显微镜」
烧录前加两行,你就拥有了打印日志的能力:
void setup() {
Serial.begin(115200); // 初始化串口,波特率 115200
Serial.println("系统启动!"); // 打印一行
}
void loop() {
static uint32_t count = 0; // static:跨 loop 保留变量
Serial.printf("第 %u 次循环\n", ++count);
delay(1000);
}
打开 工具 → 串行监视器(右上角放大镜图标),右下角选 115200。
排错:串口无输出 = ① 波特率没对上(乱码也是它)② S3/C3 未开 USB CDC On Boot ③ 用了充电线。
进阶预告:ESP32 有 3 个硬件串口(Serial/Serial1/Serial2),与 GPS、蓝牙模块通信靠它们——属于 L2 知识点。
四、实验 3:按键控制 LED(L1 收官项目)
接线(30 秒)
按键引脚 A → GPIO4(任意安全 GPIO 均可)
按键引脚 B → GND
不需要外接电阻——我们用内部上拉。
代码(电平翻转版)
#define BTN_PIN 4
#define LED_PIN 2
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP); // 输入 + 内部约45kΩ上拉:默认高电平
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
}
void loop() {
if (digitalRead(BTN_PIN) == LOW) { // 按下 = 接地 = LOW
digitalWrite(LED_PIN, HIGH);
Serial.println("按键按下");
} else {
digitalWrite(LED_PIN, LOW);
}
}
为什么按下是 LOW?——INPUT_PULLUP 模式下引脚默认被内部电阻拉到 3.3V(HIGH);按下后引脚直通 GND 变 LOW。这就是「低电平有效」接法,硬件世界最常见。
升级:按一下切换(含去抖)——真正实用的版本
#define BTN_PIN 4
#define LED_PIN 2
bool ledState = false;
bool lastBtn = HIGH; // 上一次读取的电平
uint32_t lastDebounce = 0; // 上次电平变化的时间戳
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
bool cur = digitalRead(BTN_PIN);
// 检测「松开→按下」的下降沿,且距上次触发 > 50ms(去抖)
if (lastBtn == HIGH && cur == LOW && millis() - lastDebounce > 50) {
ledState = !ledState; // 翻转状态
digitalWrite(LED_PIN, ledState);
lastDebounce = millis();
}
lastBtn = cur;
}
三个新知识点:
- 边沿检测:只关心「变化的一瞬间」,而非持续电平——
lastBtn == HIGH && cur == LOW就是下降沿 - 软件去抖:机械触点闭合时会抖动 5~20ms,产生几十次假触发;
millis() - lastDebounce > 50一行解决 - millis() 而非 delay():
millis()返回开机至今毫秒数,做时间判断不阻塞——这就是非阻塞编程范式,ESP32 进阶的分水岭
五、常见翻车现场 FAQ
Q1:LED 常亮/常灭不响应按键?
→ 检查 pinMode(BTN_PIN, INPUT_PULLUP) 是否拼错(写成 INPUT 会悬空抖动);杜邦线是否插实。
Q2:按一次 LED 闪好几下? → 没做去抖,或去抖阈值太小。用第五版代码的 50ms 方案。
Q3:GPIO 34 接按键完全没反应? → GPIO34~39 是纯输入引脚且无内部上拉,INPUT_PULLUP 无效!换 GPIO4 这类普通引脚,或外接 10kΩ 上拉电阻。引脚能力速查见 GPIO 完全入门。
Q4:为什么我把按键接 3.3V、检测 HIGH 不行? → 可以,但需要外接下拉电阻,否则松开后悬空。内部只有上拉没有下拉,所以「接地 + INPUT_PULLUP」是零元件最优解。
六、L1 验收清单
- 能不看示例独立写出 Blink(含 pinMode)
- Serial 打印 + 串口监视器正常工作
- 按键控制 LED(持续亮灭版)成功
- 理解并能解释:为什么按下读到的是 LOW
- 挑战项:边沿检测 + 去抖切换版成功
- 能口述:为什么不能直接用 GPIO 驱动电机(答:12mA 安全极限,需三极管/驱动板放大——详见 GPIO 安全电流)
全部通过?恭喜——你已经跨过嵌入式第一道门槛。下一步进入 L2:ADC 模拟输入与 PWM 调光(选题池已排期,即将上线)。
版权声明:本文采用 CC BY-SA 4.0 协议授权。转载与二创请注明出处:ESPDIY 创客工坊 - espdiy.com
Your First ESP32 Program: Reading Blink Line by Line, Serial Debugging & Button-controlled LED (with Debounce)
Prerequisite: environment set up (see the setup guide) and Blink running. This post makes you read every line of the code and finishes with the L1 capstone: a button-controlled LED. All you need is one board + one button + two jumper wires.
1. The Arduino program skeleton
void setup() { // runs exactly once at power-on/reset: "initialization"
}
void loop() { // after setup, loops forever: "main logic"
}
Analogy: setup() is moving in and wiring the utilities; loop() is daily life afterwards.
⚠️ Beginner trap #1: never write while(1) or long delay() inside loop() — they block background tasks (the Wi-Fi stack, watchdog) and cause resets or Task watchdog got triggered. Keep loop spinning fast; express logic as state checks (see §4).
2. Exercise 1: understand every line of Blink
#define LED_PIN 2 // macro: onboard LED on GPIO2 (most boards)
void setup() {
pinMode(LED_PIN, OUTPUT); // declare GPIO2 as output (mandatory!)
}
void loop() {
digitalWrite(LED_PIN, HIGH); // GPIO2 → 3.3V → LED on
delay(500); // pause 500 ms (1 second = 1000)
digitalWrite(LED_PIN, LOW); // GPIO2 → 0V → LED off
delay(500);
}
Line-by-line knowledge:
| Statement | Concept |
|---|---|
#define LED_PIN 2 | Compile-time substitution, zero RAM cost; change the pin in one place |
pinMode(pin, OUTPUT) | Undefined pins have undefined behavior — forgetting this is beginner trap #2 |
HIGH / LOW | Literally 1 and 0; digitalWrite(LED_PIN, 1) is identical |
delay(500) | Blocking pause — for teaching; real code goes non-blocking (§4) |
💡 LED not blinking on your board? Some S3/C3 boards don’t use GPIO2 for the onboard LED — check the silkscreen and change the
2.
3. Exercise 2: Serial — your debugging microscope
Two extra lines and you have logging:
void setup() {
Serial.begin(115200); // init serial at 115200 baud
Serial.println("System booted!");
}
void loop() {
static uint32_t count = 0; // static: survives across loop iterations
Serial.printf("loop #%u\n", ++count);
delay(1000);
}
Open Tools → Serial Monitor and set 115200 bottom-right.
If nothing prints: ① baud mismatch (garbage counts too) ② S3/C3 without USB CDC On Boot ③ charging cable.
Coming up: the ESP32 has three hardware UARTs (Serial/Serial1/Serial2) for GPS and Bluetooth modules — an L2 topic.
4. Exercise 3: button-controlled LED (L1 capstone)
Wiring (30 seconds)
Button pin A → GPIO4 (any safe GPIO works)
Button pin B → GND
No external resistor needed — we use the internal pull-up.
Code (hold-to-light version)
#define BTN_PIN 4
#define LED_PIN 2
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP); // input + ~45kΩ internal pull-up: default HIGH
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
}
void loop() {
if (digitalRead(BTN_PIN) == LOW) { // pressed = grounded = LOW
digitalWrite(LED_PIN, HIGH);
Serial.println("button pressed");
} else {
digitalWrite(LED_PIN, LOW);
}
}
Why is pressed LOW? — with INPUT_PULLUP the pin idles at 3.3V (HIGH) through the internal resistor; pressing shorts it to GND → LOW. That’s “active-low” wiring, the most common convention in hardware.
Upgrade: press-to-toggle (with debounce) — the truly useful version
#define BTN_PIN 4
#define LED_PIN 2
bool ledState = false;
bool lastBtn = HIGH; // last read level
uint32_t lastDebounce = 0; // timestamp of last level change
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
bool cur = digitalRead(BTN_PIN);
// falling edge (released→pressed), and >50ms since last trigger (debounce)
if (lastBtn == HIGH && cur == LOW && millis() - lastDebounce > 50) {
ledState = !ledState; // toggle
digitalWrite(LED_PIN, ledState);
lastDebounce = millis();
}
lastBtn = cur;
}
Three new concepts:
- Edge detection: react to the moment of change, not the held level —
lastBtn == HIGH && cur == LOWis the falling edge - Software debounce: mechanical contacts bounce for 5–20ms producing dozens of phantom triggers;
millis() - lastDebounce > 50fixes it in one line - millis() over delay():
millis()returns ms since boot — time checks without blocking. That’s the non-blocking paradigm, the real dividing line of ESP32 skill
5. Classic faceplants FAQ
Q1: LED stuck on/off, button ignored?
→ Check pinMode(BTN_PIN, INPUT_PULLUP) for typos (plain INPUT floats and flickers); check jumper seating.
Q2: one press flashes the LED several times? → No debounce, or threshold too small. Use the 50ms scheme in §4.
Q3: a button on GPIO 34 does nothing?
→ GPIO 34–39 are input-only without internal pull-ups — INPUT_PULLUP is a no-op there! Use a normal pin like GPIO4, or add an external 10kΩ pull-up. Pin capabilities: GPIO Complete Guide.
Q4: why not wire the button to 3.3V and detect HIGH? → You can, but it needs an external pull-down or the pin floats when released. The chip has internal pull-ups only — “to GND + INPUT_PULLUP” is the zero-component optimum.
6. L1 acceptance checklist
- Write Blink from scratch (with pinMode) without peeking
- Serial printing works in the monitor
- Button controls LED (hold version) works
- You can explain why pressed reads LOW
- Challenge: edge-detect + debounce toggle version works
- You can state why GPIOs can’t drive motors directly (answer: 12mA safe limit — see GPIO current limits)
All passed? Congratulations — the first threshold of embedded is behind you. Next up, L2: ADC input and PWM dimming (scheduled in our topic pool).
License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com