Files
xblink/docs/plans/2026-03-05-eeprom-pattern-format.md
michael a32c242859 M5 complete: boot-from-EEPROM with self-provisioning
XBLK binary format for pattern library in NTAG5 upper 1K EEPROM:
- 16-byte header (magic, version, pattern count, active index, CRC-16)
- Up to 9 × 112-byte fixed-size pattern entries
- Deserializer + serializer in src/pattern/mod.rs
- MCU self-provisions hardcoded patterns on first boot (no XBLK found)
- Subsequent boots load active pattern directly from EEPROM
- Python serializer tool for future PCSC-based pattern uploads

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

3.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     4     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.

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.