- 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>
8.0 KiB
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
- Read SRAM bytes 0-5 (header)
- Validate command ID (0x01-0x08)
- CRC-16 check over header + payload — if bad, write
BAD_CRCresponse - Read remaining payload bytes based on length field
- Execute command (see per-command logic below)
- Write response to SRAM
- Return to polling
Per-Command Logic
SYNC_START (0x05):
- Store target
pattern_count,led_current,led_modein 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
- NTAG5 SRAM driver —
src/ntag5/sram.rs: block read/write at 0xF8-0xFF - FD pin setup — Session register config at boot, GPIO input on A1
- Command parser — Read SRAM, validate header/CRC, dispatch
- Command handlers — WRITE_PATTERN, GET_STATUS, SET_ACTIVE first
- Streaming sync — SYNC_START/END, READ_LIBRARY/NEXT
- Testing — PCSC reader with Python test script, then VivoKey RawNFC app