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>
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 1next=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
- Init LP5562 (enable, clock, current)
- Read header at
0x0100(4 blocks) - If magic = "XBLK" and CRC valid:
- Read pattern at
active_pattern_index - Deserialize into
Patternstruct - Load into LP5562 engines
- Read pattern at
- If invalid (no magic, CRC mismatch, I2C error):
- Fall back to hardcoded breathe pattern (compiled into firmware)
- Config check via session registers, report via LED blinks (no NDEF write)
- Sleep (future: STANDBY)
Pattern Switching (future, hall sensor)
- Wake on hall EIC interrupt
- Read header, increment
active_pattern_index(wrap atpattern_count) - Write back the single block containing the index (1 EEPROM write, ~5ms)
- Read and load the new pattern
- 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.