# GPIO-Direct LED Pivot **Date**: 2026-03-06 **Status**: Design complete, implementation pending ## Why The LP5562 LED driver IC requires VDD >= 2.7V. Our NTAG5Link NFC energy harvesting antenna produces only 1.8V. No workaround exists -- a boost converter would exceed the power budget. We're replacing the LP5562 with SAMD21E GPIO-direct PWM driving 6 low-Vf red LEDs through current-limiting resistors. ## What Changes - **Remove**: LP5562 from BOM, `src/led/lp5562.rs` driver, LP5562 engine opcode pattern format - **Add**: TCC hardware PWM driver (`src/led/pwm.rs`), software pattern engine, XBLK v2 EEPROM format - **Keep**: NTAG5Link driver, SRAM mailbox protocol, EEPROM storage, EIC wake, hall sensor design --- ## Hardware **LEDs**: 6x Kingbright APTD1608 (0603, red, Vf ~1.7V at 1mA). 0.1V headroom at 1.8V supply. Arranged in a line. **PWM outputs**: TCC0 (4 channels) + TCC1 (2 channels) = 6 independent PWM channels. **Pin assignments** (SAMD21E, TCC-capable pins): | LED | TCC | Channel | SAMD21E Pin | XIAO Pin | |-----|------|---------|-------------|----------| | 0 | TCC0 | WO[0] | PA04 | A1 | | 1 | TCC0 | WO[1] | PA05 | A2 | | 2 | TCC0 | WO[2] | PA06 | A3 | | 3 | TCC0 | WO[3] | PA07 | A4* | | 4 | TCC1 | WO[0] | PA10 | D2 | | 5 | TCC1 | WO[1] | PA11 | D3 | *PA06/PA07 conflict with I2C (A4/A5) on XIAO M0. Dev board testing uses 4 LEDs on non-conflicting pins. Custom PCB (SAMD21E) has no conflict. **Resistors**: 10-47 ohm series per LED. At 1.8V supply, Vf=1.7V: I = 0.1V/47ohm = ~2mA (safe). Lower resistance = brighter but more current. Governor handles the budget. ## Power Governor The NTAG5 drops out entirely (hard power loss) when current budget is exceeded. The governor prevents this by running every ISR tick before writing TCC registers: ``` raw[6] = pattern engine computes brightness 0-255 per LED total = sum(raw[0..6]) if total > BUDGET: scale = BUDGET * 256 / total // fixed-point for i in 0..6: raw[i] = raw[i] * scale / 256 write raw values to TCC CC registers ``` **BUDGET**: Stored in XBLK v2 header (1 byte, units of ~0.1mA steps). Default ~50 (5mA total LED budget). Configurable via companion app or SRAM mailbox command. **Phase splitting** is the key optimization: stagger LED phase offsets so peaks don't align. A 6-LED sine wave with 60-degree phase offsets has roughly constant total brightness, maximizing perceived brightness within the budget. ## Software Pattern Engine Replaces LP5562's hardware execution engines. Runs in TC4 ISR at 50Hz. **PatternState** (per-pattern, loaded from EEPROM): ```rust struct PatternState { waveform: Waveform, // sine, triangle, square, heartbeat cycle_len: u8, // ticks per full cycle (1-255 = 20ms-5.1s) phase: [u8; 6], // phase offset per LED (0-255 = 0-360 degrees) envelope: [u8; 6], // max brightness per LED (governor input) tick: u16, // current animation tick (runtime, not stored) } ``` **Waveform lookup tables** (const, in flash): - `SINE_LUT[256]`: 8-bit sine quarter-wave, mirrored at runtime - `TRIANGLE_LUT[256]`: linear ramp up/down - `HEARTBEAT_LUT[256]`: double-pulse cardiac shape **ISR flow** (~40us at 8MHz): 1. Increment `tick`, wrap at `cycle_len * 256` 2. For each LED: `phase_pos = (tick + phase[i] * cycle_len) % (cycle_len * 256)` 3. Look up `waveform[phase_pos]`, scale by `envelope[i]` 4. Apply governor scaling 5. Write 6 TCC CC registers **MCU sleep**: IDLE mode (not STANDBY) during animation. CPU halts between interrupts, peripherals (TCC, TC4) keep running. ~0.5mA at 1.8V/8MHz. CPU active only ~40us per 20ms tick = 0.2% duty cycle. ## XBLK v2 EEPROM Format **Storage region**: Last 1KB of NTAG5 EEPROM (blocks 0x100-0x1FF), beyond normal NFC/NDEF access. ### Header (16 bytes) ``` Offset Size Field 0x00 4 magic "XBLK" 0x04 1 version 0x02 0x05 1 flags bit 0: has_playlist 0x06 1 pattern_count number of pattern entries 0x07 1 active_index pattern to load on boot 0x08 1 budget power governor budget 0x09 1 playlist_count number of playlist entries (0 if no playlist) 0x0A 4 reserved 0x0E 2 crc16 over bytes 0x00-0x0D ``` ### Pattern Entry (16 bytes each, immediately after header) ``` Offset Size Field 0x00 1 waveform (0=sine, 1=triangle, 2=square, 3=heartbeat) 0x01 1 cycle_len (in 50Hz ticks, 1-255 = 20ms-5.1s) 0x02 6 phase[6] (0-255 phase offset per LED, maps to 0-360 degrees) 0x08 6 envelope[6] (max brightness per LED, 0-255) 0x0E 1 repeat_count (times to play before advancing playlist, 0xFF=forever) 0x0F 1 reserved ``` ### Playlist Table (after all pattern entries, if flags bit 0 set) Each playlist entry is 1 byte = pattern index. Sequence plays in order, loops back to start. Max 32 entries. Example: patterns [breathe=0, chase=1, flash=2] with playlist [1, 2, 2, 0] plays: chase, flash, flash, breathe, chase, ... **Capacity**: 1024B - 16B header - 32B playlist = 976B for patterns. At 16B each: 61 patterns. ## Sleep and Power-on Behavior Three MCU states: 1. **STANDBY** (~2uA) -- No animation. Wakes on FD pin (EIC) for SRAM commands or hall sensor tap. 2. **ANIMATING** (~0.5mA MCU + LED current) -- IDLE sleep, TC4 ISR at 50Hz updates TCC duty cycles. 3. **COMMAND PROCESSING** -- Fully awake, handling SRAM mailbox. Returns to ANIMATING or STANDBY. **Power-on sequence**: 1. NTAG5 EH powers up, VOUT rises, SAMD21 boots 2. Read EEPROM header -- if valid XBLK v2, load pattern at `active_index` 3. If playlist exists, start playlist from entry 0 4. Configure TC4 (50Hz), TCC0/TCC1 (PWM), start animation 5. Enter IDLE sleep (animation runs via ISR) 6. On FD interrupt, wake fully, process SRAM command, resume **Playlist advancement**: When `repeat_count` cycles complete for current pattern, ISR loads next playlist entry. If playlist wraps, loop from start. Hall sensor tap: advance to next playlist entry (or next pattern if no playlist). **Pattern cycling (no playlist)**: Hall sensor increments `active_index`, loads next pattern. Wrapping past last pattern goes to STANDBY (LEDs off).