042 Project 42: TinyGo Button + Interrupt
July 30, 2026
042 TinyGo: Button Input and Interrupt-Driven Events
Move from a pure sleep loop to event-driven MCU code: poll a button, then (where the target supports it) use a pin interrupt so the main loop stays free for other work.
Realtime framing
| Style | Latency | CPU use | Typical use |
|---|---|---|---|
Polling in for { } |
Bounded by loop rate | Busy or delayed by sleeps | Simple labs |
| Interrupt on edge | Faster reaction | Idle-friendly | Buttons, encoders, IRQs |
| Timer IRQ / PWM | Periodic deadlines | Predictable ticks | Soft realtime control |
TinyGo is not a certified hard-realtime OS. Treat deadlines as best-effort: measure, keep ISRs short, defer heavy work to the main loop.
Edge on BUTTON pin
│
▼
IRQ handler ──set flag / push event──► main loop reacts (debounce, LED, UART)
Step 1: Polling baseline (all targets)
package main
import (
"machine"
"time"
)
func main() {
led := machine.LED
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
btn := machine.BUTTON // or explicit GPIO; active-low common with pull-up
btn.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
var last bool = true // pull-up idle high
for {
v := btn.Get()
if v != last {
// simple debounce
time.Sleep(20 * time.Millisecond)
if btn.Get() == v {
last = v
if !v { // pressed (active low)
led.Set(!led.Get())
}
}
}
time.Sleep(5 * time.Millisecond)
}
}Step 2: Interrupt pattern (when supported)
API names vary by TinyGo version/board. Sketch:
package main
import (
"machine"
"time"
)
var pressed = make(chan struct{}, 1)
func main() {
led := machine.LED
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
btn := machine.BUTTON
btn.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
// SetInterrupt API differs by target — consult tinygo.org machine docs for your board.
// Conceptual:
// btn.SetInterrupt(machine.PinFalling, func(p machine.Pin) {
// select {
// case pressed <- struct{}{}:
// default:
// }
// })
// Fallback if interrupt API unavailable: keep polling in a tight loop
// and only document the interrupt path for boards that support it.
for {
select {
case <-pressed:
led.Set(!led.Get())
time.Sleep(50 * time.Millisecond) // debounce after event
default:
// optional idle work / low-power sleep if target supports
time.Sleep(1 * time.Millisecond)
}
}
}Lab requirement: Implement working code for your board. If interrupts are unsupported, complete polling + debounce and write a short note: “Interrupt API N/A on target X; measured poll latency ≈ ….”
Step 3: Debounce theory
Mechanical buttons bounce for 5–50 ms. Strategies:
- Ignore window after edge (software).
- Require stable samples N times.
- Hardware RC (optional).
Never toggle LEDs raw in an ISR without debounce policy—you will get multi-fires.
Step 4: Measure soft-realtime behavior
| Metric | How |
|---|---|
| Reaction latency | Scope or toggle second pin; or timestamp over UART |
| Jitter | Stddev of latency over 50 presses |
| Missed events | Channel full / default branch count |
Log 50 presses; record min/avg/max latency if you have a serial console.
Learning Goals
- Poll vs interrupt trade-offs
- Debounce as a correctness requirement
- Keep interrupt work minimal
- Honest soft-realtime expectations
Extensions
- Long-press vs short-press detection.
- Two buttons with different actions.
- Disable interrupts during critical section if API allows.
Checkpoint
- Reliable press detection (no multi-toggle spam)
- Debounce implemented and explained
- Interrupt or documented fallback
- 50-press notes (even qualitative)