A digital input reads a binary signal that takes only two states: pressed or released, present or absent. Pushbutton, switch, limit switch, digital presence sensor: all of them are read with Pin.value(). The difficulty is not the reading itself but dealing with bounce: a mechanical contact produces several spurious transitions within a few milliseconds, and they have to be filtered out to get a clean signal.
What is it for?
Reading a digital input is the founding gesture of any embedded user interface: starting a cycle, confirming a setpoint, triggering a measurement, detecting a limit switch. A typical project has several of them (a Start/Stop button, a limit switch on each axis). The good practice: learn the reading with debouncing once, then duplicate it cleanly on every input.
Step by step
Four steps: wire with a pull-up, read, add debouncing, detect the edge.
1. Wire the button with PULL_UP
One side of the button on the pin (here GP14), the other on GND. No external resistor: the internal pull-up (Pin.PULL_UP) holds 3.3 V at rest and falls to GND when pressed.
Inverted logic: bouton.value() returns 1 at rest and 0 when pressed.
2. Raw reading
The console is the REPL: print() writes to it, like the Arduino serial monitor.
from machine import Pin
from time import sleep
bouton = Pin(14, Pin.IN, Pin.PULL_UP) # input + internal pull-up: rest = 1
while True:
print(bouton.value()) # shows 1 (released) or 0 (pressed)
sleep(0.01) # ~100 readings per secondRun the script and press: you see 1, 1, 0, 0, 1.... Looking closely, a few spurious transitions appear at the moment of the press (0, 1, 0, 1): that is mechanical bounce, and it lasts a few milliseconds.
3. Software debouncing
The simplest countermeasure: ignore any transition that has not been confirmed for 20–50 ms. The timing is done with [[micropython-temporisation-en|ticks_ms() and ticks_diff()]] (never a direct subtraction of ticks_ms(), which wraps around):
from machine import Pin
from time import ticks_ms, ticks_diff
bouton = Pin(14, Pin.IN, Pin.PULL_UP)
dernier_etat = 1 # last value READ (shakes during the bounce)
etat_stable = 1 # CONFIRMED state (the one we act on)
dernier_changement = ticks_ms() # time of the last change in the reading
DELAI_REBOND = 30 # quiet period required before validating (ms)
while True:
lecture = bouton.value() # raw reading on every pass
if lecture != dernier_etat: # has the reading just changed?
dernier_changement = ticks_ms() # note WHEN
dernier_etat = lecture # and the new value read
# unchanged for long enough? (the bounce is over)
if ticks_diff(ticks_ms(), dernier_changement) > DELAI_REBOND:
if lecture != etat_stable: # the confirmed state really changed
etat_stable = lecture # validate the new state
print("Button:", "pressed" if etat_stable == 0 else "released")How to read this code. The trick is held by two variables. dernier_etat follows the raw value read at this instant (it shakes during the bounce). etat_stable keeps only the confirmed state, the one we act on. On each pass through the loop:
- if the reading changes, we do not believe the button straight away: we only note the moment of the change (
dernier_changement = ticks_ms()); - as long as it changes again (bounce), that moment is pushed back over and over;
- as soon as it stays the same for 30 ms, the bounce is over:
etat_stableis validated.
ticks_ms() returns a millisecond counter running since start-up. ticks_diff(maintenant, depart) gives the time elapsed since the last shake, handling the counter wrap-around, which a direct subtraction would not. Comparing it with DELAI_REBOND amounts to asking: has the signal been quiet long enough to be believed?
From now on each press produces exactly one line, whatever the quality of the button.
4. Detect the edge (press vs hold)
Often we want to react to the press itself, not for as long as the key is held. That is falling-edge detection — inside the stable branch:
if etat_stable == 0:
# FALLING EDGE — the button has just been pressed
print("Press detected!")A toggle variant (one press inverts an LED): led.toggle() at that point.
Example — Count presses and toggle an LED
Debouncing, edge detection and a visible action.
from machine import Pin
from time import ticks_ms, ticks_diff
bouton = Pin(14, Pin.IN, Pin.PULL_UP)
led = Pin(15, Pin.OUT)
dernier_etat = 1 # the same 2 variables as in step 3
etat_stable = 1
dernier_changement = ticks_ms()
DELAI_REBOND = 30
compteur = 0 # number of presses counted
while True:
lecture = bouton.value()
if lecture != dernier_etat: # --- debounce block, identical to step 3 ---
dernier_changement = ticks_ms()
dernier_etat = lecture
if ticks_diff(ticks_ms(), dernier_changement) > DELAI_REBOND:
if lecture != etat_stable:
etat_stable = lecture
if etat_stable == 0: # falling edge = one more press
compteur += 1 # +1 press
led.toggle() # invert the LED
print("Press #", compteur)Each press increments a counter and inverts the LED — crisp behaviour, immune to bounce.
Pitfalls
No debouncing. Typical symptom: one press counts as 3 or 4. The bounce is invisible to the eye but plain to see at the processor’s millisecond.
Subtracting ticks_ms() directly. ticks_ms() wraps around and restarts at zero: ticks_ms() - depart can go negative. Always ticks_diff(maintenant, depart), which is designed to handle the wrap-around.
Floating input. With no PULL_UP (or external resistor), the pin floats: the LED toggles as a hand passes nearby. Almost always PULL_UP with the button to GND.
Getting the pull-up logic backwards. value() == 1 means released, not pressed.
Confusing hold and press. Reacting to value() == 0 acts for as long as the button is held (a counter runs away). For per-press behaviour, detect the edge.
sleep() inside the reading loop. A sleep(0.5) misses short presses. The debounce pattern above deliberately uses no sleep: it watches time through ticks_ms().
Button between 3.3 V and the pin. PULL_UP with the button to 3.3 V gives a reading that is always 1. Always take the button to GND.
Special case — Reacting fast, or many buttons
- Interrupt — so as not to miss a press during a computation,
bouton.irq(trigger=Pin.IRQ_FALLING, handler=...)fires a function on the edge, with no polling (see interrupts). Debouncing is still required. - Several buttons — duplicate the pattern (one stable state and one change timestamp per button), ideally wrapped in a function or a class.
Where it fits in the project
- Step 2 of the proof of concept phase — every command button (Start/Stop, confirm) tested in isolation with debouncing before integration.
- Step 2 of the integration and testing phase — every mechanical limit switch is validated as a digital input with debouncing.
The debounce pattern is meant to be copied and adapted from one page to the next once understood: 10 minutes here save hours of debugging spent wondering why the system miscounts presses.
See also
- MicroPython — the module hub
- Configuring MicroPython GPIO — prerequisite on
PinandPULL_UP - off output — the matching output
- Timing delays in MicroPython —
sleep()vsticks_ms(), essential to non-blocking debouncing - Interrupts in MicroPython — reacting to an edge without polling
- off input (Arduino) — the C++ equivalent