Part
2
  |  
The Hardware Layer
  |  
Chapter
7

Digital Input

Stop polling in a tight loop — the hardware already knows how to listen for events, and it's faster than your while loop.
Reading Time
10
mins
BACK TO RASPBERRY PI MASTERCLASS

The trap is checking the pin value in a while True loop. Every beginner does it. You wire up a button, then write a loop that reads the pin state a thousand times a second, burning CPU cycles to detect a press that happens maybe once every few seconds. It works, technically. But it's the hardware equivalent of hitting refresh on a web page instead of opening a WebSocket. The Pi's processor has built-in edge detection — circuitry designed to watch for the exact moment a signal transitions from LOW to HIGH or HIGH to LOW. Using a polling loop instead of edge detection is like writing a cron job that queries the database every second instead of setting up a trigger.

Polling a GPIO pin in a tight loop is the hardware equivalent of hitting refresh on a web page instead of opening a WebSocket.

I've seen this pattern where teams build a monitoring device — say, a button that triggers an alert — and the polling loop chews through 30% of the Pi's CPU doing nothing useful. On a Pi Zero with a single core, that's the difference between a responsive system and one that stutters every time the OS needs to run a background process.

Pull-Up, Pull-Down, and the Floating Pin Problem

Before we get to the code, you need to understand a hardware concept that has no direct software equivalent: the floating pin.

When a GPIO pin is configured as an input and nothing is connected to it, the pin is "floating." It's not connected to 3.3V (HIGH) or ground (LOW). It picks up electrical noise from the environment — nearby wires, radio interference, the Pi's own processor switching — and the value you read will flicker randomly between HIGH and LOW. This isn't a bug. It's physics. An unconnected wire is an antenna.

The solution is a pull-up or pull-down resistor:

  • A pull-up resistor connects the pin to 3.3V through a high-value resistor (typically 10K ohm). The pin reads HIGH by default. When you press the button, it connects the pin directly to ground, which overpowers the weak pull-up, and the pin reads LOW.
  • A pull-down resistor connects the pin to ground through a high-value resistor. The pin reads LOW by default. When you press the button, it connects the pin to 3.3V, and the pin reads HIGH.

The good news: the Raspberry Pi has internal pull-up and pull-down resistors built into every GPIO pin, configurable in software. And gpiozero enables them automatically. When you create a Button object, it activates the internal pull-up resistor by default, so the pin reads HIGH when the button is not pressed and LOW when pressed.

Key takeaway

A floating input pin is an antenna. It reads random noise. Pull-up and pull-down resistors give the pin a defined default state, and gpiozero configures them for you automatically.

from gpiozero import Button

# Internal pull-up enabled by default
# Pin reads HIGH when button is NOT pressed, LOW when pressed
button = Button(17)

# Explicitly specifying pull-up (same as default)
button_explicit = Button(17, pull_up=True)

# Using pull-down instead (pin reads LOW by default, HIGH when pressed)
button_pulldown = Button(17, pull_up=False)

The Interrupt Mindset

Framework · The Interrupt Mindset

Stop polling. Start listening. Hardware interrupts detect signal edges — the exact moment a pin transitions from LOW to HIGH or HIGH to LOW — in microseconds, without burning CPU cycles. Your job is to register a callback and get out of the way.

The Pi's BCM chip supports hardware edge detection on every GPIO pin. When enabled, the chip watches the pin in dedicated hardware and fires an interrupt — a signal to the CPU — the instant the pin changes state. This happens in microseconds, compared to the milliseconds (at best) your Python polling loop achieves.

gpiozero exposes three event-driven interfaces:

wait_for_press() — Blocking Wait

The simplest pattern. The script blocks at this call until the button is physically pressed:

from gpiozero import Button, LED

button = Button(17)
led = LED(27)

print("Waiting for button press...")
button.wait_for_press()
led.on()
print("Button pressed! LED is on.")

button.wait_for_release()
led.off()
print("Button released. LED is off.")

This is clean for sequential scripts — "wait for this, then do that." But it doesn't scale to multiple inputs. If you're waiting for button A, you can't simultaneously check button B.

when_pressed / when_released — Event Callbacks

This is the pattern you'll use in real projects. Register a function that runs when the event fires:

from gpiozero import Button, LED
from signal import pause

button = Button(17)
led = LED(27)

def handle_press():
    led.on()
    print("Pressed — LED on")

def handle_release():
    led.off()
    print("Released — LED off")

button.when_pressed = handle_press
button.when_released = handle_release

print("Listening for button events. Ctrl+C to exit.")
pause()

The callbacks run in a background thread managed by gpiozero. The pause() call keeps the main thread alive. This pattern handles multiple buttons, sensors, and inputs simultaneously because each callback is independent.

✕ Polling loop
  • Burns CPU cycles continuously
  • Response time depends on sleep interval
  • Blocks the thread — can't watch multiple inputs
  • Misses fast presses during sleep()
✓ Event callbacks
  • Near-zero CPU usage while waiting
  • Microsecond response time
  • Multiple inputs handled independently
  • Hardware edge detection catches every transition

The Toggle Pattern

A common real-world requirement: press a button to toggle an LED on, press again to toggle it off. This is where beginners write spaghetti — tracking state in global variables, debouncing manually, handling edge cases around rapid presses. gpiozero makes it one line:

from gpiozero import Button, LED
from signal import pause

button = Button(17)
led = LED(27)

button.when_pressed = led.toggle

print("Press to toggle. Ctrl+C to exit.")
pause()

That's the entire program. led.toggle is a method reference — when the button fires its when_pressed event, it calls led.toggle(), which flips the LED's state. No state variable, no if/else, no boolean tracking.

The toggle pattern in gpiozero is one line because the library does what good libraries do: it hides the state machine you'd otherwise write yourself.

Debouncing: When One Press Registers as Five

Mechanical buttons bounce. When you press a tactile switch, the metal contacts don't make a single clean connection. They vibrate — bouncing on and off several times in the first few milliseconds before settling. To your finger, it's one press. To the Pi's edge detection, it's five or ten rapid transitions.

Without debouncing, your toggle code would fire multiple times per press. The LED would flicker and land in an unpredictable state — sometimes on, sometimes off, depending on whether the bounce count was even or odd.

gpiozero handles this with the bounce_time parameter:

from gpiozero import Button, LED
from signal import pause

# 200ms debounce — ignore transitions within 200ms of each other
button = Button(17, bounce_time=0.2)
led = LED(27)

button.when_pressed = led.toggle

print("Debounced toggle. Ctrl+C to exit.")
pause()

The bounce_time=0.2 parameter tells gpiozero to ignore any state changes within 200 milliseconds of the first detected edge. This filters out the mechanical bounce while still responding to intentional presses. The default bounce_time is None (no debounce), which is why beginners hit this problem — the default assumes you want raw edge detection.

Choosing bounce time

For tactile push buttons, 50-200ms works. For toggle switches (the kind you flip up and down), 100-300ms. For reed switches (used with magnets), 20-50ms. If you set it too high, rapid intentional presses get swallowed. Start at 200ms and lower it if the button feels unresponsive.

Hardware Debounce vs Software Debounce

The bounce_time parameter is software debouncing — the Pi's processor ignores rapid transitions in code. There's also hardware debouncing: a small capacitor (0.1uF) placed across the button terminals smooths out the electrical bounce before the signal even reaches the GPIO pin. The capacitor charges and discharges slowly enough that the rapid bounces get filtered into a single clean transition.

Which should you use? For prototyping and most Pi projects, software debouncing is fine. The CPU cost is negligible, and it's adjustable without rewiring. Hardware debouncing is better when you're reading high-frequency signals (like a rotary encoder) where the software debounce window would swallow legitimate rapid transitions, or when you're building a production product and want the cleanest possible signal at the hardware level.

For the projects in this book, software debouncing with bounce_time is sufficient. If you ever find yourself tuning the debounce window and still getting unreliable reads, add the capacitor.

Hold Duration and Long Press Detection

Real products distinguish between a short press and a long press — think of holding your phone's power button versus tapping it. gpiozero's Button supports this with the hold_time and when_held properties:

from gpiozero import Button, LED
from signal import pause

button = Button(17, bounce_time=0.2, hold_time=2)
led = LED(27)

def on_press():
    led.on()
    print("Short press — LED on")

def on_release():
    led.off()
    print("Released — LED off")

def on_hold():
    print("LONG PRESS detected (held for 2+ seconds)")
    # In a real project: trigger shutdown, reset settings, enter pairing mode
    led.blink(on_time=0.1, off_time=0.1)

button.when_pressed = on_press
button.when_released = on_release
button.when_held = on_hold

print("Tap for on/off, hold 2s for long press. Ctrl+C to exit.")
pause()

The hold_time=2 parameter means when_held fires after the button has been continuously pressed for two seconds. This is a real product pattern — Raspberry Pi projects that act as appliances (media players, sensor hubs, home automation controllers) use long press for power-off or configuration mode, and short press for normal operation.

A Complete Input/Output Project: Event Counter

Here's a practical script that ties together everything in this chapter — a button that counts presses, displays the count, and toggles an LED on every fifth press:

from gpiozero import Button, LED
from signal import pause

button = Button(17, bounce_time=0.2)
led = LED(27)
press_count = 0

def on_press():
    global press_count
    press_count += 1
    print(f"Press #{press_count}")

    if press_count % 5 == 0:
        led.toggle()
        state = "ON" if led.is_lit else "OFF"
        print(f"  -> LED toggled {state} (every 5th press)")

button.when_pressed = on_press

print("Press the button. LED toggles every 5 presses. Ctrl+C to exit.")
pause()
Global state in callbacks

The global press_count pattern works for single-threaded callbacks on a Pi. But if you're building something more complex — multiple buttons, network requests in callbacks, concurrent sensor reads — switch to a class-based design or a threading.Lock. gpiozero callbacks run in background threads, and unsynchronized global state is a race condition waiting to happen.

This pattern scales directly to real projects: a door sensor that counts entries, a production-line counter that tracks items on a conveyor, or an emergency stop button that latches a shutdown state. The GPIO input is the same — what changes is the callback logic.

Combining Multiple Inputs

Real hardware projects rarely have a single button. Here's the pattern for handling multiple independent inputs:

from gpiozero import Button, LED
from signal import pause

button_a = Button(17, bounce_time=0.2)
button_b = Button(27, bounce_time=0.2)
led_red = LED(22)
led_green = LED(23)

button_a.when_pressed = led_red.toggle
button_b.when_pressed = led_green.toggle

print("Button A toggles red. Button B toggles green. Ctrl+C to exit.")
pause()

Each button-LED pair is independent. Pressing A doesn't affect B. This is the event-driven model working as designed — each input has its own callback, and the callbacks don't share state unless you explicitly connect them.

Beyond Buttons: Other Digital Input Sources

A Button in gpiozero is really just "a pin that reads HIGH or LOW." The library doesn't care whether the signal comes from a human pressing a switch or from a sensor triggering. Any device that outputs a digital signal — HIGH or LOW, 3.3V or ground — works with the same Button class:

  • PIR motion sensors — output HIGH when motion is detected, LOW when idle. Wire the sensor's output pin to a GPIO, create a Button (or better, use gpiozero's MotionSensor class which is a thin wrapper), and register when_pressed to trigger a camera capture, an alert, or a log entry.
  • Reed switches — a pair of contacts that close when a magnet is nearby. Used in door and window sensors. The switch acts exactly like a button: closed circuit when the magnet is near, open when it's removed.
  • Infrared break-beam sensors — an emitter and a receiver with a gap between them. When something passes through the gap (a person, a product on a conveyor), the beam breaks and the receiver's output changes state.
  • Limit switches — mechanical switches mounted at the end of a track. When a motor-driven mechanism reaches its travel limit, it physically presses the switch. Same interface as a button.
from gpiozero import MotionSensor
from signal import pause
from datetime import datetime

pir = MotionSensor(4)  # PIR sensor on GPIO 4

def on_motion():
    print(f"[{datetime.now().strftime('%H:%M:%S')}] Motion detected!")

def on_no_motion():
    print(f"[{datetime.now().strftime('%H:%M:%S')}] Motion stopped.")

pir.when_motion = on_motion
pir.when_no_motion = on_no_motion

print("PIR sensor active. Ctrl+C to exit.")
pause()

The abstraction is the same: a pin changes state, a callback fires. Whether the trigger is a finger on a button, a body moving past a sensor, or a door swinging open, the code structure is identical. This is why mastering digital input with a button prepares you for every binary sensor you'll ever connect to a Pi.

Key takeaway

Event-driven GPIO is the same paradigm as event-driven web programming. Register handlers, let the framework dispatch events, and keep your callbacks small and fast. The only difference is the events come from physical switches instead of HTTP requests.

What to Do Monday Morning

Wire a button to GPIO 17

Place a tactile push button across the center channel of your breadboard. Connect one side to GPIO 17 and the other side to ground. No external resistor needed — gpiozero's Button class enables the internal pull-up automatically. Run the wait_for_press() example and confirm it detects your press.

See the bounce

Write a raw polling loop that prints every state change with a timestamp. Press the button slowly and observe multiple rapid transitions in the output — that's mechanical bounce. Then add bounce_time=0.2 and watch the noise disappear. Seeing bounce with your own eyes is worth more than any explanation.

Build the toggle circuit

Wire a button and an LED. Implement the one-line toggle pattern. Press the button ten times and confirm the LED alternates cleanly between on and off. If it doesn't toggle cleanly, your bounce time is too low — increase it.

Build the event counter

Implement the press-counting script that toggles the LED every fifth press. Run it for 25 presses and verify the LED toggles exactly five times. This exercises your understanding of callbacks, state management, and debouncing in one small program.

Compare CPU usage

Run a tight polling loop (while True: if button.is_pressed: ...) and check CPU usage with top or htop. Then run the event-driven version with pause() and check again. On a Pi Zero, the difference is dramatic — 30% CPU versus less than 1%. That's the cost of ignoring hardware interrupts.

Register handlers, let the framework dispatch events, and keep your callbacks small and fast — event-driven GPIO is the same paradigm as event-driven web programming.