Lab 5: Buttons and Interaction
Time: ~45 minutes | Prerequisites: Lab 4 | Hardware: Pico 2, OLED, two push buttons
Now it can listen to your fingers
Your Pico can show you things. Now let's let you talk back. Reading a button sounds
trivial — and then you discover that one press can register as five. Let's tune in.
What You'll Build
A three-mode counter you drive with two buttons: one cycles the mode, the other applies it. Along the way you'll meet the sneakiest bug in embedded programming — switch bounce.
Learning Objectives
- Wire a push button using an internal pull-up resistor
- Explain why a pressed button reads
0and not1 - Describe switch bounce and why it breaks naive code
- Implement debouncing with a time window
- Detect the moment a button is pressed rather than whether it's held
- Build an event loop that switches between modes
Concepts Introduced
| ID | Concept |
|---|---|
| 240 | Digital Input |
| 241 | Pull Up Resistor |
| 242 | Active Low Logic |
| 243 | Switch Bounce |
| 244 | Debouncing |
| 245 | Polling Loop |
| 246 | Edge Detection |
| 247 | Event Loop |
| 248 | Mode Switching |
Background
A floating pin is a liar
An input pin not connected to anything doesn't read 0. It reads whatever the nearby
electrical noise says, flickering randomly. That's a floating pin.
The fix is a pull-up resistor: a gentle connection to 3.3 V that holds the pin at 1
whenever nothing else is driving it. The Pico has these built in — Pin.PULL_UP switches one
on.
So we wire each button between its pin and ground:
| Button state | Pin connected to | Reads |
|---|---|---|
| not pressed | pull-up → 3.3 V | 1 |
| pressed | ground | 0 |
That's active low: pressed is 0. Backwards from intuition, universal in practice.
Switch bounce: one press, many presses
Here's the part that surprises people. A button is two pieces of metal springing together. For a few milliseconds after contact they physically bounce apart and back:
1 2 3 4 5 6 | |
Your finger felt one press. Your Pico, checking millions of times a second, saw five.
Why software fixes a hardware problem
You could fix bounce with a capacitor. But bounce settles within about 20 ms, and
no human presses a button twice in 20 ms — so we just ignore any change that arrives
too soon after the last one. Free, adjustable, no extra parts. Debouncing.
Edges, not levels
There's a difference between "is the button down?" and "did the button just go down?"
If your loop runs 200 times a second and you check level, holding the button for one second
counts 200 presses. You want the edge — the single moment it changed from 1 to 0.
You detect that by remembering what it looked like last time:
1 2 | |
Wiring
Each button connects its GPIO pin to GND. No resistor needed — the pull-up is internal.
| Button | GPIO | Other leg |
|---|---|---|
| A | 14 | GND |
| B | 15 | GND |
A typical 4-pin tactile switch connects pins diagonally. If it seems permanently pressed, rotate it 90°.
Procedure
Step 1 — Watch a button, raw
Before running the full lab, try this in the REPL:
1 2 3 | |
Hold the button down and run a.value() again. It reads 0. That's active low, live.
Step 2 — Run the lab
Open 05-buttons.py and run it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
- Button A cycles: Count up → Count down → Count by 5
- Button B applies the current mode to the counter
Step 3 — Break it on purpose
Find this line and change the value to 0:
1 | |
Run it again and press B once. Watch the counter jump by 2, 3, or more. That's bounce, and you just saw it with your own eyes.
Now put it back to 50. Try 5. Try 200 — notice the button starts feeling sluggish and
ignores fast presses. Debounce timing is a real tradeoff, not a magic number.
If your counter jumps around, you haven't failed
You've reproduced a bug that has shipped in real products. Seeing it deliberately, in a
lab where nothing's at stake, is much nicer than discovering it in week ten of a project.
Step 4 — Levels versus edges
Replace the edge test with a level test:
1 | |
Hold B down. The counter runs away. Restore the pressed() version and it takes one press per
count. That's the difference an edge makes.
Expected Output
The display shows mode and count; the Shell logs each accepted press:
1 2 3 4 5 6 7 | |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Reads 0 constantly |
Button orientation | Rotate the switch 90°; pins connect diagonally |
Reads 1 always |
Not reaching ground | Check the wire from the button's other leg to GND |
| Counter jumps several per press | Debounce too short | Raise DEBOUNCE_MS toward 50 |
| Have to press very deliberately | Debounce too long | Lower DEBOUNCE_MS toward 30 |
| Counter races while held | Testing level, not edge | Use the pressed() edge helper |
| Nothing on screen | Display issue, not buttons | Re-check Lab 4 |
Challenges
- Long press. Detect a button held for more than one second and use it to reset the counter to zero. Hint: record the time on the falling edge, check on the rising one.
- Both at once. Make pressing A and B together do something distinct.
- A real menu. Use A to move a
>cursor down a list of three items and B to select. This is the pattern you'll want in Lab 24 to switch display modes on a live analyzer.
Check Your Understanding
- Why does a pressed button read
0instead of1? - What would happen with no pull-up resistor at all?
- Describe switch bounce and one way to handle it.
- What's the difference between edge detection and level detection, and when does it matter?
- Why is a 50 ms debounce window reasonable — what sets the upper and lower bounds?
You've got input and output now
Screen, buttons, and a chip that knows itself. One more setup lab and then — my
favourite part — we plug in ears.
Next: Lab 6: Deploying Code and Libraries | Previous: Lab 4