# M6: SRAM Mailbox Protocol Design **Date**: 2026-03-05 **Status**: Approved — ready for implementation ## Overview The NTAG5Link's 256-byte SRAM (64 x 4-byte blocks at I2C address 0xF8-0xFF) serves as a bidirectional mailbox between the phone (NFC/RF side) and the MCU (I2C side). The MCU polls the FD pin to detect when the phone has written a command, processes it, and writes a response back to SRAM for the phone to read. ## Pin Allocation | Pin | Function | Milestone | |-----|----------|-----------| | A0 (PA02) | LP5562 EN (open-drain, wired-AND with hall sensor) | M1 (done) | | A1 (PA04) | NTAG5 FD (SRAM write / field detect, EXTINT[4]) | M6 | | TBD | Hall sensor (EIC wake) | M8 | ## FD Pin Configuration NTAG5 session register `FD_PIN_CFG` (0x06) set at boot to "SRAM RF write complete" mode: - FD goes **low** when the RF side (phone) finishes writing to SRAM - FD returns **high** when the I2C side (MCU) reads the SRAM - Natural flow control for command/response cycles Pin wiring: NTAG5 Click FD → XIAO A1 (PA04). Configured as GPIO input with pull-up (FD is open-drain on NTAG5). M6 uses GPIO polling (~200ms interval in idle loop). M7 upgrades to EIC interrupt (EXTINT[4]) for wake-from-STANDBY. ## Command Packet Format **Phone → MCU (written to SRAM, 256 bytes max):** ``` Byte Field 0 Command ID (0x01-0x7F) 1 Sequence number (0-255, incremented per packet) 2-3 Payload length (u16 LE) 4-5 CRC-16 over bytes 0-3 + payload 6-255 Payload (up to 250 bytes) ``` **MCU → Phone (written back to SRAM):** ``` Byte Field 0 0xFF (response marker — distinguishes from commands) 1 Echo sequence number 2 Status (0x00=OK, 0x01=BAD_CRC, 0x02=BAD_CMD, 0x03=EEPROM_FAIL, 0x04=INVALID_INDEX) 3 Payload length 4-5 CRC-16 over bytes 0-3 + payload 6-255 Response payload ``` The 0xFF response marker lets the phone distinguish "MCU hasn't responded yet" (byte 0 still holds old command ID) from "response ready" (byte 0 = 0xFF). ## Command Set | ID | Name | Payload (Phone → MCU) | Response Payload (MCU → Phone) | |----|------|----------------------|-------------------------------| | 0x01 | WRITE_PATTERN | index (1B) + pattern (112B) | none | | 0x02 | GET_STATUS | none | version (1B) + pattern_count (1B) + active_index (1B) + led_current (1B) + led_mode (1B) | | 0x03 | SET_ACTIVE | index (1B) | none | | 0x05 | SYNC_START | count (1B) + current (1B) + mode (1B) | none | | 0x06 | SYNC_END | none | none | | 0x07 | READ_LIBRARY | none | pattern_count (1B) + active (1B) + current (1B) + mode (1B) | | 0x08 | READ_NEXT | none | pattern data (112B) | ## Streaming Transfer Protocol ### Write (Phone → Implant): Full Library Sync Single continuous NFC hold. Phone writes packets back-to-back, polling for MCU response between each. ``` Phone: Write SYNC_START (count=3, current=20, mode=RGBW) → SRAM MCU: Erase library, write new header skeleton, respond OK Phone: Poll byte 0 until 0xFF Phone: Write WRITE_PATTERN (index=0, 112B pattern) → SRAM MCU: Write 28 blocks to EEPROM (~140ms), respond OK Phone: Poll byte 0 until 0xFF Phone: Write WRITE_PATTERN (index=1, ...) → SRAM MCU: Write 28 blocks to EEPROM, respond OK Phone: Poll byte 0 until 0xFF Phone: Write WRITE_PATTERN (index=2, ...) → SRAM MCU: Write 28 blocks to EEPROM, respond OK Phone: Poll byte 0 until 0xFF Phone: Write SYNC_END → SRAM MCU: Recalculate header CRC, set active=0, reprogram LP5562, respond OK Phone: Poll byte 0 until 0xFF → done, show success ``` **Timing**: ~150ms per pattern (28 EEPROM blocks × 5ms each + I2C overhead). 9 patterns ≈ 1.4s of EEPROM writes. Total transfer including NFC overhead: under 3 seconds. ### Read (Implant → Phone): Library Download Phone-driven — each READ_NEXT is an explicit request for the next pattern. ``` Phone: Write READ_LIBRARY → SRAM MCU: Read header, respond with [pattern_count, active, current, mode] Phone: Poll byte 0 until 0xFF, read header info Phone: Write READ_NEXT (seq=1) → SRAM MCU: Read pattern 0 from EEPROM, write 112B to response payload Phone: Poll byte 0 until 0xFF, read pattern 0 Phone: Write READ_NEXT (seq=2) → SRAM MCU: Read pattern 1 from EEPROM, write 112B to response payload Phone: Poll byte 0 until 0xFF, read pattern 1 (repeat until pattern_count exhausted) ``` **Timing**: EEPROM reads are fast (~1ms per block). Full 9-pattern read-back under 1 second. ### Single-Pattern Update For quick edits without replacing the whole library: ``` Phone: Write WRITE_PATTERN (index=2, 112B) → SRAM MCU: Write to EEPROM, update header CRC If index == active pattern: reprogram LP5562 immediately Respond OK ``` ## MCU Processing Flow ### M6 Idle Loop ```rust loop { delay_ms(200); if fd_pin.is_low() { process_sram_command(&mut ntag, &mut lp5562, &mut delay); } } ``` ### Command Processing 1. Read SRAM bytes 0-5 (header) 2. Validate command ID (0x01-0x08) 3. CRC-16 check over header + payload — if bad, write `BAD_CRC` response 4. Read remaining payload bytes based on length field 5. Execute command (see per-command logic below) 6. Write response to SRAM 7. Return to polling ### Per-Command Logic **SYNC_START (0x05)**: - Store target `pattern_count`, `led_current`, `led_mode` in local vars - Write new XBLK header with count=0 (partial header, CRC updated at SYNC_END) - Reset internal write index to 0 **WRITE_PATTERN (0x01)** (during sync or standalone): - Validate index < 9 - Write 28 blocks to EEPROM at offset `16 + (index × 112)`, verify each block - If standalone (not in SYNC): update header pattern count and CRC - If `index == active_pattern`: reprogram LP5562 **SYNC_END (0x06)**: - Update header: set pattern_count, recalculate CRC-16 over all data - Set active_pattern = 0 - Read pattern 0, reprogram LP5562 - Clear sync state **GET_STATUS (0x02)**: - Read XBLK header from EEPROM - Return `[0x01, count, active, current, mode]` **SET_ACTIVE (0x03)**: - Validate index < pattern_count - Update header active_pattern byte (single EEPROM block write) - Read new pattern, reprogram LP5562 **READ_LIBRARY (0x07)**: - Read XBLK header - Store pattern_count for READ_NEXT iteration - Reset read index to 0 - Return `[count, active, current, mode]` **READ_NEXT (0x08)**: - Read pattern at current read index from EEPROM - Return 112 bytes of pattern data - Increment read index ## I2C Bus Sharing No special handling required. LP5562 engines run autonomously on the chip's internal oscillator after programming — no ongoing I2C traffic. The MCU switches between NTAG5 (0x54) and LP5562 (0x30) by address as needed. No engine pause necessary. ## Error Handling - **Bad CRC**: Respond with `BAD_CRC` (0x01), discard command - **Unknown command**: Respond with `BAD_CMD` (0x02) - **EEPROM write failure**: Respond with `EEPROM_FAIL` (0x03), library may be in partial state - **Invalid pattern index**: Respond with `INVALID_INDEX` (0x04) - **Phone timeout**: If phone stops sending during SYNC, library is partial. Next SYNC_START will erase and restart. No persistent corruption — header CRC won't match partial data, so boot falls back to hardcoded patterns. - **Garbage in SRAM**: Bad command ID or CRC → error response, resume polling. Each command is self-contained, no state to corrupt. ## Phone-Side Polling After writing a command to SRAM, the phone polls byte 0 via ISO15693 READ SINGLE BLOCK: - If byte 0 != 0xFF → MCU hasn't responded yet, poll again - If byte 0 == 0xFF → response ready, read full response - Sequence number in response must match sent sequence — prevents reading stale responses - Timeout after 2 seconds per command → show error to user ## Implementation Phases 1. **NTAG5 SRAM driver** — `src/ntag5/sram.rs`: block read/write at 0xF8-0xFF 2. **FD pin setup** — Session register config at boot, GPIO input on A1 3. **Command parser** — Read SRAM, validate header/CRC, dispatch 4. **Command handlers** — WRITE_PATTERN, GET_STATUS, SET_ACTIVE first 5. **Streaming sync** — SYNC_START/END, READ_LIBRARY/NEXT 6. **Testing** — PCSC reader with Python test script, then VivoKey RawNFC app