Files
xblink/docs/plans/2026-03-05-sram-mailbox-protocol.md
michael 43cda8ac3d Add M6 SRAM mailbox design doc, implementation plan, mark M6 complete
- Protocol design: streaming single-hold NFC transfer, 7 commands
- Implementation plan: 7 tasks for subagent-driven development
- STATUS.md: M6 marked complete, current milestone now M7

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

8.0 KiB
Raw Blame History

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

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 driversrc/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