hello, robot

버튼 하나를 아두이노로 읽는 회로에는 거의 항상 저항이 같이 들어가요. 스위치는 눌림과 뗌 두 상태뿐인데 왜 저항이 필요한지, 플로팅에서 시작해 풀다운·풀업·디바운싱까지 이어서 정리했어요.

보드아두이노 우노 외부 저항10 kΩ 내장 풀업20~50 kΩ · 값 고정 디바운스30 ms · millis() 방식

스위치만 연결하면 핀이 플로팅이 돼요

플로팅(floating)은 디지털 입력 핀이 5V에도 GND에도 연결되지 않아 전위가 정해지지 않은 상태를 말해요. 디지털 입력 핀은 걸린 전압을 보고 HIGH나 LOW로 판독하는데, 어느 쪽에도 묶이지 않으면 판독할 기준 전압 자체가 없어요.

택트 스위치는 다리가 4개지만 같은 편 두 다리는 항상 붙어 있고, 대각선 다리끼리는 누를 때만 연결돼요. 그래서 스위치 한쪽을 5V에, 다른 쪽을 입력 핀에 연결하면 누를 때는 핀이 5V로 확실하게 HIGH가 되지만, 뗐을 때는 핀이 어디에도 안 붙어 플로팅이 돼요.

저항이 들어가는 자리
플로팅에서 저항 하나를 어디에 붙이느냐로 풀다운과 풀업이 갈려요.
스위치만 — 플로팅뗐을 때 값이 정해지지 않아요5VGND스위치핀 (뗐을 때 어디에도 안 붙음)GND로 갈 경로가 없어요풀다운안 누름 0 · 누름 15VGND스위치10 kΩ풀업안 누름 1 · 누름 05VGND10 kΩ스위치저항의 두 다리는 서로 다른 노드에 걸쳐야 해요. 같은 노드에 둘 다 꽂으면 양단이 단락돼 아무 역할을 못 해요.

플로팅의 증상은 두 가지로 나타나요.

환경증상원리
선이 길거나 노이즈가 많음값이 마구 튐핀이 주변 노이즈를 주움
선이 짧고 조용함뗐는데 값이 안 돌아옴실린 전압이 빠져나갈 경로가 없어 직전 값을 붙잡는 래치

버튼이 “됐다 안 됐다” 하는 정체가 대개 아래쪽이에요.

풀다운은 뗄 때 0으로 되돌리는 경로예요

풀다운은 5V에서 스위치를 거쳐 입력 핀으로 오는 선에, 입력 핀과 GND 사이를 10킬로옴 저항으로 이어 붙인 구성이에요. 안 누르면 저항이 핀을 GND 쪽으로 약하게 당겨 LOW, 누르면 5V가 핀에 직접 연결돼 HIGH가 돼요.

저항의 진짜 역할은 뗄 때 핀을 확실한 0으로 되돌리는 방전 경로예요. 값을 10킬로옴으로 크게 쓰는 이유는 누를 때 5V에서 GND로 흐르는 전류를 0.5밀리암페어 수준으로 낮게 유지하면서도, 뗄 때 플로팅은 막을 만큼은 당기기 위해서예요.

이때 저항의 두 다리는 서로 다른 노드에 걸쳐야 해요. 같은 노드에 둘 다 꽂으면 저항 양단이 단락돼 아무 역할을 못 해요.

풀업은 저항과 스위치의 자리를 맞바꾼 거예요

풀업은 5V에서 저항을 거쳐 입력 핀으로 오고, 그 핀이 스위치를 거쳐 GND로 가는 구성이에요. 풀다운과 비교하면 저항과 스위치가 자리를 바꾼 것뿐인데 신호는 정반대로 뒤집혀요.

구성저항 위치안 누름누름외부 저항
풀다운핀 ↔ GND01반드시 필요
풀업핀 ↔ 5V10내장으로 대체 가능

풀다운은 누르면 1이라 직관적이고, 풀업은 누르면 0이라 역논리예요. 역논리인데도 풀업이 실무 기본이 되는 이유가 오른쪽 칸에 있어요.

우노는 내장 풀업만 있고 내장 풀다운은 없어요

내장 풀업(INPUT_PULLUP)은 칩 안에 이미 들어 있는 풀업 저항을 코드 한 줄로 켜는 기능이에요. pinMode(BTN, INPUT_PULLUP)으로 설정하면 외부 저항과 5V 배선 없이 버튼을 핀과 GND 사이에만 연결하면 돼요. 이 경우 안 누름이 HIGH, 누름이 LOW가 돼요.

내장 풀업은 전기적으로는 외부 저항과 같지만 값이 20~50킬로옴으로 고정돼 있어 정확하지 않아요. 특정 저항 값이 필요하거나 풀다운이 필요하면 외부 저항을 써야 해요.

여기서 중요한 비대칭이 하나 있어요. 아두이노 우노에는 내장 풀업은 있지만 내장 풀다운은 없어요. 그래서 저항 없이 버튼만으로 되는 건 풀업뿐이에요. 입문 예제와 실무가 풀업을 기본으로 삼는 실용적인 이유 중 하나가 이 비대칭이에요.

접점은 한 번에 붙지 않아요

버튼의 금속 접점은 누르는 순간 깔끔하게 한 번 붙지 않고, 수 밀리초 동안 여러 번 붙었다 떨어지기를 반복해요. 이걸 바운스(bounce) 또는 채터링(chattering)이라고 해요. MCU가 그 사이를 빠르게 읽으면 한 번 눌러도 여러 번 눌린 것으로 세어져요.

채터링과 디바운싱
값이 흔들리는 동안 타이머를 계속 리셋하고, 같은 값이 30 ms를 넘겨 유지될 때만 상태를 확정해요.
0153045607590경과 시간 (ms)원시 입력HIGHLOW확정 신호HIGHLOW채터링DEBOUNCE 30 ms

해결은 값이 바뀐 뒤 일정 시간(보통 20~30밀리초) 안정될 때까지 기다렸다가 진짜 변화로 인정하는 거예요. 이때 delay()로 멈춰서 기다리면 그 시간 동안 다른 입력과 출력이 전부 막히니까, 경과 시간을 재는 millis() 방식이 좋아요.

const int BTN = 2;
const unsigned long DEBOUNCE = 30;   // ms
int stableState, lastReading;
unsigned long lastChange;
int count = 0;
 
void setup() {
  pinMode(BTN, INPUT_PULLUP);        // 뗌=HIGH, 누름=LOW
  Serial.begin(9600);
  stableState = digitalRead(BTN);
  lastReading = stableState;
}
 
void loop() {
  int reading = digitalRead(BTN);
  if (reading != lastReading) {       // 값이 흔들리면 타이머 리셋
    lastChange = millis();
    lastReading = reading;
  }
  if (millis() - lastChange > DEBOUNCE && reading != stableState) {
    stableState = reading;            // DEBOUNCE보다 오래 유지되면 확정
    if (stableState == LOW) {         // 누름으로 확정되는 순간만 센다
      count++;
      Serial.print("눌림! 총 ");
      Serial.print(count);
      Serial.println("회");
    }
  }
}

확정된 상태가 LOW로 바뀌는 순간, 즉 눌린 순간에만 카운트를 올려요. 뗄 때(HIGH로 확정)는 세지 않으니 한 번 누름이 한 번으로 잡혀요.

무엇을 풀업으로, 무엇을 풀다운으로 둘까

고르는 기준은 결국 아무도 건드리지 않을 때 핀이 어느 값에 있어야 안전한가예요.

신호평소 있어야 할 값선택이유
I2C 통신선 (SDA·SCL)HIGH풀업규격상 필수
리셋·인터럽트·칩 선택HIGH풀업대기 상태가 HIGH
MOSFET 게이트LOW풀다운뜨면 의도치 않게 도통
모터·릴레이 EnableLOW풀다운부팅·고장 중 오구동 방지

버튼 하나에 저항이 따라오는 이유도 같은 원리예요. 안 누를 때 핀을 확실한 값에 묶어두는 것, 그게 풀업과 풀다운의 본질이에요.

앞 글에서 PC와 아두이노를 전선으로 잇고 신호를 주고받은 기록은 시리얼 통신 디버깅 — 안 보이는 신호를 보이게 만드는 법에 있어요.