Skip to main content
Summary

버튼은 아두이노에게 말을 거는 가장 간단한 방법입니다

LED 깜빡이기 실습에서 아두이노는 정해진 시간표대로 움직였습니다. 켜고, 기다리고, 끄고, 다시 기다리는 일을 반복했습니다. 이번에는 흐름이 달라집니다. 사람이 버튼을 누르면 LED가 켜지고, 손을 떼면 꺼집니다. 즉, 아두이노가 사용자의 행동을 읽고 반응합니다.

버튼은 전기 회로에서 문과 비슷합니다. 문이 닫히면 길이 이어지고, 문이 열리면 길이 끊깁니다. 다만 아두이노는 "버튼이 눌렸다"라는 말을 직접 이해하지 않습니다. 대신 특정 핀의 전압이 높은지 낮은지를 읽습니다. 이때 사용하는 함수가 digitalRead()입니다.

digitalRead(pin)은 지정한 핀의 상태를 읽고 HIGH 또는 LOW를 돌려줍니다. 보통 HIGH는 전압이 높은 상태, LOW는 GND에 가까운 낮은 상태로 이해하면 됩니다. 버튼, 스위치, 디지털 센서 출력처럼 두 가지 상태만 구분하면 되는 입력에 사용합니다.1

이번 장에서는 버튼 하나와 LED 하나를 연결해 다음 세 가지 동작을 만들어 봅니다.

  • 버튼을 누르는 동안만 LED 켜기

  • 버튼을 한 번 누를 때마다 LED 상태 바꾸기

  • 버튼의 흔들리는 신호를 안정적으로 읽기

디지털 입력으로 버튼 읽기AI 생성 이미지

입력 핀은 그냥 비워 두면 흔들립니다

먼저 중요한 규칙부터 보겠습니다. 아두이노의 입력 핀은 아무 데도 확실히 연결되지 않으면 값이 흔들릴 수 있습니다. 이를 "핀 입력이 떠 있다", 영어로는 floating 상태라고 부릅니다.

떠 있는 입력 핀은 마치 아무도 잡고 있지 않은 가벼운 문과 같습니다. 바람이 조금만 불어도 열렸다 닫혔다 합니다. 실제 회로에서는 주변 전기 잡음, 손의 접근, 점퍼선의 위치 때문에 핀이 HIGH처럼 읽히기도 하고 LOW처럼 읽히기도 합니다. 그래서 버튼 회로에는 "버튼을 누르지 않았을 때의 기본값"을 정해 주는 장치가 필요합니다.

이 역할을 하는 것이 풀업 저항 또는 풀다운 저항입니다.

  • 풀다운 저항: 버튼을 누르지 않았을 때 입력 핀을 GND 쪽으로 끌어내려 LOW로 고정합니다.

  • 풀업 저항: 버튼을 누르지 않았을 때 입력 핀을 전원 쪽으로 끌어올려 HIGH로 고정합니다.

둘 다 목적은 같습니다. 버튼이 눌리지 않은 순간에도 입력값이 떠다니지 않게 기준을 잡아 주는 것입니다.

풀다운 회로와 내부 풀업 회로

먼저 풀다운 회로를 생각해 봅시다. 버튼 한쪽은 5V에 연결하고, 다른 쪽은 입력 핀에 연결합니다. 그리고 입력 핀에서 GND로 저항 하나를 연결합니다. 버튼을 누르지 않으면 저항을 통해 입력 핀이 GND에 연결되어 LOW가 됩니다. 버튼을 누르면 5V가 입력 핀에 들어와 HIGH가 됩니다.

이 방식은 논리가 직관적입니다.

  • 안 누름: LOW

  • 누름: HIGH

하지만 외부 저항을 하나 더 꽂아야 합니다. 브레드보드 실습에서는 이 저항을 빼먹기 쉽고, 위치를 잘못 꽂으면 입력값이 계속 흔들립니다.

아두이노에는 이 문제를 줄여 주는 기능이 있습니다. 바로 내부 풀업 저항입니다. pinMode(buttonPin, INPUT_PULLUP);이라고 쓰면 보드 안쪽의 저항이 입력 핀을 전원 쪽으로 약하게 당겨 줍니다. 그래서 외부 저항 없이 버튼과 GND만 연결해도 안정적인 입력을 읽을 수 있습니다.2

단, 내부 풀업을 쓰면 논리가 반대로 됩니다.

  • 안 누름: HIGH

  • 누름: LOW

처음에는 어색하지만 입문 실습에서는 이 방식이 훨씬 단순합니다. 점퍼선도 줄고, 저항 연결 실수도 줄어듭니다. 이 장의 실습은 INPUT_PULLUP 방식으로 진행합니다.

버튼과 LED 연결하기

준비물은 다음과 같습니다.

  • 아두이노 Uno 계열 보드 1개

  • 브레드보드 1개

  • 푸시버튼 1개

  • LED 1개

  • 220Ω~470Ω 저항 1개

  • 점퍼선 몇 개

연결은 다음 순서로 합니다.

  1. LED의 긴 다리, 즉 애노드를 아두이노 13번 핀에 저항을 거쳐 연결합니다.

  2. LED의 짧은 다리, 즉 캐소드를 GND에 연결합니다.

  3. 버튼의 한쪽 다리를 아두이노 2번 핀에 연결합니다.

  4. 버튼의 반대쪽 다리를 GND에 연결합니다.

버튼은 브레드보드의 가운데 홈을 가로질러 꽂는 것이 안전합니다. 푸시버튼은 다리가 네 개여도 내부적으로는 두 쌍이 연결되어 있는 경우가 많습니다. 같은 쪽 다리끼리는 이미 연결되어 있으므로, 방향을 잘못 잡으면 버튼을 누르지 않아도 회로가 계속 이어질 수 있습니다.

연결 후 증상이 이상하면 다음을 먼저 확인하세요.

  • LED가 켜지지 않음: LED 방향이 반대인지, 저항이 빠졌는지, GND가 연결됐는지 확인합니다.

  • 버튼을 안 눌러도 LED가 켜짐: 코드에서 LOW와 HIGH 조건을 반대로 이해했는지 확인합니다.

  • 버튼 반응이 불규칙함: pinMode()가 INPUT_PULLUP인지, 버튼 한쪽이 GND에 연결됐는지 확인합니다.

누르는 동안만 LED 켜기

가장 작은 코드부터 시작하겠습니다. 버튼 값을 읽고, 그 값에 따라 LED를 켜거나 끕니다.

const int buttonPin = 2;
const int ledPin = 13;

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

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

  if (buttonState == LOW) {
    digitalWrite(ledPin, HIGH);
  } else {
    digitalWrite(ledPin, LOW);
  }
}

한 줄씩 보겠습니다.

const int buttonPin = 2;는 버튼이 연결된 핀 번호를 이름으로 저장합니다. 나중에 핀을 바꾸더라도 숫자 하나만 고치면 됩니다.

pinMode(buttonPin, INPUT_PULLUP);은 2번 핀을 입력으로 사용하되 내부 풀업 저항을 켭니다. 이 때문에 버튼을 누르지 않으면 2번 핀은 HIGH입니다. 버튼을 누르면 2번 핀이 GND와 연결되어 LOW가 됩니다.

digitalRead(buttonPin);은 현재 버튼 상태를 읽습니다. 읽은 값은 buttonState 변수에 저장합니다.

마지막 조건문이 핵심입니다. 내부 풀업 방식에서는 버튼을 눌렀을 때 LOW가 읽힙니다. 그래서 buttonState == LOW일 때 LED를 켭니다.

이 코드는 "누르는 동안만 작동하는 장치"에 적합합니다. 초인종 버튼, 누르고 있는 동안만 도는 모터, 누르는 동안만 켜지는 손전등을 떠올리면 됩니다.

한 번 누를 때마다 LED 상태 바꾸기

이번에는 동작이 조금 다릅니다. 버튼을 누르는 동안만 LED가 켜지는 것이 아니라, 한 번 누르면 켜지고 다시 한 번 누르면 꺼지게 만들겠습니다. 이런 동작을 토글(toggle)이라고 합니다.

토글을 만들려면 현재 LED 상태를 기억해야 합니다. 또한 버튼이 "지금 눌려 있는가"뿐 아니라 "방금 눌리기 시작했는가"를 구분해야 합니다.

const int buttonPin = 2;
const int ledPin = 13;

int ledState = LOW;
int lastButtonState = HIGH;

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

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

  if (lastButtonState == HIGH && buttonState == LOW) {
    ledState = !ledState;
    digitalWrite(ledPin, ledState);
  }

  lastButtonState = buttonState;
}

여기서 ledState는 LED의 현재 상태를 기억합니다. LOW면 꺼진 상태, HIGH면 켜진 상태입니다.

lastButtonState는 바로 직전에 읽은 버튼 상태입니다. 내부 풀업을 쓰므로 버튼을 누르지 않은 기본 상태는 HIGH입니다.

조건문을 보면 lastButtonState == HIGH && buttonState == LOW라고 되어 있습니다. 이는 "직전에는 안 눌려 있었고, 지금은 눌렸다"는 뜻입니다. 즉, 버튼을 누르기 시작한 순간만 잡아냅니다. 버튼을 계속 누르고 있는 동안에는 이 조건이 반복해서 참이 되지 않습니다.

ledState = !ledState;는 상태를 반대로 바꿉니다. HIGH였다면 LOW가 되고, LOW였다면 HIGH가 됩니다.

이제 아두이노는 단순히 버튼의 현재 상태를 따라가는 것이 아니라, 과거 상태와 현재 상태를 비교해 한 번의 동작을 판단합니다. 여기서부터 입력 장치가 조금 더 똑똑해집니다.

버튼은 한 번 눌러도 여러 번 흔들릴 수 있습니다

기계식 버튼 안에는 작은 금속 접점이 있습니다. 손으로 버튼을 누르면 접점이 닿는데, 실제로는 완벽하게 한 번에 붙지 않습니다. 아주 짧은 시간 동안 튕기듯 붙었다 떨어졌다 할 수 있습니다. 이를 채터링 또는 바운스라고 부릅니다.

사람에게는 한 번 누른 동작입니다. 하지만 아두이노는 매우 빠르게 반복해서 입력을 읽기 때문에 짧은 흔들림도 여러 번의 눌림처럼 볼 수 있습니다. 토글 코드에서 버튼을 한 번 눌렀는데 LED가 켜졌다가 바로 꺼지거나, 예상과 다르게 여러 번 바뀌는 일이 생기는 이유가 여기에 있습니다.3

가장 단순한 해결은 잠깐 기다린 뒤 다시 확인하는 것입니다.

const int buttonPin = 2;
const int ledPin = 13;

int ledState = LOW;
int lastButtonState = HIGH;

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

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

  if (lastButtonState == HIGH && buttonState == LOW) {
    delay(30);
    buttonState = digitalRead(buttonPin);

    if (buttonState == LOW) {
      ledState = !ledState;
      digitalWrite(ledPin, ledState);
    }
  }

  lastButtonState = buttonState;
}

이 코드는 버튼이 눌린 것처럼 보이면 30밀리초 기다린 뒤 다시 읽습니다. 그때도 여전히 눌린 상태라면 진짜 눌림으로 인정합니다. 간단하고 이해하기 쉽지만, delay(30) 동안 아두이노는 다른 일을 하지 못합니다. 버튼 하나와 LED 하나만 다룰 때는 큰 문제가 아니지만, 센서 여러 개와 모터를 함께 다루기 시작하면 불편해집니다.

millis()로 기다리되 멈추지 않기

조금 더 나은 방법은 millis()를 사용하는 것입니다. millis()는 아두이노가 켜진 뒤 흐른 시간을 밀리초 단위로 알려줍니다. delay()처럼 멈춰서 기다리는 대신, "마지막 변화 이후 시간이 충분히 지났는가?"를 계속 확인합니다.

const int buttonPin = 2;
const int ledPin = 13;

int ledState = LOW;
int buttonState = HIGH;
int lastReading = HIGH;

unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;

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

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

  if (reading != lastReading) {
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != buttonState) {
      buttonState = reading;

      if (buttonState == LOW) {
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
      }
    }
  }

  lastReading = reading;
}

변수가 많아 보이지만 역할을 나누면 단순합니다.

  • reading: 방금 읽은 날것의 버튼 값입니다.

  • lastReading: 직전에 읽은 날것의 버튼 값입니다.

  • buttonState: 흔들림이 지나간 뒤 안정된 버튼 상태입니다.

  • lastDebounceTime: 입력값이 마지막으로 바뀐 시간입니다.

  • debounceDelay: 안정되었다고 판단하기 위해 기다릴 시간입니다.

흐름은 이렇습니다. 버튼 값이 바뀌면 시간을 기록합니다. 그 뒤 50밀리초가 지나도 값이 유지되면 안정된 변화로 인정합니다. 그리고 그 안정된 변화가 "눌림"이라면 LED 상태를 한 번만 바꿉니다. 이 방식은 delay()처럼 프로그램 전체를 멈추지 않기 때문에, 나중에 부저나 센서를 함께 사용할 때 더 유리합니다.4

작은 인터랙션 설계하기

버튼 입력을 다룰 때는 세 가지를 함께 생각하면 좋습니다.

첫째, 입력입니다. 버튼이 눌렸는지 읽습니다. 여기서는 digitalRead(buttonPin)이 그 역할을 했습니다.

둘째, 상태입니다. LED가 지금 켜져 있는지, 버튼이 직전에는 어떤 상태였는지 기억합니다. ledState, lastButtonState, buttonState 같은 변수가 이 역할을 합니다.

셋째, 출력입니다. 판단 결과를 LED로 보여 줍니다. 여기서는 digitalWrite(ledPin, ledState)가 출력입니다.

이 세 가지를 묶으면 아주 작은 사용자 인터페이스가 됩니다. 버튼 하나로 LED를 켜고 끄는 일은 단순해 보이지만, 구조는 더 큰 프로젝트와 같습니다. 출입문 버튼, 알람 해제 버튼, 메뉴 선택 버튼, 로봇 시작 버튼도 결국 입력을 읽고, 상태를 기억하고, 출력을 바꾸는 방식으로 동작합니다.

마지막으로 실습 중 문제가 생겼을 때는 코드를 한꺼번에 의심하지 말고 기준을 나누어 확인하세요.

  • 버튼을 누르지 않았을 때 INPUT_PULLUP 입력은 HIGH여야 합니다.

  • 버튼을 눌렀을 때 입력은 LOW가 되어야 합니다.

  • LED에는 반드시 전류 제한 저항이 있어야 합니다.

  • 토글 동작에는 현재 상태를 기억하는 변수가 필요합니다.

  • 한 번 눌렀는데 여러 번 반응하면 디바운싱을 의심합니다.

버튼 입력을 이해하면 아두이노는 정해진 일을 반복하는 장치에서 사람이 조작할 수 있는 장치로 바뀝니다. 다음 단계에서는 이 입력을 숫자로 더 세밀하게 읽어 보겠습니다. 밝기, 소리, 위치처럼 두 가지로 나뉘지 않는 값을 다루면 아두이노가 주변 세계를 훨씬 풍부하게 받아들일 수 있습니다.

참고

1Arduino digitalWrite() and digitalRead(): Complete Guide with Examples

2Arduino digitalWrite() and digitalRead(): Complete Guide with Examples

3Arduino Button Debouncing Techniques

4Arduino - Button - Debounce

책 목록

아두이노 학습: 기초 회로부터 IoT 캡스톤까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기