Buttons, Interrupts, and State Machines
Summary
Students wire buttons, filter switch bounce in hardware and software, and respond with interrupt handlers. They build state machines to cycle clock modes. After this chapter they can let users set the time with buttons or an encoder.
Concepts Covered
This chapter covers the following 13 concepts from the learning graph:
| Concept | Concept Impact Score |
|---|---|
| Momentary Push Button | 6 |
| Button Wiring | 5 |
| Button Debouncing | 4 |
| State Machine | 12 |
| Hardware Debounce | 1 |
| Callback Functions | 15 |
| Interrupt Handlers | 14 |
| Software Debounce | 2 |
| IRQ Falling Edge | 1 |
| Mode Cycling | 7 |
| Rotary Encoder | 3 |
| Encoder Direction | 2 |
| Encoder Acceleration | 1 |
Prerequisites
This chapter builds on concepts from:
- Chapter 1: Computational Thinking and Physical Computing
- Chapter 2: Electronics Fundamentals and Breadboard Wiring
- Chapter 3: MicroPython Basics: Variables and Data Types
- Chapter 4: Control Flow, Functions, and Modules
- Chapter 5: The Pico Platform: Pins, Files, and Firmware
- Chapter 6: Getting the Time in MicroPython
Give Your Clock Some Buttons
A clock that can't be set is just a fancy paperweight. In this chapter you'll add buttons, tame their jittery signals, and build the mode-cycling logic that lets anyone set the hour and minute. Let's make time tick!
Letting People Talk to the Clock
So far your programs have only output information. To set the time or an alarm, a person must send information in. The simplest input device is a push button, and this chapter takes you from one wire on a breadboard to a complete "run / set hour / set minute" menu. Along the way you will meet the two hard problems of buttons, bounce and timing, and the tools for solving them: debouncing, interrupts, and state machines. The chapter ends with the rotary encoder, a knob that makes setting the time faster.
Momentary Push Button
A momentary push button is a switch that connects its terminals only while you hold it down and disconnects when you let go. "Momentary" is the opposite of a light switch that stays in position. The small square buttons in your kit are called tactile switches and have four legs.
The four legs are connected in two pairs inside the case. Legs on the same side are always joined, and pressing the button joins the two pairs. So the button acts as a single switch connected between either leg of one pair and either leg of the other. If you wire two legs on the same side, the button will never seem to work.
Because it has no memory and no power of its own, a button is a sensor in the sense of Chapter 1: it reports "pressed" or "not pressed" and nothing more.
Button Wiring
Wire the button between a GPIO pin and ground and let the internal pull-up from Chapter 5 do the rest:
- Place the button across the center gap of the breadboard so its two pairs of legs land in different strips.
- Connect one leg to the GPIO pin (for example GP14).
- Connect the opposite-side leg to a ground rail.
- In code, configure the pin with
Pin.PULL_UP.
The standard kits use GP14 for the mode button and GP15 for the cycle button, in the lower-left corner of the Pico. The pin reads 1 when idle and 0 when pressed (active low). You need no external resistor, and no 3.3 V wire.
1 2 3 | |
Button Debouncing
A mechanical switch is not a clean on/off device. When the metal contacts collide, they spring apart and touch again several times, sending a burst of rapid on/off pulses before settling. This is called contact bounce, and it typically lasts a few milliseconds up to about 20 ms.
A human sees one press. The Pico is fast enough to see the burst as ten presses. Debouncing is any technique that ignores the bounce so one physical press counts as exactly one event.
One Press, Ten Counts
If your counter jumps by 3 or 7 when you press a button once, that's bounce, not a bug in your wiring. It happens because real contacts chatter as they close. The fix is to ignore any change that arrives within about 50 ms of the last one.
Diagram: Button Bounce Oscilloscope
Button Bounce Oscilloscope
Type: MicroSim
sim-id: button-bounce-scope
Library: p5.js
Status: Specified
Learning objective: Students will analyze how bounce creates false presses and evaluate the effect of a debounce delay (Bloom: Analyzing, Evaluating).
Visual elements: A scrolling oscilloscope-style trace of the pin voltage (3.3 V high, 0 V low). Below it, a counter shows the number of presses counted.
Controls:
- "Press button" generates a realistic bounce burst (random 3 to 12 transitions over 1 to 15 ms).
- Slider "Debounce window (ms)": 0 to 100, default 0.
- Toggle "Hardware filter (0.1 µF)" that smooths the trace.
Behavior: With the window at 0, one press increments the counter several times. As the window grows past the bounce length the counter increments once.
Responsive design: The trace fills the container width.
Implementation: p5.js with a timestamped event list.
Hardware Debounce
Hardware debouncing filters the bounce in the circuit itself. The simple version places a 0.1 µF capacitor across the button, between the GPIO pin and ground. The capacitor smooths out the rapid changes because it charges and discharges slowly through the pull-up resistor. With the internal pull-up of about 50 kΩ, the time constant is
which is on the order of the bounce time. It costs one small part and no code. In practice, software debouncing is more common in this course because it needs no extra parts.
Software Debounce
Software debouncing ignores extra events in code. The most reliable method is a time window: remember when you last accepted a press, and reject any new one that arrives too soon.
1 2 3 4 5 6 7 8 9 10 11 12 | |
A window of 20 to 50 ms stops bounce but still feels instant. Larger values, like the 200 ms sometimes used in examples, also work, but make fast repeated presses feel sluggish.
Interrupt Handlers
Reading a button in a loop is called polling: you keep asking "pressed yet?" It works, but if your loop is busy redrawing the display, a short press can slip by unnoticed. An interrupt is a different approach. The hardware watches the pin and, the moment it changes, pauses your main code and runs a special function called an interrupt handler (also called an interrupt service routine or ISR). When the handler finishes, your main code resumes.
You attach a handler with the pin's irq() method:
1 2 3 4 | |
The handler receives one argument: the pin that triggered it. Interrupts make sure a press is never missed, even when the main loop is busy.
IRQ Falling Edge
An edge is the moment a signal changes level. A falling edge is the change from high (1) to low (0), and a rising edge is low to high. The trigger argument chooses which edge starts the handler.
| Trigger | Fires when | With a pull-up button |
|---|---|---|
Pin.IRQ_FALLING |
Signal goes 1 to 0 | The instant the button is pressed |
Pin.IRQ_RISING |
Signal goes 0 to 1 | The instant the button is released |
| Both constants combined | Either change | Press and release |
To listen for both edges, combine the two constants with the | operator. Because a pull-up button is active low, IRQ_FALLING is the right trigger for "the user pressed the button."
Callback Functions
A callback function is a function you give to some other code, so that code can call it back later when something happens. The handler above is a callback: you do not call on_press() yourself; you hand it to irq(), and the hardware calls it. Callbacks appear whenever your code must react to events instead of running in a fixed order.
The rules for a good interrupt callback are simple:
- Keep it short and fast. Do not draw on the display or use long
sleep()calls inside it. - Do only the minimum: set a flag (a variable) or increment a counter.
- Let the main loop notice the flag and do the real work.
Flag It and Leave
Inside an interrupt handler, just set pressed = True and return. Your main loop checks the flag, clears it, and does the heavy work. Handlers stay fast and your display code stays out of trouble.
Here is the complete pattern, combining an interrupt, a debounce window, and a flag. The handler needs the global keyword from Chapter 4 to change variables outside itself.
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 | |
State Machine
A state machine is a way of organizing a program around a small set of named states, where each state defines what the program does and which events cause a change to a different state. A traffic light is a state machine: green, yellow, and red are states, and a timer is the event that moves it along.
For a clock, the states are the modes the user is in, and the event is a button press. Consider a simple three-state design:
| State | What the display shows | Mode button press goes to |
|---|---|---|
| RUN | The current time | SET_HOUR |
| SET_HOUR | The hour, flashing | SET_MINUTE |
| SET_MINUTE | The minute, flashing | RUN |
A drawing of the same idea, with circles for states and arrows for transitions, makes the logic obvious at a glance and is a good way to plan before coding.
Ask 'What State Am I In?'
A state machine turns a mess of if statements into one question: what state am I in, and what does this event do there? The same button press can mean different things in different states, and that is exactly how a two-button clock does so much.
Diagram: Clock Mode State Machine
Clock Mode State Machine
Type: interactive diagram
sim-id: clock-mode-state-machine
Library: vis-network
Status: Specified
Learning objective: Students will apply a state machine to predict what a button press does in each mode (Bloom: Applying).
Layout: Circles for RUN, SET_HOUR, SET_MINUTE (and optionally SET_ALARM_HOUR, SET_ALARM_MINUTE). Arrows are labeled "Mode press."
Interactions:
- Two on-screen buttons, "Mode" and "Cycle." Pressing Mode moves the highlighted state along the arrows; pressing Cycle changes the value shown in a mock clock face beside the diagram (hour +1 in SET_HOUR, minute +1 in SET_MINUTE, no effect in RUN).
- Clicking a state opens an infobox describing what the display shows and what each button does there.
- A "Predict" toggle asks the student to name the next state before it moves.
Responsive design: The canvas fills the container and calls network.fit() on resize.
Implementation: vis-network with three nodes and scripted highlighting.
Mode Cycling
Mode cycling is stepping through the states in a loop, one press at a time, wrapping from the last mode back to the first. It uses the modulo trick from Chapter 4:
1 2 3 4 5 6 | |
After three presses you are back to RUN. The second button, the cycle button, changes the value that belongs to the current mode, again with a wrap: hours go through 0 to 23 (% 24) and minutes 0 to 59 (% 60).
Combining the flag pattern, the mode list, and the wrap-around gives a complete time-setting program that prints instead of drawing:
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 | |
Note that when the user enters a setting mode, the value they change should start from the current time, not from zero. The program starts at 12:00 here only to keep the example short.
Rotary Encoder
A rotary encoder is a knob that turns endlessly and reports each small step of rotation, along with the direction. Unlike a potentiometer, it has no stops and no fixed position. It is a nicer way to set a time because turning one knob is faster than pressing a button 40 times.
A typical encoder module has five pins: GND, + (3.3 V), CLK (also called A), DT (also called B), and SW (a push button built into the knob). CLK and DT are two switches that open and close as you turn, and the important thing is the order in which they change.
Encoder Direction
Both switches produce a pattern called quadrature: A and B change in a fixed sequence, offset by a quarter of a cycle. Turning clockwise gives one sequence, and counterclockwise gives the reverse.
| Turn | A and B (in order) |
|---|---|
| Clockwise | 00, 10, 11, 01 |
| Counterclockwise | 00, 01, 11, 10 |
The simplest decoding method uses only one edge. When A has a falling edge, look at B at that moment. If B is 1, the knob is turning one way, and if B is 0 it is turning the other way. (Swap the two directions if your knob feels backward.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Quadrature Takes a Minute
Two signals that change in a sequence can feel confusing at first. Try it on paper: write A and B for each click of a clockwise turn, then read the sequence backward for counterclockwise. Once you see the pattern, the code is only three lines.
Diagram: Quadrature Encoder Simulator
Quadrature Encoder Simulator
Type: MicroSim
sim-id: quadrature-encoder-sim
Library: p5.js
Status: Specified
Learning objective: Students will understand how the A and B signals encode rotation direction (Bloom: Understanding).
Visual elements: A draggable knob on the left. On the right, two square-wave traces for A and B scrolling in time, with a vertical line at each falling edge of A showing the value of B at that instant. A counter shows the running value.
Controls: Drag the knob clockwise or counterclockwise, or use "Step CW" and "Step CCW" buttons; a "Show table" toggle displays the state sequence.
Responsive design: Knob and traces scale with the container.
Implementation: p5.js with angle-to-signal mapping.
Encoder Acceleration
Encoder acceleration makes the value change faster when the user turns the knob faster. Moving from minute 0 to minute 45 one click at a time is tedious, but a quick spin should jump by larger steps. The trick is to measure the time between clicks with ticks_ms().
1 2 3 4 5 6 7 8 9 10 11 12 | |
Multiply the direction (+1 or -1) by step_size() to get the change. Slow turns still give one-at-a-time precision, so the user can fine-tune the last few minutes.
Putting It Together
| Problem | Solution |
|---|---|
| Which pin for a button? | Any GPIO with Pin.PULL_UP, wired to ground |
| One press counts many times | Debounce with a 20 to 50 ms window |
| Presses are missed | Use irq() with IRQ_FALLING |
| What do the buttons mean now? | A state machine with mode cycling |
| Setting the time is slow | Rotary encoder with acceleration |
Key Takeaways
- A push button wired between a pin and ground, with
Pin.PULL_UP, reads 0 when pressed. - Contact bounce makes one press look like many; debounce with a short time window or a small capacitor.
- Interrupt handlers catch presses immediately; keep them short and set a flag.
- A state machine plus
(mode + 1) % nimplements mode cycling. - A rotary encoder reports direction using quadrature signals, and timing between clicks enables acceleration.
Your Clock Listens Now
You can now debounce a button, catch presses with interrupts, and cycle through set-hour and set-minute modes using a state machine. That is a fully settable clock interface. Every second counts!
Practice Questions
- Why does a button wired to ground use
Pin.PULL_UP, and what doesvalue()return while it is pressed? - A counter increases by 5 when you press once. Name the problem and give two ways to fix it.
- Why should an interrupt handler set a flag instead of drawing on the display?
- Draw the state machine for a clock with modes RUN, SET_HOUR, SET_MINUTE, SET_ALARM_HOUR, and SET_ALARM_MINUTE.
- Write
step_size()thresholds so that turning the encoder very fast changes the minute by 10 per click.