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 快速空转,逻辑用「状态判断」写(见第四节)。


#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;
}

三个新知识点:

  1. 边沿检测:只关心「变化的一瞬间」,而非持续电平——lastBtn == HIGH && cur == LOW 就是下降沿
  2. 软件去抖:机械触点闭合时会抖动 5~20ms,产生几十次假触发;millis() - lastDebounce > 50 一行解决
  3. 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).


#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:

StatementConcept
#define LED_PIN 2Compile-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 / LOWLiterally 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:

  1. Edge detection: react to the moment of change, not the held level — lastBtn == HIGH && cur == LOW is the falling edge
  2. Software debounce: mechanical contacts bounce for 5–20ms producing dozens of phantom triggers; millis() - lastDebounce > 50 fixes it in one line
  3. 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