Digital I/O is the simplest thing a microcontroller does and the foundation of everything else. An output pin is either at 5 V or at 0 V. An input pin reports whether the voltage on it is nearer 5 V or nearer 0 V. Both directions have one non-obvious trap, and both traps catch nearly every beginner.
| Arduino C | What it does |
|---|---|
pinMode(pin, OUTPUT); | Configure the pin to drive a voltage |
digitalWrite(pin, HIGH); | Drive the pin to 5 V |
digitalWrite(pin, LOW); | Drive the pin to 0 V |
pinMode(pin, INPUT); | Configure the pin to read — needs an external pull resistor |
pinMode(pin, INPUT_PULLUP); | Read, using the chip's built-in pull-up resistor |
int state = digitalRead(pin); | Returns HIGH (1) or LOW (0) |
The floating input problem
Connect a button between a pin and 5 V, and press it: the pin reads HIGH, exactly as expected. Release it, and the pin is connected to nothing at all. A microcontroller input is extremely high impedance, so with nothing driving it the pin picks up stray electromagnetic noise from the mains, from your hand, from the wiring — and reports HIGH and LOW essentially at random. This is a floating input, and it is why a button circuit needs a third component.
- 10 kΩ from the pin to GROUND
- Button connects the pin to 5 V
- Resting state: LOW. Pressed: HIGH
- Reads the way you intuitively expect
- 10 kΩ from the pin to 5 V
- Button connects the pin to GROUND
- Resting state: HIGH. Pressed: LOW
- Available inside the chip — no external part needed
pinMode(2, INPUT_PULLUP); switches on a resistor built into the chip, so you wire the button between the pin and ground and need no external components at all. The price is inverted logic: pressed reads LOW. Write your test as if (digitalRead(2) == LOW) and remember why — this catches people every single time.Why 10 kΩ specifically? It has to be small enough that the stray currents in a high-impedance input cannot pull the pin away from its resting level, and large enough that when the button is pressed the current through it is negligible. At 5 V across 10 kΩ the current is 0.5 mA — firm enough to define the level, small enough to ignore. A 100 Ω resistor would work electrically but would waste 50 mA whenever the button is held.
Switch bounce
A mechanical switch does not close cleanly. Its contacts physically bounce apart and together for a few milliseconds, so a single press produces a burst of HIGHs and LOWs. A human sees one press; a microcontroller sampling millions of times per second sees five or ten. A counter incremented on every press jumps by seven, and a toggle ends up in a random state.
| Arduino C | What it does |
|---|---|
if (digitalRead(BTN) == LOW) { | Button is pressed (with INPUT_PULLUP) |
delay(50); | Wait past the bouncing window |
if (digitalRead(BTN) == LOW) { | Still pressed — so it was real, not a bounce |
count = count + 1; | Act on the press |
while (digitalRead(BTN) == LOW) { } | Wait for release, so holding it does not count repeatedly |
} | |
} |
delay(1000) blocks the processor completely — no button is read, no sensor is checked, nothing else happens for that whole second. It is fine in a simple sketch and fatal in anything that must stay responsive. The alternative is millis(), which returns the milliseconds since power-up so you can check whether enough time has passed without ever stopping.| Arduino C | What it does |
|---|---|
unsigned long previous = 0; | Remember when the last action happened |
const long interval = 1000; | How long to wait between actions |
if (millis() - previous >= interval) { | Has enough time passed? Never blocks |
previous = millis(); | Reset the timer for the next interval |
// do the timed thing here | Meanwhile loop() keeps running and reading inputs |
} |
digitalWrite(pin, LOW) turns the LED on — which surprises people reading the code later.