Learn NeoTrellis M4: From Zero to Embedded Audio-Visual Master

Goal: Completely master the Adafruit NeoTrellis M4—from basic button/LED interactions in CircuitPython, through Arduino audio synthesis, to bare-metal C programming that directly manipulates the ATSAMD51’s registers. You’ll understand the ARM Cortex-M4 architecture, I2C/SPI communication, DAC audio generation, accelerometer physics, USB protocols, and real-time embedded programming. By the end, you’ll be able to build professional MIDI controllers, synthesizers, interactive instruments, and understand exactly what happens at every level of the hardware stack.


Why NeoTrellis M4 Matters

The Adafruit NeoTrellis M4 isn’t just another microcontroller board—it’s a complete embedded systems education platform disguised as a music controller. Here’s why mastering it matters:

The Hardware Convergence Point

The NeoTrellis M4 combines multiple complex subsystems into a single board:

                         NeoTrellis M4 Architecture
┌────────────────────────────────────────────────────────────────────────┐
│                                                                        │
│   ┌─────────────────────────────────────────────────────────────────┐  │
│   │                    ATSAMD51J19 (ARM Cortex-M4)                  │  │
│   │                                                                 │  │
│   │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │  │
│   │  │   120 MHz   │  │  512KB      │  │   Hardware DSP          │  │  │
│   │  │   Core      │  │  Flash      │  │   - FPU                 │  │  │
│   │  │             │  │             │  │   - Single-cycle MAC    │  │  │
│   │  └─────────────┘  └─────────────┘  │   - SIMD instructions   │  │  │
│   │                                    └─────────────────────────┘  │  │
│   │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │  │
│   │  │  192KB      │  │  8MB        │  │   Peripherals           │  │  │
│   │  │  SRAM       │  │  External   │  │   - 2x 12-bit DAC       │  │  │
│   │  │             │  │  Flash      │  │   - 16x 12-bit ADC      │  │  │
│   │  └─────────────┘  └─────────────┘  │   - 6x SERCOM           │  │  │
│   │                                    │   - USB Native          │  │  │
│   │                                    │   - DMA Controller      │  │  │
│   └────────────────────────────────────┴─────────────────────────┴──┘  │
│                            │                                           │
│                            │ I2C/GPIO                                  │
│         ┌──────────────────┼──────────────────┐                        │
│         │                  │                  │                        │
│         ▼                  ▼                  ▼                        │
│   ┌───────────┐      ┌───────────┐      ┌───────────┐                  │
│   │  ADXL343  │      │  32x      │      │  Button   │                  │
│   │  3-Axis   │      │  NeoPixel │      │  Matrix   │                  │
│   │  Accel.   │      │  LEDs     │      │  4x8      │                  │
│   │  (I2C)    │      │  (WS2812) │      │  Dioded   │                  │
│   └───────────┘      └───────────┘      └───────────┘                  │
│                                                                        │
│   ┌───────────────────────────────────────────────────────────────┐    │
│   │                        Audio Subsystem                        │    │
│   │  ┌──────────┐    ┌──────────┐    ┌──────────────────────┐    │    │
│   │  │ Dual DAC │───▶│ TRRS     │◀───│ MAX4466 Mic Preamp   │    │    │
│   │  │ L/R Out  │    │ Jack     │    │ (ADC Input)          │    │    │
│   │  └──────────┘    └──────────┘    └──────────────────────┘    │    │
│   └───────────────────────────────────────────────────────────────┘    │
│                                                                        │
│   ┌─────────────────┐                    ┌─────────────────────────┐   │
│   │   USB Native    │                    │   4-JST Expansion       │   │
│   │   - CDC Serial  │                    │   - I2C/ADC/UART        │   │
│   │   - USB MIDI    │                    │   - 3.3V Power          │   │
│   │   - Mass Storage│                    └─────────────────────────┘   │
│   └─────────────────┘                                                  │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

Why This Board for Learning?

1. Multiple Abstraction Levels

  • CircuitPython: High-level Python for rapid prototyping (learn concepts)
  • Arduino: C++ with libraries (learn embedded patterns)
  • Bare-Metal C: Direct register access (learn the hardware truth)

2. Real-Time Constraints

  • Audio synthesis requires precise timing (samples at 44.1kHz = 22.6μs per sample)
  • LED animations must be smooth (60 FPS = 16.6ms per frame)
  • Button scanning must be responsive (< 10ms latency for playability)

3. Professional Applications This exact hardware powers commercial MIDI controllers, synthesizers, and performance instruments. The skills transfer directly to:

  • Music technology industry
  • Embedded systems development
  • Real-time audio processing
  • IoT device development

The Specifications That Matter

Component Specification Why It Matters
Processor ATSAMD51J19, ARM Cortex-M4 @ 120MHz Hardware FPU for real-time audio DSP
Memory 512KB Flash, 192KB SRAM Enough for complex synth algorithms
External Flash 8MB QSPI Store audio samples, configuration
Audio Output Dual 12-bit DAC @ 500KSPS True analog stereo, not PWM
Accelerometer ADXL343, I2C Motion control for expressive input
LEDs 32x WS2812B NeoPixels Visual feedback, status indication
Buttons 4x8 matrix with diodes No ghosting, proper polyphonic support
USB Native USB (not UART converter) True USB MIDI, low latency

Prerequisites & Background Knowledge

Essential Prerequisites (Must Have)

Before starting these projects, you should:

Programming Foundations:

  • Basic Python syntax (variables, loops, functions, classes)
  • Understanding of bits and bytes (binary, hexadecimal)
  • Familiarity with arrays/lists and basic data structures

Hardware Basics:

  • Know what GPIO (General Purpose Input/Output) means
  • Understand voltage levels (3.3V logic)
  • Can follow a simple wiring diagram

Development Environment:

  • Comfortable with command line/terminal
  • Can install software packages
  • Have a text editor you’re comfortable with

Helpful But Not Required

  • Prior microcontroller experience (Arduino Uno, Raspberry Pi Pico)
  • Basic understanding of audio/music concepts (notes, frequencies)
  • C programming experience (will be taught progressively)
  • Knowledge of USB protocols (will be explained)

Self-Assessment Questions

Before starting, can you answer these?

  1. Python: What’s the difference between a list and a dictionary?
  2. Binary: What’s 0xFF in decimal? What’s 0b10101010?
  3. Hardware: If a pin outputs 3.3V, is it HIGH or LOW?
  4. Timing: How many milliseconds in one second? How many microseconds?

If you struggled with any of these, spend a day reviewing those fundamentals first.

Required Hardware

For all projects in this guide, you need:

  1. Adafruit NeoTrellis M4 with Enclosure and Buttons Kit Pack (Product 4020)
  2. USB-C cable (for programming and power)
  3. Headphones or powered speakers (3.5mm jack for audio projects)
  4. Computer with USB port (Windows, macOS, or Linux)
  • USB-MIDI capable software (Ableton, GarageBand, REAPER, or free VCV Rack)
  • Oscilloscope or logic analyzer (for advanced debugging)
  • Multimeter (for verifying connections)
  • J-Link or Atmel-ICE debugger (for bare-metal debugging)

Development Environment Setup

For CircuitPython:

  1. Download latest CircuitPython for Trellis M4 from circuitpython.org
  2. Install Mu Editor or use VS Code with CircuitPython extension
  3. Install Adafruit libraries bundle

For Arduino:

  1. Install Arduino IDE 2.x
  2. Add Adafruit SAMD board support via Board Manager
  3. Install libraries: Adafruit_NeoTrellis, Adafruit_NeoPixel, Adafruit_ADXL343
  4. Install Adafruit fork of PJRC Audio library

For Bare-Metal C:

  1. Install ARM GCC toolchain (arm-none-eabi-gcc)
  2. Install OpenOCD or BOSSA for flashing
  3. Download SAMD51 datasheet and header files
  4. (Optional) Install SEGGER J-Link software

Time Investment Reality Check

Learning Path Time Commitment Prerequisites
CircuitPython only 2-3 weeks Python basics
Arduino + Audio 4-6 weeks C++ basics
Bare-Metal C 8-12 weeks Strong C, some assembly
Complete Mastery 3-4 months All of the above

Core Concept Analysis

The ARM Cortex-M4 Architecture

The ATSAMD51 is built on the ARM Cortex-M4 core—the industry workhorse for embedded systems.

                    ARM Cortex-M4 Core Architecture
┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                    Instruction Pipeline                     │   │
│   │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │   │
│   │  │  Fetch   │──▶│  Decode  │──▶│ Execute │──▶│Writeback│    │   │
│   │  │          │  │          │  │          │  │          │    │   │
│   │  └──────────┘  └──────────┘  └──────────┘  └──────────┘    │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│   ┌──────────────────────────┼──────────────────────────────────┐   │
│   │                          │                                  │   │
│   │  ┌────────────────┐  ┌───┴───────────┐  ┌────────────────┐  │   │
│   │  │   Registers    │  │    ALU        │  │     FPU        │  │   │
│   │  │   R0-R12       │  │               │  │   (Hardware    │  │   │
│   │  │   SP (R13)     │  │  ┌─────────┐  │  │    Floating    │  │   │
│   │  │   LR (R14)     │  │  │ + - *   │  │  │    Point)      │  │   │
│   │  │   PC (R15)     │  │  │ / & |   │  │  │                │  │   │
│   │  │   xPSR         │  │  │ ^ << >> │  │  │  Single-cycle  │  │   │
│   │  │                │  │  └─────────┘  │  │  multiply-add  │  │   │
│   │  └────────────────┘  └───────────────┘  └────────────────┘  │   │
│   │                                                              │   │
│   └──────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              │ Bus Matrix                            │
│                              │                                       │
│   ┌──────────────────────────┼──────────────────────────────────┐    │
│   │                          │                                  │    │
│   ▼                          ▼                                  ▼    │
│ ┌──────────┐           ┌──────────┐                      ┌──────────┐│
│ │   AHB    │           │   APB    │                      │   DMA    ││
│ │  Bridge  │           │  Bridge  │                      │Controller││
│ │          │           │          │                      │          ││
│ │ -Flash   │           │ -GPIO    │                      │ -Audio   ││
│ │ -SRAM    │           │ -UART    │                      │ -SPI     ││
│ │ -QSPI    │           │ -I2C     │                      │ -Memory  ││
│ │          │           │ -DAC/ADC │                      │          ││
│ └──────────┘           └──────────┘                      └──────────┘│
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Key Concepts:

  1. 3-Stage Pipeline: Fetch-Decode-Execute runs in parallel, so while one instruction executes, the next is decoded, and a third is fetched.

  2. 32-bit Registers: R0-R12 are general purpose, R13 is Stack Pointer, R14 is Link Register (return address), R15 is Program Counter.

  3. FPU (Floating Point Unit): Single-cycle multiplication and add—critical for audio DSP where you’re doing thousands of sample = sample * gain + offset per second.

  4. Memory-Mapped I/O: All peripherals appear as memory addresses. Writing to address 0x41008000 doesn’t access RAM—it controls GPIO pins.

The NeoPixel Protocol (WS2812B)

Each LED requires precise timing—there’s no clock line, just data:

                    WS2812B Timing Protocol

    Data Line Voltage
    │
3.3V┤     ┌───┐     ┌───────┐     ┌───────┐     ┌───┐
    │     │   │     │       │     │       │     │   │
    │     │   │     │       │     │       │     │   │
 0V ┤─────┘   └─────┘       └─────┘       └─────┘   └─────
    │
    │◀─ 0 ─▶│◀──── 1 ────▶│◀──── 1 ────▶│◀─ 0 ─▶│
    │                                              │
    │◀─────────────────── 1 Bit ──────────────────▶│

Timing:
    "0" bit: HIGH for 0.35μs, LOW for 0.8μs   (T0H=350ns, T0L=800ns)
    "1" bit: HIGH for 0.7μs,  LOW for 0.6μs   (T1H=700ns, T1L=600ns)

    24 bits per LED (GRB order): G7-G0, R7-R0, B7-B0

                     LED Chain Data Flow

    ┌──────────┐      ┌──────────┐      ┌──────────┐
    │  LED 0   │──DIN─▶│  LED 1   │──DIN─▶│  LED 2   │──▶ ...
    │          │      │          │      │          │
    │ Takes 1st│      │ Takes 2nd│      │ Takes 3rd│
    │ 24 bits  │      │ 24 bits  │      │ 24 bits  │
    └──────────┘      └──────────┘      └──────────┘

    MCU sends: [G0R0B0][G1R1B1][G2R2B2]...
    LED0 grabs first 24 bits, passes rest through

Why This Matters: At 120MHz, the SAMD51 executes ~42 instructions per microsecond. The NeoPixel protocol requires sub-microsecond precision. This is why DMA or hardware peripherals are essential—software bit-banging struggles to maintain timing.

I2C Protocol (For Accelerometer)

The ADXL343 communicates over I2C—a two-wire protocol:

                        I2C Bus Structure

    VCC (3.3V)
      │       │
      R       R         R = Pull-up resistors (typically 4.7kΩ)
      │       │
    ──┴───────┴──────────────────────────────────
      │       │           │           │
     SDA     SCL        SDA         SCL
      │       │           │           │
    ┌─┴───────┴─┐     ┌───┴───────────┴───┐
    │   SAMD51  │     │      ADXL343      │
    │  (Master) │     │      (Slave)      │
    │           │     │   Address: 0x1D   │
    └───────────┘     └───────────────────┘


                    I2C Transaction Example

    Reading X-axis from ADXL343:

    SDA: │ START │ ADDRESS (0x1D) │ W │ ACK │ REG (0x32) │ ACK │
    SCL: ──┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐──
          └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘

    SDA: │ Sr │ ADDRESS (0x1D) │ R │ ACK │ DATA_L │ ACK │ DATA_H │ NACK │ STOP │
    SCL: ──┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐──────
          └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘

    START  = SDA falls while SCL high
    STOP   = SDA rises while SCL high
    Sr     = Repeated START
    W = 0  = Write mode
    R = 1  = Read mode
    ACK    = Acknowledge (SDA low)
    NACK   = No Acknowledge (SDA high, signals end of read)

Digital Audio Fundamentals

The SAMD51’s dual 12-bit DACs convert digital samples to analog audio:

                    Digital-to-Analog Conversion

    Sample Rate: 44,100 Hz (CD quality)
    Each sample = 1 / 44100 = 22.68 μs

    Digital Samples (12-bit: 0-4095)        Analog Output

    4095 ─│    ●                            ─│    ╭───╮
          │   ╱ ╲    ●                       │   ╱     ╲
          │  ●   ●  ╱                        │  ╱       ╲
    2048 ─│       ╲╱                        ─│ ╱         ╲
          │                ●    ●            │            ╲
          │               ╱ ╲  ╱             │             ╲   ╱
          │              ●   ╲╱              │              ╲ ╱
       0 ─│──────────────────────────       ─│───────────────╲───
          │ 0   1   2   3   4   5   6         Time (continuous)
          Sample Number (discrete)


                    Sine Wave Generation

    For a 440 Hz tone (A4 note):

    samples_per_cycle = sample_rate / frequency
                      = 44100 / 440
                      = 100.23 samples

    For sample n:
        angle = (n * 2π) / samples_per_cycle
        value = sin(angle) * 2047 + 2048    (center at mid-scale)


              Memory Layout for Audio Buffer (Double-buffering)

    ┌─────────────────────────────────────────────────────────────┐
    │                         SRAM                                │
    ├──────────────────────────┬──────────────────────────────────┤
    │      Buffer A            │           Buffer B               │
    │   (Being output by DAC)  │   (Being filled by CPU)          │
    │                          │                                  │
    │ [S0][S1][S2]...[S255]   │ [S0][S1][S2]...[S255]            │
    └──────────────────────────┴──────────────────────────────────┘
                │                           │
                │ DMA transfers             │ CPU writes
                ▼                           ▼
            ┌──────┐                   ┌──────────┐
            │ DAC  │                   │ Synthesis │
            │      │                   │ Algorithm │
            └──────┘                   └──────────┘

USB MIDI Protocol

The NeoTrellis M4 can appear as a USB MIDI device—no drivers needed:

                    USB MIDI Architecture

    ┌─────────────────────────────────────────────────────────────┐
    │                     Host Computer                           │
    │                                                             │
    │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
    │  │  DAW        │  │   Synth     │  │  MIDI Monitor       │  │
    │  │ (Ableton)   │  │  (VCV Rack) │  │                     │  │
    │  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────┘  │
    │         │                │                     │            │
    │         └────────────────┼─────────────────────┘            │
    │                          │                                  │
    │                    OS MIDI Layer                            │
    │                          │                                  │
    └──────────────────────────┼──────────────────────────────────┘
                               │ USB
                               │
    ┌──────────────────────────┼──────────────────────────────────┐
    │                          │         NeoTrellis M4            │
    │                   ┌──────┴──────┐                           │
    │                   │  USB MIDI   │                           │
    │                   │  Endpoint   │                           │
    │                   └──────┬──────┘                           │
    │                          │                                  │
    │                   ┌──────┴──────┐                           │
    │                   │   Your      │                           │
    │                   │   Code      │                           │
    │                   └─────────────┘                           │
    └─────────────────────────────────────────────────────────────┘


                    MIDI Message Format

    ┌────────────────┬────────────────┬────────────────┐
    │   Status Byte  │   Data Byte 1  │   Data Byte 2  │
    │   (1xxx xxxx)  │   (0xxx xxxx)  │   (0xxx xxxx)  │
    └────────────────┴────────────────┴────────────────┘

    Note On:  0x9n  nn  vv    (n=channel, nn=note, vv=velocity)
    Note Off: 0x8n  nn  vv
    CC:       0xBn  cc  vv    (cc=controller#, vv=value)

    Example: Note C4 (60) on channel 1 at velocity 100:
             0x90  0x3C  0x64

Memory Map of the SAMD51

Understanding where things live in memory is essential for bare-metal programming:

                    SAMD51 Memory Map (Simplified)

    0xFFFF_FFFF ┌─────────────────────────────────────┐
                │         System/Reserved             │
    0xE000_0000 ├─────────────────────────────────────┤
                │         Cortex-M4 Internals         │
                │         (NVIC, SysTick, etc.)       │
    0x6000_0000 ├─────────────────────────────────────┤
                │         External Memory (QSPI)      │
                │         8MB Flash for files         │
    0x4100_0000 ├─────────────────────────────────────┤ ◀─ Peripherals
                │   PORT (GPIO)         0x4100_8000   │    start here
                │   SERCOM0-5 (I2C/SPI) 0x4000_0000+  │
                │   DAC                 0x4300_2000   │
                │   ADC0/1              0x4300_0000+  │
                │   TC/TCC (Timers)     0x4001_6000+  │
                │   USB                 0x4100_0000   │
    0x2004_0000 ├─────────────────────────────────────┤
                │         SRAM (192KB)                │
                │         Variables, stack, heap      │
    0x2000_0000 ├─────────────────────────────────────┤
                │         (Reserved)                  │
    0x0008_0000 ├─────────────────────────────────────┤
                │         Flash (512KB)               │
                │         Your program code           │
    0x0000_0000 └─────────────────────────────────────┘


    Example: Setting a GPIO pin high (bare-metal style)

    // PORT peripheral base address
    #define PORT_BASE 0x41008000

    // Group A, pin 10 (NeoPixel data pin)
    // OUTSET register offset = 0x18

    *(volatile uint32_t *)(PORT_BASE + 0x18) = (1 << 10);

    Or using CMSIS headers:
    PORT->Group[0].OUTSET.reg = PORT_PA10;

Concept Summary Table

Concept Cluster What You Need to Internalize
ARM Cortex-M4 32-bit RISC core with 3-stage pipeline, hardware FPU for DSP, memory-mapped peripherals
NeoPixel Protocol Timing-critical single-wire protocol, 24 bits per LED (GRB), DMA essential for smooth animation
I2C Communication Two-wire protocol (SDA/SCL), master-slave architecture, 7-bit addresses, ACK/NACK handshaking
Digital Audio Sample rate determines frequency range, bit depth determines dynamic range, double-buffering prevents glitches
USB MIDI Device appears as class-compliant MIDI, 3-byte messages (status + 2 data), no custom drivers needed
Memory Mapping All peripherals are memory addresses, volatile keyword required, bit manipulation for register access
DMA CPU-independent data transfer, essential for audio/LED timing, interrupt on completion
Button Matrix Row-column scanning saves pins, diodes prevent ghosting, debouncing required for clean input

Deep Dive Reading by Concept

This section maps each concept from above to specific book chapters for deeper understanding. Read these before or alongside the projects to build strong mental models.

ARM Cortex-M Architecture

Concept Book & Chapter
ARM overview “Computer Organization and Design RISC-V Edition” by Patterson & Hennessy — Ch. 2: “Instructions: Language of the Computer”
Cortex-M specifics “The Definitive Guide to ARM Cortex-M4” by Joseph Yiu — Ch. 1-4
Register conventions “Bare Metal C” by Steve Oualline — Ch. 3: “ARM Architecture”
Interrupt handling “Making Embedded Systems, 2nd Ed” by Elecia White — Ch. 8: “Interrupts”

Embedded C Programming

Concept Book & Chapter
Memory-mapped I/O “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron — Ch. 6: “The Memory Hierarchy”
Volatile and registers “Effective C, 2nd Ed” by Robert C. Seacord — Ch. 8: “Memory Management”
Bit manipulation “C Programming: A Modern Approach” by K.N. King — Ch. 20: “Low-Level Programming”
Linker scripts “Bare Metal C” by Steve Oualline — Ch. 6: “Linker Scripts”

Digital Audio

Concept Book & Chapter
Sampling theory “The Audio Programming Book” by Richard Boulanger — Ch. 1-2
DSP fundamentals “Understanding Digital Signal Processing” by Richard Lyons — Ch. 1-4
Audio synthesis “Designing Sound” by Andy Farnell — Part II: “Technique”
Real-time audio “Real-Time Digital Signal Processing” by Kuo & Gan — Ch. 1-3

Communication Protocols

Concept Book & Chapter
I2C protocol “Making Embedded Systems, 2nd Ed” by Elecia White — Ch. 7: “Communication”
USB fundamentals “USB Complete” by Jan Axelson — Ch. 1-4
MIDI protocol “MIDI: A Comprehensive Introduction” by Joseph Rothstein — Ch. 3-5

Essential Reading Order

For maximum comprehension, read in this order:

  1. Foundation (Week 1-2):
    • Making Embedded Systems Ch. 1-4 (overview of embedded development)
    • Bare Metal C Ch. 1-3 (toolchain and basic concepts)
  2. ARM Architecture (Week 3-4):
    • Definitive Guide to ARM Cortex-M4 Ch. 1-8 (core concepts)
    • Computer Systems: A Programmer’s Perspective Ch. 3 (machine-level programming)
  3. Audio & Real-Time (Week 5-6):
    • Audio Programming Book Ch. 1-4 (audio fundamentals)
    • Making Embedded Systems Ch. 8-10 (timing and interrupts)

Quick Start Guide (First 48 Hours)

Feeling overwhelmed? Here’s exactly what to do:

Hour 1-4: Setup

  1. Assemble NeoTrellis M4 (snap on buttons, connect enclosure)
  2. Connect USB-C cable
  3. It should mount as CIRCUITPY drive
  4. Download CircuitPython libraries bundle

Hour 5-8: First Project

  1. Open code.py in Mu Editor
  2. Copy the basic button/LED example from Project 1
  3. Modify colors, see LEDs respond to buttons
  4. You now have a working device!

Hour 9-16: Explore CircuitPython

  1. Complete Projects 1-3 (basic interactions)
  2. Read the NeoTrellis M4 Overview Guide
  3. Try modifying code—break things, fix them

Hour 17-24: Your First MIDI

  1. Install Arduino IDE and libraries
  2. Upload basic MIDI example
  3. Connect to a DAW (GarageBand/Ableton)
  4. Trigger sounds with buttons!

Hour 25-48: Audio Synthesis

  1. Try the audio library examples
  2. Understand double-buffering
  3. Create a simple synth

Path A: Music Creator (Fastest to Making Sound)

Project 1 → Project 2 → Project 4 → Project 6 → Project 7 → Project 9
   │           │           │           │           │           │
   ▼           ▼           ▼           ▼           ▼           ▼
 Button     Color       USB          Audio      Step         MIDI
 Basics     Mixer       MIDI         Synth      Sequencer    Drums

Time: 3-4 weeks | Focus: Musical applications

Path B: Embedded Systems Deep-Dive

Project 1 → Project 3 → Project 5 → Project 11 → Project 12 → Project 14
   │           │           │            │            │            │
   ▼           ▼           ▼            ▼            ▼            ▼
 Basics     Accel.      Timer        Bare-Metal   GPIO         DMA
            I2C         Precision    Blink        Registers    Audio

Time: 6-8 weeks | Focus: Low-level understanding

Path C: Complete Mastery (All Projects)

Complete in order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14 → 15 → 16 → 17 → 18

Time: 3-4 months Focus: Full stack embedded expertise

Project List

Projects are ordered from foundational CircuitPython through Arduino audio to bare-metal C programming.


Project 1: Interactive Button-LED Matrix (CircuitPython)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: CircuitPython (Python)
  • Alternative Programming Languages: MicroPython, Arduino C++
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 1: Beginner
  • Knowledge Area: GPIO, Event Handling, Color Theory
  • Software or Tool: NeoTrellis M4, Mu Editor
  • Main Book: “CircuitPython Essentials” by Adafruit (online guide)

What you’ll build: A fully interactive 4x8 button grid where each button press triggers a unique LED color, with animations like ripples, fades, and rainbow waves responding to your touch.

Why it teaches NeoTrellis fundamentals: This forces you to understand the relationship between the button matrix scanning and NeoPixel addressing. You’ll learn how the 32 buttons map to the 32 LEDs, how event callbacks work in CircuitPython, and the basics of HSV vs RGB color spaces.

Core challenges you’ll face:

  • Mapping buttons to LEDs → Understanding the matrix coordinate system (row, column)
  • Handling simultaneous button presses → Learning event-driven programming with callbacks
  • Creating smooth animations → Managing timing without blocking the main loop
  • Color mathematics → Converting between RGB, HSV, and understanding gamma correction

Key Concepts:

  • Event-driven programming: NeoTrellis M4 Overview - Adafruit
  • NeoPixel control: “Adafruit NeoPixel Überguide” — LED addressing and timing
  • HSV color space: “Computer Graphics from Scratch” by Gabriel Gambetta — Ch. 2

Difficulty: Beginner Time estimate: Weekend (4-8 hours) Prerequisites: Basic Python (variables, functions, loops), USB drive navigation


Real World Outcome

You’ll have a responsive light-up button pad that reacts instantly to touch. When you press any button, it lights up in a unique color. Press multiple buttons and they all respond independently. Release and watch fade animations. Tilt the board and watch colors shift.

Example Interaction:

Press button (0,0) → LED turns bright red
Press button (1,0) → LED turns orange (while red still lit)
Press button (2,0) → LED turns yellow
Release button (0,0) → Red LED fades out over 200ms
Press all buttons in row 0 → Rainbow gradient appears across row
Hold button (3,7) → LED pulses/breathes white

Serial output:
Button pressed: x=0, y=0, index=0
Setting LED 0 to RGB(255, 0, 0)
Button pressed: x=1, y=0, index=1
Setting LED 1 to RGB(255, 127, 0)
Button released: x=0, y=0, index=0
Fading LED 0...

When running the rainbow animation mode:

                    Visual Result (4x8 grid)
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
Row0│ 🔴 │ 🟠 │ 🟡 │ 🟢 │ 🔵 │ 🟣 │ 💗 │ 🔴 │ ← Colors shift →
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row1│ 🟠 │ 🟡 │ 🟢 │ 🔵 │ 🟣 │ 💗 │ 🔴 │ 🟠 │
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row2│ 🟡 │ 🟢 │ 🔵 │ 🟣 │ 💗 │ 🔴 │ 🟠 │ 🟡 │
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row3│ 🟢 │ 🔵 │ 🟣 │ 💗 │ 🔴 │ 🟠 │ 🟡 │ 🟢 │
    └────┴────┴────┴────┴────┴────┴────┴────┘
    Animation flows diagonally at 60 FPS

The Core Question You’re Answering

“How does a microcontroller know when a physical button is pressed, and how does it control individual LEDs in a matrix?”

Before writing any code, understand this: The 32 buttons aren’t connected to 32 separate pins—that would require too many GPIO pins. Instead, they’re wired as a 4×8 matrix, scanned rapidly to detect which intersection is pressed. Similarly, the 32 NeoPixels are daisy-chained on a single data wire, receiving 24 bits each in sequence.


Concepts You Must Understand First

Stop and research these before coding:

  1. Matrix Scanning
    • How can 32 buttons be read with fewer than 32 pins?
    • What is “ghosting” in button matrices and how do diodes prevent it?
    • Why does the scan happen faster than human perception?
    • Book Reference: “Making Embedded Systems” Ch. 7 - Elecia White
  2. NeoPixel Addressing
    • Why does the first LED in a chain get the first 24 bits?
    • What’s the difference between physical position and logical index?
    • Why does color order matter (GRB vs RGB)?
    • Book Reference: “Adafruit NeoPixel Überguide” — Adafruit Learning System
  3. Event Callbacks
    • What’s the difference between polling and event-driven design?
    • Why are callbacks more efficient than checking state in a loop?
    • How does the NeoTrellis library manage button state internally?

Questions to Guide Your Design

Before implementing, think through these:

  1. Data Organization
    • How will you map button coordinates (x, y) to LED index?
    • Will you store current LED colors in an array for animation?
    • How will you track which buttons are currently held?
  2. Animation Architecture
    • How will you fade LEDs without blocking button detection?
    • What’s the relationship between main loop timing and animation smoothness?
    • Should color calculations happen per-frame or be pre-computed?
  3. User Experience
    • What should happen if the user presses faster than your animation cycle?
    • How will you distinguish between tap, hold, and double-tap?

Thinking Exercise

Trace the Event Flow

Before coding, trace what happens when button (2, 3) is pressed:

1. Hardware level:
   - Matrix scanner activates row 2
   - Column 3 reads as connected
   - Interrupt generated (or polled state changes)

2. Library level:
   - NeoTrellis library detects state change
   - Callback function invoked with (x=2, y=3, edge=PRESSED)

3. Your code:
   - Calculate LED index: index = y * 8 + x = 3 * 8 + 2 = 26
   - Calculate color based on position
   - Set trellis.pixels[26] = (r, g, b)

4. NeoPixel level:
   - Library sends 32 × 24 = 768 bits to LED chain
   - LED 26 captures bits 625-648

Questions while tracing:

  • Why is the index formula y * 8 + x and not x * 4 + y?
  • What happens if you set the color before the library finishes scanning?
  • How many microseconds does updating all 32 LEDs take?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the difference between polling and interrupt-driven I/O. Which would you use for button detection and why?”

  2. “Your LED animation is stuttering. What are the likely causes and how would you debug it?”

  3. “If you needed to detect a button being held for 2 seconds, how would you implement that without blocking?”

  4. “Why do NeoPixels use GRB color order instead of RGB? What problems can wrong color order cause?”

  5. “A user reports that pressing certain button combinations causes wrong LEDs to light. What’s your first hypothesis?”


Hints in Layers

Hint 1: Starting Point Begin with the Adafruit NeoTrellis example code. Understand the callback registration pattern: trellis.activate_key(x, y, NeoTrellis.EDGE_RISING).

Hint 2: Color Mapping Strategy Use HSV (Hue, Saturation, Value) for position-based colors. Hue from 0-255 maps to rainbow. Position (0,0) = Hue 0, position (3,7) = Hue 255.

Hint 3: Animation Without Blocking Store target colors and current colors separately. Each main loop iteration, move current colors slightly toward target. This is called “interpolation” or “tweening.”

Hint 4: Debugging Tools Use print() statements to serial monitor. Print button coordinates and calculated indices. If LEDs light wrong positions, your index formula is incorrect.


Books That Will Help

Topic Book Chapter
Event-driven design “Making Embedded Systems” by Elecia White Ch. 5
Color theory for LEDs “Programming Graphics” (online) Color spaces
Python patterns “Fluent Python” by Luciano Ramalho Ch. 7 (functions)

Common Pitfalls & Debugging

Problem Cause Fix Verification
LEDs don’t light Brightness set to 0 Set trellis.pixels.brightness = 0.2 Print brightness value
Wrong LED lights Index formula wrong Check index = y * width + x Print index, verify visually
Colors look wrong GRB vs RGB mismatch CircuitPython NeoPixel handles this Test with (255,0,0) = red
Buttons unresponsive Callbacks not registered Call activate_key() for each button Print in callback to verify
Animation jerky Blocking code in loop Remove time.sleep(), use time delta Measure loop time

Learning Milestones

  1. Single button works → You understand the callback pattern and LED addressing
  2. All 32 buttons respond correctly → You’ve mastered the coordinate-to-index mapping
  3. Smooth animations running → You understand non-blocking timing and state management

Project 2: RGB Color Mixer Instrument (CircuitPython)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: CircuitPython (Python)
  • Alternative Programming Languages: Arduino C++, MicroPython
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 1: Beginner
  • Knowledge Area: Color Theory, State Machines, UI/UX Design
  • Software or Tool: NeoTrellis M4, Mu Editor
  • Main Book: “Interaction Design” by Preece, Rogers, Sharp

What you’ll build: A tactile color mixing interface where dedicated rows control Red, Green, Blue, and brightness. Pressing multiple buttons blends colors in real-time, with a preview area showing the result. Export the color as hex code via serial.

Why it teaches UI state management: This project forces you to think about multi-input state—what happens when the user changes red while green is still held? You’ll implement a state machine that tracks multiple simultaneous inputs and computes derived values.

Core challenges you’ll face:

  • Multi-input tracking → Maintaining state for 32 possible simultaneous button states
  • Value scaling → Mapping 8 buttons to 256 brightness levels
  • Live preview → Updating display while user is still adjusting
  • State persistence → Remembering the last color when buttons are released

Key Concepts:

  • State machines: “Making Embedded Systems” Ch. 5 - Elecia White
  • Color models: “Computer Graphics from Scratch” Ch. 2 - Gabriel Gambetta
  • Human-computer interaction: “The Design of Everyday Things” - Don Norman

Difficulty: Beginner Time estimate: Weekend (6-10 hours) Prerequisites: Project 1 completed, understanding of RGB color model


Real World Outcome

Your NeoTrellis becomes a physical color picker. The grid is divided into functional zones:

                    Color Mixer Layout
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
Row0│ R0 │ R1 │ R2 │ R3 │ R4 │ R5 │ R6 │ R7 │ ← Red (0-255)
    ├────┼────┼────┼────┼────┼────┼────┼────┤   Press R3 = Red 109
Row1│ G0 │ G1 │ G2 │ G3 │ G4 │ G5 │ G6 │ G7 │ ← Green (0-255)
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row2│ B0 │ B1 │ B2 │ B3 │ B4 │ B5 │ B6 │ B7 │ ← Blue (0-255)
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row3│ ▓▓ │ ▓▓ │ ▓▓ │ ▓▓ │ ▓▓ │ ▓▓ │ SAV│ CPY│ ← Preview + Actions
    └────┴────┴────┴────┴────┴────┴────┴────┘
                                   │     │
                              Save slot   Copy hex
                                         to serial

**Example Interaction:**

Press R4 → Red channel lights up (buttons 0-4 lit indicating value) Red = (4/7) * 255 = 145

Press G2 → Green channel shows value Green = (2/7) * 255 = 73

Press B6 → Blue channel shows value Blue = (6/7) * 255 = 218

Row 3 preview → Shows mixed color: RGB(145, 73, 218) = purple

Press CPY button → Serial output: “Color: #9149DA RGB(145, 73, 218)”


---

## The Core Question You're Answering

> "How do you design a physical interface that allows continuous adjustment of multiple parameters simultaneously, while giving clear feedback?"

This is the fundamental challenge of instrument design. A piano doesn't wait for you to release middle C before you can play G—it handles simultaneous input. Your color mixer must similarly track independent channel states.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **State Machines**
   - What states can each color channel be in? (Idle, Adjusting, Locked)
   - How do state transitions trigger actions?
   - Why is explicit state management better than implicit boolean flags?
   - *Book Reference:* "Making Embedded Systems" Ch. 5 - Elecia White

2. **Color Channel Independence**
   - Why are RGB channels orthogonal?
   - How does 8-bit per channel create 16.7 million colors?
   - What's the perceptual difference between R:128 and R:255?

3. **Input Latching**
   - Should color persist after button release?
   - How do you distinguish "user is adjusting" from "user is done"?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Value Representation**
   - 8 buttons for 256 values—how do you map this?
   - Linear mapping (each button = 32) or logarithmic?
   - How do you show the current value visually?

2. **Interaction Model**
   - Does pressing a new red button change red, or do you need to release first?
   - What if user presses R3 then R5 without releasing R3?
   - Should there be an "undo" button?

3. **Feedback Design**
   - How does the user know what value they've selected?
   - Should unselected buttons be dimmed or off?
   - How bright should the preview row be?

---

## Thinking Exercise

### Design the State Machine

Before coding, sketch the state machine for one color channel:

                Red Channel State Machine

┌─────────────────────────────────────────────────┐
│                                                 │
│              ┌──────────┐                       │
│   ┌──────────│   IDLE   │──────────┐            │
│   │          │ value=0  │          │            │
│   │          └────┬─────┘          │            │
│   │ R(n)          │          R(n)  │            │
│   │ pressed       │          pressed            │
│   │               │                │            │
│   ▼               │                ▼            │
│ ┌─────────────────┴─────────────────────┐       │
│ │              ACTIVE                   │       │
│ │   value = n * (255/7)                │       │
│ │   LEDs 0..n lit                      │       │
│ └───────────────────────────────────────┘       │
│   │                                             │
│   │ Another R(m) pressed                        │
│   │                                             │
│   ▼                                             │
│   Update value = m * (255/7)                    │
│   Keep state ACTIVE                             │
│                                                 │
└─────────────────────────────────────────────────┘ ```

Questions:

  • When does value persist vs reset?
  • What happens to the state machine when switching between colors?
  • How do three independent state machines interact?

The Interview Questions They’ll Ask

  1. “Describe the state machine for your color mixer. How did you handle simultaneous input from multiple channels?”

  2. “Why did you choose the mapping between button position and color value? What alternatives did you consider?”

  3. “Your user testing shows people expect the leftmost button to mean ‘off’ but your code treats it as the lowest non-zero value. How do you resolve this?”

  4. “How would you extend this to support HSV or HSL color models?”

  5. “The preview updates feel ‘laggy’. Walk me through how you’d profile and optimize the update path.”


Hints in Layers

Hint 1: Starting Point Create three arrays: red_value, green_value, blue_value. Each starts at 0. Button presses in rows 0-2 update the corresponding array.

Hint 2: Value Mapping Use integer division: value = (button_x * 255) // 7. Button 0 = 0, Button 7 = 255. The // is integer division in Python.

Hint 3: Visual Feedback For the red row, light buttons 0 through the selected one in red. Dim red (64,0,0) for unselected, bright red (255,0,0) for selected.

Hint 4: Preview Row The preview row shows the mixed color. Update all 6 preview LEDs (indices 24-29) to (red_value, green_value, blue_value) whenever any channel changes.


Books That Will Help

Topic Book Chapter
State machines “Making Embedded Systems” by Elecia White Ch. 5
Color perception “Interaction Design” by Preece et al. Ch. 6
Interface design “The Design of Everyday Things” by Don Norman Ch. 1-2

Common Pitfalls & Debugging

Problem Cause Fix Verification
Colors don’t match expectations RGB order wrong Verify with pure red (255,0,0) Check single channel
Value jumps unexpectedly Multiple button detection Use edge detection, not level Add debounce delay
Preview lags behind input Update not called Call preview update after any change Add print statements
Can’t get pure white Brightness too low Increase pixels.brightness Test with (255,255,255)

Learning Milestones

  1. Single channel adjustable → You understand value mapping and LED feedback
  2. All three channels work independently → You’ve implemented parallel state tracking
  3. Smooth, responsive preview → You’ve optimized the update path

Project 3: Accelerometer-Controlled Light Show (CircuitPython)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: CircuitPython (Python)
  • Alternative Programming Languages: Arduino C++, MicroPython
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Sensors, I2C Protocol, Physics, Signal Processing
  • Software or Tool: NeoTrellis M4, ADXL343 Accelerometer
  • Main Book: “Making Embedded Systems” by Elecia White

What you’ll build: A motion-reactive light display where tilting the board shifts colors like liquid, shaking triggers sparkle effects, and tap detection changes modes. The LEDs respond to real physics—gravity pulls colors “down” to whichever edge is lowest.

Why it teaches sensor integration: This project forces you to understand the ADXL343 accelerometer’s I2C interface, interpret signed acceleration values, and convert raw sensor data into meaningful visualizations. You’ll learn sensor fusion basics and real-time signal processing.

Core challenges you’ll face:

  • I2C communication → Reading from the ADXL343 at its 0x1D address
  • Signed data interpretation → Understanding two’s complement for negative acceleration
  • Coordinate transformation → Mapping 3D acceleration to 2D LED grid
  • Tap detection → Configuring the accelerometer’s built-in tap detection registers
  • Noise filtering → Smoothing sensor data without introducing lag

Key Concepts:

  • I2C Protocol: “Making Embedded Systems” Ch. 7 - Elecia White
  • Accelerometer physics: ADXL343 Datasheet - Analog Devices
  • Signal filtering: “Digital Signal Processing” Ch. 1-2 - Smith

Difficulty: Intermediate Time estimate: 1 week (10-15 hours) Prerequisites: Project 1 completed, basic understanding of physics (acceleration, gravity)


Real World Outcome

Your NeoTrellis becomes a motion-sensitive light sculpture:

                    Tilt Response Visualization

    Board Flat:              Tilted Right:           Tilted Forward:
    ┌─────────────────┐      ┌─────────────────┐     ┌─────────────────┐
    │ ░ ░ ░ ░ ░ ░ ░ ░ │      │ ░ ░ ░ ░ ░ ░ ▓ █ │     │ ░ ░ ░ ░ ░ ░ ░ ░ │
    │ ░ ▓ ▓ ▓ ▓ ▓ ░ ░ │      │ ░ ░ ░ ░ ░ ░ ▓ █ │     │ ░ ░ ░ ░ ░ ░ ░ ░ │
    │ ░ ▓ ▓ ▓ ▓ ▓ ░ ░ │  →   │ ░ ░ ░ ░ ░ ░ ▓ █ │     │ ░ ░ ░ ░ ░ ░ ░ ░ │
    │ ░ ░ ░ ░ ░ ░ ░ ░ │      │ ░ ░ ░ ░ ░ ░ ▓ █ │     │ █ █ █ █ █ █ █ █ │
    └─────────────────┘      └─────────────────┘     └─────────────────┘
    "Blob" centered          Blob "flows" right      Blob "flows" forward
    X≈0, Y≈0                 X>0, Y≈0                X≈0, Y>0

**Serial Output:**

Acceleration: X=-0.05g Y=0.02g Z=1.00g (Board flat) Acceleration: X=0.42g Y=-0.03g Z=0.91g (Tilted right ~25°) Acceleration: X=0.71g Y=0.00g Z=0.71g (Tilted right ~45°) TAP DETECTED! Single tap Switching to mode: SPARKLE Acceleration: X=0.12g Y=0.89g Z=0.43g (Tilted forward ~60°) SHAKE DETECTED! Magnitude: 2.3g Triggering explosion effect!


**Tap → Changes display mode (liquid, sparkle, pulse)**
**Shake → Triggers special effect (explosion, reset)**
**Tilt → Continuously shifts color position**

---

## The Core Question You're Answering

> "How do you read physical sensor data over I2C and transform it into meaningful application behavior?"

The accelerometer outputs numbers in milligravities (thousandths of g). At rest, it reads approximately (0, 0, 1000) for X, Y, Z because gravity pulls down. Tilting changes these values. You must interpret these numbers as physical orientation.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **I2C Protocol**
   - What are SDA and SCL? Why two wires?
   - What is a 7-bit address? Why is ADXL343 at 0x1D?
   - How do you read a 16-bit value from two 8-bit registers?
   - *Book Reference:* "Making Embedded Systems" Ch. 7 - Elecia White

2. **Accelerometer Physics**
   - What does "1g" mean? What about "-1g"?
   - When the board is flat, why is Z ≈ 1g?
   - How does tilting change the X and Y values?
   - *Book Reference:* ADXL343 Datasheet - Application Notes section

3. **Two's Complement**
   - How are negative numbers represented in binary?
   - Why is 0xFF not 255 but -1 in signed representation?
   - How do you convert raw register bytes to signed integers?

4. **Tap Detection**
   - What physical motion constitutes a "tap"?
   - How does the ADXL343 detect taps internally?
   - What's the difference between single and double tap?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Coordinate Mapping**
   - The accelerometer X axis vs the LED grid X axis—are they aligned?
   - If you tilt "right," which accelerometer axis increases?
   - How will you map (-1g, +1g) range to (0, 7) LED columns?

2. **Response Characteristics**
   - Should small tilts be ignored (dead zone)?
   - Should extreme tilts saturate at the edge?
   - How fast should the visual update when tilting?

3. **Mode Switching**
   - How many display modes will you implement?
   - How will the user know which mode is active?
   - Should mode state persist after tap or cycle through?

---

## Thinking Exercise

### Map the Physics to Visual

Before coding, work out the math:

Scenario: Board tilted 30° to the right

Physics:

  • Gravity vector g = (0, 0, -9.8) m/s² in world frame
  • Board tilted 30° around Y axis
  • Accelerometer reads in board frame

Calculation:

  • X_accel = g × sin(30°) = 9.8 × 0.5 = 4.9 m/s² ≈ 0.5g
  • Z_accel = g × cos(30°) = 9.8 × 0.866 = 8.5 m/s² ≈ 0.87g

What ADXL343 reports (in ±2g mode, 10-bit):

  • X = 0.5g × 256 = 128 (scaled value)
  • Z = 0.87g × 256 = 223

LED mapping:

  • X range: -1g to +1g → LED columns 0 to 7
  • X = 0.5g → column = (0.5 + 1) / 2 × 7 = 5.25 → column 5

Visual: The “blob” should be in column 5-6 (right side of grid)


*Questions:*
- What happens at exactly 90° tilt? (X = 1g, Z = 0)
- How do you handle tilts beyond ±45°?
- What if the user rotates the board (changes which edge is "forward")?

---

## The Interview Questions They'll Ask

1. "Explain the I2C read sequence for getting the X-axis acceleration. What bytes are sent and received?"

2. "The accelerometer outputs signed integers. How did you convert the raw bytes to a floating-point g value?"

3. "Your tap detection is either too sensitive or not sensitive enough. How would you tune it?"

4. "The LED response feels 'jittery' with sensor noise. What filtering approach did you use?"

5. "How would you add orientation detection (portrait vs landscape) to your design?"

---

## Hints in Layers

**Hint 1: Starting Point**
Use the Adafruit_ADXL343 library. Initialize with `accelerometer = adafruit_adxl34x.ADXL343(i2c)`. Read with `x, y, z = accelerometer.acceleration`.

**Hint 2: Coordinate Alignment**
Print raw values while physically tilting the board. Note which axis increases when tilting right/left/forward/back. The axes may not match the LED grid intuition.

**Hint 3: Simple Mapping**

Map acceleration (-10 to +10 m/s²) to column (0-7)

column = int((x + 10) / 20 * 8) column = max(0, min(7, column)) # Clamp to valid range


**Hint 4: Noise Reduction**
Use exponential moving average:

smoothed_x = 0.9 * smoothed_x + 0.1 * raw_x

The 0.9/0.1 ratio trades responsiveness for smoothness.

---

## Books That Will Help

| Topic | Book | Chapter |
|-------|------|---------|
| I2C fundamentals | "Making Embedded Systems" by Elecia White | Ch. 7 |
| Accelerometer applications | ADXL343 Datasheet | Application Notes |
| Signal filtering | "DSP First" by McClellan et al. | Ch. 4 |
| Embedded sensors | "Sensors and Signal Conditioning" | Ch. 3 |

---

## Common Pitfalls & Debugging

| Problem | Cause | Fix | Verification |
|---------|-------|-----|--------------|
| I2C error on init | Wrong address or wiring | Check address 0x1D, verify SDA/SCL | Use I2C scanner |
| Values always near zero | Wrong scale factor | Check library returns m/s² not raw | Print raw values |
| Axes seem swapped | Board orientation mismatch | Print X/Y/Z while tilting physically | Map experimentally |
| Jittery display | Sensor noise | Add low-pass filter | Print filtered vs raw |
| Tap never triggers | Threshold too high | Lower THRESH_TAP register | Test sensitivity |

---

## Learning Milestones

1. **Raw values printed correctly** → You understand I2C communication and data format
2. **LEDs respond to tilt** → You've correctly mapped sensor coordinates to display
3. **Smooth, responsive animations** → You've mastered filtering and real-time updates
4. **Tap detection working** → You understand interrupt-driven sensor events

---

## Project 4: USB MIDI Controller (CircuitPython)

> **[View Detailed Guide](LEARN_NEOTRELLIS_M4_DEEP_DIVE/P04-usb-midi-controller-circuitpython.html)**

- **File**: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
- **Main Programming Language**: CircuitPython (Python)
- **Alternative Programming Languages**: Arduino C++
- **Coolness Level**: Level 4: Hardcore Tech Flex
- **Business Potential**: 3. The "Service & Support" Model
- **Difficulty**: Level 2: Intermediate
- **Knowledge Area**: USB Protocol, MIDI, Music Technology
- **Software or Tool**: NeoTrellis M4, DAW (Ableton/GarageBand/REAPER)
- **Main Book**: "MIDI: A Comprehensive Introduction" by Joseph Rothstein

**What you'll build**: A class-compliant USB MIDI controller that appears as a MIDI input device on any computer. Buttons send note-on/off messages with velocity, the accelerometer maps to control change (CC) messages, and visual feedback shows what's being transmitted.

**Why it teaches USB protocols**: This project demystifies how USB devices work. The NeoTrellis appears as a "Human Interface Device" variant—no drivers needed because it follows the USB MIDI class specification. You'll understand enumeration, endpoints, and protocol compliance.

**Core challenges you'll face**:
- **USB enumeration** → Understanding how the device identifies itself to the host
- **MIDI message format** → Constructing valid note-on, note-off, and CC messages
- **Velocity mapping** → Converting button press speed/force to MIDI velocity
- **Latency management** → Ensuring messages are sent within acceptable musical timing
- **Visual feedback** → Showing transmitted notes without interfering with timing

**Key Concepts**:
- **USB MIDI specification**: USB Device Class Definition for MIDI Devices
- **MIDI protocol**: "MIDI: A Comprehensive Introduction" Ch. 3-5 - Rothstein
- **Real-time requirements**: "Making Embedded Systems" Ch. 8 - White

**Difficulty**: Intermediate
**Time estimate**: 1 week (10-15 hours)
**Prerequisites**: Project 1 completed, basic understanding of musical notes

---

## Real World Outcome

Your NeoTrellis becomes a professional MIDI controller:

                MIDI Controller Layout

┌────┬────┬────┬────┬────┬────┬────┬────┐ Row0│ C3 │ C#3│ D3 │ D#3│ E3 │ F3 │ F#3│ G3 │  Channel 1
├────┼────┼────┼────┼────┼────┼────┼────┤  Notes 48-55 Row1│ G#3│ A3 │ A#3│ B3 │ C4 │ C#4│ D4 │ D#4│  Notes 56-63
├────┼────┼────┼────┼────┼────┼────┼────┤ Row2│ E4 │ F4 │ F#4│ G4 │ G#4│ A4 │ A#4│ B4 │  Notes 64-71
├────┼────┼────┼────┼────┼────┼────┼────┤ Row3│CC1 │CC2 │CC3 │CC4 │ Oct↓│Oct↑│Ch- │Ch+ │  Controls
└────┴────┴────┴────┴────┴────┴────┴────┘
      ↑              ↑
Mod/Express    Octave shift

When connected to a DAW:

$ dmesg (Linux) or System Report (Mac):
  USB MIDI Device: "NeoTrellis M4 MIDI"
  Manufacturer: Adafruit
  Ports: 1 In, 1 Out

In DAW MIDI Monitor:
  10:23:45.123  Note On   Ch1  C4   Vel:100
  10:23:45.234  Note On   Ch1  E4   Vel: 87
  10:23:45.345  Note On   Ch1  G4   Vel: 92
  10:23:45.456  Note Off  Ch1  C4   Vel:  0
  10:23:45.567  CC        Ch1  CC1  Val: 64  (tilt-controlled)
  10:23:45.678  Note Off  Ch1  E4   Vel:  0
  10:23:45.789  Note Off  Ch1  G4   Vel:  0

**Visual Feedback:**
- Button pressed: LED turns white (sending note)
- Button held: LED pulses (sustaining)
- Velocity shown: Brighter = higher velocity
- CC active: Row 3 buttons glow proportionally

The Core Question You’re Answering

“How does a USB device communicate with a computer without custom drivers, and what makes MIDI ‘class-compliant’?”

USB class compliance means the device follows a published specification. When the NeoTrellis says “I am a USB MIDI device,” the operating system already knows how to talk to it. No drivers needed. This is why you can plug any class-compliant MIDI controller into any computer.


Concepts You Must Understand First

Stop and research these before coding:

  1. USB Enumeration
    • What happens in the first 100ms when you plug in a USB device?
    • What are descriptors and why does the device send them?
    • What’s the difference between high-speed and full-speed USB?
    • Book Reference: “USB Complete” by Jan Axelson Ch. 4
  2. MIDI Protocol
    • What are the three bytes in a Note On message?
    • What’s the difference between a Channel Message and a System Message?
    • Why does velocity 0 sometimes act as Note Off?
    • Book Reference: “MIDI: A Comprehensive Introduction” Ch. 3
  3. Note Numbers
    • What MIDI note number is Middle C? (Hint: there’s controversy—60 or 48 depending on convention)
    • How do octaves map to note numbers?
    • What’s the valid range of note numbers?
  4. Control Change Messages
    • What is CC1 (Mod Wheel) vs CC7 (Volume)?
    • Which CC numbers are “standard” vs “undefined”?
    • Can CC messages be sent simultaneously with notes?

Questions to Guide Your Design

Before implementing, think through these:

  1. Note Mapping
    • How will you map the 4×8 grid to musical notes?
    • Chromatic scale? Specific scale (major, minor)?
    • Should the layout be piano-like or grid-optimized?
  2. Velocity Generation
    • The buttons are on/off—no pressure sensitivity. How will you determine velocity?
    • Time between press start and full press? Random? Fixed?
    • What’s a musically useful velocity range?
  3. Visual Latency
    • Updating LEDs takes time (~1ms for 32 pixels). When do you update?
    • Before MIDI send (visual before sound) or after?
    • Does visual update affect MIDI timing?

Thinking Exercise

Construct a MIDI Message

Before coding, construct the bytes manually:

Goal: Send Note On for Middle C (note 60) on Channel 1 at velocity 100

MIDI Note On format:
┌────────────────┬────────────────┬────────────────┐
│   Status Byte  │   Data Byte 1  │   Data Byte 2  │
│   1001 nnnn    │   0nnn nnnn    │   0vvv vvvv    │
│   └──┬──┘└─┬─┘ │   └────┬────┘  │   └────┬────┘  │
│   Note  Channel│      Note #    │    Velocity    │
│   On   (0-15)  │    (0-127)     │    (0-127)     │
└────────────────┴────────────────┴────────────────┘

For Channel 1 (0 in zero-indexed), Note 60, Velocity 100:
- Status: 1001 0000 = 0x90
- Note:   0011 1100 = 0x3C = 60
- Velocity: 0110 0100 = 0x64 = 100

Message: [0x90, 0x3C, 0x64]

Verify:
- 0x90: Note On, Channel 1 ✓
- 0x3C = 60: Middle C ✓
- 0x64 = 100: Strong velocity ✓

Questions:

  • What would the message be for Note Off?
  • How would you change the channel to Channel 10 (drums)?
  • What happens if you send velocity 0 with Note On?

The Interview Questions They’ll Ask

  1. “Explain the structure of a MIDI Note On message. How did you construct and send it via USB?”

  2. “Your controller has noticeable latency compared to commercial products. Where is the delay coming from?”

  3. “How would you implement running status to reduce bandwidth for rapid note sequences?”

  4. “The user wants to control both note velocity and aftertouch. How would you map this to buttons without pressure sensitivity?”

  5. “Describe how USB enumeration works. What happens when the NeoTrellis is plugged in?”


Hints in Layers

Hint 1: Starting Point Use adafruit_midi library. Initialize with midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1]). Send notes with midi.send(NoteOn(60, 100)).

Hint 2: Button to Note Mapping Simple chromatic starting at C3:

base_note = 48  # C3
note = base_note + (row * 8) + col

Hint 3: Simulated Velocity Without pressure, use timing. Start a timer on button edge, send Note On when stable (after debounce). Velocity = f(debounce_time). Faster press = higher velocity.

Hint 4: LED Feedback Color by note: Use hue = (note % 12) * (255/12) to make each pitch class a distinct color. C = red, D = orange, etc.


Books That Will Help

Topic Book Chapter
MIDI protocol “MIDI: A Comprehensive Introduction” by Rothstein Ch. 3-5
USB fundamentals “USB Complete” by Jan Axelson Ch. 1-4, 11
Real-time constraints “Making Embedded Systems” by Elecia White Ch. 8

Common Pitfalls & Debugging

Problem Cause Fix Verification
Device not recognized USB descriptor issue Reflash CircuitPython Check dmesg/System Report
No MIDI output Wrong USB port index Try port[0] or port[1] Print port names
Stuck notes Note Off not sent Always pair On with Off Monitor in DAW
High latency Visual update before MIDI Send MIDI first, then update LED Time with stopwatch app
Wrong notes Off-by-one in mapping Print note numbers Use MIDI monitor

Learning Milestones

  1. Device appears in DAW → You understand USB enumeration and class compliance
  2. Notes trigger sounds → You’ve correctly constructed MIDI messages
  3. All 32 buttons work → You’ve mastered the button-to-note mapping
  4. Visual feedback matches output → You’ve synchronized display with transmission

Project 5: Precision Timer and Metronome (CircuitPython + Low-Level)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: CircuitPython with low-level access
  • Alternative Programming Languages: Arduino C++, Bare-metal C
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Timer Peripherals, Real-Time Systems, Audio Timing
  • Software or Tool: NeoTrellis M4
  • Main Book: “Making Embedded Systems” by Elecia White

What you’ll build: A rock-solid BPM (beats per minute) metronome that uses hardware timers for precise timing, outputs audio clicks through the DAC, visual beats on LEDs, and can sync to external MIDI clock. Accuracy must be within 0.1% of target BPM.

Why it teaches timer fundamentals: This project forces you to understand the difference between software timing (time.sleep()) and hardware timer peripherals. You’ll learn why time.sleep(0.5) doesn’t give you exactly 500ms, and how to achieve microsecond-accurate timing.

Core challenges you’ll face:

  • Timer drift → Understanding why software loops accumulate timing errors
  • Hardware timer configuration → Setting prescaler and period for exact frequencies
  • Interrupt-driven design → Moving timing-critical code to interrupt context
  • DAC audio output → Generating click sounds without CPU involvement
  • Multi-rate synchronization → LED update rate vs audio sample rate vs BPM

Key Concepts:

  • Timer peripherals: SAMD51 Datasheet TC/TCC sections
  • Interrupt latency: “Making Embedded Systems” Ch. 8 - White
  • Real-time systems: “Real-Time Systems” by Liu - Ch. 1-3

Difficulty: Intermediate Time estimate: 1-2 weeks (15-20 hours) Prerequisites: Projects 1-4 completed, understanding of frequency/period relationship


Real World Outcome

Your NeoTrellis becomes a professional-grade metronome:

                    Metronome Display

    ┌────┬────┬────┬────┬────┬────┬────┬────┐
Row0│ ▓▓ │ ░░ │ ░░ │ ░░ │ ▓▓ │ ░░ │ ░░ │ ░░ │  Beat visualization
    ├────┼────┼────┼────┼────┼────┼────┼────┤  (4/4 time signature)
Row1│1 2 0│    │    │    │    │    │    │    │  BPM Display
    ├────┼────┼────┼────┼────┼────┼────┼────┤  (7-segment style)
Row2│ << │ <  │ ▶  │ >  │ >> │    │ TAP│SYNC│  Controls
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row3│ 4/4│ 3/4│ 6/8│7/8 │BEAT│SUBD│TRIP│    │  Time signatures
    └────┴────┴────┴────┴────┴────┴────┴────┘

Controls:
- << / >> : BPM ± 10
- < / >   : BPM ± 1
- ▶       : Start/Stop
- TAP     : Tap tempo (average of last 4 taps)
- SYNC    : Sync to incoming MIDI clock

**Serial Output:**

Metronome initialized Hardware Timer TC3 configured: Prescaler: 1024 Period: 58593 (for 120 BPM) Actual frequency: 2.0000 Hz Error: 0.0001%

Running at 120 BPM (4/4) Beat 1 | ████░░░░░░░░ | +0.023ms drift Beat 2 | ░░░░████░░░░ | +0.018ms drift Beat 3 | ░░░░░░░░████ | +0.025ms drift Beat 4 | ░░░░░░░░████ | +0.021ms drift

Tap tempo: 4 taps detected Intervals: 502ms, 498ms, 501ms Average: 500.33ms Calculated BPM: 119.92

MIDI Clock received: syncing… External tempo: 125.0 BPM (locked)


**Audio output**: Clean click sound through headphone jack, precisely on each beat.

---

## The Core Question You're Answering

> "Why does `time.sleep()` accumulate timing errors, and how do hardware timers achieve precise, drift-free timing?"

Software timing is fundamentally flawed for precision work. Consider:
```python
while True:
    do_beat()      # Takes 5ms (variable!)
    time.sleep(0.5)  # Sleep 500ms
    # Total: 505ms, not 500ms
    # After 100 beats: 500ms accumulated error!

Hardware timers run independently of software execution time. They count clock cycles directly, guaranteeing precision.


Concepts You Must Understand First

Stop and research these before coding:

  1. Timer Architecture
    • What’s the relationship between CPU clock, prescaler, and timer frequency?
    • What’s a timer period/compare register?
    • How does a timer generate interrupts?
    • Book Reference: SAMD51 Datasheet, TC chapter
  2. Prescaler Mathematics
    • If CPU clock is 120MHz and prescaler is 1024, what’s the timer input frequency?
    • For 120 BPM (2 beats/sec), what period value do you need?
    • What’s the maximum period length with different prescaler values?
  3. Interrupt Context
    • What code runs in “interrupt context” vs “main context”?
    • Why must interrupt handlers be fast?
    • What’s “interrupt latency” and why does it matter?
    • Book Reference: “Making Embedded Systems” Ch. 8
  4. DAC Timing
    • How do you generate audio samples at a fixed rate?
    • What’s the relationship between sample rate and audio quality?
    • How do DMA transfers help with timing?

Questions to Guide Your Design

Before implementing, think through these:

  1. Timer Selection
    • The SAMD51 has multiple timer types (TC, TCC). Which one for BPM timing?
    • What resolution do you need for 30-300 BPM range?
    • How will you handle BPM changes without stopping the timer?
  2. Audio Click Generation
    • What waveform makes a good “click”? (Sine burst, noise burst, frequency sweep?)
    • How many samples for a 10ms click at 44.1kHz?
    • Should click generation be in interrupt context or pre-computed?
  3. Visual Synchronization
    • The LED update takes ~1ms. When do you trigger it relative to the audio?
    • Should LEDs update in the timer interrupt or main loop?

Thinking Exercise

Calculate Timer Values

Before coding, work out the math for 120 BPM:

Given:
- CPU Clock: 120 MHz = 120,000,000 Hz
- Desired BPM: 120 (= 2 beats per second)
- Beat period: 1/2 = 0.5 seconds = 500,000 μs

Timer calculation:
- Prescaler options: 1, 2, 4, 8, 16, 64, 256, 1024
- Timer input frequency = CPU Clock / Prescaler

With Prescaler = 1024:
- Timer frequency = 120,000,000 / 1024 = 117,187.5 Hz
- Ticks per beat = 117,187.5 × 0.5 = 58,593.75

Since period must be integer, use 58594:
- Actual beat period = 58594 / 117187.5 = 0.50000426 seconds
- Actual BPM = 60 / 0.50000426 = 119.999 BPM
- Error = 0.0008%

16-bit timer max = 65535
With Prescaler 1024, max period = 65535 / 117187.5 = 0.559 seconds
Min BPM = 60 / 0.559 = 107 BPM

For slower tempos (< 107 BPM), use 32-bit timer or different prescaler.

Questions:

  • What prescaler gives the best resolution for tempo fine-tuning?
  • How would you implement fractional BPM (e.g., 120.5)?
  • What’s the jitter if you update the period while the timer is running?

The Interview Questions They’ll Ask

  1. “Explain why time.sleep() causes timing drift and how hardware timers solve this.”

  2. “Calculate the prescaler and period values needed for a 90 BPM metronome with < 0.01% error.”

  3. “Your interrupt handler takes 50μs but you’re getting timing errors. What’s happening?”

  4. “How would you implement tap tempo? What statistical method handles outliers?”

  5. “The user wants to sync to external MIDI clock. How does MIDI clock work and how do you lock to it?”


Hints in Layers

Hint 1: Starting Point In CircuitPython, you can access hardware timers, but it’s limited. Consider using the pulseio or audiopwmio for timing. For true hardware timer access, you may need Arduino.

Hint 2: Tap Tempo Algorithm Store the last N tap timestamps. Calculate intervals between consecutive taps. Throw out outliers (> 2× average). Average remaining intervals. BPM = 60000 / average_interval_ms.

Hint 3: Click Sound Generation A simple click: exponentially decaying sine wave.

for i in range(441):  # 10ms at 44.1kHz
    envelope = exp(-i/100)
    sample = sin(2*pi*1000*i/44100) * envelope
    buffer[i] = int(sample * 2047 + 2048)

Hint 4: Visual Sync Trigger LED update from the same timer interrupt that plays the click. Set a flag in interrupt, process in main loop. This ensures visual and audio stay synchronized.


Books That Will Help

Topic Book Chapter
Timer peripherals SAMD51 Datasheet TC/TCC chapters
Interrupt handling “Making Embedded Systems” by Elecia White Ch. 8
Real-time systems “Real-Time Systems” by Jane Liu Ch. 1-3
Audio timing “Real-Time DSP” by Kuo & Gan Ch. 2

Common Pitfalls & Debugging

Problem Cause Fix Verification
Tempo drifts over time Using time.sleep() Use hardware timer Measure 100 beats, calculate actual BPM
Audio clicks have pops Buffer underrun Use double buffering Listen for glitches
Visual lags behind audio LED update in main loop Set flag in ISR, update immediately Record with phone, check sync
Can’t get slow tempos Timer period overflow Use larger prescaler or 32-bit timer Calculate max period
Tap tempo erratic No outlier rejection Filter extreme intervals Print all intervals

Learning Milestones

  1. Software metronome works → You understand the basic timing requirement
  2. Hardware timer configured → You’ve mastered prescaler/period calculations
  3. Audio clicks are precise → You’ve integrated DAC output with timer
  4. Tap tempo accurate → You’ve implemented statistical tempo detection
  5. Visual perfectly synced → You’ve coordinated multiple output systems

Project 6: Polyphonic Synthesizer (Arduino + PJRC Audio)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: Arduino C++
  • Alternative Programming Languages: Bare-metal C, Rust (embedded)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Digital Audio Synthesis, DSP, Real-Time Systems
  • Software or Tool: Arduino IDE, PJRC Audio Library (Adafruit fork)
  • Main Book: “The Audio Programming Book” by Richard Boulanger

What you’ll build: A 4-voice polyphonic synthesizer with multiple waveforms (sine, saw, square, triangle), ADSR envelopes, low-pass filter with resonance, and LFO modulation. Each button plays a note, multiple buttons create chords, and the accelerometer controls filter cutoff.

Why it teaches audio DSP: This project takes you inside audio synthesis. You’ll understand how digital waveforms are generated sample-by-sample, how envelopes shape sounds, and how filters modify frequency content—all in real-time at 44.1kHz.

Core challenges you’ll face:

  • Voice allocation → Managing which oscillator plays which note when only 4 voices are available
  • ADSR envelope → Understanding Attack-Decay-Sustain-Release for musical shaping
  • Filter implementation → Understanding how low-pass filters work mathematically
  • Real-time constraints → Computing audio samples fast enough (< 22.6μs per sample)
  • Audio library architecture → Understanding the node-graph model of audio processing

Key Concepts:

  • Digital oscillators: “The Audio Programming Book” Ch. 1-2 - Boulanger
  • Envelope generators: “Designing Sound” Ch. 9 - Farnell
  • Filter theory: “Introduction to Digital Filters” by Julius O. Smith III (online)
  • PJRC Audio: Audio System Design Tool documentation

Difficulty: Advanced Time estimate: 2-3 weeks (20-30 hours) Prerequisites: Projects 1-5 completed, basic understanding of sound waves and frequency


Real World Outcome

Your NeoTrellis becomes a real musical instrument:

                    Synthesizer Architecture

    ┌──────────────────────────────────────────────────────────────────┐
    │                         Audio Graph                              │
    │                                                                  │
    │  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐      │
    │  │ Voice 1  │   │ Voice 2  │   │ Voice 3  │   │ Voice 4  │      │
    │  │┌────────┐│   │┌────────┐│   │┌────────┐│   │┌────────┐│      │
    │  ││  OSC   ││   ││  OSC   ││   ││  OSC   ││   ││  OSC   ││      │
    │  │└───┬────┘│   │└───┬────┘│   │└───┬────┘│   │└───┬────┘│      │
    │  │    │     │   │    │     │   │    │     │   │    │     │      │
    │  │┌───▼────┐│   │┌───▼────┐│   │┌───▼────┐│   │┌───▼────┐│      │
    │  ││  ENV   ││   ││  ENV   ││   ││  ENV   ││   ││  ENV   ││      │
    │  │└───┬────┘│   │└───┬────┘│   │└───┬────┘│   │└───┬────┘│      │
    │  └────┼─────┘   └────┼─────┘   └────┼─────┘   └────┼─────┘      │
    │       │              │              │              │             │
    │       └──────────────┴──────┬───────┴──────────────┘             │
    │                             │                                    │
    │                      ┌──────▼──────┐                             │
    │                      │    MIXER    │                             │
    │                      └──────┬──────┘                             │
    │                             │                                    │
    │     ┌────────────┐   ┌──────▼──────┐                             │
    │     │    LFO     │──▶│   FILTER    │◀── Accelerometer            │
    │     └────────────┘   └──────┬──────┘                             │
    │                             │                                    │
    │                      ┌──────▼──────┐                             │
    │                      │   OUTPUT    │──▶ Headphone Jack           │
    │                      │   (DAC)     │                             │
    │                      └─────────────┘                             │
    └──────────────────────────────────────────────────────────────────┘

**Button Layout:**
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
Row0│ C3 │ C#3│ D3 │ D#3│ E3 │ F3 │ F#3│ G3 │ ← Note triggers
    ├────┼────┼────┼────┼────┼────┼────┼────┤    (24 notes)
Row1│ G#3│ A3 │ A#3│ B3 │ C4 │ C#4│ D4 │ D#4│
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row2│ E4 │ F4 │ F#4│ G4 │ G#4│ A4 │ A#4│ B4 │
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row3│WAVE│ A  │ D  │ S  │ R  │FILT│ LFO│ OCT│ ← Controls
    └────┴────┴────┴────┴────┴────┴────┴────┘

**Serial Output:**

Audio System Design Tool Export: 4x AudioSynthWaveform (oscillators) 4x AudioEffectEnvelope (ADSR) 1x AudioMixer4 (voice mix) 1x AudioFilterStateVariable (filter) 1x AudioOutputAnalogStereo (DAC)

CPU Usage: 12.4% (at 4-voice polyphony) Memory: 8 audio blocks allocated

Note C4 (261.63 Hz) triggered → Voice 0 Envelope: Attack=50ms, Decay=100ms, Sustain=0.7, Release=300ms Note E4 (329.63 Hz) triggered → Voice 1 Note G4 (392.00 Hz) triggered → Voice 2 Playing C Major chord

Filter cutoff: 2000 Hz (tilt X = 0.3g) Filter resonance: 2.5 LFO → Filter: depth=500Hz, rate=2Hz

Note C4 released → Voice 0 entering release phase


**Audio Output**: Rich, musical tones through the headphone jack. Chords sound simultaneously.

---

## The Core Question You're Answering

> "How do computers generate sound, and how can you shape raw waveforms into musical tones?"

Every sound you hear from a computer starts as numbers—samples output 44,100 times per second. A sine wave at 440 Hz (note A4) is generated by computing `sin(2π × 440 × t)` for each sample. The PJRC Audio Library abstracts this into a node graph, but understanding what happens inside those nodes is essential.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **Digital Audio Fundamentals**
   - What is a sample? What is sample rate?
   - Why 44.1kHz? Why 16-bit? (Nyquist theorem, dynamic range)
   - What's the relationship between frequency and pitch?
   - *Book Reference:* "The Audio Programming Book" Ch. 1

2. **Waveform Generation**
   - How do you compute a sine wave sample-by-sample?
   - What's a wavetable and why use it instead of calculating?
   - How do sawtooth, square, and triangle waves differ harmonically?
   - *Book Reference:* "Designing Sound" Ch. 3-5

3. **ADSR Envelopes**
   - What happens during Attack, Decay, Sustain, Release phases?
   - How do envelope times affect musicality?
   - What's the difference between linear and exponential curves?
   - *Book Reference:* "Designing Sound" Ch. 9

4. **Filters**
   - What does a low-pass filter do to a waveform?
   - What is cutoff frequency? What is resonance (Q)?
   - How do filters create "analog synth" sounds?
   - *Book Reference:* "Introduction to Digital Filters" by Smith (online)

5. **Polyphony and Voice Allocation**
   - With N voices but M pressed keys (M > N), how do you decide what to play?
   - What's "voice stealing"? What strategies exist?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Audio Graph Architecture**
   - What objects do you need? (Oscillators, envelopes, mixers, filters)
   - How do they connect? (Output of osc → input of envelope → mixer)
   - The PJRC Audio Design Tool generates code—do you understand what it generates?

2. **Voice Management**
   - How will you track which voice is playing which note?
   - When a note is released, when does the voice become available? (After release phase!)
   - What happens if all 4 voices are active and a 5th note is pressed?

3. **Control Mapping**
   - How will buttons in row 3 control parameters?
   - Should parameters change immediately or smoothly interpolate?
   - How will the accelerometer map to filter cutoff?

---

## Thinking Exercise

### Trace Audio Sample Generation

Before coding, trace what happens for ONE sample:

Time t = 0.001 seconds (sample #44 at 44.1kHz) Voice 0 playing A4 (440 Hz)

  1. Oscillator computes: phase = (440 / 44100) * 2π * 44 = 2.7646 radians For sine: sample = sin(2.7646) = 0.371

  2. Envelope applies: We’re at t=1ms, in Attack phase (50ms total) envelope_level = t / attack_time = 0.001 / 0.050 = 0.02 sample = 0.371 * 0.02 = 0.00742

  3. Voice 0 output: 0.00742

  4. Mixer combines all 4 voices: output = (voice0 + voice1 + voice2 + voice3) / 4 If only voice 0 active: output = 0.00742 / 4 = 0.00186

  5. Filter processes: (Complex IIR calculation—for now, assume passthrough) output = 0.00186

  6. Convert to DAC value: dac_value = (output * 2047) + 2048 = 2052

  7. DAC outputs 2052, which becomes ~1.65V (halfway between 0V and 3.3V) ```

Questions:

  • Why divide by 4 in the mixer? (Prevent clipping when all voices are at full volume)
  • Why does the envelope ramp up slowly? (Prevents clicks/pops)
  • What happens if you skip the envelope? (Instantaneous starts sound unnatural)

The Interview Questions They’ll Ask

  1. “Explain how you generate a 440 Hz sine wave at 44.1kHz sample rate. Show the math.”

  2. “Your synth has 4 voices and 5 notes are pressed. Describe your voice allocation algorithm.”

  3. “What’s the difference between a low-pass and high-pass filter? How does resonance create that ‘synth’ sound?”

  4. “Your audio is ‘clicking’ at note starts. What’s causing this and how do you fix it?”

  5. “The CPU usage jumps to 80% with complex waveforms. How would you optimize?”


Hints in Layers

Hint 1: Starting Point Use the PJRC Audio System Design Tool at https://www.pjrc.com/teensy/gui/. Create your audio graph visually, then export the code. The Adafruit NeoTrellis M4 examples include a basic synth.

Hint 2: Voice Structure Create a struct for each voice:

struct Voice {
  AudioSynthWaveform* osc;
  AudioEffectEnvelope* env;
  int8_t note;      // -1 if free
  uint32_t startTime;
};

Hint 3: Voice Allocation Simple algorithm: find first voice where note == -1. If none free, steal the oldest voice (smallest startTime).

Hint 4: Frequency Calculation MIDI note to frequency: freq = 440.0 * pow(2.0, (note - 69) / 12.0)


Books That Will Help

Topic Book Chapter
Audio synthesis “The Audio Programming Book” by Boulanger Ch. 1-4
Sound design “Designing Sound” by Andy Farnell Ch. 3-9
Digital filters “Introduction to Digital Filters” by Smith Online, Ch. 1-4
Synth architecture “Welsh’s Synthesizer Cookbook” Ch. 1-5

Common Pitfalls & Debugging

Problem Cause Fix Verification
No sound Audio objects not connected Check AudioConnection Use Serial to print CPU%
Clicking at note start No envelope attack Ensure Attack > 0 Slow attack to 100ms, listen
Distorted audio Clipping (volume > 1.0) Reduce mixer levels Watch for flat-topped waveforms
Only one note plays Voice allocation bug Check voice state Print voice note assignments
Filter doesn’t change Wrong frequency units Frequency in Hz, not normalized Print cutoff value

Learning Milestones

  1. Single oscillator makes sound → You understand the audio graph
  2. ADSR shapes the tone → You understand envelope generators
  3. 4-voice polyphony works → You’ve mastered voice allocation
  4. Filter responds to accelerometer → You’ve integrated sensor control
  5. Sounds musical and playable → You’ve tuned the synthesis parameters

Project 7: 8-Step Drum Machine Sequencer (Arduino)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: Arduino C++
  • Alternative Programming Languages: CircuitPython
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Sequencing, Sample Playback, Rhythm Programming
  • Software or Tool: Arduino IDE, PJRC Audio Library
  • Main Book: “The Audio Programming Book” by Richard Boulanger

What you’ll build: An 8-step, 4-track drum sequencer where rows represent different drum sounds (kick, snare, hi-hat, clap) and columns represent beats. LEDs show current playback position, buttons toggle steps on/off. Tempo is adjustable, and patterns are saveable to flash.

Why it teaches sequencer architecture: Drum machines are fundamentally about timing and state management. You’ll understand how professional sequencers work—maintaining pattern state, triggering samples precisely on time, and managing the relationship between user interaction and audio playback.

Core challenges you’ll face:

  • Step state management → Maintaining a 4×8 grid of on/off states
  • Precise timing → Triggering samples exactly on beat, regardless of loop timing
  • Sample playback → Loading and playing drum samples from flash
  • Visual synchronization → LED playhead shows current step in sync with audio
  • Pattern storage → Saving/loading patterns to non-volatile memory

Key Concepts:

  • Step sequencing: Electronic music production fundamentals
  • Sample playback: “The Audio Programming Book” Ch. 5
  • Pattern storage: QSPI flash access on SAMD51
  • Real-time scheduling: “Making Embedded Systems” Ch. 8

Difficulty: Advanced Time estimate: 2-3 weeks (20-30 hours) Prerequisites: Project 6 completed, understanding of BPM and rhythm


Real World Outcome

Your NeoTrellis becomes a classic drum machine:

                    Drum Machine Layout

    Step:  1    2    3    4    5    6    7    8
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
Row0│ ██ │    │    │    │ ██ │    │    │    │ ← Kick   🔴
    ├────┼────┼────┼────┼────┼────┼────┼────┤         Red when active
Row1│    │    │ ██ │    │    │    │ ██ │    │ ← Snare  🔵
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row2│ ██ │ ██ │ ██ │ ██ │ ██ │ ██ │ ██ │ ██ │ ← Hi-Hat 🟡
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row3│    │    │    │ ██ │    │    │    │ ██ │ ← Clap   🟢
    └────┴────┴────┴────┴────┴────┴────┴────┘
         ↑
         Playhead (bright white column)

**Visual Feedback:**
- Step programmed: Track color (dim)
- Current step playing: White column overlay
- Step programmed + playing: Bright track color

**Serial Output:**

Drum Machine initialized Loaded 4 samples from QSPI flash: kick.wav: 4410 samples (100ms) snare.wav: 6615 samples (150ms) hihat.wav: 2205 samples (50ms) clap.wav: 3307 samples (75ms)

Pattern 1 loaded: Kick: [X . . . X . . .] Snare: [. . X . . . X .] Hi-Hat: [X X X X X X X X] Clap: [. . . X . . . X]

Playing at 120 BPM (step = 125ms) Step 1: KICK HIHAT Step 2: HIHAT Step 3: SNARE HIHAT Step 4: HIHAT CLAP Step 5: KICK HIHAT Step 6: HIHAT Step 7: SNARE HIHAT Step 8: HIHAT CLAP [Loop]


---

## The Core Question You're Answering

> "How do professional drum machines maintain precise timing while allowing real-time user interaction with the pattern?"

The challenge is that user input (button presses) is asynchronous, but drum triggers must be precisely timed to the beat. You can't delay the beat to wait for a button handler. The solution involves separating the "pattern editing" system from the "playback engine" with clear interfaces between them.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **Step Sequencing Fundamentals**
   - What's the relationship between BPM, step length, and step resolution?
   - At 120 BPM with 16th notes, how many milliseconds per step?
   - What's "swing" and how does it affect step timing?

2. **Sample Playback**
   - What's a WAV file structure?
   - How do you play a sample once (one-shot) vs looping?
   - What happens when samples overlap (same drum hit before previous ends)?
   - *Book Reference:* "The Audio Programming Book" Ch. 5

3. **Pattern State Management**
   - How do you represent a pattern? (2D array? Bitmask?)
   - How do you modify the pattern while it's playing?
   - What's the difference between "live record" and "step edit"?

4. **Flash Storage**
   - How does the 8MB QSPI flash work?
   - What filesystem is used? (LittleFS, FatFS, raw?)
   - How do you load samples at startup?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Data Structures**
   - How will you represent the pattern? `bool pattern[4][8]`? Bitmask?
   - Where are samples stored during playback? (Memory? Stream from flash?)
   - How do you handle multiple patterns?

2. **Timing Architecture**
   - What triggers the next step? (Timer interrupt? Main loop check?)
   - How do you prevent audio glitches when editing the pattern?
   - What's the latency from button press to pattern update?

3. **User Experience**
   - How does the user know which step is currently playing?
   - Can the user change tempo while playing?
   - What feedback when toggling a step on/off?

---

## Thinking Exercise

### Design the Step Timing

Before coding, calculate the timing:

BPM = 120 (beats per minute) 4/4 time signature 8 steps = 2 beats (each step is 8th note)

Step duration:

  • 1 beat = 60000ms / 120 BPM = 500ms
  • 8 steps in 2 beats = 1000ms total
  • 1 step = 1000ms / 8 = 125ms

Hardware timer approach:

  • Timer fires every 125ms
  • On interrupt:
    1. Advance step (0→1→2→…→7→0)
    2. Check pattern[track][step] for each track
    3. If true, trigger sample playback
    4. Update playhead LED

Button handling (separate from timing):

  • On button press callback:
    1. Toggle pattern[row][col]
    2. Update LED for that cell
    3. (No timing impact!) ```

Questions:

  • What happens if timer interrupt fires while button callback is running?
  • Should you protect pattern array with a mutex? (On single-core, interrupts are enough)
  • How do you prevent the playhead LED update from causing flicker?

The Interview Questions They’ll Ask

  1. “Describe your timing architecture. How do you ensure drum hits are precisely on time?”

  2. “The user wants to edit the pattern while it’s playing. How do you handle concurrent access to the pattern data?”

  3. “Your drum machine has noticeable ‘jitter’ at high tempos. What’s causing this?”

  4. “How do you handle the case where a drum sample is longer than one step, and the same drum is triggered again?”

  5. “Describe how you load and manage samples from flash. What’s the tradeoff between loading all samples to RAM vs streaming?”


Hints in Layers

Hint 1: Starting Point Use AudioPlayMemory for short samples stored in program flash, or AudioPlaySdRaw pattern for QSPI flash samples. The Audio library handles mixing automatically.

Hint 2: Pattern Representation

// Simple 2D array
bool pattern[4][8];

// Or compact bitmask (1 byte per track)
uint8_t pattern[4];  // pattern[track] & (1 << step)

Hint 3: Timer Setup Use a hardware timer for step advancement. In Arduino on SAMD51, use the TC/TCC peripherals. The Audio library uses some timers—check which are free.

Hint 4: Visual Feedback Don’t update all 32 LEDs every step—only update the previous step column (turn off playhead) and current step column (turn on playhead). This reduces flicker.


Books That Will Help

Topic Book Chapter
Drum machine design “The Audio Programming Book” Ch. 5, 12
Sample playback “Designing Sound” by Farnell Ch. 35-38
MIDI/sequencing “MIDI Systems and Control” Ch. 4-6
Real-time systems “Making Embedded Systems” Ch. 8, 10

Common Pitfalls & Debugging

Problem Cause Fix Verification
Timing drift Using millis() instead of timer Use hardware timer Record audio, check beat grid
Clicks between steps LED update blocks audio Update LEDs in main loop, not ISR Remove LED code, check audio
Samples cut off New trigger stops old Use multiple players per sound Listen for complete samples
Pattern edits delayed Editing in ISR context Set flag in ISR, process in loop Print timestamps
Flash read slow Loading samples each trigger Pre-load to RAM at startup Measure load time

Learning Milestones

  1. Single drum plays on time → You understand basic timing
  2. 4 tracks play simultaneously → You’ve mastered audio mixing
  3. Pattern editing while playing → You’ve separated state from playback
  4. Patterns save/load → You’ve implemented persistence
  5. Musical and playable → You’ve tuned the UX

Project 8: Audio Spectrum Analyzer / FFT Visualizer (Arduino)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: Arduino C++
  • Alternative Programming Languages: Bare-metal C (with CMSIS-DSP)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: FFT, Signal Processing, Audio Analysis
  • Software or Tool: Arduino IDE, PJRC Audio Library (FFT objects)
  • Main Book: “Understanding Digital Signal Processing” by Richard Lyons

What you’ll build: A real-time audio spectrum analyzer that captures audio from the onboard microphone, performs FFT analysis, and displays frequency bins on the LED grid. The 8 columns represent frequency bands from bass to treble, and rows show intensity. Watch music “come alive” on the display.

Why it teaches FFT and spectral analysis: The Fast Fourier Transform is one of the most important algorithms in signal processing. This project makes the abstract concept concrete—you’ll see how time-domain audio (amplitude over time) transforms into frequency-domain (energy at each frequency).

Core challenges you’ll face:

  • ADC configuration → Capturing audio from the MAX4466 mic preamp
  • FFT windowing → Understanding why windowing is necessary
  • Bin mapping → Mapping 256 or 1024 FFT bins to 8 display columns
  • Logarithmic scaling → Human hearing is logarithmic, not linear
  • Temporal smoothing → Preventing flickery display from noisy FFT output

Key Concepts:

  • Discrete Fourier Transform: “Understanding DSP” Ch. 3-4 - Lyons
  • Windowing functions: “DSP First” Ch. 7 - McClellan
  • Logarithmic perception: Psychoacoustics basics
  • Real-time FFT: ARM CMSIS-DSP library

Difficulty: Advanced Time estimate: 2 weeks (15-25 hours) Prerequisites: Project 6 completed, basic understanding of frequency/waveforms


Real World Outcome

Your NeoTrellis becomes a visual audio analyzer:

                    Spectrum Analyzer Display

    Freq: ~60Hz │ 150Hz │ 350Hz │ 800Hz │ 2kHz │ 4kHz │ 8kHz │ 16kHz
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
Row0│    │    │    │    │    │    │    │    │ ← Highest intensity
    ├────┼────┼────┼────┼────┼────┼────┼────┤    (loudest)
Row1│    │    │    │    │    │    │    │    │
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row2│ ▓▓ │    │ ██ │ ▓▓ │    │    │    │    │
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row3│ ██ │ ▓▓ │ ██ │ ██ │ ▓▓ │ ░░ │    │    │ ← Lowest intensity
    └────┴────┴────┴────┴────┴────┴────┴────┘
    Bass ◄────────────────────────────────► Treble

**Example with Music Playing:**

Bass-heavy track (hip-hop/EDM):
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
    │ ██ │ ██ │ ▓▓ │    │    │    │    │    │  ← Bass pumping
    │ ██ │ ██ │ ██ │ ▓▓ │    │    │    │    │
    │ ██ │ ██ │ ██ │ ██ │ ▓▓ │ ░░ │    │    │
    │ ██ │ ██ │ ██ │ ██ │ ██ │ ▓▓ │ ░░ │    │
    └────┴────┴────┴────┴────┴────┴────┴────┘

Voice/Podcast:
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
    │    │    │    │ ▓▓ │ ▓▓ │    │    │    │  ← Voice fundamentals
    │    │    │ ░░ │ ██ │ ██ │ ▓▓ │    │    │     (200Hz-2kHz)
    │    │    │ ▓▓ │ ██ │ ██ │ ██ │ ░░ │    │
    │    │ ░░ │ ██ │ ██ │ ██ │ ██ │ ▓▓ │ ░░ │
    └────┴────┴────┴────┴────┴────┴────┴────┘

**Serial Output:**

FFT Spectrum Analyzer ADC configured: 44100 Hz sample rate FFT size: 256 bins Window function: Hanning

Frequency bins: Col 0: 0- 172 Hz (bass) Col 1: 172- 344 Hz Col 2: 344- 689 Hz Col 3: 689- 1378 Hz Col 4: 1378- 2756 Hz Col 5: 2756- 5512 Hz Col 6: 5512-11025 Hz Col 7: 11025-22050 Hz (treble)

Live readings (dB): Bass: -12dB | Lo-Mid: -18dB | Hi-Mid: -24dB | Treble: -36dB


---

## The Core Question You're Answering

> "How do you decompose a complex audio signal into its constituent frequencies, and why does this reveal the 'content' of sound?"

The Fourier Transform reveals a fundamental truth: any signal can be represented as a sum of sine waves at different frequencies and amplitudes. When you hear a chord, you're hearing multiple frequencies simultaneously. The FFT decomposes this, showing you exactly which frequencies are present and how loud each is.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **Fourier Transform Basics**
   - What does the Fourier Transform do mathematically?
   - What's the relationship between time domain and frequency domain?
   - Why does a square wave contain odd harmonics?
   - *Book Reference:* "Understanding DSP" Ch. 3

2. **FFT Mechanics**
   - Why is FFT faster than DFT? (O(N log N) vs O(N²))
   - What's the relationship between FFT size and frequency resolution?
   - What happens if your FFT size is 256? (You get 128 useful frequency bins)
   - *Book Reference:* "Understanding DSP" Ch. 4

3. **Windowing**
   - What's spectral leakage and why does it happen?
   - How does a Hanning window help? What about Blackman?
   - What's the tradeoff between frequency resolution and spectral leakage?
   - *Book Reference:* "DSP First" Ch. 7

4. **Logarithmic Frequency Perception**
   - Why does each octave double in frequency? (C4=262Hz, C5=523Hz)
   - Why should frequency bin mapping be logarithmic for display?
   - What's the mel scale?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **FFT Configuration**
   - What FFT size? (Larger = better frequency resolution, worse time resolution)
   - What window function? (Hanning is a good default)
   - How often to compute FFT? (Every N samples = N/sample_rate seconds)

2. **Bin to Column Mapping**
   - 256-point FFT gives 128 bins (0-22050 Hz). How do you map to 8 columns?
   - Linear mapping (16 bins per column) or logarithmic (more bass detail)?
   - How do you average multiple bins into one column?

3. **Intensity to Row Mapping**
   - FFT output is magnitude (or magnitude squared). What's a reasonable dB range?
   - -60dB (quiet) to 0dB (loud)? How does that map to 4 rows?
   - Should you use peak hold? Decay? Averaging?

---

## Thinking Exercise

### Calculate Frequency Bins

Before coding, work out the math:

Given:

  • Sample rate: 44100 Hz
  • FFT size: 256
  • Useful bins: 128 (bins 0-127, the rest are mirror)

Frequency resolution:

  • Each bin represents: 44100 / 256 = 172.27 Hz

Bin 0: 0 Hz (DC offset) Bin 1: 172 Hz Bin 2: 344 Hz … Bin 127: 21,879 Hz

Logarithmic mapping to 8 columns:

  • Bass (0): 20 Hz - 80 Hz → bins 0-0 (but we skip DC)
  • (1): 80 Hz - 300 Hz → bins 1-2
  • (2): 300 Hz - 700 Hz → bins 2-4
  • (3): 700 Hz - 1.5 kHz → bins 4-9
  • (4): 1.5 kHz - 3 kHz → bins 9-17
  • (5): 3 kHz - 6 kHz → bins 17-35
  • (6): 6 kHz - 12 kHz → bins 35-70
  • Treble (7): 12 kHz - 22 kHz → bins 70-127

For each column, average the magnitudes of all bins in that range.


*Questions:*
- Why is the bass range (20-80 Hz) only ~1 bin? (Frequency resolution limitation)
- How would you get better bass resolution? (Larger FFT size)
- What happens if you display raw magnitude instead of dB? (Bass overwhelms display)

---

## The Interview Questions They'll Ask

1. "Explain the relationship between FFT size, sample rate, and frequency resolution."

2. "Why do you apply a window function before computing the FFT? What happens if you don't?"

3. "Your spectrum analyzer shows significant energy at 0 Hz. What's causing this and how do you fix it?"

4. "The display is flickering rapidly. How do you smooth the output while maintaining responsiveness?"

5. "How would you detect a specific note (like A4 = 440 Hz) from the FFT output?"

---

## Hints in Layers

**Hint 1: Starting Point**
Use `AudioAnalyzeFFT256` from the PJRC Audio library. It handles FFT computation internally. Call `fft.read(bin_number)` to get bin magnitudes (0.0 to 1.0).

**Hint 2: Logarithmic Mapping**
```cpp
// Octave-based bands (each doubles in frequency)
int bandBins[8][2] = {
  {1, 2},    // ~60-300 Hz
  {2, 4},    // ~300-700 Hz
  {4, 8},    // ~700-1400 Hz
  {8, 16},   // ~1.4-2.8 kHz
  {16, 32},  // ~2.8-5.5 kHz
  {32, 64},  // ~5.5-11 kHz
  {64, 96},  // ~11-16 kHz
  {96, 128}  // ~16-22 kHz
};

Hint 3: dB Conversion

float magnitude = fft.read(bin);
float dB = 20 * log10(magnitude + 0.00001);  // Avoid log(0)
// Map -60dB...0dB to 0...4 rows
int row = map(dB, -60, 0, 0, 4);

Hint 4: Temporal Smoothing

// Exponential moving average
smoothed[col] = 0.8 * smoothed[col] + 0.2 * current[col];

Books That Will Help

Topic Book Chapter
FFT fundamentals “Understanding DSP” by Richard Lyons Ch. 3-4
Windowing “DSP First” by McClellan et al. Ch. 7
Audio analysis “The Audio Programming Book” Ch. 6
Spectral display “Real-Time DSP” by Kuo & Gan Ch. 5

Common Pitfalls & Debugging

Problem Cause Fix Verification
No signal ADC not configured Check AudioInputAnalog setup Print raw ADC values
DC spike (bin 0 always high) DC offset in signal Remove DC bias or ignore bin 0 Play pure tone, check bins
Spectral leakage No windowing Enable Hanning window Compare with/without window
Display flickers No smoothing Add exponential average Slow update rate, check
Bass always low Linear bin mapping Use logarithmic mapping Play bass-heavy music

Learning Milestones

  1. FFT output visible → You understand the audio graph
  2. Bins map to columns → You understand frequency resolution
  3. Logarithmic scaling works → You understand human hearing
  4. Display is smooth → You’ve implemented temporal filtering
  5. Reacts musically → You’ve tuned sensitivity and mapping

Project 9: Sample Player with Live Effects (Arduino)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: Arduino C++
  • Alternative Programming Languages: Bare-metal C with custom DSP
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Audio Effects, Real-Time DSP, User Interface Design
  • Software or Tool: Arduino IDE, PJRC Audio Library
  • Main Book: “DAFX: Digital Audio Effects” edited by Udo Zölzer

What you’ll build: A sample player that loads audio clips from flash, triggers them with buttons, and applies real-time effects: delay, reverb, bitcrusher, and filter. Multiple effect parameters are controllable via button combinations and accelerometer.

Why it teaches audio effects processing: Digital audio effects are the foundation of modern music production. You’ll understand how delay lines work, what convolution reverb actually does, and how bit reduction creates “lo-fi” textures—all implemented in real-time.

Core challenges you’ll face:

  • Sample loading → Loading WAV files from 8MB QSPI flash
  • Delay implementation → Understanding circular buffers and delay lines
  • Reverb basics → Multiple delay taps with feedback create space
  • Bitcrusher → Sample rate and bit depth reduction for effect
  • Parameter mapping → Intuitive control of multiple parameters

Key Concepts:

  • Delay effects: “DAFX” Ch. 2 - Delay-based effects
  • Reverb algorithms: “DAFX” Ch. 5 - Reverberation
  • Distortion/bitcrush: “DAFX” Ch. 4 - Nonlinear processing
  • Audio buffer management: Real-time DSP patterns

Difficulty: Advanced Time estimate: 2-3 weeks (20-30 hours) Prerequisites: Projects 6-7 completed


Real World Outcome

Your NeoTrellis becomes an effects-laden sampler:

                    Sample Player Layout

    ┌────┬────┬────┬────┬────┬────┬────┬────┐
Row0│ S1 │ S2 │ S3 │ S4 │ S5 │ S6 │ S7 │ S8 │ ← Sample triggers
    ├────┼────┼────┼────┼────┼────┼────┼────┤    (8 samples)
Row1│DEL+│DEL-│REV+│REV-│BIT+│BIT-│FLT+│FLT-│ ← Effect controls
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row2│SYNC│HALF│REV │CHOP│ LP │ HP │ BP │ BY │ ← Effect modes
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row3│ PAT1│PAT2│PAT3│PAT4│SAVE│LOAD│ << │ >> │ ← Pattern management
    └────┴────┴────┴────┴────┴────┴────┴────┘

**Effect Chain:**
    ┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
    │ Sample │──▶│ Filter │──▶│BitCrush│──▶│ Delay  │──▶│ Reverb │──▶ Out
    │ Player │   │ LP/HP  │   │        │   │ Sync   │   │        │
    └────────┘   └────────┘   └────────┘   └────────┘   └────────┘
                     ↑            ↑            ↑            ↑
                 Tilt Y       Button       Tempo       Tilt X

**Serial Output:**

Sample Player initialized Loaded 8 samples from QSPI flash: S1: kick.wav (4410 samples, 100ms) S2: snare.wav (6615 samples, 150ms) S3: chord.wav (44100 samples, 1000ms) S4: vocal.wav (88200 samples, 2000ms) …

Effect chain configured: [Filter] Mode: LowPass, Cutoff: 5000Hz, Resonance: 1.5 [BitCrush] Bits: 12, Sample Rate: 44100 (off) [Delay] Time: 250ms, Feedback: 40%, Mix: 30% [Reverb] Size: 0.7, Damping: 0.3, Mix: 20%

Playing S3 with effects: Filter sweeping (accelerometer Y) Delay synced to 125ms (120 BPM / 2) Reverb tail: 2.1 seconds


---

## The Core Question You're Answering

> "How do audio effects transform sound, and what are the fundamental building blocks of delay, reverb, and distortion?"

Most audio effects can be understood as combinations of basic operations: delay lines (storing past samples), feedback (mixing output back to input), filtering (changing frequency content), and nonlinear processing (waveshaping, clipping, quantization).

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **Delay Lines**
   - What is a circular buffer and why use it for audio?
   - How does feedback create echoes?
   - What's the relationship between delay time and buffer size?
   - *Book Reference:* "DAFX" Ch. 2

2. **Reverb Basics**
   - How do multiple delay taps create the impression of space?
   - What's the difference between early reflections and reverb tail?
   - What does "damping" mean in reverb context?
   - *Book Reference:* "DAFX" Ch. 5

3. **Bitcrushing**
   - What happens when you reduce bit depth? (Quantization noise)
   - What happens when you reduce sample rate? (Aliasing)
   - Why does this create "lo-fi" character?
   - *Book Reference:* "DAFX" Ch. 4

4. **Filters in Audio**
   - How does a low-pass filter change timbre?
   - What's filter resonance and why does it create emphasis?
   - How do you sweep a filter in real-time?
   - *Book Reference:* "Introduction to Digital Filters" by Smith

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Effect Order**
   - Does filter before delay sound different than delay before filter?
   - Where should bitcrusher go? (Usually early—affects subsequent processing)
   - Should reverb always be last? (Usually yes—reverb on dry signal)

2. **Control Mapping**
   - How do you make effect control intuitive?
   - Should accelerometer control be global or per-effect?
   - How do you indicate current effect settings visually?

3. **Real-Time Constraints**
   - Can you run all effects simultaneously without dropout?
   - What's the CPU budget for effects vs. LED updates?
   - How do you prioritize if CPU is overloaded?

---

## Thinking Exercise

### Design a Delay Effect

Before coding, trace how a 250ms delay with 50% feedback works:

Sample rate: 44100 Hz Delay time: 250ms Buffer size: 44100 * 0.250 = 11025 samples

Time t=0: Input sample S0 arrives Buffer[0] = S0 Output = S0 + Buffer[11025] (initially 0)

Time t=1/44100: Input sample S1 Buffer[1] = S1 Output = S1 + Buffer[11026]

Time t=250ms: Input sample S11025 Buffer[11025] = S11025 + 0.5 * Output_from_250ms_ago = S11025 + 0.5 * S0 Output = S11025 + Buffer[0] = S11025 + S0 (the delayed signal!)

Time t=500ms: Buffer[0] now contains S0 * 0.5 (feedback) Output includes S0 * 0.5 (second echo, quieter)

Decay pattern: 0ms: Original signal (100%) 250ms: First echo (50%) 500ms: Second echo (25%) 750ms: Third echo (12.5%) …


*Questions:*
- What feedback percentage creates infinite echo? (100%)
- What happens if feedback > 100%? (Runaway feedback, clipping)
- How do you sync delay time to BPM? (delay_ms = 60000 / BPM / subdivision)

---

## The Interview Questions They'll Ask

1. "Explain how a circular buffer implements a delay line. Show the read/write pointer relationship."

2. "Your reverb sounds 'metallic'. What's causing this and how would you fix it?"

3. "How do you implement tempo-synced delay? What happens when tempo changes while delay is active?"

4. "The bitcrusher creates aliasing artifacts. Is this desirable? How would you eliminate them if not?"

5. "Your effects chain is using 95% CPU. How do you optimize without removing effects?"

---

## Hints in Layers

**Hint 1: Starting Point**
Use `AudioEffectDelay` and `AudioEffectReverb` from PJRC library. They handle the DSP internally. Use `AudioEffectBitcrusher` for lo-fi effects (if available, or use `AudioEffectWaveshaper` for similar effect).

**Hint 2: Delay Control**
```cpp
delay1.delay(0, delayTime);  // Channel 0, time in ms
// For feedback, route delay output back to input:
// mix -> delay -> mix (one channel is feedback path)

Hint 3: Filter Sweep

// Map accelerometer (-10 to +10) to frequency (100 to 10000)
float freq = map(accel_y * 100, -1000, 1000, 100, 10000);
filter1.frequency(freq);
filter1.resonance(2.0);  // Slight emphasis

Hint 4: CPU Monitoring

Serial.print("CPU: ");
Serial.print(AudioProcessorUsage());
Serial.println("%");
// If > 90%, effects may glitch

Books That Will Help

Topic Book Chapter
Delay effects “DAFX” edited by Zölzer Ch. 2
Reverb “DAFX” Ch. 5
Distortion/crush “DAFX” Ch. 4
Effect design “Designing Audio Effect Plugins in C++” Ch. 1-6

Common Pitfalls & Debugging

Problem Cause Fix Verification
Audio dropout CPU overload Reduce effect complexity Check AudioProcessorUsage()
Metallic reverb Feedback too high Reduce reverb time or damping Compare to commercial reverb
Delay runaway Feedback ≥ 100% Cap feedback at 95% max Listen for escalating volume
Filter clicks Sudden parameter change Interpolate parameter changes Slow down sweep, listen
Sample pops Loading while playing Double buffer sample loading Pre-load all samples

Learning Milestones

  1. Samples play cleanly → You understand audio playback
  2. One effect working → You understand audio routing
  3. Multiple effects chain → You’ve mastered audio graph connections
  4. Real-time control works → You’ve integrated sensors with effects
  5. Musical and playable → You’ve tuned parameters for usability

Project 10: Capacitive Touch Theremin (Arduino + Hardware Mod)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: Arduino C++
  • Alternative Programming Languages: Bare-metal C
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Capacitive Sensing, Audio Synthesis, Human Interface
  • Software or Tool: Arduino IDE, External wire/foil for antenna
  • Main Book: “Making Embedded Systems” by Elecia White

What you’ll build: A theremin-style instrument using the NeoTrellis M4’s GPIO for capacitive sensing. Moving your hand near a wire “antenna” controls pitch; a second antenna controls volume. Visual feedback on LEDs shows the current note and amplitude. Pure audio synthesis creates the classic theremin warble.

Why it teaches capacitive sensing and continuous control: Buttons are binary (on/off), but capacitive sensing provides continuous analog input based on proximity. This project bridges discrete digital systems with continuous real-world interaction—a fundamental embedded systems skill.

Core challenges you’ll face:

  • Capacitive sensing → Using GPIO and timing to detect hand proximity
  • Calibration → Accounting for environmental variations
  • Continuous pitch control → Mapping noisy sensor data to musical pitch
  • Vibrato effect → The classic theremin sound requires subtle frequency modulation
  • Latency minimization → Theremin requires near-zero latency for playability

Key Concepts:

  • Capacitive touch sensing: RC timing measurement
  • Analog-to-digital considerations: “Making Embedded Systems” Ch. 7
  • Continuous controllers: “Designing Sound” - continuous input
  • Theremin design: Historical instrument design

Difficulty: Advanced Time estimate: 2-3 weeks (20-30 hours) Prerequisites: Projects 6-7 completed, soldering skills for antenna attachment


Real World Outcome

Your NeoTrellis becomes a touchless musical instrument:

                    Theremin Setup

                      Pitch Antenna
                           │
                           │ (wire ~20cm)
                           │
    ┌──────────────────────┼─────────────────────────────────┐
    │                      │                                 │
    │  [NeoTrellis M4] ────┴──── GPIO Pin (with antenna)     │
    │                                                        │
    │        ●────────────────── Volume Antenna (optional)   │
    │                                                        │
    │  Hand approaches ──▶ Capacitance increases ──▶ Pitch ↑ │
    └────────────────────────────────────────────────────────┘


                    Visual Feedback Display

    ┌────┬────┬────┬────┬────┬────┬────┬────┐
Row0│ C  │ C# │ D  │ D# │ E  │ F  │ F# │ G  │ ← Current note indicator
    ├────┼────┼────┼────┼────┼────┼────┼────┤    (bright = current pitch)
Row1│ G# │ A  │ A# │ B  │ C  │ C# │ D  │ D# │
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row2│ ▓  │ ▓  │ ▓  │ ▓  │ ▓  │ ▓  │ ░  │    │ ← Volume meter
    ├────┼────┼────┼────┼────┼────┼────┼────┤
Row3│CAL │SCAL│OCT-│OCT+│VIB-│VIB+│HOLD│MUTE│ ← Controls
    └────┴────┴────┴────┴────┴────┴────┴────┘

**Serial Output:**

Theremin initialized Capacitive sensing on GPIO A0 (pitch), A1 (volume)

Calibrating… (remove hands from antennas) Baseline pitch: 1523 (raw count) Baseline volume: 1489 (raw count) Calibration complete.

Range: C3 (130.81 Hz) to C5 (523.25 Hz) Vibrato: Rate=5Hz, Depth=10 cents

Live readings: Raw pitch: 1687 → Distance: 12cm → Note: G4 (392 Hz) Raw volume: 1601 → Amplitude: 0.67 Output frequency: 392.0 Hz + vibrato


**Audio output**: Continuous, ethereal theremin tones through headphones.

---

## The Core Question You're Answering

> "How can you detect proximity without physical contact, and how do you turn noisy analog measurements into precise musical pitch?"

Capacitive sensing exploits a fundamental physics principle: your body has capacitance. When you move your hand near a conductor, you change the capacitance of the system. By measuring how long it takes to charge/discharge that capacitance, you can detect proximity. The challenge is turning these noisy measurements into stable, musical pitch.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **Capacitive Sensing Basics**
   - What is capacitance? (Ability to store charge)
   - How does proximity change capacitance?
   - What's the RC time constant and why does it matter?

2. **Measurement Techniques**
   - How do you measure capacitance with a digital GPIO?
   - What's the "charge transfer" method?
   - How does environmental noise affect readings?

3. **Calibration**
   - Why is baseline calibration necessary?
   - What environmental factors affect capacitance? (Humidity, nearby objects)
   - How do you recalibrate dynamically?

4. **Pitch Mapping**
   - Linear mapping vs logarithmic for pitch?
   - How do you quantize to musical notes (if desired)?
   - What's "portamento" and how do you implement it?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Hardware Setup**
   - What GPIO pins can you use? (Check NeoTrellis M4 available pins)
   - How long should the antenna wire be? (Longer = more sensitive but noisier)
   - How do you isolate pitch and volume antennas?

2. **Signal Processing**
   - Raw readings will be noisy. How do you smooth them?
   - How fast can you sample? (Speed vs noise tradeoff)
   - What's acceptable latency for theremin playing? (< 10ms ideal)

3. **Musical Design**
   - What pitch range? (2 octaves is typical)
   - Continuous pitch or quantized to scale?
   - How strong should vibrato be?

---

## Thinking Exercise

### Design the Sensing Circuit

Before coding, understand the measurement:

Capacitive Sensing via Charge Time

  VCC (3.3V)
   │
   R (1MΩ resistor)
   │
   ├───── GPIO Pin (measures voltage)
   │
   C (capacitance: your hand + antenna)
   │
  GND

Charge cycle:

  1. Set pin to OUTPUT LOW (discharge C)
  2. Set pin to INPUT (high impedance)
  3. Wait for external pullup through R
  4. Count time until pin reads HIGH

Time constant: τ = R × C Time to reach ~63% of VCC: t = τ = R × C

Example: R = 1MΩ C (no hand) = 10pF → τ = 10μs C (hand near) = 50pF → τ = 50μs

By measuring the charge time, we measure capacitance, which varies with hand proximity!


*Questions:*
- What happens if the resistor is too small? (Charges too fast to measure)
- What happens if too large? (Too slow, affects update rate)
- How do you handle readings that vary wildly? (Filtering)

---

## The Interview Questions They'll Ask

1. "Explain how you measure capacitance using only a digital GPIO pin."

2. "Your theremin pitch is unstable. What filtering would you apply?"

3. "The readings drift over time. What's causing this and how do you compensate?"

4. "How would you implement pitch quantization to a specific musical scale?"

5. "Compare capacitive sensing to other proximity detection methods (IR, ultrasonic). What are the tradeoffs?"

---

## Hints in Layers

**Hint 1: Starting Point**
Use the Arduino `CapacitiveSensor` library or implement your own charge-time measurement. On SAMD51, use `PTC` (Peripheral Touch Controller) if available in your setup.

**Hint 2: Basic Measurement**
```cpp
// Simple charge time measurement
long measureCapacitance(int pin) {
    long count = 0;
    pinMode(pin, OUTPUT);
    digitalWrite(pin, LOW);  // Discharge
    delayMicroseconds(10);
    pinMode(pin, INPUT);     // Let charge
    while (!digitalRead(pin) && count < 10000) {
        count++;
    }
    return count;
}

Hint 3: Filtering

// Running average over last N readings
#define N 8
long readings[N];
int index = 0;

long getFiltered(int pin) {
    readings[index] = measureCapacitance(pin);
    index = (index + 1) % N;
    long sum = 0;
    for (int i = 0; i < N; i++) sum += readings[i];
    return sum / N;
}

Hint 4: Pitch Mapping

// Map filtered reading to frequency
float baseline = 1500;  // Calibrated
float maxReading = 3000;  // Hand touching
float minFreq = 130.81;   // C3
float maxFreq = 523.25;   // C5

float freq = map(filtered, baseline, maxReading, minFreq, maxFreq);
freq = constrain(freq, minFreq, maxFreq);

Books That Will Help

Topic Book Chapter
Capacitive sensing “Making Embedded Systems” by White Ch. 7
Sensor interfacing “Sensors and Signal Conditioning” Ch. 5
Theremin design “Electronic Music” by Collins Ch. 8
Audio synthesis “The Audio Programming Book” Ch. 1-2

Common Pitfalls & Debugging

Problem Cause Fix Verification
No change with hand Antenna too short Use longer wire (15-30cm) Print raw readings
Very noisy readings EMI interference Add shielding, better grounding Look at reading variance
Readings drift Temperature/humidity change Implement auto-calibration Track baseline over time
Pitch jumps Insufficient filtering Increase filter window Graph filtered readings
High latency Filter too aggressive Reduce filter window Measure response time

Learning Milestones

  1. Raw capacitance readings vary with hand → Hardware works
  2. Filtered readings are stable → Signal processing works
  3. Pitch tracks hand smoothly → Mapping and calibration work
  4. Audio output sounds like theremin → Synthesis and vibrato work
  5. Playable musical instrument → Full system integration complete

Bare-Metal C Programming Projects (11-15)

These projects remove all abstractions (CircuitPython, Arduino) and work directly with the SAMD51 hardware. You’ll write your own startup code, linker scripts, and register-level drivers.


Project 11: Bare-Metal LED Blinker (Pure C)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: C (bare-metal)
  • Alternative Programming Languages: Assembly (ARM)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 4: Expert
  • Knowledge Area: ARM Architecture, Linker Scripts, Startup Code, GPIO Registers
  • Software or Tool: arm-none-eabi-gcc, OpenOCD, BOSSA
  • Main Book: “Bare Metal C” by Steve Oualline

What you’ll build: A completely from-scratch LED blinker—no libraries, no Arduino, no runtime. You’ll write the startup code, linker script, and directly manipulate the PORT registers to toggle the NeoPixel data pin. This is the “Hello World” of bare-metal embedded development.

Why it teaches embedded fundamentals: This project strips away every abstraction. You’ll understand what happens between power-on and main(), how the linker places code in flash, and what a “register” actually is at the memory address level.

Core challenges you’ll face:

  • Toolchain setup → Installing and configuring arm-none-eabi-gcc
  • Startup code → Initializing .data and .bss sections, setting up stack
  • Linker script → Placing vector table, code, and data at correct addresses
  • Clock configuration → Enabling and configuring the 120 MHz main clock
  • GPIO configuration → Setting pin direction, output value via PORT registers

Key Concepts:

  • ARM Cortex-M startup: “Bare Metal C” Ch. 3-4 - Oualline
  • Linker scripts: “Bare Metal C” Ch. 6 - Oualline
  • SAMD51 registers: SAMD51 Datasheet - PORT chapter
  • Memory map: “Computer Systems: A Programmer’s Perspective” Ch. 7

Difficulty: Expert Time estimate: 2 weeks (15-25 hours) Prerequisites: Strong C programming, understanding of compilation/linking, Projects 1-10 completed


Real World Outcome

You’ll have a completely standalone binary that runs on the SAMD51:

                    Bare-Metal Project Structure

    project/
    ├── Makefile                    # Build rules
    ├── linker.ld                   # Memory layout
    ├── startup.c                   # Vector table, initialization
    ├── main.c                      # Your application (LED blink)
    └── samd51.h                    # Register definitions

**Build Output:**
```bash
$ make
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -c -o startup.o startup.c
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -c -o main.o main.c
arm-none-eabi-gcc -T linker.ld -nostdlib -nostartfiles -o firmware.elf startup.o main.o
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin

Memory usage:
  text: 512 bytes
  data: 0 bytes
  bss:  4 bytes
  Total flash: 512 bytes (0.1% of 512KB)

$ bossac -p /dev/ttyACM0 -e -w -v firmware.bin
Erase flash
Write 512 bytes to flash
Verify 512 bytes in flash
Done!

Observed Result: The NeoPixel data pin (PA10 on SAMD51) toggles every ~500ms. Connect an oscilloscope or LED to verify:

  • Pin HIGH for 500ms
  • Pin LOW for 500ms
  • Repeat forever

No serial output (we haven’t implemented UART yet!)—but the hardware proves the code works.


The Core Question You’re Answering

“What EXACTLY happens between power-on and when my main() function starts executing?”

When you press the reset button:

  1. CPU loads initial stack pointer from address 0x00000000
  2. CPU loads reset vector from address 0x00000004
  3. CPU jumps to reset handler
  4. Reset handler initializes .data, clears .bss, sets up clocks
  5. Reset handler calls main()

Without startup code, there IS no main()—just undefined behavior.


Concepts You Must Understand First

Stop and research these before coding:

  1. Vector Table
    • What is the vector table and where must it be located?
    • What are the first two entries? (Initial SP, Reset handler)
    • How does the CPU find interrupt handlers?
    • Book Reference: “The Definitive Guide to ARM Cortex-M4” Ch. 3
  2. Linker Script Basics
    • What are MEMORY and SECTIONS commands?
    • What’s the difference between VMA (Virtual Memory Address) and LMA (Load Memory Address)?
    • Why does .data need to be copied from flash to RAM?
    • Book Reference: “Bare Metal C” Ch. 6
  3. Startup Code
    • Why must .bss be zeroed? (C standard says uninitialized globals = 0)
    • Why must .data be copied from flash to RAM?
    • What’s the difference between bss_start and bss_end?
    • Book Reference: “Bare Metal C” Ch. 4
  4. SAMD51 Clock System
    • What clock sources are available? (DFLL, DPLL, XOSC)
    • What’s the default clock after reset?
    • How do you configure GCLK (Generic Clock Controller)?
    • Book Reference: SAMD51 Datasheet Ch. 14-16

Questions to Guide Your Design

Before implementing, think through these:

  1. Memory Layout
    • Where does flash start? (0x00000000)
    • Where does RAM start? (0x20000000)
    • How big is flash? (512KB) How big is RAM? (192KB)
    • Where should you put the stack? (End of RAM, growing down)
  2. Startup Sequence
    • What happens if you try to use a global variable before .data is copied?
    • What happens if you call a function that uses the stack before SP is set?
    • In what order must you initialize things?
  3. GPIO Access
    • What’s the base address of the PORT peripheral?
    • What registers control pin direction (DIRSET) and output value (OUTSET/OUTCLR)?
    • How do you specify “pin 10 of port A” in register terms?

Thinking Exercise

Trace the Boot Sequence

Before coding, trace what happens at power-on:

Power-on (or reset):

1. CPU reads address 0x00000000 (first 4 bytes of flash)
   → This contains the initial stack pointer value
   → CPU loads this into SP register
   → Typically points to end of RAM: 0x20030000

2. CPU reads address 0x00000004 (next 4 bytes of flash)
   → This contains the reset handler address
   → CPU loads this into PC (Program Counter)
   → This is the address of your Reset_Handler function

3. CPU begins executing at Reset_Handler:
   Reset_Handler:
     // Copy .data from flash to RAM
     src = &__data_load__;  // In flash (LMA)
     dst = &__data_start__; // In RAM (VMA)
     while (dst < &__data_end__) {
       *dst++ = *src++;
     }

     // Zero .bss
     dst = &__bss_start__;
     while (dst < &__bss_end__) {
       *dst++ = 0;
     }

     // (Optional) Configure clocks

     // Call main
     main();

     // If main returns, hang
     while(1);

4. main() starts executing
   → At this point, global variables work correctly
   → Stack is set up
   → Clocks are at default speed (unless you configured them)

Questions:

  • What if you put the wrong value at address 0x00000000?
  • What if Reset_Handler doesn’t copy .data—what variables would be wrong?
  • Why does Reset_Handler need to be in the vector table, not just in code?

The Interview Questions They’ll Ask

  1. “Explain what happens from CPU reset to the first line of main(). Be specific about memory addresses.”

  2. “Your linker script has .data with different VMA and LMA. What does this mean and why is it necessary?”

  3. “You wrote a bare-metal project and global variables have garbage values. What went wrong?”

  4. “How would you add interrupt handling to your bare-metal project? Where does the handler address go?”

  5. “Compare the binary size of your bare-metal blinker to an Arduino Blink sketch. Why is yours smaller?”


Hints in Layers

Hint 1: Starting Point Download the SAMD51 header files from Microchip (or use the ones from ASF/START). These define register addresses. Your linker script needs MEMORY sections for FLASH and RAM.

Hint 2: Minimal Linker Script

MEMORY
{
  FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 512K
  RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 192K
}

SECTIONS
{
  .text : { *(.vectors) *(.text*) } > FLASH
  .data : { *(.data*) } > RAM AT > FLASH
  .bss : { *(.bss*) } > RAM
}

Hint 3: Minimal Vector Table

extern void Reset_Handler(void);
extern unsigned long _estack;

__attribute__((section(".vectors")))
void (*const vectors[])(void) = {
  (void (*)(void))&_estack,  // Initial SP
  Reset_Handler,              // Reset
  // ... other handlers (can be 0 for now)
};

Hint 4: GPIO Toggle

#define PORT_BASE 0x41008000
#define PORT_DIRSET (*(volatile uint32_t *)(PORT_BASE + 0x08))
#define PORT_OUTTGL (*(volatile uint32_t *)(PORT_BASE + 0x1C))

// Set PA10 as output
PORT_DIRSET = (1 << 10);

// Toggle PA10
PORT_OUTTGL = (1 << 10);

Books That Will Help

Topic Book Chapter
Bare-metal C “Bare Metal C” by Steve Oualline Ch. 1-6
ARM startup “The Definitive Guide to ARM Cortex-M4” by Yiu Ch. 3-4
Linker scripts “Linker and Loaders” by Levine Ch. 3-4
SAMD51 specifics SAMD51 Datasheet PORT, GCLK chapters

Common Pitfalls & Debugging

Problem Cause Fix Verification  
Code doesn’t run Vector table not at 0x0 Check linker script .vectors section hexdump -C firmware.bin head
Global variables wrong .data not copied Implement startup copy loop Print variable values (need UART)  
Random crashes .bss not zeroed Implement startup zero loop Static analysis  
No toggle observed Wrong pin or register Check datasheet, verify with scope Use debugger  
Build fails Missing toolchain Install arm-none-eabi-gcc arm-none-eabi-gcc –version  

Learning Milestones

  1. Toolchain works → You can compile for ARM Cortex-M4
  2. Binary flashes → Linker script places code correctly
  3. Code executes → Startup code reaches main()
  4. Pin toggles → You understand GPIO registers
  5. Correct timing → You’ve implemented delay (even busy-wait)

Project 12: Bare-Metal NeoPixel Driver (Pure C)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: C (bare-metal)
  • Alternative Programming Languages: Assembly for timing-critical parts
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 5: Master
  • Knowledge Area: DMA, SERCOM/SPI, Bit-Banging, Real-Time Constraints
  • Software or Tool: arm-none-eabi-gcc, Logic analyzer recommended
  • Main Book: “The Definitive Guide to ARM Cortex-M4” by Joseph Yiu

What you’ll build: A complete NeoPixel driver from scratch—no libraries. You’ll implement the precise timing protocol (350ns/700ns pulses) using either bit-banging with cycle counting, or SPI/SERCOM peripheral + DMA for efficient transfers.

Why it teaches real-time embedded constraints: NeoPixels require sub-microsecond timing precision. This project forces you to count CPU cycles, understand peripheral configuration, and potentially use DMA to offload timing-critical work from the CPU.

Core challenges you’ll face:

  • Timing precision → Generating 350ns and 700ns pulses at 120MHz
  • Peripheral configuration → Setting up SERCOM as SPI master
  • DMA setup → Configuring DMA to feed SPI automatically
  • Double buffering → Updating LED data while previous frame transmits
  • Interrupt handling → Knowing when transfer completes

Key Concepts:

  • NeoPixel protocol: WS2812B datasheet - timing requirements
  • SERCOM/SPI: SAMD51 Datasheet - SERCOM chapter
  • DMA controller: SAMD51 Datasheet - DMAC chapter
  • Cycle counting: ARM instruction timing

Difficulty: Master Time estimate: 3-4 weeks (25-40 hours) Prerequisites: Project 11 completed, understanding of SPI protocol


Real World Outcome

You control all 32 NeoPixels with pure C code you wrote:

                    NeoPixel Driver Architecture

    ┌─────────────────────────────────────────────────────────────────┐
    │                       Your Code                                 │
    │                                                                 │
    │   uint8_t framebuffer[32 * 3];  // 32 LEDs × 3 bytes (GRB)     │
    │                                                                 │
    │   // Set LED 5 to red                                          │
    │   framebuffer[5*3 + 0] = 0;      // Green                      │
    │   framebuffer[5*3 + 1] = 255;    // Red                        │
    │   framebuffer[5*3 + 2] = 0;      // Blue                       │
    │                                                                 │
    │   neopixel_show(framebuffer);    // Trigger DMA transfer        │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                      DMA Controller                             │
    │                                                                 │
    │   Source: framebuffer (RAM)                                     │
    │   Destination: SERCOM_SPI->DATA register                        │
    │   Count: 32 * 3 * 8 = 768 bytes (bit-expanded)                 │
    │   Trigger: SERCOM TX empty                                      │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                     SERCOM (SPI Mode)                           │
    │                                                                 │
    │   Clock: 2.4 MHz (to match NeoPixel timing)                    │
    │   MOSI: PA10 (NeoPixel data pin)                               │
    │   Encoding: Each NeoPixel bit = 3 SPI bits                     │
    │   "0" bit = 0b100 (high-low-low)                               │
    │   "1" bit = 0b110 (high-high-low)                              │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                     NeoPixel Chain                              │
    │                                                                 │
    │   → LED0 → LED1 → LED2 → ... → LED31 →                         │
    │                                                                 │
    │   Each LED grabs first 24 bits, passes rest to next            │
    └─────────────────────────────────────────────────────────────────┘

**Serial Output (via UART you implement in Project 13):**

NeoPixel driver initialized SPI clock: 2.4 MHz DMA channel: 0 Bytes per frame: 768 (32 LEDs × 24 bits × 3 SPI bits / 8)

Frame 0: All red Frame 1: All green Frame 2: All blue Frame 3: Rainbow pattern … FPS: 60 (16.7ms per frame) DMA transfer time: 320μs CPU usage during transfer: 0% (DMA handles it!)


---

## The Core Question You're Answering

> "How do you generate precise sub-microsecond timing signals without consuming 100% CPU?"

The brute-force approach (bit-banging with delays) works but blocks the CPU entirely. The elegant solution uses SPI hardware to generate the timing, with DMA to feed data automatically. Your CPU is free to compute the next frame while hardware sends the current one.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **NeoPixel Timing**
   - A "0" bit is HIGH for 350ns, LOW for 800ns
   - A "1" bit is HIGH for 700ns, LOW for 600ns
   - Total bit time: ~1.25μs
   - Reset: LOW for > 50μs between frames

2. **SPI Trick for NeoPixels**
   - SPI outputs bits at fixed rate
   - Encode each NeoPixel bit as 3 SPI bits
   - At 2.4MHz SPI, each SPI bit = 417ns
   - "0" = 0b100 = HIGH-LOW-LOW ≈ 417ns-834ns ✓
   - "1" = 0b110 = HIGH-HIGH-LOW ≈ 834ns-417ns ✓

3. **SERCOM Configuration**
   - SERCOM can be I2C, SPI, or UART
   - Must configure CTRLA, CTRLB, BAUD registers
   - Pin mux must route SERCOM to correct GPIO

4. **DMA Basics**
   - DMA moves data without CPU involvement
   - Source: RAM address (your framebuffer)
   - Destination: peripheral register (SPI data)
   - Trigger: peripheral "ready for data" signal

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Encoding Strategy**
   - 24 bits per LED × 3 SPI bits per NeoPixel bit = 72 SPI bits = 9 bytes per LED
   - 32 LEDs = 288 bytes transmitted per frame
   - Do you pre-encode or encode on-the-fly?

2. **DMA vs Bit-Bang**
   - Bit-banging: CPU blocked for entire frame transmission
   - DMA: CPU free during transmission
   - What's the frame transmission time? (32 × 24 × 1.25μs = ~960μs)

3. **Double Buffering**
   - If CPU modifies framebuffer during DMA, corruption occurs
   - Solution: two buffers, swap after DMA complete
   - How do you know DMA is done? (Interrupt or poll status)

---

## Thinking Exercise

### Calculate the SPI Encoding

Before coding, work out the bit encoding:

NeoPixel “0” bit: 350ns HIGH, 800ns LOW (total 1150ns) NeoPixel “1” bit: 700ns HIGH, 600ns LOW (total 1300ns) WS2812B tolerance: ±150ns

SPI approach: 3 SPI bits per NeoPixel bit Target total: ~1250ns per NeoPixel bit SPI bit time: 1250ns / 3 = 417ns SPI frequency: 1 / 417ns = 2.4 MHz

Encoding: NeoPixel “0” = SPI 0b100 HIGH: 417ns (within 350±150 = 200-500ns ✓) LOW: 834ns (within 800±150 = 650-950ns ✓)

NeoPixel “1” = SPI 0b110 HIGH: 834ns (within 700±150 = 550-850ns ✓) LOW: 417ns (within 600±150 = 450-750ns ✓)

Example: Send green=0x00, red=0xFF, blue=0x00 to LED 0: Green 0x00 = 0b00000000 Encoded: 100 100 100 100 100 100 100 100 = 0x924924 (3 bytes)

Red 0xFF = 0b11111111 Encoded: 110 110 110 110 110 110 110 110 = 0xDB6DB6 (3 bytes)

Blue 0x00 = 0b00000000 Encoded: same as green = 0x924924 (3 bytes)

Total 9 bytes transmitted for one LED.


*Questions:*
- Why GRB order instead of RGB? (WS2812B specification)
- What happens if SPI clock is slightly off? (Timing errors, flickering)
- How do you generate the reset pulse? (Stop SPI, wait > 50μs)

---

## The Interview Questions They'll Ask

1. "Explain the SPI encoding trick for NeoPixels. Why does it work?"

2. "Your NeoPixel driver flickers occasionally. What could cause this?"

3. "Compare bit-banging vs SPI+DMA for NeoPixels. What are the tradeoffs?"

4. "How do you ensure the CPU can update the framebuffer while DMA is sending the previous frame?"

5. "What happens if an interrupt fires during bit-banged NeoPixel transmission?"

---

## Hints in Layers

**Hint 1: Starting Point**
First get SPI transmitting anything (verify with oscilloscope). Then work on encoding. Then add DMA.

**Hint 2: SERCOM Configuration**
```c
// Enable SERCOM clock
MCLK->APBDMASK.bit.SERCOMx_ = 1;
GCLK->PCHCTRL[SERCOMx_GCLK_ID_CORE].reg = GCLK_PCHCTRL_GEN_GCLK0 | GCLK_PCHCTRL_CHEN;

// Configure SPI
SERCOMx->SPI.CTRLA.reg = SERCOM_SPI_CTRLA_MODE_SPI_MASTER |
                         SERCOM_SPI_CTRLA_DOPO(0);
SERCOMx->SPI.CTRLB.reg = SERCOM_SPI_CTRLB_RXEN;  // Enable receiver for status
SERCOMx->SPI.BAUD.reg = (F_CPU / (2 * 2400000)) - 1;  // 2.4 MHz
SERCOMx->SPI.CTRLA.bit.ENABLE = 1;

Hint 3: DMA Setup

// Configure DMA descriptor
descriptor.BTCTRL.reg = DMAC_BTCTRL_VALID | DMAC_BTCTRL_BEATSIZE_BYTE |
                        DMAC_BTCTRL_SRCINC | DMAC_BTCTRL_BLOCKACT_NOACT;
descriptor.BTCNT.reg = 288;  // Bytes to transfer
descriptor.SRCADDR.reg = (uint32_t)&encoded_buffer[288];  // End of source
descriptor.DSTADDR.reg = (uint32_t)&SERCOMx->SPI.DATA.reg;

Hint 4: Pre-encode Lookup Table

// Precompute all 256 byte encodings
// Each input byte becomes 3 output bytes
uint8_t encode_table[256][3];
// Or compute on-the-fly using bit manipulation

Books That Will Help

Topic Book Chapter
DMA fundamentals “The Definitive Guide to ARM Cortex-M4” Ch. 12
SERCOM details SAMD51 Datasheet SERCOM chapter
Real-time constraints “Making Embedded Systems” by White Ch. 8
WS2812B protocol WS2812B Datasheet Timing specs

Common Pitfalls & Debugging

Problem Cause Fix Verification
No output Pin mux wrong Check PMUX register Scope on MOSI pin
Wrong colors GRB vs RGB Check byte order Send pure red, verify
Flickering Timing off Adjust SPI baud Measure with logic analyzer
First LED wrong Reset timing Ensure > 50μs gap Time gap with scope
DMA doesn’t start Trigger not configured Check CHCTRLA.TRIGSRC Poll DMA status

Learning Milestones

  1. SPI transmits → You understand SERCOM configuration
  2. Correct encoding → You understand the timing protocol
  3. Single LED works → End-to-end data path verified
  4. All 32 LEDs work → Full frame transmission working
  5. DMA transfers → CPU offloading achieved

Project 13: Bare-Metal UART Console (Pure C)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: C (bare-metal)
  • Alternative Programming Languages: None (C is ideal)
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 4: Expert
  • Knowledge Area: UART Protocol, SERCOM, Ring Buffers, Printf Implementation
  • Software or Tool: arm-none-eabi-gcc, USB-Serial adapter or native USB CDC
  • Main Book: “Bare Metal C” by Steve Oualline

What you’ll build: A complete UART driver with transmit/receive, interrupt handling, and a minimal printf implementation. This gives you debugging capability for all future bare-metal projects.

Why it teaches serial communication: UART is the simplest communication protocol—no clock line, just TX and RX. Understanding it thoroughly prepares you for more complex protocols and gives you essential debugging capability.

Core challenges you’ll face:

  • Baud rate calculation → Setting BAUD register for exact timing
  • SERCOM as UART → Different configuration than SPI mode
  • Interrupt-driven receive → Non-blocking character reception
  • Ring buffer implementation → Efficient producer-consumer queue
  • Printf implementation → Variadic functions and string formatting

Key Concepts:

  • UART protocol: Start bit, data bits, stop bit
  • SERCOM UART mode: SAMD51 Datasheet - SERCOM chapter
  • Ring buffers: “Algorithms in C” - circular queue
  • Variadic functions: C stdarg.h

Difficulty: Expert Time estimate: 2 weeks (15-20 hours) Prerequisites: Project 11 completed


Real World Outcome

You have printf-style debugging for all bare-metal projects:

                    UART Driver Architecture

    ┌─────────────────────────────────────────────────────────────────┐
    │                      Application Code                           │
    │                                                                 │
    │   uart_printf("LED %d: R=%d G=%d B=%d\n", led, r, g, b);       │
    │                                                                 │
    │   while (1) {                                                   │
    │     if (uart_available()) {                                    │
    │       char c = uart_getc();                                    │
    │       // Process command                                        │
    │     }                                                           │
    │   }                                                             │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                     UART Driver Layer                           │
    │                                                                 │
    │   TX Ring Buffer     ┌──────────┐     RX Ring Buffer           │
    │   [  |  |  |  |  ]◄──│ SERCOM   │──►[  |  |  |  |  ]          │
    │     ▲                │  UART    │                ▲              │
    │     │                │  Mode    │                │              │
    │   uart_putc()        └──────────┘           RX Interrupt        │
    │                           │                                     │
    │                           ▼                                     │
    │                      TX/RX Pins                                 │
    └─────────────────────────────────────────────────────────────────┘

**Terminal Output:**

=== NeoTrellis M4 Bare-Metal Console === Clock: 120 MHz UART: 115200 baud, 8N1

help Available commands: led - Set LED color adc - Read accelerometer mem - Dump memory reset - Software reset

led 0 255 0 0 LED 0 set to R=255 G=0 B=0

adc X: -0.05g Y: 0.02g Z: 0.98g

mem 0x41008000 0x41008000: 00 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 (PORT peripheral base) ```


The Core Question You’re Answering

“How does asynchronous serial communication work without a clock signal, and how do you build a reliable driver with buffering?”

UART works because both sides agree on timing (baud rate). The start bit signals “data coming”—the receiver samples at precise intervals to read each bit. Without careful timing, bits are misread.


Concepts You Must Understand First

Stop and research these before coding:

  1. UART Frame Format
    • What’s a start bit? (Transition from IDLE to LOW)
    • What’s a stop bit? (Return to IDLE)
    • What does “8N1” mean? (8 data, No parity, 1 stop)
  2. Baud Rate
    • What’s the relationship between baud rate and bit timing?
    • At 115200 baud, how long is each bit? (1/115200 = 8.68μs)
    • How do you calculate the BAUD register value?
  3. Ring Buffers
    • Why use a ring buffer instead of blocking I/O?
    • How do head and tail pointers work?
    • What happens when the buffer is full?
  4. Printf Implementation
    • What are variadic functions? (va_list, va_start, va_arg, va_end)
    • How do you parse a format string?
    • How do you convert integers to strings?

Questions to Guide Your Design

Before implementing, think through these:

  1. Buffer Sizing
    • How big should TX and RX buffers be?
    • What happens if TX buffer fills? (Block? Drop? Error?)
    • What happens if RX buffer fills? (Overrun error)
  2. Interrupt vs Polling
    • Polling: while (!uart_rx_ready()) {} — wastes CPU
    • Interrupts: CPU notified when data arrives
    • Which for TX? Which for RX?
  3. Printf Complexity
    • Full printf is complex (%d, %x, %f, %s, %p, width, precision, flags)
    • Minimal printf: just %d, %x, %s, %c?
    • How much code size is acceptable?

Thinking Exercise

Design the Ring Buffer

Before coding, trace operations:

Buffer size: 8 (indices 0-7)
Initially: head=0, tail=0, empty

Operation: put('A')
  buffer[head] = 'A'
  head = (head + 1) % 8 = 1
  State: buffer=[A,_,_,_,_,_,_,_], head=1, tail=0

Operation: put('B')
  buffer[1] = 'B'
  head = 2
  State: buffer=[A,B,_,_,_,_,_,_], head=2, tail=0

Operation: get() → returns 'A'
  c = buffer[tail] = buffer[0] = 'A'
  tail = (tail + 1) % 8 = 1
  State: buffer=[_,B,_,_,_,_,_,_], head=2, tail=1

Operation: put('C'), put('D'), put('E'), put('F'), put('G'), put('H')
  State: buffer=[_,B,C,D,E,F,G,H], head=0, tail=1
  (head wrapped around!)

Operation: put('I')
  Check: (head + 1) % 8 == tail?  → 1 == 1? YES!
  Buffer is FULL! Cannot put.
  (We sacrifice one slot to distinguish full from empty)

is_empty: head == tail
is_full:  (head + 1) % size == tail
count:    (head - tail + size) % size

Questions:

  • Why does the “one slot sacrificed” approach work?
  • What’s the alternative? (Keep a count variable)
  • How do you make this interrupt-safe? (Atomic operations or critical sections)

The Interview Questions They’ll Ask

  1. “Explain the UART frame format. How does the receiver know when data starts?”

  2. “Your UART is receiving garbage. What are the likely causes?”

  3. “Implement a ring buffer. How do you handle the full and empty cases?”

  4. “Your printf uses too much stack. How would you reduce it?”

  5. “How do you make your ring buffer interrupt-safe?”


Hints in Layers

Hint 1: Starting Point Get TX working first (polling). Then RX (polling). Then add interrupts. Then ring buffers. Then printf.

Hint 2: SERCOM UART Configuration

// UART mode
SERCOMx->USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE_USART_INT_CLK |
                           SERCOM_USART_CTRLA_RXPO(1) |  // RX on PAD1
                           SERCOM_USART_CTRLA_TXPO(0);   // TX on PAD0

// 8N1
SERCOMx->USART.CTRLB.reg = SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_RXEN;

// Baud = 115200
// BAUD = 65536 * (1 - 16 * (115200 / F_CPU))

Hint 3: Minimal Printf

void uart_printf(const char* fmt, ...) {
  va_list args;
  va_start(args, fmt);
  while (*fmt) {
    if (*fmt == '%') {
      fmt++;
      switch (*fmt) {
        case 'd': print_int(va_arg(args, int)); break;
        case 's': print_str(va_arg(args, char*)); break;
        // ... etc
      }
    } else {
      uart_putc(*fmt);
    }
    fmt++;
  }
  va_end(args);
}

Hint 4: Interrupt Handler

void SERCOMx_Handler(void) {
  if (SERCOMx->USART.INTFLAG.bit.RXC) {
    char c = SERCOMx->USART.DATA.reg;
    ring_buffer_put(&rx_buffer, c);
  }
}

Books That Will Help

Topic Book Chapter
UART protocol “Making Embedded Systems” by White Ch. 7
Ring buffers “Algorithms in C” by Sedgewick Ch. 4
Printf internals “The C Programming Language” by K&R Ch. 7
SERCOM UART SAMD51 Datasheet SERCOM chapter

Common Pitfalls & Debugging

Problem Cause Fix Verification
Garbage output Wrong baud rate Recalculate BAUD register Scope TX line, measure bit time
No output TX not enabled Check CTRLB.TXEN Scope TX line for any activity
Missed characters No RX interrupt Enable INTENSET.RXC Check INTFLAG in debugger
Overrun errors Buffer too small Increase buffer size Monitor INTFLAG.ERROR
Printf crashes Stack overflow Reduce buffer sizes Check SP in debugger

Learning Milestones

  1. TX character works → You understand SERCOM UART basics
  2. RX character works → Bidirectional communication established
  3. Interrupts working → Non-blocking receive implemented
  4. Printf works → String formatting operational
  5. Full console → Interactive debugging available

Project 14: Bare-Metal DAC Audio Output (Pure C)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: C (bare-metal)
  • Alternative Programming Languages: Assembly for optimization
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 5: Master
  • Knowledge Area: DAC Peripheral, DMA, Timer-Triggered Transfers, Audio Buffers
  • Software or Tool: arm-none-eabi-gcc, Headphones, Oscilloscope optional
  • Main Book: “Real-Time Digital Signal Processing” by Kuo & Gan

What you’ll build: A complete audio output system using the SAMD51’s dual 12-bit DACs, with timer-triggered DMA for sample-accurate playback. Generate tones, play samples from flash, and understand exactly how digital-to-analog conversion works.

Why it teaches real-time audio: Audio demands precise timing—samples must output at exactly the sample rate. One late sample causes audible distortion. This project teaches you to achieve sample-accurate timing using hardware peripherals, not software loops.

Core challenges you’ll face:

  • DAC configuration → Enabling and configuring the 12-bit DACs
  • Timer-triggered DMA → Automatic sample transfer at fixed rate
  • Sample rate accuracy → Achieving exactly 44100 Hz (or close)
  • Double buffering → Generating samples while playing others
  • Interrupt timing → Minimal ISR for buffer swap

Key Concepts:

  • DAC operation: SAMD51 Datasheet - DAC chapter
  • Timer-triggered DMA: SAMD51 Datasheet - DMAC chapter
  • Audio buffers: Double buffering pattern
  • Sample rate: Digital audio fundamentals

Difficulty: Master Time estimate: 3-4 weeks (25-40 hours) Prerequisites: Projects 11-13 completed, understanding of digital audio


Real World Outcome

Pure audio tones and samples from your bare-metal code:

                    Audio Pipeline Architecture

    ┌─────────────────────────────────────────────────────────────────┐
    │                      Application Code                           │
    │                                                                 │
    │   // Generate sine wave at 440 Hz                               │
    │   for (int i = 0; i < BUFFER_SIZE; i++) {                      │
    │     float t = sample_index / SAMPLE_RATE;                      │
    │     buffer[i] = (uint16_t)(sin(2*PI*440*t) * 2047 + 2048);     │
    │     sample_index++;                                             │
    │   }                                                             │
    │   audio_submit_buffer(buffer);                                  │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                     Double Buffer Manager                       │
    │                                                                 │
    │   Buffer A [████████████████]  ← Currently playing via DMA     │
    │   Buffer B [░░░░░░░░░░░░░░░░]  ← Being filled by CPU           │
    │                                                                 │
    │   On DMA complete interrupt:                                    │
    │     - Swap buffers                                              │
    │     - Signal CPU to fill new back buffer                        │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                     Timer → DMA → DAC                           │
    │                                                                 │
    │   TC3 fires every 22.676 μs (44100 Hz)                         │
    │     ↓                                                           │
    │   DMA triggered: moves one sample from buffer to DAC           │
    │     ↓                                                           │
    │   DAC0/DAC1 output analog voltage (0-3.3V)                     │
    │     ↓                                                           │
    │   Headphone jack                                                │
    └─────────────────────────────────────────────────────────────────┘

**Serial Output:**

Audio subsystem initialized DAC0: Enabled, VREF=AVCC (3.3V) DAC1: Enabled, VREF=AVCC Sample rate: 44100 Hz Timer: TC3, period=2720 (120MHz / 44100) DMA: Channel 0 → DAC0, Channel 1 → DAC1 Buffer size: 256 samples Latency: 5.8ms (256 / 44100)

Playing 440 Hz sine wave… Buffer underrun count: 0 CPU usage: 8% (for sine generation)

Playing sample from flash… Sample: piano_c4.raw, 44100 Hz, 16-bit mono Duration: 2.3 seconds


**Audio output**: Clean 440 Hz tone (A4 note) through headphones.

---

## The Core Question You're Answering

> "How do you output audio samples at precisely 44,100 Hz without using any CPU time during playback?"

The answer is the timer-triggered DMA pattern:
1. Timer fires at sample rate (44100 Hz)
2. Each timer event triggers a DMA transfer
3. DMA moves one sample from buffer to DAC
4. DAC converts to analog
5. CPU is not involved at all during playback!

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **DAC Basics**
   - How does a 12-bit DAC convert numbers to voltage?
   - What's the output range? (0 to VREF)
   - What's the settling time? (How fast can values change?)

2. **Timer-Triggered DMA**
   - What's a DMA trigger source?
   - How do you configure TC overflow as DMA trigger?
   - What's the transfer descriptor format?

3. **Double Buffering**
   - Why do you need two buffers?
   - What happens if you only have one buffer?
   - How do you synchronize CPU and DMA access?

4. **Sample Rate Timing**
   - 44100 Hz = one sample every 22.676 μs
   - At 120 MHz, that's 2721 clock cycles per sample
   - What's the closest achievable? How much error?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Timer Configuration**
   - Which timer (TC/TCC) should you use? (Audio library uses some)
   - What's the period value for 44100 Hz?
   - What's the actual sample rate achieved?

2. **DMA Descriptors**
   - For circular buffer: what's the descriptor chain?
   - How do you know when to refill the buffer?
   - Interrupt on half-complete? On complete?

3. **Buffer Size Tradeoff**
   - Larger buffer = more latency, less CPU overhead
   - Smaller buffer = less latency, more frequent interrupts
   - What's acceptable for your application?

---

## Thinking Exercise

### Design the Timer-DMA-DAC Chain

Before coding, trace one sample's journey:

Timer TC3 configuration: Mode: 16-bit counter Clock: GCLK0 (120 MHz) Period: 2720 (gives 44117.6 Hz, error = 0.04%) Event output: Overflow → DMA trigger

DMA Channel 0 configuration: Trigger: TC3 overflow Source: buffer[index] (increment after each transfer) Destination: DAC0->DATA.reg (fixed) Beat size: 16-bit (12-bit DAC, upper bits ignored) Block size: 256 samples

Sequence at time T:

  1. TC3 counter reaches 2720 → overflow event
  2. DMA sees trigger, reads buffer[current_index]
  3. DMA writes value to DAC0->DATA.reg
  4. DAC starts conversion (settling time ~1μs)
  5. DMA increments source address
  6. TC3 counter resets to 0
  7. Next sample in ~22.68μs

After 256 samples:

  • DMA block complete interrupt fires
  • ISR: swap buffers, reset DMA source to new buffer
  • CPU: start filling the old buffer with new samples
  • No audio glitch because DMA continues with new buffer ```

Questions:

  • What happens if CPU doesn’t finish filling buffer in time? (Underrun)
  • How much time does CPU have to fill 256 samples? (256 / 44100 = 5.8ms)
  • What sample rate error is audible? (Generally > 0.1% noticeable)

The Interview Questions They’ll Ask

  1. “Explain the timer-triggered DMA pattern for audio. Why is this better than software timing?”

  2. “Your audio has occasional ‘pops’. What could cause buffer underruns?”

  3. “Calculate the timer period for 48000 Hz sample rate at 120 MHz system clock.”

  4. “How do you achieve stereo output with the SAMD51’s dual DACs?”

  5. “What’s the tradeoff between buffer size and latency?”


Hints in Layers

Hint 1: Starting Point First get DAC outputting any value (no timer, no DMA). Then add timer. Then add DMA. Then add double buffering.

Hint 2: DAC Configuration

// Enable DAC clock
MCLK->APBDMASK.bit.DAC_ = 1;

// Configure DAC
DAC->CTRLA.bit.ENABLE = 0;  // Disable for config
DAC->CTRLB.reg = DAC_CTRLB_REFSEL_AVCC;  // 3.3V reference
DAC->DACCTRL[0].reg = DAC_DACCTRL_ENABLE | DAC_DACCTRL_CCTRL_CC12M;
DAC->CTRLA.bit.ENABLE = 1;

// Write sample
DAC->DATA[0].reg = sample;  // 0-4095

Hint 3: Timer for DMA Trigger

// TC3 as DMA trigger
TC3->COUNT16.CTRLA.reg = TC_CTRLA_MODE_COUNT16 | TC_CTRLA_PRESCALER_DIV1;
TC3->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;  // Match frequency mode
TC3->COUNT16.CC[0].reg = 2720;  // 44100 Hz at 120 MHz
TC3->COUNT16.EVCTRL.reg = TC_EVCTRL_OVFEO;  // Overflow event output
TC3->COUNT16.CTRLA.bit.ENABLE = 1;

Hint 4: DMA Trigger Connection

// Configure DMA to trigger on TC3 overflow
DMAC->Channel[0].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(TC3_DMAC_ID_OVF) |
                                DMAC_CHCTRLA_TRIGACT_BURST;

Books That Will Help

Topic Book Chapter
DAC fundamentals “The Art of Electronics” by Horowitz & Hill Ch. 13
Audio programming “The Audio Programming Book” Ch. 1-2
Real-time audio “Real-Time DSP” by Kuo & Gan Ch. 2
SAMD51 DAC SAMD51 Datasheet DAC chapter

Common Pitfalls & Debugging

Problem Cause Fix Verification
No output DAC not enabled Check CTRLA.ENABLE Write test value, measure voltage
Wrong frequency Timer period wrong Recalculate period Measure with scope/frequency counter
Distorted audio Buffer underrun Increase buffer size Monitor underrun counter
DC offset Wrong sample format Center samples at mid-scale (2048) Check DC voltage
DMA doesn’t trigger Event routing wrong Check EVSYS config Poll DMA status

Learning Milestones

  1. DAC outputs voltage → Basic DAC working
  2. Timer fires at 44100 Hz → Timing established
  3. DMA transfers samples → Automated playback
  4. Double buffer works → Glitch-free continuous audio
  5. Sine wave sounds clean → Full audio pipeline operational

Project 15: Bare-Metal I2C Driver for ADXL343 (Pure C)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: C (bare-metal)
  • Alternative Programming Languages: None
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 4: Expert
  • Knowledge Area: I2C Protocol, SERCOM I2C Mode, Sensor Drivers
  • Software or Tool: arm-none-eabi-gcc, Logic analyzer helpful
  • Main Book: “Making Embedded Systems” by Elecia White

What you’ll build: A complete I2C master driver and ADXL343 accelerometer driver from scratch. Read X/Y/Z acceleration values, configure data rate and range, and implement tap detection—all without any library code.

Why it teaches I2C communication: I2C is ubiquitous in embedded systems—sensors, EEPROMs, displays all use it. Understanding the protocol at the register level (start conditions, addressing, ACK/NACK) makes you capable of working with any I2C device.

Core challenges you’ll face:

  • I2C protocol implementation → Start/stop conditions, addressing
  • SERCOM as I2C master → Different from SPI/UART configuration
  • Multi-byte reads → Reading 6 bytes (X/Y/Z, each 16-bit)
  • Register access patterns → Write register address, then read data
  • Sensor configuration → Power modes, data rates, ranges

Key Concepts:

  • I2C protocol: “Making Embedded Systems” Ch. 7
  • SERCOM I2C mode: SAMD51 Datasheet - SERCOM chapter
  • ADXL343 registers: ADXL343 Datasheet
  • Two’s complement: Signed integer representation

Difficulty: Expert Time estimate: 2-3 weeks (20-30 hours) Prerequisites: Projects 11-13 completed


Real World Outcome

Direct accelerometer access from bare-metal code:

                    I2C Driver Architecture

    ┌─────────────────────────────────────────────────────────────────┐
    │                      Application Code                           │
    │                                                                 │
    │   // Initialize accelerometer                                   │
    │   adxl343_init();                                              │
    │   adxl343_set_range(ADXL343_RANGE_2G);                         │
    │                                                                 │
    │   // Read acceleration                                          │
    │   int16_t x, y, z;                                             │
    │   adxl343_read_xyz(&x, &y, &z);                                │
    │   printf("X: %d  Y: %d  Z: %d\n", x, y, z);                    │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                    ADXL343 Driver Layer                         │
    │                                                                 │
    │   uint8_t adxl343_read_reg(uint8_t reg) {                      │
    │     i2c_start();                                               │
    │     i2c_write(ADXL343_ADDR << 1);     // Write mode            │
    │     i2c_write(reg);                    // Register address     │
    │     i2c_repeated_start();                                      │
    │     i2c_write((ADXL343_ADDR << 1) | 1); // Read mode           │
    │     uint8_t data = i2c_read_nack();    // Read with NACK      │
    │     i2c_stop();                                                │
    │     return data;                                                │
    │   }                                                             │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                     I2C Driver Layer                            │
    │                                                                 │
    │   i2c_start():    SDA ↓ while SCL high                         │
    │   i2c_stop():     SDA ↑ while SCL high                         │
    │   i2c_write(b):   Shift out 8 bits, read ACK                   │
    │   i2c_read_ack(): Shift in 8 bits, send ACK                    │
    │   i2c_read_nack(): Shift in 8 bits, send NACK                  │
    └─────────────────────────┬───────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                     SERCOM Hardware                             │
    │                                                                 │
    │   SDA: PA08 (SERCOM2 PAD0)                                     │
    │   SCL: PA09 (SERCOM2 PAD1)                                     │
    │   Clock: 400 kHz (Fast mode)                                   │
    └─────────────────────────────────────────────────────────────────┘

**Serial Output:**

I2C Driver initialized SERCOM: 2 SDA: PA08, SCL: PA09 Clock: 400 kHz (Fast mode)

Scanning I2C bus… Device found at 0x1D (ADXL343 accelerometer) Device found at 0x53 (alt address)

ADXL343 initialized Device ID: 0xE5 (correct!) Data rate: 100 Hz Range: ±2g

Continuous readings: X: 5 Y: -12 Z: 256 (flat on desk) X: 128 Y: 3 Z: 223 (tilted right) X: -5 Y: 250 Z: 42 (tilted forward) TAP detected! (X-axis)


---

## The Core Question You're Answering

> "How does the I2C protocol allow multiple devices to share two wires, and how do you implement it at the register level?"

I2C uses two open-drain lines (SDA, SCL) with pull-up resistors. Every transaction starts with a START condition (SDA falls while SCL high), followed by a 7-bit address to select which device responds. Only the addressed device ACKs, all others ignore.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **I2C Electrical**
   - What's "open-drain"? Why does it matter?
   - What do pull-up resistors do?
   - What happens if two devices drive the bus?

2. **I2C Protocol**
   - What defines START and STOP conditions?
   - How does addressing work? (7-bit address + R/W bit)
   - What's ACK vs NACK? When does each occur?

3. **Register Access Pattern**
   - Write: START, addr+W, reg, data, STOP
   - Read: START, addr+W, reg, rSTART, addr+R, [read data], STOP
   - Why is repeated start needed for reads?

4. **ADXL343 Specifics**
   - What registers contain X/Y/Z data?
   - How is data formatted? (16-bit, two's complement)
   - How do you convert raw values to g?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Abstraction Layers**
   - Low level: i2c_start(), i2c_write(), i2c_read()
   - Mid level: i2c_write_reg(), i2c_read_reg()
   - High level: adxl343_read_xyz()
   - How much abstraction is appropriate?

2. **Error Handling**
   - What if no device ACKs? (Bus error)
   - What if device NACKs mid-transfer?
   - How do you recover from a hung bus?

3. **Multi-Byte Reads**
   - ADXL343 X/Y/Z are 6 bytes (DATAX0/X1, Y0/Y1, Z0/Z1)
   - How do you read all 6 efficiently? (Auto-increment)
   - Why read all 6 in one transaction? (Data consistency)

---

## Thinking Exercise

### Trace an I2C Read Transaction

Before coding, trace reading register 0x00 (DEVID) from ADXL343:

ADXL343 address: 0x1D (7-bit)

Transaction to read DEVID:

  1. START condition SDA: ─────╲___ SCL: ─────────

  2. Send address byte (0x1D « 1 | 0 = 0x3A) SDA: [0][0][1][1][1][0][1][0] ← 0x3A SCL: ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲_ After 8 bits, release SDA for ACK SDA: ____╲ (device pulls low = ACK) SCL: ____╱╲

  3. Send register address (0x00) SDA: [0][0][0][0][0][0][0][0] SCL: ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲_ SDA: _______╲ (ACK)

  4. Repeated START SDA: ────╱───╲__ SCL: ────────╱──

  5. Send address byte for read (0x3B) SDA: [0][0][1][1][1][0][1][1] ← 0x3B (bit 0 = 1 = read) SCL: … SDA: _______╲ (ACK)

  6. Read data byte Master releases SDA ADXL343 drives: [1][1][1][0][0][1][0][1] ← 0xE5 (DEVID) Master reads each bit on SCL rising edge Master sends NACK (SDA high during 9th clock) to signal end

  7. STOP condition SDA: _____╱───── SCL: ───╱─────── ```

Questions:

  • Why does the master release SDA after address byte?
  • Why NACK on the last read byte?
  • What happens if you send ACK on the last byte? (Device tries to send more)

The Interview Questions They’ll Ask

  1. “Walk me through an I2C read transaction at the bit level.”

  2. “The ADXL343 isn’t responding. How would you debug this?”

  3. “Why does I2C need pull-up resistors? What value would you use?”

  4. “How do you convert raw ADXL343 data to g? Show the math.”

  5. “What’s clock stretching in I2C and how do you handle it?”


Hints in Layers

Hint 1: Starting Point First implement I2C bus scan (try all addresses, see who ACKs). This verifies basic I2C before worrying about specific registers.

Hint 2: SERCOM I2C Configuration

// SERCOM2 as I2C master
SERCOM2->I2CM.CTRLA.reg = SERCOM_I2CM_CTRLA_MODE_I2C_MASTER |
                          SERCOM_I2CM_CTRLA_SPEED(0);  // Standard mode
SERCOM2->I2CM.BAUD.reg = ((F_CPU / (2 * 400000)) - 1);  // 400kHz
SERCOM2->I2CM.CTRLA.bit.ENABLE = 1;
// Force bus state to IDLE
SERCOM2->I2CM.STATUS.bit.BUSSTATE = 1;

Hint 3: Write Byte with ACK Check

bool i2c_write(uint8_t data) {
  SERCOM2->I2CM.DATA.reg = data;
  while (!SERCOM2->I2CM.INTFLAG.bit.MB);  // Wait for master on bus
  return !SERCOM2->I2CM.STATUS.bit.RXNACK;  // Return true if ACK received
}

Hint 4: ADXL343 Data Conversion

// Raw values are 10-bit, signed, left-justified
// In ±2g mode: 1 LSB = 3.9 mg
int16_t raw = (DATAX1 << 8) | DATAX0;
float g = raw * 0.0039;  // Convert to g

Books That Will Help

Topic Book Chapter
I2C protocol “Making Embedded Systems” by White Ch. 7
SERCOM I2C SAMD51 Datasheet SERCOM chapter
ADXL343 ADXL343 Datasheet All
Sensor interfacing “Sensors and Signal Conditioning” Ch. 4

Common Pitfalls & Debugging

Problem Cause Fix Verification
No ACK Wrong address Check address (0x1D, not 0x3A) Bus scan all addresses
Bus stuck No pull-ups Add 4.7kΩ pull-ups to VCC Check SDA/SCL with scope
Wrong data Byte order ADXL343 is little-endian Read DEVID (should be 0xE5)
Data = 0 Device not active Write to POWER_CTL register Check device status registers
Bus errors Clock stretching Implement timeout Monitor bus state

Learning Milestones

  1. I2C scan finds device → Basic I2C working
  2. DEVID reads 0xE5 → Register reads working
  3. X/Y/Z values make sense → Multi-byte reads working
  4. Values change with tilt → Full sensor operational
  5. Tap detection works → Advanced features implemented


TIER 4: ADVANCED INTEGRATION PROJECTS

These final projects combine everything you’ve learned—blending CircuitPython concepts, Arduino audio expertise, and bare-metal C knowledge into complex, impressive systems that demonstrate complete mastery.


Project 16: Complete MIDI DAW Controller (Full Integration)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: Arduino C++ (with bare-metal optimizations)
  • Alternative Programming Languages: Bare-metal C, CircuitPython
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: USB MIDI, Real-Time Systems, Human Interface Design
  • Software or Tool: Arduino IDE, Ableton Live/Logic Pro X
  • Main Book: “Making Embedded Systems” by Elecia White

What you’ll build: A professional-grade MIDI controller that rivals commercial products like Novation Launchpad or Ableton Push. Features include: multiple operational modes (clip launch, mixer, drum machine, step sequencer), RGB feedback from the DAW, velocity-sensitive input, accelerometer-based expression control, and seamless mode switching. This is a controller you’d actually use for music production.

Why it teaches integration mastery: This project requires synthesizing every skill: USB protocols, real-time audio concepts, LED driving, sensor input, and clean architecture. You must balance responsiveness, visual feedback, and musical timing—the complete embedded systems challenge.

Core challenges you’ll face:

  • Mode management → Clean state machine for multiple operational modes
  • DAW bidirectional communication → Sending MIDI to DAW AND receiving LED feedback
  • Velocity handling → Converting button press timing to velocity (or using pressure)
  • Performance timing → Sub-millisecond latency for live performance
  • User experience design → Making complex features discoverable and intuitive

Key Concepts:

  • USB MIDI bidirectional: MIDI to/from DAW for visual feedback
  • State machines: “Clean Code” Ch. 9 - managing complexity
  • Real-time constraints: “Making Embedded Systems” Ch. 10
  • HID design: Creating intuitive interfaces

Difficulty: Expert Time estimate: 4-6 weeks (50-80 hours) Prerequisites: Projects 4 (MIDI), 6 (Audio), and 11-15 (bare-metal understanding)


Real World Outcome

You have a controller that integrates seamlessly with professional DAWs:

                    DAW Controller Architecture

    ┌──────────────────────────────────────────────────────────────┐
    │                         DAW (Ableton/Logic)                   │
    │                                                               │
    │   ┌─────────────┐   ┌─────────────┐   ┌─────────────────┐   │
    │   │   Session   │   │    Mixer    │   │    Drum Rack   │   │
    │   │   View      │   │    View     │   │      View      │   │
    │   └──────┬──────┘   └──────┬──────┘   └───────┬────────┘   │
    │          │                 │                   │            │
    │          │         MIDI Feedback (LED colors)  │            │
    │          └─────────────────┼───────────────────┘            │
    │                            │                                 │
    └────────────────────────────┼─────────────────────────────────┘
                                 │ USB MIDI
                                 ▼
    ┌────────────────────────────────────────────────────────────────┐
    │                        NeoTrellis M4                           │
    │                                                                │
    │   Mode: [SESSION]  [MIXER]  [DRUMS]  [SEQUENCER]              │
    │                                                                │
    │   ┌────┬────┬────┬────┬────┬────┬────┬────┐                   │
    │   │Clp1│Clp2│Clp3│Clp4│Clp5│Clp6│Clp7│Clp8│  ← Track 1       │
    │   ├────┼────┼────┼────┼────┼────┼────┼────┤    clips          │
    │   │ G  │ G  │ Y  │    │ R  │    │    │    │  ← Color from DAW │
    │   ├────┼────┼────┼────┼────┼────┼────┼────┤    (G=playing,    │
    │   │Clp1│Clp2│Clp3│Clp4│Clp5│Clp6│Clp7│Clp8│    Y=triggered,  │
    │   ├────┼────┼────┼────┼────┼────┼────┼────┤    R=recording)   │
    │   │MODE│STOP│REC │PLAY│◄◄  │ ►► │SHFT│    │  ← Controls       │
    │   └────┴────┴────┴────┴────┴────┴────┴────┘                   │
    │                                                                │
    │   Accelerometer: X → Pan control                               │
    │                  Y → Filter sweep                              │
    │                  Z → Expression (pressure substitute)          │
    └────────────────────────────────────────────────────────────────┘


                    Mode Switching State Machine

                         ┌──────────────┐
                         │    BOOT      │
                         │ (load prefs) │
                         └──────┬───────┘
                                │
                                ▼
                    ┌───────────────────────┐
            ┌───────│      SESSION          │───────┐
            │       │  (clip launching)     │       │
            │       └───────────┬───────────┘       │
            │                   │                   │
            │ MODE+1           MODE+2           MODE+3
            ▼                   ▼                   ▼
    ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
    │    MIXER     │   │    DRUMS     │   │  SEQUENCER   │
    │ (faders/pan) │   │ (drum pads)  │   │ (step edit)  │
    └──────────────┘   └──────────────┘   └──────────────┘
            │                   │                   │
            └───────────────────┴───────────────────┘
                     Any MODE button returns


**Serial Debug Output:**

NeoTrellis DAW Controller v2.0 Initializing… NeoPixels: OK (32 LEDs, DMA) Accelerometer: OK (ADXL343 @ 0x1D) USB MIDI: OK (Device: “NeoTrellis Controller”)

Waiting for DAW connection… DAW connected! (Ableton Live 11) Receiving LED feedback on channel 16

[SESSION MODE] Button 0,0 pressed (velocity 112) → Note C3 sent DAW feedback: LED 0,0 → GREEN (clip playing) DAW feedback: LED 2,0 → YELLOW (clip triggered, waiting for 1)

Mode button pressed → MIXER MODE Accelerometer X mapped to Track 1 Pan: -23 Accelerometer Y mapped to Track 1 Filter: 6.2kHz

Mode button pressed → DRUMS MODE Velocity sensing active (time-based) Button 3,2 pressed → D2 (velocity 89) → Snare

Mode button pressed → SEQUENCER MODE Step 1: C3 on Step 2: – off Step 3: E3 on … Pattern length: 16 steps BPM: 120 (synced from DAW)


---

## The Core Question You're Answering

> "How do you build a complex interactive system with multiple operational modes while maintaining real-time responsiveness and clean architecture?"

This is the quintessential embedded systems design challenge. You must balance competing requirements: visual responsiveness, input latency, clean code architecture, and user experience—all on a microcontroller with limited resources.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **State Machine Architecture**
   - How do you represent multiple modes cleanly?
   - What's a state transition table?
   - How do you handle mode-specific behavior without massive switch statements?
   - *Book Reference:* "Clean Code" Ch. 9 - State machines

2. **MIDI Bidirectional Communication**
   - How does MIDI feedback from DAW work?
   - What's SysEx and how is it used for LED control?
   - How do you handle MIDI running status?
   - *Book Reference:* MIDI 1.0 Specification - System Exclusive

3. **Velocity Sensing Without Pressure Sensors**
   - How do commercial controllers detect velocity?
   - What's the relationship between press time and velocity?
   - How do you calibrate for different playing styles?

4. **Real-Time System Design**
   - What's the acceptable latency for live performance? (~10ms max)
   - How do you prioritize tasks?
   - What can cause latency spikes?
   - *Book Reference:* "Making Embedded Systems" Ch. 10

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Mode Management**
   - How many operational modes do you need?
   - What state persists across mode switches?
   - How do you indicate current mode visually?

2. **DAW Communication**
   - How do you receive LED color commands from the DAW?
   - What MIDI channel/notes for feedback vs. output?
   - How do you handle different DAW protocols (Ableton vs. Logic)?

3. **Performance**
   - What's your target latency?
   - Which operations can be batched?
   - How do you handle 32 LEDs updating at 60fps?

4. **User Experience**
   - How does the user switch modes?
   - How do you handle modifier keys (Shift)?
   - What visual feedback confirms actions?

---

## Thinking Exercise

### Design the State Machine

Before coding, diagram the state machine for mode switching:

States: BOOT, SESSION, MIXER, DRUMS, SEQUENCER, SHIFT_HELD

Transitions: BOOT → SESSION (after initialization) SESSION → MIXER (MODE + button 0) SESSION → DRUMS (MODE + button 1) SESSION → SEQUENCER (MODE + button 2) Any → SHIFT_HELD (SHIFT pressed) SHIFT_HELD → previous (SHIFT released)

For each state, define:

  • Entry action (what happens on transition IN)
  • Exit action (what happens on transition OUT)
  • Button map (what each button does in this mode)
  • LED pattern (how to display this mode) ```

Questions:

  • How do you store the “previous state” for returning from SHIFT?
  • What happens if MODE is pressed during SHIFT?
  • How do you animate the mode transition?

The Interview Questions They’ll Ask

  1. “Walk me through your state machine architecture. How do you avoid massive switch statements?”

  2. “What’s your latency budget from button press to MIDI output? How did you measure it?”

  3. “How do you handle DAW-specific MIDI protocols without recompiling?”

  4. “The LEDs flicker when the DAW sends rapid updates. How would you fix this?”

  5. “How would you add velocity sensitivity to buttons that don’t have pressure sensors?”


Hints in Layers

Hint 1: Starting Point Start with just two modes (SESSION and DRUMS). Get mode switching and basic MIDI working before adding complexity. Use a simple array of function pointers for mode-specific behavior.

Hint 2: State Machine Structure

typedef void (*ModeHandler)(uint8_t x, uint8_t y, bool pressed);
typedef void (*ModeEnter)();
typedef void (*ModeExit)();

struct Mode {
  ModeEnter enter;
  ModeExit exit;
  ModeHandler buttonHandler;
  ModeHandler ledUpdate;
};

Mode modes[] = {
  {sessionEnter, sessionExit, sessionButton, sessionLED},
  {mixerEnter, mixerExit, mixerButton, mixerLED},
  // ...
};

uint8_t currentMode = 0;

Hint 3: Velocity from Timing

// Track when button was first detected
uint32_t buttonDownTime[32];

// On button scan:
if (justPressed) {
  buttonDownTime[i] = millis();
}
if (justReleased) {
  uint32_t pressTime = millis() - buttonDownTime[i];
  // Faster press = higher velocity
  // Typical: 5ms = velocity 127, 100ms = velocity 1
  uint8_t velocity = constrain(map(pressTime, 5, 100, 127, 1), 1, 127);
  sendMIDI(NOTE_ON, notes[currentMode][i], velocity);
}

Hint 4: DAW Feedback Handling

// MIDI callback for incoming messages
void handleNoteOn(byte channel, byte note, byte velocity) {
  if (channel == 16) {  // Feedback channel
    // Note = button index, velocity = color
    uint8_t x = note % 8;
    uint8_t y = note / 8;
    uint32_t color = velocityToColor(velocity);
    setLED(x, y, color);
  }
}

uint32_t velocityToColor(byte v) {
  // Map Ableton's color palette
  switch(v) {
    case 1: return 0x00FF00;  // Green = playing
    case 2: return 0xFFFF00;  // Yellow = triggered
    case 3: return 0xFF0000;  // Red = recording
    // ...
  }
}

Books That Will Help

Topic Book Chapter
State machines “Clean Code” by Martin Ch. 9
Real-time design “Making Embedded Systems” by White Ch. 10
MIDI protocol MIDI 1.0 Specification SysEx, Running Status
USB MIDI USB MIDI Specification All
HID design “Don’t Make Me Think” by Krug Ch. 1-3

Common Pitfalls & Debugging

Problem Cause Fix Verification
Mode switch lag Too much work in transition Defer LED updates to main loop Profile with millis()
LED flicker DAW sending many updates Implement change detection Log incoming MIDI
Button double-triggers Debounce issue Add 20ms debounce Serial log press times
MIDI choke Too many messages Implement running status Monitor MIDI stream
Accelerometer jitter No filtering Add low-pass filter Plot raw vs filtered

Learning Milestones

  1. Two modes work → Basic state machine
  2. DAW receives notes → USB MIDI output working
  3. LEDs update from DAW → Bidirectional MIDI working
  4. Velocity works → Timing-based velocity
  5. All modes polished → Complete product

Project 17: Real-Time Audio Visualizer with External Display (Hardware Extension)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: Bare-metal C (with assembly optimization)
  • Alternative Programming Languages: Arduino C++
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 5: Master
  • Knowledge Area: Digital Signal Processing, SPI Displays, DMA, FFT
  • Software or Tool: arm-none-eabi-gcc, External SPI LCD (e.g., ILI9341)
  • Main Book: “The Scientist and Engineer’s Guide to Digital Signal Processing” by Steven Smith

What you’ll build: A real-time audio spectrum analyzer that takes line-in audio (via ADC), performs FFT analysis, and displays a high-resolution visualization on an external SPI LCD. The NeoTrellis buttons control visualization modes (spectrum bars, oscilloscope, waterfall), and NeoPixels provide ambient lighting that pulses with the beat. This is the ultimate integration: audio input, DSP, and multiple outputs all in bare-metal C.

Why it teaches complete system integration: This project combines every hardware subsystem: ADC for audio input, FFT for processing, SPI for display, DMA for transfers, timers for synchronization, and GPIO for buttons. It’s the capstone of bare-metal programming.

Core challenges you’ll face:

  • ADC audio capture → Sampling audio at 44.1kHz with DMA
  • FFT implementation → Computing 512-point FFT in real-time
  • SPI display driving → Pushing pixels fast enough for smooth animation
  • Beat detection → Extracting rhythm from spectrum data
  • All running simultaneously → Balancing multiple real-time tasks

Key Concepts:

  • ADC with DMA: Audio capture without CPU intervention
  • FFT algorithm: “DSP Guide” Ch. 12 - The FFT
  • SPI displays: ILI9341 command protocol
  • Real-time scheduling: Managing multiple time-critical tasks

Difficulty: Master Time estimate: 6-8 weeks (80-100 hours) Prerequisites: All previous projects, especially 11-15 (bare-metal), 8 (FFT concepts)


Real World Outcome

A stunning audio visualizer with external display:

                    System Architecture

    ┌─────────────────────────────────────────────────────────────────┐
    │                     Audio Input Stage                           │
    │                                                                 │
    │   Line In ─────┬───────[Bias Circuit]───────┬───→ ADC (PA02)   │
    │                │                             │                  │
    │            Audio Jack                    DC Blocking            │
    │            3.5mm                         + Bias to 1.65V        │
    │                                                                 │
    │   ADC: 12-bit @ 44.1kHz, DMA to buffer                         │
    └────────────────────────────┬────────────────────────────────────┘
                                 │
                                 ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                     DSP Processing                              │
    │                                                                 │
    │   ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐   │
    │   │  Window  │ → │  512-pt  │ → │ Magnitude│ → │   Peak   │   │
    │   │ (Hanning)│   │   FFT    │   │   Calc   │   │  Detect  │   │
    │   └──────────┘   └──────────┘   └──────────┘   └──────────┘   │
    │                                                                 │
    │   Processing: 512 samples = 11.6ms window                       │
    │   FFT bins: 256 usable (up to 22kHz)                           │
    │   Update rate: ~60 FPS                                          │
    └────────────────────────────┬────────────────────────────────────┘
                                 │
                    ┌────────────┴────────────┐
                    │                         │
                    ▼                         ▼
    ┌──────────────────────────┐   ┌──────────────────────────────┐
    │     ILI9341 Display      │   │      NeoTrellis LEDs         │
    │       (320x240)          │   │       (4x8 = 32)             │
    │                          │   │                              │
    │   ┌───────────────────┐  │   │   ┌───┬───┬───┬───┬───┐     │
    │   │▓▓▓▓              │  │   │   │ █ │   │ █ │   │ █ │     │
    │   │▓▓▓▓▓▓            │  │   │   │ █ │   │ █ │ █ │ █ │     │
    │   │▓▓▓▓▓▓▓▓          │  │   │   │ █ │ █ │ █ │ █ │ █ │     │
    │   │▓▓▓▓▓▓▓▓▓▓        │  │   │   │ █ │ █ │ █ │ █ │ █ │     │
    │   │▓▓▓▓▓▓▓▓▓▓▓▓▓▓    │  │   │   └───┴───┴───┴───┴───┘     │
    │   │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│  │   │   Bars pulse with bass      │
    │   └───────────────────┘  │   │                              │
    │   31.25Hz ──────── 16kHz │   │   Mode buttons on bottom row │
    │                          │   │                              │
    │   SPI @ 40MHz + DMA      │   │   [SPEC][WAVE][WTFL][BEAT]   │
    └──────────────────────────┘   └──────────────────────────────┘


                    Visualization Modes

    ┌────────────────────────────────────────────────────────────────┐
    │ SPECTRUM: Classic bar graph, 32 bands, logarithmic scale      │
    │                                                                │
    │   ▓▓▓▓                                                        │
    │   ▓▓▓▓▓▓▓▓                                                    │
    │   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓                                              │
    │   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                                      │
    │   32Hz  125Hz  500Hz  2kHz  8kHz                              │
    └────────────────────────────────────────────────────────────────┘

    ┌────────────────────────────────────────────────────────────────┐
    │ OSCILLOSCOPE: Waveform display, trigger on zero-crossing      │
    │                                                                │
    │        ╭─╮      ╭─╮      ╭─╮                                   │
    │       ╱   ╲    ╱   ╲    ╱   ╲                                  │
    │   ───╱─────╲──╱─────╲──╱─────╲──                               │
    │               ╲   ╱    ╲   ╱                                   │
    │                ╰─╯      ╰─╯                                    │
    └────────────────────────────────────────────────────────────────┘

    ┌────────────────────────────────────────────────────────────────┐
    │ WATERFALL: Scrolling spectrogram, time flows down             │
    │                                                                │
    │   ████░░░░████░░░░░░████░░░░                 ← Now            │
    │   ░░████░░░░████░░░░░░████░░                                   │
    │   ░░░░████░░░░████░░░░░░████                                   │
    │   ████░░░░████░░░░░░████░░░░                                   │
    │   Color: blue(low) → green → yellow → red(high)               │
    └────────────────────────────────────────────────────────────────┘


**Serial Debug Output:**

Audio Visualizer v1.0 (Bare-metal) Initializing… Clock: 120 MHz ADC: 12-bit, 44100 Hz, DMA channel 0 FFT: 512-point, Hanning window Display: ILI9341 320x240, SPI @ 40MHz, DMA channel 1 NeoPixels: 32 LEDs, DMA channel 2

Audio input: Line-in on PA02 DC bias: 1.647V (good) Signal detected: -12 dBFS peak

Performance: ADC fill time: 11.61 ms (512 samples) FFT compute: 0.89 ms (ARM CMSIS DSP) Display render: 4.2 ms NeoPixel update: 1.0 ms Total frame: 17.7 ms (56 FPS)

Beat detection: Bass energy: 2341 (threshold: 2000) BEAT! (interval: 500ms = 120 BPM)

Mode: SPECTRUM Peak: 125 Hz (bass), 2.5 kHz (vocals)


---

## The Core Question You're Answering

> "How do you build a system that handles continuous real-time audio processing while simultaneously driving multiple output devices, all in bare-metal C?"

This is the ultimate embedded systems integration challenge. You must orchestrate: ADC sampling at precise intervals, FFT processing within the audio window, display updates at visual frame rate, and LED animations—all with minimal CPU involvement using DMA.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **ADC with DMA**
   - How does ADC continuous conversion mode work?
   - How do you trigger DMA from ADC completion?
   - What's double-buffering and why is it essential?
   - *Book Reference:* SAMD51 Datasheet - ADC + DMAC chapters

2. **FFT Fundamentals**
   - What does FFT output represent?
   - Why do you need a window function?
   - How do you convert FFT bins to frequencies?
   - *Book Reference:* "DSP Guide" Ch. 12 - The FFT

3. **SPI Display Protocol**
   - How does ILI9341 command/data work?
   - What's the pixel format (RGB565)?
   - How do you set a drawing window?
   - *Book Reference:* ILI9341 Datasheet

4. **DMA Chaining**
   - How do you run multiple DMA channels simultaneously?
   - What's DMA priority and how does it affect performance?
   - How do you synchronize DMA completion with processing?

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **Timing Budget**
   - 44100 Hz sampling = 22.68 µs per sample
   - 512 samples = 11.6 ms of audio
   - Display at 60 FPS = 16.7 ms per frame
   - How do you fit FFT + display in this budget?

2. **Buffer Strategy**
   - How many audio buffers do you need? (Double buffering minimum)
   - When does FFT run? (While next buffer fills)
   - How do you prevent audio glitches during display updates?

3. **Display Optimization**
   - Drawing 32 bars × 200 pixels = 6400 pixels minimum
   - At 16 bits/pixel, SPI @ 40MHz = 2.56 ms minimum
   - How do you optimize? (Only redraw changed areas)

4. **Memory Layout**
   - FFT needs 512 complex floats = 4KB
   - Display buffer = 320×240×2 = 150KB (won't fit!)
   - Solution: Line-by-line rendering

---

## Thinking Exercise

### Design the DMA Architecture

Before coding, diagram the DMA channels and their interactions:

DMA Channel 0: ADC → Audio Buffer A/B Trigger: ADC RESRDY Transfer: 16-bit, 512 samples Ping-pong: Buffer A fills while processing B

DMA Channel 1: Display Buffer → SPI Trigger: SPI DRE (data register empty) Transfer: 16-bit pixels Action: Set up next line transfer on completion

DMA Channel 2: LED Buffer → SERCOM (SPI mode for NeoPixels) Trigger: Manual start Transfer: 8-bit, 96 bytes (32 LEDs × 3 colors) Timing: Run after audio processing, before next ADC window

Timing Diagram: |— 11.6 ms audio window —|— 11.6 ms —| [ ADC → Buffer A (DMA) ][ ADC → Buffer B ] [ FFT(B) ] [ FFT(A) ] [ Display update (DMA) ] [ NeoPixels ]


*Questions:*
- What happens if FFT takes longer than 11.6 ms?
- How do you detect buffer overrun?
- What's the interrupt load for this design?

---

## The Interview Questions They'll Ask

1. "Walk me through your DMA architecture. How do you handle multiple channels?"

2. "Your display updates cause audio glitches. What's the cause and fix?"

3. "How do you convert FFT bins to logarithmically-spaced frequency bands?"

4. "Explain the beat detection algorithm. What's the latency?"

5. "How would you add audio output (passthrough) without affecting the visualizer?"

---

## Hints in Layers

**Hint 1: Starting Point**
Start with just ADC + serial output of peak amplitude. Verify sampling rate is correct (check with known frequency tone). Then add FFT. Display comes last.

**Hint 2: CMSIS-DSP for FFT**
ARM provides optimized FFT:
```c
#include "arm_math.h"

arm_rfft_fast_instance_f32 fft_instance;
float32_t fft_input[512];
float32_t fft_output[512];
float32_t magnitudes[256];

// Initialize once
arm_rfft_fast_init_f32(&fft_instance, 512);

// Each frame:
arm_rfft_fast_f32(&fft_instance, fft_input, fft_output, 0);
arm_cmplx_mag_f32(fft_output, magnitudes, 256);

Hint 3: Frequency Bin to Band Mapping

// Map 256 FFT bins to 32 display bands (log scale)
float band_edges[33] = {
  20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160,
  200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600,
  2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500, 16000, 20000
};

for (int band = 0; band < 32; band++) {
  int bin_start = freq_to_bin(band_edges[band]);
  int bin_end = freq_to_bin(band_edges[band + 1]);
  float sum = 0;
  for (int b = bin_start; b < bin_end; b++) {
    sum += magnitudes[b];
  }
  band_values[band] = sum / (bin_end - bin_start);
}

Hint 4: Beat Detection

float bass_energy = 0;
for (int b = 1; b < 8; b++) {  // 40-350 Hz
  bass_energy += magnitudes[b];
}

static float avg_bass = 0;
static uint32_t last_beat = 0;

avg_bass = avg_bass * 0.95 + bass_energy * 0.05;  // Running average
if (bass_energy > avg_bass * 1.5 && millis() - last_beat > 200) {
  // BEAT detected!
  last_beat = millis();
  trigger_led_flash();
}

Books That Will Help

Topic Book Chapter
FFT theory “DSP Guide” by Smith Ch. 8-12
Window functions “DSP Guide” by Smith Ch. 16
ARM CMSIS DSP ARM CMSIS Documentation DSP Library
SPI displays “Making Embedded Systems” Ch. 7
DMA design SAMD51 Datasheet DMAC chapter

Common Pitfalls & Debugging

Problem Cause Fix Verification
Audio glitches DMA underrun Use double buffering Check buffer flags
FFT too slow Not using CMSIS Use arm_rfft_fast Time with cycle counter
Display slow Full redraw each frame Only update changed areas Profile with GPIO toggle
Beat too sensitive Threshold too low Use running average Compare to manual tap
Bands wrong Linear not log Implement log binning Test with sine sweep

Learning Milestones

  1. ADC captures audio → Input stage working
  2. FFT produces spectrum → DSP pipeline working
  3. Display shows bars → Output working
  4. Smooth animation → Real-time performance achieved
  5. Beat detection works → Complete visualizer

Project 18: Bare-Metal USB Mass Storage + Bootloader (Self-Modifying System)

View Detailed Guide

  • File: LEARN_NEOTRELLIS_M4_DEEP_DIVE.md
  • Main Programming Language: Bare-metal C
  • Alternative Programming Languages: Assembly (for critical sections)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 5. The “Industry Disruptor”
  • Difficulty: Level 5: Master
  • Knowledge Area: USB Protocol, Flash Programming, Bootloaders, File Systems
  • Software or Tool: arm-none-eabi-gcc, USB analyzer (optional)
  • Main Book: “USB Complete” by Jan Axelson

What you’ll build: A complete bare-metal bootloader that presents the NeoTrellis M4 as a USB mass storage device (like a flash drive). When you drag new firmware files onto the “drive,” the bootloader automatically flashes them—just like the UF2 bootloader that comes with CircuitPython, but written from scratch by you. This is the ultimate proof that you understand the entire system from USB packets to flash memory.

Why it teaches complete system mastery: Building a bootloader means you understand everything: USB enumeration, bulk transfers, file system parsing, flash memory programming, and safe firmware updates. This is what separates hobbyists from professional embedded engineers.

Core challenges you’ll face:

  • USB device stack → Implementing USB protocol from scratch
  • Mass Storage Class → SCSI commands, bulk-only transport
  • FAT filesystem → Parsing FAT12/16 to find firmware files
  • Flash programming → Erasing and writing SAMD51 flash safely
  • Self-update → Flashing new code without bricking the device

Key Concepts:

  • USB protocol: Enumeration, descriptors, endpoints
  • Mass Storage Class: BOT, SCSI, inquiry/read/write
  • FAT filesystem: Boot sector, FAT table, directory entries
  • Flash memory: Pages, blocks, NVM controller
  • Bootloader design: Reset vectors, application handoff

Difficulty: Master Time estimate: 8-12 weeks (100-150 hours) Prerequisites: All previous projects, especially 11-15. Deep understanding of USB recommended.


Real World Outcome

Your NeoTrellis becomes a self-programming device:

                    Bootloader Architecture

    ┌────────────────────────────────────────────────────────────────┐
    │                         USB Host (Computer)                     │
    │                                                                 │
    │   1. Detect USB device                                         │
    │   2. See mass storage drive "NEOTRELLIS"                       │
    │   3. Drag firmware.bin onto drive                              │
    │   4. File system write triggers flash                          │
    │   5. Device reboots into new firmware                          │
    └───────────────────────────────┬────────────────────────────────┘
                                    │ USB
                                    ▼
    ┌────────────────────────────────────────────────────────────────┐
    │                    NeoTrellis M4 (Bootloader Mode)             │
    │                                                                 │
    │   ┌────────────────────────────────────────────────────────┐   │
    │   │                    USB Device Stack                     │   │
    │   │  Endpoint 0: Control (enumeration, descriptors)        │   │
    │   │  Endpoint 1: Bulk OUT (host → device, SCSI commands)   │   │
    │   │  Endpoint 2: Bulk IN (device → host, SCSI responses)   │   │
    │   └────────────────────────────────────────────────────────┘   │
    │                              │                                  │
    │                              ▼                                  │
    │   ┌────────────────────────────────────────────────────────┐   │
    │   │                 Mass Storage Class                      │   │
    │   │  SCSI Inquiry: "NEOTRELLIS BOOT"                       │   │
    │   │  SCSI Read Capacity: 8MB                                │   │
    │   │  SCSI Read/Write: Access virtual FAT filesystem         │   │
    │   └────────────────────────────────────────────────────────┘   │
    │                              │                                  │
    │                              ▼                                  │
    │   ┌────────────────────────────────────────────────────────┐   │
    │   │                Virtual FAT Filesystem                   │   │
    │   │                                                         │   │
    │   │  Boot sector ──→ "NEOTRELLIS  " volume label           │   │
    │   │  Root directory ──→ INFO.TXT, CURRENT.BIN              │   │
    │   │  File writes ──→ Trigger flash programming             │   │
    │   └────────────────────────────────────────────────────────┘   │
    │                              │                                  │
    │                              ▼                                  │
    │   ┌────────────────────────────────────────────────────────┐   │
    │   │              Flash Programming Engine                   │   │
    │   │                                                         │   │
    │   │  NVM Controller: SAMD51's flash programming interface  │   │
    │   │  Page size: 512 bytes                                   │   │
    │   │  Block size: 8KB (must erase before write)              │   │
    │   │                                                         │   │
    │   │  Safety: Verify writes, checksum, don't brick!          │   │
    │   └────────────────────────────────────────────────────────┘   │
    │                                                                 │
    └────────────────────────────────────────────────────────────────┘


                    Memory Map (Flash Layout)

    0x00000000 ┌──────────────────────────────────┐
               │        Bootloader (16KB)         │ ← Your bootloader lives here
               │  - USB stack                     │    Never overwritten!
               │  - Mass storage                  │
               │  - Flash programmer              │
    0x00004000 ├──────────────────────────────────┤
               │      Application (496KB)         │ ← User firmware goes here
               │  - Vector table (at 0x4000)      │
               │  - User code                     │
               │  - Data                          │
    0x00080000 └──────────────────────────────────┘
               │         (End of Flash)           │


                    Boot Flow Decision Tree

                         ┌─────────────┐
                         │   RESET     │
                         └──────┬──────┘
                                │
                                ▼
                    ┌───────────────────────┐
                    │  Read boot button     │
                    │  (Button 0,0 on grid) │
                    └───────────┬───────────┘
                                │
               ┌────────────────┼────────────────┐
               │ Button held    │                │ Button not held
               ▼                │                ▼
    ┌──────────────────┐       │      ┌──────────────────────┐
    │  BOOTLOADER MODE │       │      │ Check app validity   │
    │  - LEDs: Pulsing │       │      │ (valid reset vector?)│
    │  - USB: MSC      │       │      └──────────┬───────────┘
    └──────────────────┘       │                 │
                               │        ┌────────┴────────┐
                               │        │ Valid           │ Invalid
                               │        ▼                 ▼
                               │  ┌──────────┐    ┌──────────────┐
                               │  │ Jump to  │    │ BOOTLOADER   │
                               │  │ App at   │    │ MODE (no app)│
                               │  │ 0x4000   │    └──────────────┘
                               │  └──────────┘


**What Computer Sees:**

$ lsusb Bus 001 Device 042: ID 239A:0024 Adafruit NeoTrellis Bootloader

$ ls /Volumes/NEOTRELLIS/ INFO.TXT CURRENT.BIN

$ cat /Volumes/NEOTRELLIS/INFO.TXT NeoTrellis M4 Bootloader v1.0 Bare-metal by [You]

Chip: ATSAMD51J19 (512KB Flash, 192KB RAM) Application: Valid (0x4000) Last flash: 2024-12-15 14:32:05

Drag a .BIN file here to flash new firmware.

$ cp my_firmware.bin /Volumes/NEOTRELLIS/


**Serial Debug Output (from bootloader):**

NeoTrellis Bootloader v1.0 Reset reason: External Boot button: HELD Entering bootloader mode…

USB initialization: Device attached Address assigned: 12 Configuration set: 1 Mass Storage Class active

Virtual filesystem: Volume: NEOTRELLIS Total: 8MB, Free: 7.5MB Files: INFO.TXT (512B), CURRENT.BIN (32KB)

SCSI command: INQUIRY SCSI command: READ CAPACITY (16MB, 512B sectors) SCSI command: READ(10) sector 0 (boot sector) SCSI command: READ(10) sector 1-32 (FAT)

File write detected: FIRMWARE.BIN (48KB) Verifying file… File valid (ARM reset vector detected)

Erasing application space… Block 0 (0x4000): Erased Block 1 (0x6000): Erased … Block 5 (0xE000): Erased

Programming… Page 0: Written, verified OK Page 1: Written, verified OK … Page 95: Written, verified OK

Flash complete! (48KB in 1.2 seconds) Checksum: 0x3A7F valid

Rebooting into new firmware…


---

## The Core Question You're Answering

> "How does a device program itself, and how do you implement USB mass storage from scratch to enable it?"

This is the deepest question in embedded systems. The bootloader is the foundation—the code that can never be corrupted because it's the only code that can recover from a bad flash. Understanding this means you can build any embedded product.

---

## Concepts You Must Understand First

**Stop and research these before coding:**

1. **USB Fundamentals**
   - What are USB endpoints? (Control, Bulk, Interrupt, Isochronous)
   - What happens during enumeration?
   - What are descriptors? (Device, Configuration, Interface, Endpoint)
   - *Book Reference:* "USB Complete" Ch. 1-5

2. **USB Mass Storage Class**
   - What's Bulk-Only Transport (BOT)?
   - What SCSI commands must you implement? (Inquiry, ReadCapacity, Read10, Write10)
   - What's the Command/Status Wrapper?
   - *Book Reference:* "USB Complete" Ch. 9

3. **FAT Filesystem**
   - What's in the boot sector?
   - How does the FAT table work?
   - What's a directory entry format?
   - *Book Reference:* "Practical File System Design" Ch. 2

4. **SAMD51 Flash (NVM)**
   - What's the page size? Block size?
   - How do you unlock flash for writing?
   - What's the write sequence? (Erase block, write page)
   - *Book Reference:* SAMD51 Datasheet - NVM Controller

5. **Bootloader Design**
   - Where does the bootloader live in memory?
   - How do you jump to application code?
   - How do you determine if application is valid?
   - *Book Reference:* "Making Embedded Systems" Ch. 6

---

## Questions to Guide Your Design

**Before implementing, think through these:**

1. **USB Stack**
   - Will you write USB from scratch or use TinyUSB?
   - How do you handle enumeration?
   - How do you handle USB suspend/resume?

2. **Virtual Filesystem**
   - Do you implement a real FAT or fake it?
   - How do you detect file writes? (Monitor specific sectors)
   - What happens if user writes non-firmware file?

3. **Flash Safety**
   - How do you ensure bootloader is never overwritten?
   - What if power fails during flash?
   - How do you verify the new firmware before rebooting?

4. **User Experience**
   - How does user enter bootloader mode?
   - What visual feedback during flashing?
   - How long should flashing take? (Target: < 5 seconds)

---

## Thinking Exercise

### Design the Flash Update Protocol

Before coding, design the protocol for safe firmware updates:

Problem: You’re running code from flash while trying to write to flash. Solution: Execute critical code from RAM.

Flash Write Sequence:

  1. Receive file data over USB into RAM buffer
  2. Validate:
    • File size < application space
    • First word looks like valid reset vector (0x20000000-0x20030000)
    • Optional: Check for magic bytes or checksum
  3. Disable interrupts
  4. Copy flash_write_page() function to RAM
  5. Call RAM-resident function to: a. Erase block (8KB) b. Write pages (512B each) with verification c. Repeat for all blocks
  6. Verify entire image
  7. Write “valid” marker
  8. Reset (NVIC_SystemReset)

Recovery:

  • If flash fails, bootloader still works (never overwritten)
  • If app is corrupt, bootloader detects and stays in bootloader mode
  • User can always re-flash ```

Questions:

  • Why must flash_write_page() be in RAM?
  • What’s the “valid marker” and where do you store it?
  • How do you handle a file larger than the buffer?

The Interview Questions They’ll Ask

  1. “Walk me through USB enumeration. What descriptors are exchanged?”

  2. “Explain the FAT filesystem boot sector. What fields are critical for a minimal implementation?”

  3. “Why is the bootloader stored at address 0x0 and not the application?”

  4. “How do you safely program flash from code running on the same flash?”

  5. “What happens if power fails mid-flash? How do you recover?”


Hints in Layers

Hint 1: Starting Point Use TinyUSB for the USB stack initially. Get mass storage working with a simple 1-sector “filesystem.” Then either customize TinyUSB or replace it.

Hint 2: Minimal FAT Implementation

// You don't need full FAT - just enough to fool the OS
struct FATBootSector {
  uint8_t jump[3];
  char oem[8];
  uint16_t bytes_per_sector;     // 512
  uint8_t sectors_per_cluster;   // 1
  uint16_t reserved_sectors;     // 1
  uint8_t num_fats;              // 1
  uint16_t root_entries;         // 16
  uint16_t total_sectors;        // 16384 (8MB)
  uint8_t media_type;            // 0xF8
  uint16_t fat_sectors;          // 32
  // ... more fields
} __attribute__((packed));

// Return fixed responses for sector reads:
// Sector 0: Boot sector (above)
// Sectors 1-32: FAT table (mostly 0xFF)
// Sector 33+: Root directory + file data

Hint 3: Detect File Write

// Monitor writes to root directory area
void handle_write(uint32_t sector, uint8_t* data) {
  if (sector >= ROOT_DIR_SECTOR && sector < ROOT_DIR_SECTOR + ROOT_SECTORS) {
    // Check if this is a new file
    for (int i = 0; i < 16; i++) {  // 16 entries per sector
      struct DirEntry* entry = (struct DirEntry*)(data + i * 32);
      if (entry->name[0] != 0x00 && entry->name[0] != 0xE5) {
        if (ends_with(entry->name, "BIN")) {
          start_flash_sequence(entry);
        }
      }
    }
  } else if (sector >= DATA_START_SECTOR) {
    // This is file data - buffer it for flashing
    buffer_firmware_data(sector, data);
  }
}

Hint 4: RAM-Resident Flash Write

// This function MUST be copied to and executed from RAM
__attribute__((section(".ramfunc")))
void flash_write_page(uint32_t address, uint8_t* data) {
  // Unlock NVM
  NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | NVMCTRL_CTRLA_CMD_PBC;
  while (!NVMCTRL->INTFLAG.bit.READY);

  // Write 512 bytes (16 words at a time for SAMD51)
  uint32_t* src = (uint32_t*)data;
  uint32_t* dst = (uint32_t*)address;
  for (int i = 0; i < 128; i++) {
    *dst++ = *src++;
  }

  // Execute write
  NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | NVMCTRL_CTRLA_CMD_WP;
  while (!NVMCTRL->INTFLAG.bit.READY);
}

// Copy function to RAM and call:
memcpy(ram_buffer, flash_write_page, FLASH_WRITE_SIZE);
((void (*)(uint32_t, uint8_t*))ram_buffer)(address, data);

Books That Will Help

Topic Book Chapter
USB protocol “USB Complete” by Axelson Ch. 1-5
Mass Storage Class “USB Complete” Ch. 9
FAT filesystem “Practical File System Design” Ch. 2
Flash programming SAMD51 Datasheet NVM Controller
Bootloader design “Making Embedded Systems” by White Ch. 6

Common Pitfalls & Debugging

Problem Cause Fix Verification
USB not recognized Descriptor error Validate with USB analyzer Use USBlyzer/Wireshark
Drive not mounting FAT format error Fix boot sector fields Compare with real FAT
Flash write fails Flash locked Unlock with security bit Check NVM STATUS
Device bricks Bad app vector Add validity check in bootloader Test with invalid file
Slow flashing Page-by-page writes Use block writes Profile with timer

Learning Milestones

  1. USB enumerates → Basic USB stack working
  2. Drive mounts → Mass storage + FAT working
  3. File writes detected → Virtual filesystem parsing
  4. Flash programming works → NVM controller mastered
  5. Full bootloader → Complete self-programming system


PROJECT COMPARISON TABLE

# Project Tier Difficulty Time Coolness Business Key Learning
1 Interactive Button-LED Matrix CircuitPython Beginner Weekend Level 3 Resume Gold GPIO, NeoPixels basics
2 RGB Color Mixer CircuitPython Beginner Weekend Level 3 Resume Gold Color spaces, animation
3 Accelerometer Light Show CircuitPython Intermediate 1 week Level 4 Resume Gold I2C sensors, motion
4 USB MIDI Controller CircuitPython Intermediate 1-2 weeks Level 4 Micro-SaaS USB protocols, MIDI
5 Precision Timer CircuitPython Intermediate 1 week Level 3 Resume Gold Low-level timing
6 Polyphonic Synthesizer Arduino Advanced 2-3 weeks Level 5 Micro-SaaS DSP, audio synthesis
7 Drum Machine Sequencer Arduino Intermediate 1-2 weeks Level 4 Micro-SaaS Sequencing, timing
8 FFT Spectrum Analyzer Arduino Advanced 2-3 weeks Level 5 Resume Gold FFT, DSP theory
9 Sample Player with Effects Arduino Advanced 2-3 weeks Level 4 Micro-SaaS Audio effects, routing
10 Capacitive Theremin Arduino Advanced 2-3 weeks Level 5 Micro-SaaS Capacitive sensing
11 Bare-Metal Blinker Bare-Metal C Beginner 1 week Level 4 Resume Gold Startup code, registers
12 Bare-Metal NeoPixels Bare-Metal C Intermediate 2 weeks Level 4 Resume Gold DMA, SPI, timing
13 Bare-Metal UART Bare-Metal C Intermediate 1-2 weeks Level 4 Resume Gold Serial protocols
14 Bare-Metal DAC Audio Bare-Metal C Advanced 3-4 weeks Level 5 Resume Gold Audio output, DMA
15 Bare-Metal I2C Driver Bare-Metal C Advanced 2-3 weeks Level 4 Resume Gold I2C protocol
16 Complete DAW Controller Integration Expert 4-6 weeks Level 5 Open Core Full MIDI integration
17 Audio Visualizer + Display Integration Master 6-8 weeks Level 5 Micro-SaaS Complete DSP system
18 USB Bootloader Integration Master 8-12 weeks Level 5 Disruptor Complete USB stack

RECOMMENDED LEARNING PATHS

Path 1: Music Producer (Fastest to Fun)

Goal: Create usable music tools as quickly as possible Timeline: 4-6 weeks Projects: 4 → 7 → 6 → 9 → 16

  • Start with MIDI controller (instant DAW integration)
  • Build drum machine for beat-making
  • Add synthesizer for melodic content
  • Combine with sample player
  • Graduate to complete DAW controller

Path 2: Audio Engineer (DSP Focus)

Goal: Deep understanding of digital signal processing Timeline: 8-12 weeks Projects: 6 → 8 → 9 → 14 → 17

  • Learn synthesis fundamentals
  • Master FFT and frequency domain
  • Implement audio effects
  • Build bare-metal DAC output
  • Create complete visualizer system

Path 3: Systems Programmer (Hardware Mastery)

Goal: Complete understanding from registers to USB Timeline: 12-16 weeks Projects: 11 → 12 → 13 → 14 → 15 → 18

  • Start with bare-metal basics
  • Master peripherals one by one
  • Build complete I/O ecosystem
  • Graduate to USB bootloader

Path 4: Complete Mastery (All 18 Projects)

Goal: Professional embedded engineer level Timeline: 20-30 weeks (5-7 months) Order: 1 → 2 → 3 → 4 → 5 → 11 → 12 → 13 → 6 → 7 → 8 → 14 → 15 → 9 → 10 → 16 → 17 → 18

  • CircuitPython foundation
  • Transition to bare-metal basics
  • Add Arduino audio
  • Complete bare-metal peripherals
  • Advanced audio projects
  • Integration projects

FINAL PROJECT: The Ultimate NeoTrellis (Combines Everything)

After completing all 18 projects, challenge yourself with this capstone:

Build a standalone groovebox that rivals commercial products:

  1. Bare-metal core - No libraries, pure register programming
  2. Polyphonic synthesis - 8 voices with waveform selection
  3. Drum machine - 16-step sequencer with sample playback
  4. Effects chain - Delay, reverb, filter with accelerometer control
  5. USB MIDI - Full bidirectional integration with DAW
  6. SD card storage - Save/load patterns and samples
  7. Custom bootloader - Drag-drop firmware updates

This project uses every concept from every project: USB stacks, audio synthesis, file systems, DMA, sensors, and real-time processing—all running simultaneously on a single $55 board.


SUMMARY

This learning path covers the NeoTrellis M4 through 18 comprehensive projects across 4 tiers of complexity.

All Projects at a Glance

# Project Name Main Language Difficulty Time Estimate
1 Interactive Button-LED Matrix CircuitPython Beginner Weekend
2 RGB Color Mixer Instrument CircuitPython Beginner Weekend
3 Accelerometer Light Show CircuitPython Intermediate 1 week
4 USB MIDI Controller CircuitPython Intermediate 1-2 weeks
5 Precision Timer/Metronome CircuitPython Intermediate 1 week
6 Polyphonic Synthesizer Arduino C++ Advanced 2-3 weeks
7 8-Step Drum Machine Arduino C++ Intermediate 1-2 weeks
8 FFT Spectrum Analyzer Arduino C++ Advanced 2-3 weeks
9 Sample Player with Effects Arduino C++ Advanced 2-3 weeks
10 Capacitive Theremin Arduino C++ Advanced 2-3 weeks
11 Bare-Metal LED Blinker C Beginner 1 week
12 Bare-Metal NeoPixel Driver C Intermediate 2 weeks
13 Bare-Metal UART Console C Intermediate 1-2 weeks
14 Bare-Metal DAC Audio C Advanced 3-4 weeks
15 Bare-Metal I2C Driver C Advanced 2-3 weeks
16 Complete DAW Controller Arduino/C Expert 4-6 weeks
17 Audio Visualizer + Display C Master 6-8 weeks
18 USB Mass Storage Bootloader C Master 8-12 weeks

Expected Outcomes

After completing these projects, you will:

  1. Understand CircuitPython for rapid prototyping and USB device development
  2. Master Arduino’s audio ecosystem for professional synthesizer and sequencer development
  3. Program bare-metal C with complete understanding of SAMD51 peripherals
  4. Implement USB protocols from mass storage to MIDI
  5. Design real-time systems balancing multiple time-critical tasks
  6. Build production-quality tools that rival commercial products
  7. Debug at every level from register bits to USB packets
  8. Read any datasheet and implement any peripheral

You’ll have built 18 working projects that demonstrate deep understanding of embedded systems—from blinking an LED to writing your own USB bootloader. This is the difference between following tutorials and truly mastering embedded development.

Total estimated time: 40-60 weeks for all projects (can be done selectively in 8-16 weeks)


Remember: The goal isn’t to complete every project—it’s to deeply understand the concepts each one teaches. Pick the path that matches your interests, and build until you truly understand.