Project 1: Interactive Button-LED Matrix (CircuitPython)

Build a fully interactive 4×8 button grid where each button press triggers a unique LED color, with animations like ripples, fades, and rainbow waves responding to your touch.

Quick Reference

Attribute Value
Difficulty Level 1: Beginner
Time Estimate 1-2 weeks
Language CircuitPython (Alternatives: MicroPython, Arduino C++)
Prerequisites Basic Python, understanding of loops and functions
Key Topics Event-driven programming, NeoPixel control, HSV color space

Real World Outcome

When you complete this project, you will have a physical, working interactive button grid on your NeoTrellis M4 board.

What You’ll Actually See On The Hardware:

  1. Power it on - Connect the NeoTrellis M4 via USB
  2. Initial state - All 32 LEDs will be OFF (dark)
  3. Press button #5 (row 1, column 0):
    • The corresponding LED (#5) immediately lights up in RED
    • Release the button → LED fades from red to off over 0.5 seconds
  4. Press button #12 (row 1, column 4):
    • LED #12 lights up in BLUE
    • While still holding button #12, press button #3
    • LED #3 lights up in GREEN (simultaneous)
    • Both LEDs fade independently when released
  5. Rainbow wave mode (activate by pressing top-left + bottom-right corners simultaneously):
    • All 32 LEDs cycle through rainbow colors in a wave pattern
    • Wave flows from left-to-right across columns
    • Each column shifts hue by 30°
    • Updates 30 times per second (smooth animation)
  6. Ripple effect (press any button during rainbow mode):
    • A “ripple” of brightness radiates from the pressed button
    • Adjacent LEDs brighten/dim in a circular pattern
    • Ripple expands over 1 second, then fades

Physical Observation Example:

NeoTrellis M4 Board State (viewed from above):

Initial State (all LEDs OFF):
Row 0: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫
Row 1: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫
Row 2: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫
Row 3: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫

After pressing button at (row=1, col=2):
Row 0: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫
Row 1: ⚫ ⚫ 🔴 ⚫ ⚫ ⚫ ⚫ ⚫    ← LED #10 glows RED (255, 0, 0)
Row 2: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫
Row 3: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫

While holding that button, press (row=2, col=5):
Row 0: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫
Row 1: ⚫ ⚫ 🔴 ⚫ ⚫ ⚫ ⚫ ⚫    ← Still RED
Row 2: ⚫ ⚫ ⚫ ⚫ ⚫ 🔵 ⚫ ⚫    ← LED #21 glows BLUE (0, 0, 255)
Row 3: ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫ ⚫

Release both buttons → Both LEDs fade to black over 500ms

Serial Console Output (Optional Debugging):

# Connect to serial port at 115200 baud, you'll see:
CircuitPython 8.2.9 on 2024-01-01; Adafruit NeoTrellis M4 with samd51j19
>>> import code
>>> Running button_led_matrix.py...
[INIT] NeoTrellis M4 initialized - 32 buttons, 32 pixels
[INIT] Button matrix: 4 rows × 8 columns
[EVENT] Button pressed: row=1, col=2, index=10
[LED] Setting pixel 10 to RGB(255, 0, 0) - RED
[EVENT] Button released: row=1, col=2, index=10
[LED] Fading pixel 10 to OFF over 500ms
[MODE] Rainbow wave activated (2 corners pressed)
[ANIM] Wave cycle 1/: hue_offset=0°
[ANIM] Wave cycle 2/: hue_offset=12°
...

Success Verification Checklist:

  • Pressing any button lights up the corresponding LED immediately (< 50ms latency)
  • Each button has a different color (or you can map it that way)
  • Multiple buttons can be pressed simultaneously without conflict
  • LEDs fade smoothly when buttons are released (no flickering)
  • Rainbow wave mode animates at 30 FPS (visually smooth)
  • Ripple effect radiates from the pressed button location
  • Board operates continuously without crashes or freezes

The Core Question You’re Answering

“How do I map physical button events to visual LED feedback in real-time on embedded hardware?”

Before you write any code, sit with this question. This project forces you to understand:

  • The relationship between the 4×8 button matrix and the 32 NeoPixel LEDs
  • How event callbacks work in CircuitPython (asynchronous button handling)
  • Why HSV color space is easier than RGB for animations
  • How to manage timing without blocking the main loop (cooperative multitasking)

Concepts You Must Understand First

Stop and research these before coding:

  1. Button Matrix Scanning
    • How are 32 buttons connected with only 12 GPIO pins (4 rows + 8 columns)?
    • What is “anti-ghosting” and why do we need diodes?
    • How does the NeoTrellis library detect button presses vs releases?
    • Book Reference: “Making Embedded Systems” by Elecia White - Ch. 4 (I/O Handling)
  2. NeoPixel (WS2812B) Addressing
    • How does one data pin control 32 individual LEDs?
    • What is the WS2812B protocol timing (0.4µs vs 0.8µs pulse widths)?
    • Why must you disable interrupts during NeoPixel updates?
    • Book Reference: “Making Embedded Systems” by Elecia White - Ch. 7 (Peripherals)
  3. HSV Color Space
    • What do Hue, Saturation, Value represent?
    • Why is HSV easier for rainbow animations than RGB?
    • How do you convert HSV to RGB?
    • Reference: Adafruit FancyLED Guide
  4. Event-Driven Programming
    • What is a callback function?
    • How does CircuitPython’s trellis.sync() detect events without blocking?
    • What is the difference between polling and interrupt-driven I/O?
    • Book Reference: “The Linux Programming Interface” by Michael Kerrisk - Ch. 63 (I/O Multiplexing)

Questions to Guide Your Design

Before implementing, think through these:

  1. Button-to-LED Mapping
    • Do button indices (0-31) directly match LED indices (0-31)?
    • What if you want button #5 to control LED #20?
    • How will you store the mapping (list, dictionary)?
  2. Color Assignment Strategy
    • Random color per button?
    • Fixed rainbow gradient across the grid?
    • User-configurable palette?
  3. Animation Timing
    • How will you implement a 500ms fade without time.sleep() (which blocks)?
    • What data structure tracks the “fade state” of each LED?
    • How many times per second do you need to update LEDs for smooth animation?
  4. Event Handling
    • What happens if a button is pressed while an animation is running?
    • How do you detect “special” button combinations (e.g., 2 corners for mode switch)?
    • Should button events interrupt animations or queue them?

Thinking Exercise

Button Matrix Internals

Before looking at the library code, draw the circuit:

  Col 0   Col 1   Col 2   Col 3   Col 4   Col 5   Col 6   Col 7
    │       │       │       │       │       │       │       │
Row 0─┬─────┼───┬───┼───┬───┼───┬───┼───┬───┼───┬───┼───┬───┼───┬
      │  SW0│   │ SW1   │ SW2   │ SW3   │ SW4   │ SW5   │ SW6   │ SW7
      ▼     │   ▼       ▼       ▼       ▼       ▼       ▼       ▼
Row 1─┬─────┘   └───┬───────┬───────┬───────┬───────┬───────┬───────┬
      │  SW8        │  SW9  │ SW10  │ SW11  │ SW12  │ SW13  │ SW14  │ SW15
      ▼             ▼       ▼       ▼       ▼       ▼       ▼       ▼
...

Questions while analyzing:

  • If you set Row 0 HIGH and Col 2 reads HIGH, which button is pressed?
  • What happens if buttons SW2 and SW9 are pressed simultaneously?
  • Why is this called a “matrix” scan instead of 32 individual GPIO pins?
  • How does the scan rate (Hz) affect button responsiveness?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain how a button matrix works. Why is it more efficient than individual GPIO pins for each button?”
    • Expected answer: Reduces GPIO requirements from O(n) to O(√n). 32 buttons need only 12 pins (4×8) instead of 32 pins.
  2. “What is the WS2812B protocol, and why can’t you use regular SPI to control NeoPixels?”
    • Expected answer: WS2812B uses a self-clocking 800kHz data stream with pulse-width encoding (0.4µs vs 0.8µs). Standard SPI has a separate clock line and different timing.
  3. “How would you implement a non-blocking fade animation in an embedded system?”
    • Expected answer: Store start time and target color per LED. Each loop iteration, calculate elapsed_time / total_duration and interpolate RGB values. Avoid time.sleep().
  4. “What is the difference between RGB and HSV color spaces? When would you prefer HSV?”
    • Expected answer: RGB specifies red, green, blue intensities directly. HSV specifies hue (color angle), saturation (color purity), and value (brightness). HSV makes rainbow gradients trivial (just increment hue) vs complex RGB math.
  5. “How does CircuitPython handle button events without blocking the main loop?”
    • Expected answer: The NeoTrellis library implements a state machine. Calling trellis.sync() polls button states, compares with previous states, and triggers registered callbacks for changes. It returns immediately (non-blocking).
  6. “What are the performance constraints of running animations on a microcontroller vs a desktop PC?”
    • Expected answer: Microcontrollers have limited CPU speed (120 MHz vs 3+ GHz), no GPU, and real-time constraints. Must complete LED updates within 16ms (60 FPS) or animations stutter. No OS scheduler - cooperative multitasking only.

Hints in Layers

Hint 1: Start with the hardware test Don’t write animation code yet. First, verify you can:

  1. Detect a single button press
  2. Light up the corresponding LED in a fixed color (e.g., white)
  3. Print the button index to the serial console

This confirms your hardware and basic NeoTrellis library setup works.

Hint 2: Understand the coordinate system The NeoTrellis uses (row, column) coordinates for buttons but linear indices (0-31) for LEDs. The mapping is:

led_index = (row * 8) + column
# Example: Button at (row=2, col=5) → LED index = 2*8 + 5 = 21

Hint 3: Use HSV for rainbow animations CircuitPython’s adafruit_fancyled library provides HSV_to_RGB(). For a rainbow:

import adafruit_fancyled.adafruit_fancyled as fancy

# Hue ranges from 0.0 to 1.0 (0° to 360°)
for i in range(32):
    hue = i / 32.0  # Each LED gets a different hue
    color = fancy.CHSV(hue, 1.0, 1.0)  # Full saturation, full brightness
    rgb = color.pack()  # Convert to 24-bit RGB
    pixels[i] = rgb

Hint 4: Non-blocking fade with linear interpolation Create a FadeTracker class:

class LEDFader:
    def __init__(self):
        self.fades = {}  # {led_index: (start_rgb, end_rgb, start_time, duration)}

    def start_fade(self, led_index, start_color, end_color, duration_ms):
        self.fades[led_index] = (start_color, end_color, time.monotonic(), duration_ms / 1000.0)

    def update(self, pixels):
        now = time.monotonic()
        for led_index, (start, end, start_time, duration) in list(self.fades.items()):
            elapsed = now - start_time
            if elapsed >= duration:
                pixels[led_index] = end
                del self.fades[led_index]
            else:
                progress = elapsed / duration  # 0.0 to 1.0
                # Interpolate each RGB channel
                r = int(start[0] + (end[0] - start[0]) * progress)
                g = int(start[1] + (end[1] - start[1]) * progress)
                b = int(start[2] + (end[2] - start[2]) * progress)
                pixels[led_index] = (r, g, b)

Hint 5: Ripple effect math For a ripple centered at (center_row, center_col):

def distance(r1, c1, r2, c2):
    return math.sqrt((r2 - r1)**2 + (c2 - c1)**2)

max_dist = distance(0, 0, 3, 7)  # Diagonal corner-to-corner

for row in range(4):
    for col in range(8):
        dist = distance(center_row, center_col, row, col)
        brightness = 1.0 - (dist / max_dist)  # Closer = brighter
        # Apply brightness to LED at (row, col)

Books That Will Help

Topic Book Chapter
Embedded I/O patterns “Making Embedded Systems” by Elecia White Ch. 4, 7
Event-driven programming “The Linux Programming Interface” by Michael Kerrisk Ch. 63
ARM Cortex-M4 peripherals “The Definitive Guide to ARM Cortex-M3/M4” by Joseph Yiu Ch. 9-11
Color theory “Computer Graphics from Scratch” by Gabriel Gambetta Ch. 1

Common Pitfalls & Debugging

Problem 1: “Button presses aren’t detected”

  • Why: Forgot to call trellis.sync() in the main loop
  • Fix: Ensure trellis.sync() is called every iteration
  • Quick test:
    while True:
        trellis.sync()  # ← Must be called every loop!
        time.sleep(0.01)
    

Problem 2: “LEDs flicker or show wrong colors”

  • Why: NeoPixel timing is disrupted by USB serial interrupts
  • Fix: Reduce serial output frequency or disable during LED updates
  • Quick test: Comment out all print() statements and observe if flicker stops

Problem 3: “Fade animations are choppy”

  • Why: Updating LEDs too slowly (< 20 FPS) or using time.sleep() blocks
  • Fix: Ensure main loop runs at least 30 times per second. Replace sleep() with time-based state checks.
  • Quick test:
    import time
    loop_count = 0
    start = time.monotonic()
    while True:
        # Your animation code
        loop_count += 1
        if time.monotonic() - start >= 1.0:
            print(f"FPS: {loop_count}")  # Should be 30+
            loop_count = 0
            start = time.monotonic()
    

Problem 4: “Can’t press multiple buttons simultaneously”

  • Why: Button matrix has ghosting issues (missing diodes) OR callback logic doesn’t handle concurrent events
  • Fix: NeoTrellis M4 has anti-ghosting diodes, so this is likely a software issue. Ensure you store button states in a set/list, not a single variable.
  • Quick test:
    active_buttons = set()
    
    def button_press(event):
        if event.edge == NeoTrellis.EDGE_RISING:
            active_buttons.add(event.number)
        elif event.edge == NeoTrellis.EDGE_FALLING:
            active_buttons.discard(event.number)
    

Problem 5: “Rainbow wave stops when I press a button”

  • Why: Button callback is blocking (e.g., has a time.sleep() inside)
  • Fix: Never use blocking calls in callbacks. Set a flag and handle animation updates in the main loop.
  • Quick test: Add timing debug:
    def button_callback(event):
        start = time.monotonic()
        # Your callback code
        elapsed = time.monotonic() - start
        if elapsed > 0.01:
            print(f"WARNING: Callback took {elapsed*1000:.1f}ms")  # Should be < 1ms
    

Definition of Done

  • Hardware test passes: All 32 buttons trigger their corresponding LEDs
  • Simultaneous presses work: Pressing 3+ buttons shows 3+ LEDs lit
  • Fade animation is smooth: LEDs fade from color to black over 500ms without stutter
  • Rainbow wave runs at 30 FPS: Measured via loop counter (see Pitfall #3)
  • Ripple effect visible: Brightness radiates from pressed button
  • No crashes after 10 minutes: Board operates continuously without freezing
  • Code is documented: Each function has a docstring explaining its purpose
  • Serial debug output works: Console shows button events and mode changes

Extensions & Challenges

Beginner Extensions

  • Customizable button colors: Store a color palette in a list and let users assign colors per button
  • Brightness control: Use a potentiometer on an analog pin to adjust global LED brightness
  • Button hold detection: Different action if a button is held for > 2 seconds

Intermediate Extensions

  • Save/load color schemes: Store user-defined palettes in the 8MB flash memory
  • Audio-reactive mode: Use the onboard microphone (if available) to make LEDs pulse with sound amplitude
  • Velocity-sensitive presses: Measure the time between button press and release to determine “velocity” (harder press = brighter LED)

Advanced Extensions

  • Game of Life on the grid: Implement Conway’s Game of Life using the 4×8 LED matrix
  • Serial MIDI output: Send MIDI note-on messages when buttons are pressed (prepare for Project 4)
  • Custom animation engine: Build a timeline-based animation system that can play pre-programmed LED sequences

Real-World Connections

Industry Applications

  • MIDI controllers: Novation Launchpad, Ableton Push use similar button/LED matrices
  • DJ equipment: Pioneer DDJ controllers have RGB button grids for clip launching
  • IoT dashboards: Physical status boards (e.g., build status indicators, server monitoring)
  • Arcade controls: Custom fight stick mods with LED feedback
  • Adafruit UNTZ: Audio visualizer with NeoPixel matrix
  • Monome Grid: Open-source MIDI grid controller (same concept, different hardware)
  • LED Matrix Games: Tetris, Snake, Pong implementations on LED grids

Interview Relevance

  • Embedded event handling and real-time constraints
  • Understanding hardware protocols (WS2812B, I2C matrix scanning)
  • Non-blocking animation patterns (critical for responsive UIs)
  • Color space conversions (HSV ↔ RGB)

Resources

Essential Reading

  • “Making Embedded Systems” by Elecia White - Ch. 4 (I/O), Ch. 7 (Peripherals)
  • Adafruit NeoTrellis M4 Guide: https://learn.adafruit.com/adafruit-neotrellis-m4
  • WS2812B Datasheet: https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf

Video Resources

  • Adafruit’s CircuitPython tutorials: https://learn.adafruit.com/welcome-to-circuitpython
  • NeoPixel Überguide: https://learn.adafruit.com/adafruit-neopixel-uberguide

Tools & Documentation

  • Mu Editor: Best beginner-friendly IDE for CircuitPython (auto-detects board, serial console built-in)
  • CircuitPython Libraries: Download the bundle from https://circuitpython.org/libraries
  • Serial monitor: PuTTY (Windows), screen (macOS/Linux), or Mu’s built-in console
  • Project 2: RGB Color Mixer (builds on LED control)
  • Project 3: Accelerometer Light Show (adds motion input)
  • Project 4: USB MIDI Controller (adds MIDI protocol)

Self-Assessment Checklist

Understanding

  • I can explain how a button matrix reduces GPIO requirements
  • I can draw the WS2812B timing diagram from memory
  • I can convert HSV(0.5, 1.0, 0.8) to RGB by hand
  • I understand why time.sleep() is forbidden in animation loops

Implementation

  • My code handles button events without blocking
  • Animations run smoothly at 30+ FPS
  • Multiple buttons can be pressed simultaneously
  • LEDs update within 50ms of button presses

Growth

  • I can add a new animation mode without breaking existing code
  • I can debug timing issues using serial output and loop counters
  • I could port this to Arduino or bare-metal C if needed

This expanded project guide is part of the LEARN_NEOTRELLIS_M4_DEEP_DIVE sprint. For the complete learning path and theory primer, see the main guide.