Files
xblink/docs/plans/2026-03-05-sram-mailbox-protocol.md
michael f437e4e281 Remove application-layer CRC from SRAM mailbox protocol
ISO15693 already provides CRC-16 at the transport layer, making
the application-layer CRC redundant. Simplifies header from 6 to
4 bytes, removes crc16_continue/verify_crc, and drops STATUS_BAD_CRC.
Also updates design doc and fixes markdown table formatting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:40:56 -08:00

221 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-255 Payload (up to 252 bytes)
```
No application-layer CRC — ISO15693 already provides CRC-16 at the transport layer.
**MCU → Phone (written back to SRAM):**
```
Byte Field
0 0xFF (response marker — distinguishes from commands)
1 Echo sequence number
2 Status (0x00=OK, 0x02=BAD_CMD, 0x03=EEPROM_FAIL, 0x04=INVALID_INDEX)
3 Payload length
4-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-3 (header)
2. Validate command ID (0x01-0x08)
3. Read remaining payload bytes based on length field
4. Execute command (see per-command logic below)
5. Write response to SRAM
6. 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
- **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 error response, resume polling. Each command is self-contained, no state to corrupt.
- **Transport integrity**: ISO15693 provides CRC-16 at the link layer no application-layer CRC needed.
## 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, 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