Chapter 03 · Arduino 入门

数字 IO 与按键

数字引脚的读与写、上拉/下拉电阻的意义、为什么按键会"抖动",以及两种可靠的消抖写法。

课程进度25%

关键概念

pinMode(pin, mode)
配置引脚方向的函数,通常写在 setup() 里。三种模式:OUTPUT(输出,能对外驱动电平)、INPUT(输入,高阻态读取)、INPUT_PULLUP(输入并启用芯片内部上拉电阻)。忘记设模式,引脚行为将不可预测。
digitalWrite(pin, value)
让输出引脚变为 HIGH(5V) 或 LOW(0V)。只对已设为 OUTPUT 的引脚有效。对 INPUT_PULLUP 引脚 digitalWrite(HIGH) 会开启内部上拉,写 LOW 关闭——这是它的特殊用法。
digitalRead(pin)
读取输入引脚当前电平,返回 HIGH(1) 或 LOW(0)。用于检测按键是否按下、传感器是否触发。读取的是"此刻"的电平快照,因此要放在 loop 里反复读。
悬空 (Floating) 引脚
输入引脚若既不接高也不接低,就处于"悬空"高阻态,会像天线一样拾取环境噪声,digitalRead 返回随机的 0/1。这就是为什么按键电路必须有上拉或下拉电阻——给引脚一个确定的默认电平。
上拉电阻 (Pull-Up)
一个连接到 VCC(5V) 的电阻,默认把引脚"拉高"到 HIGH。按键另一端接 GND,按下时引脚被拉低到 LOW。逻辑是"平时高、按下低"(低电平有效)。ATmega328P 内置约 20-50kΩ 上拉电阻,用 INPUT_PULLUP 即可启用,省掉外部电阻。
下拉电阻 (Pull-Down)
连接到 GND 的电阻,默认把引脚拉低到 LOW,按键接 VCC,按下变 HIGH(高电平有效)。Arduino 芯片没有内置下拉,需外接 10kΩ 电阻。初学者用内置上拉更省事,所以本教程主推 INPUT_PULLUP。
按键抖动 (Bounce)
机械按键的金属触点接触瞬间会因弹性反复通断几毫秒,产生一串杂乱的电平跳变。单片机跑得极快,一次按压会被误读成十几次。消抖(debounce)就是过滤掉这段抖动,只认可稳定后的电平。
边沿检测 (Edge Detection)
区分"电平状态"和"电平变化"。很多时候我们要的不是"按键正被按住",而是"按键刚刚被按下的那一瞬间"(下降沿)。通过保存上一次的状态、与当前比较,即可捕捉这个跳变边沿,实现"按一次执行一次"。

面包板与接线基础

面包板是免焊接的原型工具,理解它的内部连通关系是接线不出错的前提:

面包板内部连通规则 + ───────────────────────────── ← 电源正极总线(整条横向相连) - ───────────────────────────── ← 电源负极/GND 总线(整条相连) a b c d e f g h i j 1 ○ ○ ○ ○ ○○ ○ ○ ○ ○ ← 每行 a-e 内部相连,f-j 内部相连 2 ○ ○ ○ ○ ○○ ○ ○ ○ ○ 中间凹槽两侧【不】连通 3 ○ ○ ○ ○ ○○ ○ ○ ○ ○ (方便插 IC 芯片跨槽) ⋮ 记忆:竖着的电源轨整条通,横着的每行 5 孔一组通,凹槽隔开左右。

接一个外部 LED

把 LED 接到 D8,练习数字输出。注意 LED 有极性:

LED 接线(D8 输出,高电平点亮) Arduino D8 ○───────┳━━━[ 330Ω 限流电阻 ]━━━┓ ┃ ┃ ┃ LED 长脚(+阳极) ┃ ▼ ┃ ╲LED╱ ┃ │ ┃ LED 短脚(-阴极) Arduino GND ○──────┻─────────────────────────┛ 电流路径:D8(HIGH,5V) → 电阻 → LED阳极 → LED阴极 → GND 限流电阻必不可少!否则大电流会烧毁 LED 和引脚。 电阻取值:(5V - 2V压降) / 0.01A ≈ 300Ω,取 330Ω 常用。
// 外部 LED 闪烁,接在 D8
const int ledPin = 8;

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  digitalWrite(ledPin, HIGH);
  delay(300);
  digitalWrite(ledPin, LOW);
  delay(300);
}

读取按键:INPUT_PULLUP 接法

用内部上拉,按键一端接引脚、另一端直接接 GND,无需外部电阻——这是最省事的经典接法。

按键接线(INPUT_PULLUP,低电平有效) Arduino D2 ○──────┳────────[ 按键 ]────────○ GND ┃ (内部上拉电阻已启用, 平时把 D2 拉到 HIGH) 状态: 没按 → D2 = HIGH (被内部上拉拉高) 按下 → D2 = LOW (按键把 D2 直接接到 GND) 所以判断"按下"的条件是 digitalRead(D2) == LOW
// 按键控制 LED:按下亮,松开灭
const int btnPin = 2;
const int ledPin = 8;

void setup() {
  pinMode(btnPin, INPUT_PULLUP);  // 启用内部上拉
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // 按键按下时引脚为 LOW
  if (digitalRead(btnPin) == LOW) {
    digitalWrite(ledPin, HIGH);
  } else {
    digitalWrite(ledPin, LOW);
  }
}
为什么不能直接把引脚接 5V/GND 判断?

如果按键一端接 5V、另一端接引脚,而引脚是普通 INPUT(无上拉/下拉),那么松开按键时引脚悬空,读到的是随机噪声,LED 会乱闪。必须靠上拉或下拉电阻给"松开"状态一个确定电平。用 INPUT_PULLUP 就是让芯片替你接好上拉。

抖动问题:按一次却触发多次

想实现"按一次按键,LED 翻转一次亮/灭状态"。天真写法会因抖动失效:

机械按键触点的真实电平(放大看) 理想: HIGH ─────┐ ┌─────── └───────────────┘ 按下 松开 实际: HIGH ──┐ ┌┐ ┌─┐ ┌┐ ┌──── └─┘└──┘ └──────────┘└─┘ ← 抖动 2~10ms → ← 抖动 → 单片机每秒读几十万次,会把这几毫秒的抖动 误读成"按了好多下"。必须消抖!

消抖法一:延时消抖(简单)

// 检测到按下后延时 20ms 再确认,简单但会阻塞
const int btnPin = 2;
const int ledPin = 8;
bool ledState = false;
int lastReading = HIGH;

void setup() {
  pinMode(btnPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  int reading = digitalRead(btnPin);

  // 检测下降沿:从 HIGH 变到 LOW = 刚刚按下
  if (lastReading == HIGH && reading == LOW) {
    delay(20);                     // 等抖动过去
    if (digitalRead(btnPin) == LOW) { // 再确认一次
      ledState = !ledState;         // 翻转状态
      digitalWrite(ledPin, ledState);
    }
  }
  lastReading = reading;
}

消抖法二:millis() 非阻塞消抖(推荐)

延时法的缺点是 delay 会卡住整个程序。用 millis() 记录时间戳,不阻塞、可同时干别的事——这是工业级写法。

// millis() 非阻塞消抖:不卡程序,专业推荐
const int btnPin = 2;
const int ledPin = 8;

bool ledState = false;
int lastSteady = HIGH;            // 上一次稳定电平
int lastFlicker = HIGH;           // 上一次原始读数
unsigned long lastChangeTime = 0;   // 电平变化的时刻
const unsigned long DEBOUNCE = 30;   // 消抖窗口 30ms

void setup() {
  pinMode(btnPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  int reading = digitalRead(btnPin);

  // 原始读数一变,就记录当前时间
  if (reading != lastFlicker) {
    lastChangeTime = millis();
    lastFlicker = reading;
  }

  // 距上次变化已超过 30ms,说明电平稳定了
  if (millis() - lastChangeTime > DEBOUNCE) {
    // 稳定值发生了改变
    if (reading != lastSteady) {
      lastSteady = reading;
      // 只在"稳定地按下"(变为 LOW)时翻转
      if (lastSteady == LOW) {
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
      }
    }
  }
}
millis() 是什么?

millis() 返回开机以来经过的毫秒数(unsigned long)。它像一块从不停歇的秒表。"记录起点时刻 → 反复用当前时刻减起点 → 到时才行动"是 Arduino 非阻塞编程的核心套路,第 9 章会系统展开。约 49.7 天后 millis 会溢出归零,但用减法比较能自动正确处理溢出,无需担心。

常见坑:把消抖阈值设太大

消抖窗口 20-50ms 合适。设成 200ms 以上会让按键"迟钝",快速连按会漏掉。另外不要在中断里加 delay 消抖(第 9 章讲中断),会拖慢整个系统。

本章小结

数字 IO 三大函数:pinMode 设方向、digitalWrite 设输出电平(HIGH/LOW)、digitalRead 读输入电平。输入引脚绝不能悬空,否则读到随机噪声——必须用上拉或下拉电阻给默认电平。Arduino 内置上拉(约 20-50kΩ),用 pinMode(pin, INPUT_PULLUP) 启用,按键另一端接 GND,逻辑变成"平时 HIGH、按下 LOW"(低电平有效),这是最省事的接法;下拉需外接 10kΩ 电阻。接 LED 输出务必串限流电阻(约 330Ω)并注意长脚为正极。机械按键触点接触瞬间有 2-10ms 抖动,会被高速的 CPU 误读成多次按压,必须消抖:简单法是检测到跳变后 delay(20) 再确认(会阻塞),推荐法用 millis() 记录电平变化时刻、稳定超过 30ms 才认可(非阻塞)。要实现"按一次做一次"需做边沿检测:保存上次状态与当前比较,只在下降沿动作。

⚒️ 配套开发者工具箱 · 17 款免费在线工具