Lesson 03: Reading the Physical World — Push Buttons and Digital Inputs
Robotics from Scratch · Lesson 03 · Beginner · ~15 min read · Updated April 2026
Your Board Can Listen
In Lesson 02, you controlled something in the physical world. You wrote code that set a GPIO pin HIGH, and current flowed, and an LED lit up. The ESP32 talked to the world.
Now it listens.
A push button is one of the simplest input devices that exists. You press it, a circuit closes, and the ESP32 detects that closure and responds. That is the entire mechanism — but it opens up everything that follows. Sensors, switches, encoders, door contacts, limit switches on a robot arm. They are all versions of the same idea: something in the physical world changes, and your code finds out about it.
You are about to make that happen. When you are done, you will have a button that lights an LED on press. And then you will make it toggle — press once, LED on. Press again, LED off. That is the foundation of every user interface you have ever used.
Let's begin.
What a Push Button Actually Does
A push button is a mechanical switch. When you press it, two contacts inside the button connect to each other, and current can flow through. When you release it, a spring pushes the contacts apart and the circuit opens again.
The buttons in your kit are called 4-pin tactile buttons. They have four terminals — two pairs. Inside the button, each pair of pins that are across from each other are already connected. The button has a little metal bridge inside that connects the two pairs when you press it.
Here is what it looks like mechanically:
```
TOP VIEW (button from above)
+-----+
| O | <- the plastic button cap you press
+-----+
+- -+
| |
o o o o <- the four legs (two pairs)
| | | |
| | | |
pin1 pin2 pin3 pin4
When you press: pin1 connects to pin2
pin3 connects to pin4
When you release: all four pins are isolated from each other
```
That is the whole thing. No electricity flows through the button itself to power anything. The button is just a gate. It either lets current pass or it doesn't.
Why You Need a Pull Resistor
Here is a problem you might not expect: when a GPIO pin is set to input mode, it is not connected to anything by default. It just sits there, listening. But it is extremely sensitive — it can pick up stray electrical signals from the air, from nearby wires, from radio interference. This is called a floating pin. A floating pin gives unpredictable readings. It might read HIGH one moment and LOW the next, entirely on its own.
A pull resistor solves this. It anchors the pin to a known state — either HIGH (pull-up) or LOW (pull-down) — so the pin always has a definite value unless something deliberate overrides it. When you press the button, that deliberate override wins.
The two configurations:
Pull-down (button connects to 3.3V):
- Resistor connects GPIO pin to GND (ground)
- Without the button pressed, the pin reads LOW (0V) — the pull-down anchors it
- When you press the button, 3.3V flows into the pin and it reads HIGH
- The button "wins" because it has lower resistance than the pull-down resistor
Pull-up (button connects to GND):
- Resistor connects GPIO pin to 3.3V
- Without the button pressed, the pin reads HIGH (3.3V) — the pull-up anchors it
- When you press the button, GND connects to the pin and it reads LOW
- Logic is inverted: button pressed = LOW, button released = HIGH
The ESP32 has built-in pull-up and pull-down resistors on every GPIO pin. You do not need to wire external resistors for pull-ups — you enable them in code. This is the cleanest approach for beginners, and it is what we will use.
Wiring the Button
Wire the button to GPIO 4 on your ESP32. Here is the breadboard layout using only plain ASCII:
```
Breadboard layout (ASCII, groups of 5 holes)
+----------------------------------------------------+
| 1 2 3 4 5 6 7 8 9 10|
+----------------------------------------------------+
| |
a | [ESP32 3.3V] ---- wire ----+----[Button]---- (leg 1) |
| | |
b | | (leg 3) |
| | |
c | +----+----+ |
| | | |
d | [ESP32 GPIO4] ------------+---------+ |
| | | |
e | +----+----+ |
| |
f | |
+----------------------------------------------------+
Button leg 1: 3.3V through button to GPIO4
Button leg 3: connected to same row as leg 1 (both pairs bridged inside button)
(legs 2 and 4 are the other pair — also internally bridged)
Full circuit path:
3.3V ---> Button (closed = short) ---> GPIO4
|
internal pull-down (enabled in code)
|
v
GND
When button is NOT pressed: GPIO4 sees GND through pull-down = reads LOW
When button IS pressed: 3.3V flows into GPIO4 = reads HIGH
```
The four legs straddle the center channel of the breadboard. This is intentional — the legs on each side of the channel are in separate rows. The two legs on the left are internally connected (pair 1), and the two legs on the right are internally connected (pair 2). The left pair connects to 3.3V, the right pair connects to GPIO 4 (and through a pull-down inside the ESP32 to GND).
The Code — MicroPython
Here is the basic code to read the button state:
```python
from machine import Pin
import time
GPIO 4 — change if you used a different pin
button = Pin(4, Pin.IN)
while True:
state = button.value() # Read the pin: 1 = HIGH, 0 = LOW
if state == 1:
print("Button is pressed!")
else:
print("Button is released.")
time.sleep(0.1) # Poll every 100ms — fast enough to feel instant
```
Run this. Open your serial terminal. Press the button. You should see "Button is pressed!" printed when you hold it down, and "Button is released." when you let go.
This is all it takes to read a digital input. button.value() returns 1 (HIGH) when 3.3V is present at the pin, and 0 (LOW) when the pin is at GND.
Adding the LED Back In
Now connect the LED from Lesson 02 to GPIO 2 (same as before). The wiring is unchanged — the LED circuit is separate from the button circuit. They only share ground.
```python
from machine import Pin
import time
led = Pin(2, Pin.OUT) # LED on GPIO 2 (output)
button = Pin(4, Pin.IN) # Button on GPIO 4 (input)
while True:
if button.value() == 1: # Button pressed — 3.3V at the pin
led.value(1) # Turn LED on
else: # Button released — pin is LOW
led.value(0) # Turn LED off
time.sleep(0.05)
```
Press and hold the button. The LED lights. Release it. The LED goes out. That is input and output working together.
Toggle Mode — Your Turn Challenge
Here is where it gets interesting. Instead of the LED being on only while the button is held, make it toggle each time you press it:
- Press once: LED turns ON and stays ON
- Press again: LED turns OFF and stays OFF
- The LED remembers its state between presses
The challenge: you need to detect the moment the button is pressed, not just whether it is pressed right now. The trick is to remember the last state and only act when the state changes.
Try it yourself before looking at the solution. This is the actual pattern used in every button-based UI — a game controller, a microwave keypad, a light switch in a smart home.
A Solution
```python
from machine import Pin
import time
led = Pin(2, Pin.OUT)
button = Pin(4, Pin.IN)
led_state = False # Track whether the LED is on or off
last_button = 0 # Track the previous button reading
while True:
current_button = button.value()
# Detect a press: button went from 0 (released) to 1 (pressed)
if current_button == 1 and last_button == 0:
led_state = not led_state # Flip the state (True -> False, False -> True)
led.value(1 if led_state else 0) # Apply the new state to the LED
last_button = current_button
time.sleep(0.05)
```
Test this. Press the button — the LED comes on and stays on. Press it again — it goes off and stays off. Each press toggles.
The critical part is the edge detection:
if current_button == 1 and last_button == 0:
This fires only at the moment of pressing, not continuously while held. Without this, the LED would flicker rapidly as the code looped through multiple toggles per press.
Pull-Up Configuration (Optional)
Some projects use pull-up instead of pull-down — the logic is inverted. With pull-up enabled internally:
```python
button = Pin(4, Pin.IN, Pin.PULL_UP)
```
Now the pin reads LOW when the button is pressed and HIGH when released. Flip your if condition accordingly:
```python
if current_button == 0 and last_button == 1:
# Button was just pressed (inverted logic with pull-up)
```
Both approaches are correct. The ESP32 has built-in pull-ups on every GPIO pin — take advantage of them. You do not need external pull resistors for simple button circuits.
Why This Matters Beyond Buttons
A push button is the simplest possible sensor. Everything that follows — a temperature sensor, a motion detector, a distance ranger, a limit switch on a robot joint — uses the same pattern:
- Something in the physical world changes
- The ESP32 reads a GPIO pin
- Your code decides what to do based on that reading
Once you can read a button, you can read anything that speaks in on/off signals. That is most of the sensors in the world.
Key Concepts Learned
- Digital input: A GPIO pin configured to read voltage — HIGH (3.3V) or LOW (0V).
- Push button: A normally-open mechanical switch. Closes when pressed, opens when released. Has two pairs of internally connected terminals.
- Pull resistor: Anchors a floating GPIO pin to a known state. ESP32 has built-in pull-ups and pull-downs.
- Pull-down: Keeps pin reading LOW by default. Button to 3.3V overrides and reads HIGH when pressed.
- Pull-up: Keeps pin reading HIGH by default. Button to GND overrides and reads LOW when pressed.
- Edge detection: Detecting the moment a signal changes, not just its current state. Essential for toggle behavior.
What You Need
Everything from Lesson 02 plus:
- A push button — the 4-pin tactile kind included in most starter kits. A budget kit with buttons, resistors, and jumper wires: Push button kit on Amazon (affiliate link)
The push button is the only new component. Everything else you already have.
Troubleshooting
The button reading is erratic — it flips on and off randomly.
The pin is floating. Make sure you enabled the pull-down in code:
`button = Pin(4, Pin.IN)`
If wiring externally, add a 10k Ohm resistor from GPIO 4 to GND.
The LED is always on (or always off) regardless of the button.
Check your wiring. Make sure the button legs straddle the center channel of the breadboard. Verify which leg connects to 3.3V and which to GPIO 4 using a continuity tester or by carefully tracing the circuit.
The toggle fires twice per press.
You are detecting the button state continuously instead of detecting the edge. The edge detection condition must check that the previous state was 0 AND the current state is 1. If your condition fires when current == 1 without checking last_button, it will trigger on every loop iteration while the button is held.
The button reads inverted (pressed = 0, released = 1).
You either have a pull-up configured instead of pull-down, or you wired the button to GND instead of 3.3V. Either is fine — just invert your logic to match.
What's Next
You now have bidirectional communication: the ESP32 can control things (output) and be told about things (input). That is the complete picture of a robot's sensory-motor loop. Sense, decide, act.
In Lesson 04, we connect the ESP32 to WiFi and build a robot you can control from your phone. The lesson after that adds motors and movement. You are building toward a fully autonomous robot — one that senses its environment and responds to commands over the internet.
The hardware basics are done. From here, it is software and connectivity.
You made the ESP32 listen. That is not a small thing.
CryptoTavern Robotics from Scratch is an evolving course. Each lesson improves over time as the community learns. If something was unclear, if you got stuck, or if something just worked and you want to say so — that is what the course is for.
Next lesson: Lesson 04 — Wireless Control: Building a WiFi Robot