跳到主要内容

数字 I/O

数字输入输出(Digital I/O)是 Arduino 最基础也是最常用的功能。本章将详细介绍如何使用数字引脚控制 LED、读取按钮状态等。

数字引脚概述

Arduino Uno 有 14 个数字引脚(D0-D13),每个引脚都可以配置为输入或输出模式:

  • D0 (RX)D1 (TX):用于串口通信,一般不建议他用
  • D2-D13:可自由使用的数字引脚
  • D3、D5、D6、D9、D10、D11:支持 PWM 输出(带 ~ 标记)
  • D13:连接板载 LED

引脚模式设置

在使用数字引脚前,必须先使用 pinMode() 函数设置其模式:

void setup() {
pinMode(pin, mode);
}

三种引脚模式

模式说明用途
INPUT输入模式(高阻抗)读取传感器、按钮
OUTPUT输出模式控制 LED、继电器
INPUT_PULLUP输入上拉模式简化按钮接线

数字输出

digitalWrite() 函数

控制数字引脚输出高电平(5V)或低电平(0V):

digitalWrite(pin, value);
  • pin:引脚编号(0-13)
  • valueHIGH(高电平)或 LOW(低电平)

点亮 LED 示例

硬件连接

Arduino D13  ──┬── LED 正极(长脚)
└── 220Ω 电阻 ── GND

代码

const int LED_PIN = 13;  // 使用引脚 13

void setup() {
pinMode(LED_PIN, OUTPUT); // 设置为输出模式
}

void loop() {
digitalWrite(LED_PIN, HIGH); // 点亮 LED
delay(1000); // 等待 1 秒
digitalWrite(LED_PIN, LOW); // 熄灭 LED
delay(1000); // 等待 1 秒
}

流水灯效果

硬件连接:将 4 个 LED 分别连接到 D2-D5,每个串联 220Ω 电阻到 GND

const int ledPins[] = {2, 3, 4, 5};  // LED 连接的引脚
const int numLEDs = 4;

void setup() {
// 初始化所有 LED 引脚
for (int i = 0; i < numLEDs; i++) {
pinMode(ledPins[i], OUTPUT);
}
}

void loop() {
// 从左到右依次点亮
for (int i = 0; i < numLEDs; i++) {
digitalWrite(ledPins[i], HIGH);
delay(200);
digitalWrite(ledPins[i], LOW);
}

// 从右到左依次点亮
for (int i = numLEDs - 1; i >= 0; i--) {
digitalWrite(ledPins[i], HIGH);
delay(200);
digitalWrite(ledPins[i], LOW);
}
}

数字输入

digitalRead() 函数

读取数字引脚的电平状态:

int value = digitalRead(pin);
  • 返回 HIGH(高电平,约 5V)或 LOW(低电平,约 0V)

读取按钮状态

硬件连接方式一:外部下拉电阻

5V ── 按钮 ── D2 ──┬── 10kΩ 电阻 ── GND

Arduino 读取
const int BUTTON_PIN = 2;

void setup() {
pinMode(BUTTON_PIN, INPUT); // 输入模式
Serial.begin(9600);
}

void loop() {
int buttonState = digitalRead(BUTTON_PIN);

if (buttonState == HIGH) {
Serial.println("按钮被按下");
} else {
Serial.println("按钮未按下");
}

delay(100); // 减少串口输出频率
}

硬件连接方式二:内部上拉电阻(推荐)

D2 ── 按钮 ── GND
const int BUTTON_PIN = 2;

void setup() {
// 使用内部上拉电阻,按钮未按下时为 HIGH
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
}

void loop() {
int buttonState = digitalRead(BUTTON_PIN);

// 注意:使用上拉时,按下为 LOW,未按下为 HIGH
if (buttonState == LOW) {
Serial.println("按钮被按下");
}

delay(100);
}

按钮消抖

机械按钮按下时会产生抖动,导致多次触发。抖动通常持续 5-20ms,如果不处理会导致意外的多次触发。

为什么需要消抖

机械按钮的触点在按下和释放瞬间会产生多次弹跳:

理想信号:    ────────┐            ┌────────
│ │
└────────────┘

实际信号: ────────┐ ┌┐ ┌┐ ┌┐ ┌──────
│ ││ ││ ││ │
└─┘└─┘└─────┘└─┘
抖动区域

Arduino 的执行速度非常快,一次抖动可能被识别为多次按钮按下。例如,一个简单的按钮计数程序,按下一次可能计数增加 5-10 次。

方法一:延时消抖(简单但阻塞)

const int BUTTON_PIN = 2;
const int LED_PIN = 13;

void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}

void loop() {
if (digitalRead(BUTTON_PIN) == LOW) { // 按钮按下
delay(50); // 等待抖动稳定

// 等待按钮释放
while (digitalRead(BUTTON_PIN) == LOW) {
// 等待释放
}

delay(50); // 等待释放抖动稳定

// 执行按钮动作
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
}
缺点

使用 delay() 会阻塞整个程序,期间无法响应其他事件。对于简单项目可以接受,但不推荐用于复杂系统。

方法二:基于 millis() 的非阻塞消抖(推荐)

这是最常用的消抖方法,不会阻塞主循环:

const int BUTTON_PIN = 2;
const int LED_PIN = 13;

int ledState = LOW;
int lastButtonState = HIGH;
int buttonState = HIGH;

unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 50; // 消抖延时 50ms

void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, ledState);
}

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

// 如果按钮状态发生变化(可能是抖动开始)
if (reading != lastButtonState) {
lastDebounceTime = millis(); // 重置消抖计时器
}

// 检查是否超过消抖时间
if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
// 如果按钮状态确实改变了
if (reading != buttonState) {
buttonState = reading;

// 只有当按钮从高变低(按下)时才切换 LED
if (buttonState == LOW) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
}
}

lastButtonState = reading;
}

这种方法的工作原理:

  1. 检测到按钮状态变化时,重置计时器
  2. 只有当状态稳定超过 50ms 后,才确认状态改变
  3. 整个过程中主循环继续运行,不阻塞其他代码

方法三:使用 Bounce2 库

Bounce2 是一个专门处理按钮消抖的库,使用更简洁:

#include <Bounce2.h>

const int BUTTON_PIN = 2;
const int LED_PIN = 13;

Bounce debouncer = Bounce();
int ledState = LOW;

void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);

// 配置 Bounce 对象
debouncer.attach(BUTTON_PIN);
debouncer.interval(25); // 25ms 消抖间隔
}

void loop() {
// 更新按钮状态
debouncer.update();

// 检测下降沿(按钮按下)
if (debouncer.fell()) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}

// 其他代码可以在这里继续执行...
}

Bounce2 库提供的方法:

  • fell():检测下降沿(从高到低,按下)
  • rose():检测上升沿(从低到高,释放)
  • read():读取当前稳定状态
  • changed():检测状态是否发生变化

硬件消抖

除了软件消抖,也可以使用硬件电路消抖:

RC 滤波电路

5V ──┬── 按钮 ──┬──── D2 (Arduino)
│ │
10kΩ 0.1μF
│ │
GND ───────┴──── GND

RC 电路的时间常数 τ = R × C。例如 10kΩ × 0.1μF = 1ms,足以滤除高频抖动。

施密特触发器: 对于更干净的信号,可以使用施密特触发器(如 74HC14)对信号进行整形。

消抖最佳实践

  1. 选择合适的消抖时间:一般 20-50ms 足够覆盖大部分按钮的抖动
  2. 使用非阻塞方法:避免 delay(),使用 millis() 或专用库
  3. 检测边沿而非电平:检测按下或释放的瞬间,而不是持续检测电平
  4. 根据应用选择方法:简单项目用延时消抖,复杂项目用状态机或库

综合示例:交通灯

模拟交通信号灯的工作流程:

硬件连接

  • 红灯 → D10
  • 黄灯 → D9
  • 绿灯 → D8
// 引脚定义
const int RED_PIN = 10;
const int YELLOW_PIN = 9;
const int GREEN_PIN = 8;

// 时间设置(毫秒)
const int RED_TIME = 5000; // 红灯 5 秒
const int YELLOW_TIME = 2000; // 黄灯 2 秒
const int GREEN_TIME = 5000; // 绿灯 5 秒

void setup() {
// 初始化引脚
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);

// 初始状态:全部熄灭
allOff();
}

void loop() {
// 绿灯亮(通行)
digitalWrite(GREEN_PIN, HIGH);
delay(GREEN_TIME);
digitalWrite(GREEN_PIN, LOW);

// 黄灯亮(警示)
digitalWrite(YELLOW_PIN, HIGH);
delay(YELLOW_TIME);
digitalWrite(YELLOW_PIN, LOW);

// 红灯亮(停止)
digitalWrite(RED_PIN, HIGH);
delay(RED_TIME);
digitalWrite(RED_PIN, LOW);
}

// 辅助函数:关闭所有灯
void allOff() {
digitalWrite(RED_PIN, LOW);
digitalWrite(YELLOW_PIN, LOW);
digitalWrite(GREEN_PIN, LOW);
}

按键控制 LED

实现按下按钮切换 LED 开关状态:

const int BUTTON_PIN = 2;
const int LED_PIN = 13;

bool ledState = false; // LED 当前状态
bool buttonPressed = false; // 按钮是否已被处理

void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}

void loop() {
int buttonState = digitalRead(BUTTON_PIN);

// 检测按钮按下(上拉模式下 LOW 为按下)
if (buttonState == LOW && !buttonPressed) {
ledState = !ledState; // 切换状态
digitalWrite(LED_PIN, ledState); // 更新 LED
buttonPressed = true; // 标记已处理
delay(200); // 简单消抖
}

// 检测按钮释放
if (buttonState == HIGH) {
buttonPressed = false;
}
}

按钮的不同触发模式

根据应用需求,按钮可以有多种触发方式:

按下触发(Press)

按钮按下的瞬间触发动作:

// 检测下降沿
if (currentButtonState == LOW && lastButtonState == HIGH) {
// 按钮刚被按下
executeAction();
}

释放触发(Release)

按钮释放的瞬间触发动作:

// 检测上升沿
if (currentButtonState == HIGH && lastButtonState == LOW) {
// 按钮刚被释放
executeAction();
}

长按检测

检测按钮长按超过指定时间:

const int BUTTON_PIN = 2;
const int LED_PIN = 13;

const unsigned long LONG_PRESS_TIME = 1000; // 长按时间阈值

unsigned long pressStartTime = 0;
bool isPressed = false;

void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}

void loop() {
int buttonState = digitalRead(BUTTON_PIN);

if (buttonState == LOW && !isPressed) {
// 按钮刚被按下
isPressed = true;
pressStartTime = millis();
}

if (buttonState == HIGH && isPressed) {
// 按钮释放
unsigned long pressDuration = millis() - pressStartTime;
isPressed = false;

if (pressDuration >= LONG_PRESS_TIME) {
Serial.println("长按触发");
// 长按动作
} else {
Serial.println("短按触发");
// 短按动作
}
}
}

双击检测

检测短时间内连续两次按下:

const int BUTTON_PIN = 2;
const unsigned long DOUBLE_CLICK_INTERVAL = 400; // 双击间隔

int lastButtonState = HIGH;
unsigned long lastClickTime = 0;
int clickCount = 0;

void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
}

void loop() {
int buttonState = digitalRead(BUTTON_PIN);

// 检测按钮按下
if (buttonState == LOW && lastButtonState == HIGH) {
delay(50); // 简单消抖

if (digitalRead(BUTTON_PIN) == LOW) {
unsigned long currentTime = millis();

if (currentTime - lastClickTime < DOUBLE_CLICK_INTERVAL) {
// 双击
Serial.println("双击!");
clickCount = 0;
} else {
// 可能是单击,等待确认
clickCount = 1;
}

lastClickTime = currentTime;
}
}

// 检测单击(超时后确认)
if (clickCount == 1 && millis() - lastClickTime > DOUBLE_CLICK_INTERVAL) {
Serial.println("单击");
clickCount = 0;
}

lastButtonState = buttonState;
}

多按钮处理

使用数组管理多个按钮:

const int NUM_BUTTONS = 3;
const int BUTTON_PINS[NUM_BUTTONS] = {2, 3, 4};
const int LED_PINS[NUM_BUTTONS] = {10, 11, 12};

int buttonStates[NUM_BUTTONS];
int lastButtonStates[NUM_BUTTONS] = {HIGH, HIGH, HIGH};

void setup() {
for (int i = 0; i < NUM_BUTTONS; i++) {
pinMode(BUTTON_PINS[i], INPUT_PULLUP);
pinMode(LED_PINS[i], OUTPUT);
}
}

void loop() {
for (int i = 0; i < NUM_BUTTONS; i++) {
buttonStates[i] = digitalRead(BUTTON_PINS[i]);

// 检测按下
if (buttonStates[i] == LOW && lastButtonStates[i] == HIGH) {
delay(50); // 消抖

// 切换对应 LED
digitalWrite(LED_PINS[i], !digitalRead(LED_PINS[i]));
}

lastButtonStates[i] = buttonStates[i];
}
}

多位数码管显示

使用 74HC595 移位寄存器控制数码管(节省引脚):

// 74HC595 引脚连接
const int DATA_PIN = 11; // DS (数据)
const int LATCH_PIN = 12; // ST_CP (锁存)
const int CLOCK_PIN = 13; // SH_CP (时钟)

// 数码管段码(共阴极,0-9)
const byte segmentPatterns[] = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F // 9
};

void setup() {
pinMode(DATA_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
}

void loop() {
// 循环显示 0-9
for (int i = 0; i <= 9; i++) {
displayNumber(i);
delay(1000);
}
}

// 显示单个数字
void displayNumber(int num) {
digitalWrite(LATCH_PIN, LOW);
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, segmentPatterns[num]);
digitalWrite(LATCH_PIN, HIGH);
}

常见问题

1. LED 不亮

检查清单

  • LED 极性是否正确(长脚接正极)
  • 电阻是否正确连接(220Ω-1kΩ)
  • 引脚号是否与代码一致
  • 是否设置了 pinMode(pin, OUTPUT)

2. 按钮读取不稳定

解决方法

  • 添加消抖延时(硬件或软件)
  • 使用 INPUT_PULLUP 模式简化接线
  • 检查按钮接线是否松动

3. 引脚冲突

注意

  • D0、D1 用于串口通信,使用时无法上传程序
  • D13 连接板载 LED,同时使用时可能互相影响
  • PWM 引脚(带 ~)才能使用 analogWrite()

下一步

掌握了数字 I/O 后,我们将学习模拟 I/O,包括读取模拟传感器和使用 PWM 实现更精细的控制。