Files
xblink/docs/plans/2026-03-06-gpio-led-pivot.md
michael 5cd728c298 Pivot from LP5562 to GPIO-direct PWM LEDs, add NTAG5 EH provisioning
LP5562 requires 2.7V min but NFC energy harvesting produces only 1.8V.
New architecture: SAMD21 drives 6 red LEDs directly via TCC0/TCC1
hardware PWM through current-limiting resistors.

Key changes:
- Add TCC PWM driver (src/led/pwm.rs) for 6 GPIO-direct LED channels
- Rewrite pattern engine: software waveform LUTs (sine/triangle/square/
  heartbeat) replace LP5562 hardware execution engines
- Add XBLK v2 EEPROM format with 16-byte pattern entries + playlist
- Add TC4 50Hz ISR for animation, power governor for current budget
- Add NTAG5 EH provisioning: persistent config + session trigger
- Critical finding: SRAM passthrough in persistent EEPROM blocks all
  NFC access when MCU unpowered — CONFIG_1 must be session-only
- Add provision_eh.py PCSC tool for ACR1552 reader
- Update CLAUDE.md and STATUS.md for new architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:44:12 -08:00

145 lines
6.2 KiB
Markdown

# 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).