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:
- Power it on - Connect the NeoTrellis M4 via USB
- Initial state - All 32 LEDs will be OFF (dark)
- 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
- 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
- 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)
- 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:
- 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)
- 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)
- 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
- 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:
- 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)?
- Color Assignment Strategy
- Random color per button?
- Fixed rainbow gradient across the grid?
- User-configurable palette?
- 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?
- How will you implement a 500ms fade without
- 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:
- “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.
- “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.
- “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_durationand interpolate RGB values. Avoidtime.sleep().
- Expected answer: Store start time and target color per LED. Each loop iteration, calculate
- “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.
- “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).
- Expected answer: The NeoTrellis library implements a state machine. Calling
- “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:
- Detect a single button press
- Light up the corresponding LED in a fixed color (e.g., white)
- 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
Related Open Source Projects
- 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
Related Projects in This Series
- 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.