Raspberry Pi Pico Push Button Input with Interrupts and Debouncing
Reading a push button looks trivial until you try it: an unconnected input floats and reads random values, a single press registers as five presses, and a loop that is busy elsewhere misses short taps completely. This tutorial fixes all three problems on the Raspberry Pi Pico. You will use the internal pull-up resistor, compare polling with interrupts, and write a debounced interrupt handler that counts presses reliably and toggles an LED.
Inputs, Floating Pins and Pull Resistors
A GPIO configured as an input has a very high impedance, so if nothing drives it the voltage drifts with noise and your hand's capacitance. A pull resistor gives it a defined idle level. The RP2040 and RP2350 have internal pull-up and pull-down resistors on every GPIO, roughly 50 kilo-ohm, enabled from software.
| Wiring | Pin setting | Idle reading | Pressed reading |
|---|---|---|---|
| Button between GPIO and GND | Pin(14, Pin.IN, Pin.PULL_UP) | 1 | 0 |
| Button between GPIO and 3V3(OUT) | Pin(14, Pin.IN, Pin.PULL_DOWN) | 0 | 1 |
The pull-up arrangement (button to ground) is the most common because a short to ground is harmless, and it avoids running 3.3 V wires around the board. Its logic is inverted: pressed reads 0.
Circuit
| Part | Connection |
|---|---|
| Push button A | One leg to GP14 (physical pin 19), other leg to GND (pin 18) |
| Push button B | One leg to GP15 (physical pin 20), other leg to GND (pin 18) |
| LED + 330 ohm resistor | GP16 (pin 21) through resistor to LED anode, cathode to GND (pin 23) |
Four-leg tactile switches have two pairs of legs that are always connected. Place the switch across the breadboard's centre gap and use legs on diagonally opposite corners to be sure you are switching through the contacts.
Polling: The Simple Approach
Polling means checking the pin in a loop. It is fine for simple programs, and detecting the change from 1 to 0 (an edge) rather than the level prevents the LED toggling continuously while the button is held.
# poll_button.py - edge detection by polling
from machine import Pin
import time
button = Pin(14, Pin.IN, Pin.PULL_UP) # pressed = 0
led = Pin(16, Pin.OUT)
last = button.value()
while True:
now = button.value()
if last == 1 and now == 0: # falling edge = new press
led.toggle()
time.sleep_ms(30) # crude debounce: ignore bounce
last = now
time.sleep_ms(5)
The weakness is obvious when the loop also has slow work to do, such as reading a sensor that takes 250 ms. A press shorter than that can be missed entirely.
Interrupts with Pin.irq and Debouncing
An interrupt makes the hardware call a function the moment the pin changes, whatever the main program is doing. Mechanical contacts bounce for a few milliseconds when they close, producing several edges, so the handler must ignore edges that arrive too soon after the previous accepted one. The program below counts presses on two buttons: button A increments and toggles the LED, button B resets the count.
# irq_buttons.py - interrupt-driven buttons with time-based debounce
from machine import Pin
import time
DEBOUNCE_MS = 50 # ignore edges closer together than this
btn_a = Pin(14, Pin.IN, Pin.PULL_UP)
btn_b = Pin(15, Pin.IN, Pin.PULL_UP)
led = Pin(16, Pin.OUT)
press_count = 0
reset_flag = False
last_a = 0
last_b = 0
def on_a(pin):
# Called on the falling edge of GP14 (button pressed)
global press_count, last_a
now = time.ticks_ms()
if time.ticks_diff(now, last_a) > DEBOUNCE_MS:
last_a = now
press_count += 1
led.toggle()
def on_b(pin):
# Only set a flag; the main loop does the printing
global reset_flag, last_b
now = time.ticks_ms()
if time.ticks_diff(now, last_b) > DEBOUNCE_MS:
last_b = now
reset_flag = True
btn_a.irq(trigger=Pin.IRQ_FALLING, handler=on_a)
btn_b.irq(trigger=Pin.IRQ_FALLING, handler=on_b)
shown = -1
while True:
if reset_flag:
reset_flag = False
press_count = 0
print('Counter reset')
if press_count != shown:
shown = press_count
print('Presses:', shown)
time.sleep_ms(250) # simulate slow work; presses are still counted
How the Interrupt Code Works
btn_a.irq(trigger=Pin.IRQ_FALLING, handler=on_a)registers the handler for the high-to-low edge, which is the press on a pull-up circuit. UsePin.IRQ_RISINGfor the release, or OR both together to catch either.time.ticks_ms()andtime.ticks_diff()handle counter wrap-around correctly. Never subtract tick values directly.- Handlers keep work minimal: update a counter, toggle a pin or set a flag. Printing, sensor reads and network calls belong in the main loop.
- On the rp2 port, pin handlers are soft interrupts by default, scheduled by MicroPython shortly after the edge, so they may allocate memory. If you pass
hard=Truefor lower latency, the handler must not allocate memory, which rules out creating lists, strings or floats inside it. - Even with a 250 ms sleep in the main loop, every press is counted because the hardware captured the edge.
50 ms is a reasonable debounce window for most tactile switches. Cheap or worn switches may need 80 to 100 ms. Hardware debouncing, a 100 nF capacitor across the switch with the pull-up resistor forming an RC filter, reduces bounce further and is worth adding when wires to the button are long.
Troubleshooting
- Count jumps by 2 or 3 per press: increase
DEBOUNCE_MS, or add the 100 nF capacitor. - Random presses with nothing touched: the pull-up is missing or the wire is on another pin. Check
Pin.PULL_UPand the GPIO number. - Button always reads 0: the switch is rotated 90 degrees on the breadboard so those legs are permanently connected.
NameErroror count never changes: aglobaldeclaration is missing inside the handler.- MemoryError in handler: you used
hard=Trueand created an object inside it; switch to a soft IRQ or pre-allocate.
Summary
Buttons need a defined idle level, which the Pico's internal pull-ups provide. Polling works for simple loops, while Pin.irq catches every press regardless of what the main program is doing. A time-based debounce using ticks_diff turns noisy contacts into clean single events, and keeping interrupt handlers short avoids hard-to-find bugs. Next, you will use PWM to fade an LED smoothly instead of switching it on and off.