Lesson 3 of 6 · 18 min
Buttons and digital input
Outputs are easy: you decide the voltage. Inputs are harder, because now the outside world decides, and the world is noisy. A push button looks like the simplest component in electronics, yet reading it correctly requires understanding floating pins, pull resistors and contact bounce. Get this right and every sensor, limit switch and bumper on your future robots will behave.
What a digital input actually measures
A pin configured with pinMode(pin, INPUT) has a very high input impedance, more than 100 MΩ. It draws almost no current and just compares the voltage on it to a threshold. For a 5 V Uno, anything above about 3 V reads as HIGH, anything below about 1.5 V reads as LOW, and the zone between is undefined.
A button only makes or breaks a connection. When it is open, the pin is connected to nothing at all, and that is the problem.
The floating pin
A pin connected to nothing is floating. Its voltage is set by stray capacitance and whatever electric fields are nearby: your hand, a phone, the mains wiring in the wall. The reading flickers between HIGH and LOW at random.
The fix is a pull resistor, which gives the pin a default voltage when the button is not pressed.
- Pull-down: a resistor from the pin to GND. Idle reads LOW; the button connects the pin to 5 V and reads HIGH.
- Pull-up: a resistor from the pin to 5 V. Idle reads HIGH; the button connects the pin to GND and reads LOW.
Why a resistor and not a plain wire? Because when the button is pressed, the pin is pulled the other way, and a plain wire would create a short circuit. With a 10 kΩ pull-up, pressing the button to GND draws only:
I = V / R = 5 V / 10,000 Ω = 0.5 mA
That is a trivial current, and the resistor is weak enough that the button easily wins when pressed. When the button is open, almost no current flows through the resistor, so there is almost no voltage drop and the pin sits at 5 V.
INPUT_PULLUP: the resistor is already inside
The ATmega328P has an internal pull-up resistor of 20 kΩ to 50 kΩ on every pin, which you switch on in software. That removes one component from your breadboard:
const uint8_t BUTTON_PIN = 2; // button between pin 2 and GND
const uint8_t LED_PIN = 13; // built-in LED
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP); // internal pull-up: idle = HIGH
pinMode(LED_PIN, OUTPUT);
}
void loop() {
// The logic is inverted: pressed connects the pin to GND, so it reads LOW.
bool pressed = (digitalRead(BUTTON_PIN) == LOW);
digitalWrite(LED_PIN, pressed ? HIGH : LOW);
}
Wire one leg of the button to pin 2 and the other leg to GND. Nothing else is needed. The inverted logic (pressed means LOW) feels odd for a day and then becomes natural.
Switch bounce
A mechanical button is two pieces of metal that physically collide. On contact they rebound, so the signal does not go cleanly from HIGH to LOW. It chatters for roughly 1 to 20 ms:
HIGH, LOW, HIGH, LOW, LOW, HIGH, LOW, LOW, LOW ...
Your Uno runs at 16 MHz and your loop might repeat every 10 microseconds, so it sees every single chatter as a separate press. A program that counts presses can count 5 or 10 for one touch of the finger.
Debouncing means ignoring changes until the signal has been stable for a short time, usually 20 to 50 ms. Using delay(50) would work but blocks the whole program, as you saw in lesson 2. Use millis() instead: it returns the number of milliseconds since power-up, so you can compare times without stopping.
Debounce with millis() and edge detection
Often you do not care that the button is held down; you care that it was just pressed. That moment of change is an edge: falling (HIGH to LOW) is the press, rising (LOW to HIGH) is the release. This sketch toggles the LED once per press.
const uint8_t BUTTON_PIN = 2;
const uint8_t LED_PIN = 13;
const unsigned long DEBOUNCE_MS = 30; // signal must be stable this long
int lastReading = HIGH; // previous raw reading
int stableState = HIGH; // debounced state
unsigned long lastChangeTime = 0;
bool ledOn = false;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int reading = digitalRead(BUTTON_PIN);
// Any raw change restarts the stability timer.
if (reading != lastReading) {
lastChangeTime = millis();
lastReading = reading;
}
// Accept the new state only after it has been steady for DEBOUNCE_MS.
if ((millis() - lastChangeTime) > DEBOUNCE_MS && reading != stableState) {
stableState = reading;
if (stableState == LOW) { // falling edge: the button was just pressed
ledOn = !ledOn; // toggle once per press
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
}
}
}
The loop never waits, so you can add sensor reading or motor control beside it. With a 30 ms window the fastest press you can detect is about 17 per second, far faster than any human finger.
Check yourself
A button connects pin 2 to GND, and the pin is set to INPUT_PULLUP. What does digitalRead(2) return while the button is NOT pressed?
Check yourself
A 10 kΩ pull-up is connected to 5 V and the button shorts it to GND. How much current flows through the resistor while the button is pressed?