Files
xblink/docs/plans/2026-03-05-eeprom-pattern-format.md
michael 2fade7ad8b Add pattern chaining support to XBLK format
Pattern entry bytes 108-109 now hold next_pattern (0xFF=loop forever,
0-8=chain to index) and loop_count. MCU will poll LP5562 STATUS register
to detect engine completion and load the successor pattern.

Updated XBLK format spec, Rust Pattern struct (serializer + deserializer),
and React Native app design doc (binary format, data model, resolved
open questions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:52:33 -08:00

4.3 KiB

EEPROM Pattern Library Format (M5)

Memory Layout

The NTP53x2 has 2048 bytes (512 x 4-byte blocks) of user EEPROM.

Region Blocks Bytes Purpose
NFC/NDEF 0-255 0-1023 CC + NDEF data (phone-visible, MCU doesn't touch)
Pattern library 256-511 1024-2047 XBLK header + up to 9 patterns

I2C base address for the pattern library: 0x0100 (block 256).

Header (16 bytes, blocks 256-259)

Offset  Size  Field
0       4     Magic: "XBLK" (0x58 0x42 0x4C 0x4B)
4       1     Version: 0x01
5       1     Pattern count (1-9)
6       1     Active pattern index (0-based, wraps at count)
7       1     LED current (0-255, 0.1mA/step)
8       1     LED mode (0x00 = RGBW, 0x01 = Mono3)
9       5     Reserved (0x00)
14      2     CRC-16 over bytes 0-13 + all pattern data

Current and LED mode are global -- all patterns share them.

Pattern Entry (112 bytes, 28 blocks each)

Fixed-size entries for direct seeking: offset = 16 + (index * 112).

Offset  Size  Field
0       1     Engine count (0-3)
1       1     LED_MAP register value (raw LP5562 register byte)
2       4     Direct PWM [B, G, R, W] for I2C-mapped channels
6       2     Engine 1 command count (big-endian, 0 = unused)
8       32    Engine 1 commands (up to 16 x 2 bytes BE, pad with 0x0000)
40      2     Engine 2 command count
42      32    Engine 2 commands
74      2     Engine 3 command count
76      32    Engine 3 commands
108     1     Next pattern index (0xFF = loop forever, 0-8 = chain to that index)
109     1     Loop count (how many full cycles before chaining, 0 = use next_pattern as-is)
110     2     Reserved

Max capacity: (1024 - 16) / 112 = 9 patterns.

The 16-command-per-engine limit is a hardware constraint of the LP5562 (48 bytes engine SRAM). The branch command enables infinite looping, so pattern duration is unlimited.

Pattern Chaining

A pattern can specify a successor via next_pattern_index. When set to anything other than 0xFF, the pattern's engine programs should use finite-count branch(0, N) instead of branch(0, 0) (infinite loop). When all engines stop (detected by polling LP5562 STATUS register 0x0C for engine interrupt bits), the MCU loads the next pattern.

The LP5562 has no interrupt output pin — the MCU must poll STATUS periodically. During the idle loop, a poll every ~500ms is sufficient and costs negligible power vs. the LED current.

Chain examples:

  • next=0xFF: Loop forever (default, current behavior)
  • next=1, loops=0: Play once, then switch to pattern 1
  • next=0, loops=3: Play 3 full cycles, then switch to pattern 0 (can create A→B→A→B sequences)
  • Circular chains (A→B→A) create alternating pattern sequences

Boot Sequence

  1. Init LP5562 (enable, clock, current)
  2. Read header at 0x0100 (4 blocks)
  3. If magic = "XBLK" and CRC valid:
    • Read pattern at active_pattern_index
    • Deserialize into Pattern struct
    • Load into LP5562 engines
  4. If invalid (no magic, CRC mismatch, I2C error):
    • Fall back to hardcoded breathe pattern (compiled into firmware)
  5. Config check via session registers, report via LED blinks (no NDEF write)
  6. Sleep (future: STANDBY)

Pattern Switching (future, hall sensor)

  1. Wake on hall EIC interrupt
  2. Read header, increment active_pattern_index (wrap at pattern_count)
  3. Write back the single block containing the index (1 EEPROM write, ~5ms)
  4. Read and load the new pattern
  5. Sleep

Error Handling

  • EEPROM read fails at boot: use hardcoded fallback
  • CRC mismatch: use hardcoded fallback
  • Pattern data malformed (engine_count > 3, cmd_count > 16): skip to next, wrap around; if all bad, use fallback
  • Active index >= pattern count: reset to 0

Python Serializer

tools/xblk_serialize.py converts JSON pattern definitions to binary and writes to EEPROM via ntag5sensor/PCSC.

Input JSON:

{
  "current": 20,
  "mode": "rgbw",
  "active": 0,
  "patterns": [
    {
      "name": "breathe",
      "led_map": {"b": "engine1", "g": "engine1", "r": "engine1", "w": "direct"},
      "direct_pwm": [0, 0, 0, 0],
      "engines": [
        [18176, 18176, 26368, 26368, 15408, 40960],
        [],
        []
      ]
    }
  ]
}

Engine commands are raw u16 values. The serializer validates constraints, builds the binary blob, computes CRC-16, and writes to EEPROM blocks 256+ via I2C.