首页 / 学习路径 / 第 03 章 · GPIO 数字输入与输出
03
核心 · 外设

GPIO 数字输入与输出

引脚模式、内部上拉、按键消抖、长按检测。搞懂 GPIO 就等于拿到了和真实世界对话的能力,也是所有后续章节的地基。

阅读时长 约 50 分钟硬件 4 件实战案例 1 个前置要求 第 2 章
1

本章要点

引脚模式决定一个引脚是「听」还是「说」。OUTPUT 让你控制外设,INPUT 让你读取外设,INPUT_PULLUP 则额外开启内部上拉电阻——引脚悬空时被拉到 3.3V 读到 HIGH,按下按键把引脚接到 GND 就读到 LOW。

机械按键按下瞬间,触点会抖动 5~20ms,程序若不处理会误判成多次按压。软件消抖的思路是:读数变化后,必须再稳定超过 50ms 才承认状态真的变了。

长按判定靠一个「是否已触发过」的标记位。不设这个标记,长按期间每隔 50ms 就会被判一次短按,现象就是 LED 疯狂翻转——这正是新手最常踩的坑。

2

硬件清单与原理

名称数量作用与原理
轻触按键1 个一对常开触点,按下导通、松开断开。机械触点有 5~20ms 抖动,代码必须做消抖。
LED1 个发光二极管,单向导电。正向压降约 1.8~2.2V,长脚为阳极、短脚为阴极。
220Ω 电阻1 个限流保护。把 LED 电流限制在约 (3.3-2.0)/220 ≈ 6mA,既够亮又不会烧坏。
有源蜂鸣器1 个内置驱动电路,通电即响,电流约 20~30mA。必须用有源款。
3

接线说明

接线示意 · 按键 + LED + 蜂鸣器 ESP32 DevKit V1 GPIO4 / 18 / 19 GPIO4 按键 K1 内部上拉 GPIO18 220Ω LED GPIO19 BUZ 有源蜂鸣器 GND 共地
按键用内部上拉,只需两根线。LED 与蜂鸣器各接一个 GPIO。

GPIO4 → 按键 K1 一端 按键另一端 → GND

GPIO18 → [220Ω] → LED 阳极 LED 阴极 → GND

① GPIO19 → 蜂鸣器正极 蜂鸣器负极 → GND

② 通电前务必确认:所有 GND 已共地,无 5V 接入 GPIO

提示:蜂鸣器必须用「有源」款;无源蜂鸣器需要自己输出方波,现象完全不同。

4

实战案例 03-1 · 按键控制 LED + 蜂鸣器报警

功能用途

综合练习 GPIO 的输入与输出两个方向,短按切换 LED、长按触发蜂鸣器。

预期现象

短按一次按键 LED 状态翻转;长按按键,蜂鸣器连续响 3 秒后停止。

ch03_gpio.ino编译通过
/*
 * ============================================================================
 * 第 3 章 · 实战案例 03-1:按键控制 LED + 蜂鸣器报警(输入输出综合)
 * ----------------------------------------------------------------------------
 * 硬件清单与元器件原理:
 *   1) 轻触按键 × 1 —— 一对常开触点,按下时导通、松开时断开,本质是一个「机械开关」。
 *      机械触点存在约 5~20ms 的抖动(bounce),程序必须做消抖处理,否则一次按压
 *      会被识别成好几次。
 *   2) LED × 1 —— 二极管单向导电,正向导通电压约 1.8~2.2V,超过额定电流会烧毁。
 *      串 220Ω 电阻做限流,把电流限制在约 (3.3-2.0)/220 ≈ 6mA。
 *   3) 有源蜂鸣器 × 1 —— 内置驱动电路,给电就响,通电电流约 20~30mA。
 *      ⚠ 必须用「有源」蜂鸣器;无源蜂鸣器需要自己输出方波,本章用的是有源款。
 *   4) 220Ω 电阻 × 1(LED 限流用)
 *
 * 接线方式:
 *   按键 K1:
 *     GPIO4  →  按键一侧
 *     按键另一侧 → GND
 *     (程序内部开启内部上拉,按下时读到 LOW)
 *
 *   LED D1:
 *     GPIO18 → [220Ω] → LED 阳极(长脚)
 *     LED 阴极(短脚)→ GND
 *
 *   蜂鸣器 B1(有源):
 *     GPIO19 → 蜂鸣器正极
 *     蜂鸣器负极 → GND
 *
 * 功能用途:
 *   综合练习 GPIO 的「输入」和「输出」两种方向:
 *   - 短按按键 → 切换 LED 亮灭
 *   - 长按按键(≥1.5 秒)→ 触发蜂鸣器报警 3 秒
 *
 * 预期现象:
 *   短按一次按键,LED 状态翻转;长按按键,蜂鸣器连续响 3 秒后停止。
 * ============================================================================
 */

#include <Arduino.h>   // Arduino 核心库

/* ==========================================================================
 * 一、引脚定义
 * ======================================================================== */
#define BUTTON_PIN  4        // 按键接 GPIO4(内部上拉,无需外部电阻)
#define LED_PIN     18       // LED 接 GPIO18
#define BUZZER_PIN  19       // 有源蜂鸣器接 GPIO19

/* ==========================================================================
 * 二、参数与常量
 * ======================================================================== */
const class="kw">unsigned long DEBOUNCE_MS    = 50;      // 消抖时间:抖动通常在 20ms 内,50ms 足够
const class="kw">unsigned long LONG_PRESS_MS  = 1500;    // 长按判定阈值:按住超过 1.5 秒算长按
const class="kw">unsigned long BUZZER_DUR_MS  = 3000;    // 蜂鸣器持续响的时间

// 数字逻辑的「真 / 假」用常量表达,比直接写 1 和 0 更容易读懂
const bool LED_ON  = true;
const bool LED_OFF = false;

/* ==========================================================================
 * 三、状态变量
 * ======================================================================== */
bool ledState = LED_OFF;             // LED 当前状态
bool buzzerActive = false;           // 蜂鸣器是否正在响

class="kw">unsigned long buttonPressedAt = 0;   // 按键被按下的时刻
bool longPressHandled = false;       // 本次按压是否已经触发过长按(避免重复触发)
bool lastButtonReading = HIGH;       // 上一次读到的按键电平,用于检测「按下瞬间」

class="kw">unsigned long buzzerOffAt = 0;       // 蜂鸣器应该停止的时刻
class="kw">unsigned long lastDebounceChange = 0;// 按键电平最后一次「稳定变化」的时刻

/* ==========================================================================
 * 四、初始化
 * ======================================================================== */
void setup() {
  Serial.begin(115200);   // 启动串口
  delay(200);            // 等待串口芯片就绪

  // 按键引脚:INPUT_PULLUP 表示「内部上拉」——
  // 引脚内部接了一个拉到 3.3V 的电阻,悬空时读到的就是 HIGH;
  // 按键按下把引脚接到 GND,读到 LOW。这样只需要 2 根线,不用外接上拉电阻。
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // LED 和蜂鸣器都是输出
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);

  digitalWrite(LED_PIN, HIGH);       // LED 初始熄灭(低电平点亮,故写 HIGH)
  digitalWrite(BUZZER_PIN, LOW);     // 蜂鸣器初始不响(有源蜂鸣器高电平响)

  Serial.println(class="st">"[GPIO] 初始化完成");
  Serial.println(class="st">"[GPIO] 短按按键 = 切换 LED;长按 1.5 秒 = 蜂鸣器响 3 秒");
}

/* ==========================================================================
 * 五、自定义函数:让蜂鸣器开始响
 * ======================================================================== */
void startBuzzer() {
  buzzerActive = true;               // 标记为「正在响」
  digitalWrite(BUZZER_PIN, HIGH);    // 有源蜂鸣器:高电平通电发声
  buzzerOffAt = millis() + BUZZER_DUR_MS;  // 记录应该停止的时刻
  Serial.println(class="st">"[GPIO] >>> 触发报警:蜂鸣器响 3 秒");
}

/* ==========================================================================
 * 六、自定义函数:切换 LED 状态
 * ======================================================================== */
void toggleLed() {
  ledState = !ledState;              // 取反:true↔false
  digitalWrite(LED_PIN, ledState ? LOW : HIGH);   // 低电平点亮
  Serial.print(class="st">"[GPIO] LED 状态:");
  Serial.println(ledState ? class="st">"ON" : class="st">"OFF");
}

/* ==========================================================================
 * 七、主循环
 * ======================================================================== */
void loop() {
  class="kw">unsigned long now = millis();

  /* ---- 1. 蜂鸣器超时自动停止 ---- */
  if (buzzerActive && now >= buzzerOffAt) {
    buzzerActive = false;
    digitalWrite(BUZZER_PIN, LOW);   // 拉低,蜂鸣器停止
    Serial.println(class="st">"[GPIO] <<< 报警结束,蜂鸣器已停止");
  }

  /* ---- 2. 读取按键 + 消抖 ---- */
  // 读取当前电平。因为是上拉模式,按下 = LOW,松开 = HIGH
  bool reading = digitalRead(BUTTON_PIN);

  // 只有当读数与「上次稳定读数」不同,且距离上次变化已超过消抖时间,才认为状态真的变了。
  // 这样可以滤掉机械触点在接触瞬间产生的一串快速通断。
  if (reading != lastButtonReading && (now - lastDebounceChange) > DEBOUNCE_MS) {
    lastDebounceChange = now;        // 记录这次「确认有效」的变化时刻
    lastButtonReading = reading;     // 更新稳定读数

    if (reading == LOW) {
      // 确认检测到「按下」动作
      buttonPressedAt = now;         // 记下按下的时刻,用于判断长按
      longPressHandled = false;      // 本次按压还没触发过长按
      Serial.println(class="st">"[GPIO] 检测到按键按下");
    } else {
      // 检测到「松开」
      if (!longPressHandled) {
        // 如果整个按压过程中没有触发过长按,说明这是一次「短按」→ 切换 LED
        toggleLed();
      } else {
        Serial.println(class="st">"[GPIO] 长按结束(本次不切换 LED)");
      }
    }
  }

  /* ---- 3. 长按检测 ---- */
  // 条件:按键仍处于按下状态(reading == LOW)且按住时间已超过阈值,且本次尚未触发过
  if (reading == LOW && !longPressHandled && (now - buttonPressedAt) >= LONG_PRESS_MS) {
    longPressHandled = true;         // 标记已触发,防止抖动导致连响
    startBuzzer();                   // 启动蜂鸣器
  }
}
完整源码 160 行 · ch03_gpio/ch03_gpio.inoarduino-cli 编译通过
5

本章小结

① INPUT_PULLUP 用芯片内部电阻代替外部上拉,按键只需 2 根线,抗干扰更好。

② 用上拉模式后逻辑要反过来:按键按下 = LOW,松开 = HIGH。

③ 机械抖动是物理现象,软件消抖是唯一可靠解法:读数变化 + 稳定超过 50ms 才认账。

④ longPressHandled 这类一次性标记是防止重复触发的关键。

⑤ 蜂鸣器分有源(通电响)和无源(需方波驱动),买错型号现象完全不同。

6

本章练习

练习 1

把 LONG_PRESS_MS 改成 3000,观察长按判定变慢。

练习 2

去掉 longPressHandled 标记,观察长按时 LED 疯狂翻转。

练习 3

去掉消抖逻辑,用手指按住按键看串口是否多次触发。

练习 4

增加第二按键(GPIO33),短按切换 LED、长按关闭蜂鸣器。

7

下一章

04
NEXT · 第 04 章

串口通信 UART

波特率、全双工、字符串缓冲、指令解析。串口是调试单片机最重要的窗口。

开始学习