042 Project 42: TinyGo Button + Interrupt

Updated

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)
    }
}
tinygo flash -target=YOUR_BOARD .

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:

  1. Ignore window after edge (software).
  2. Require stable samples N times.
  3. 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

  1. Long-press vs short-press detection.
  2. Two buttons with different actions.
  3. 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)