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>
221 lines
8.0 KiB
Markdown
221 lines
8.0 KiB
Markdown
# 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
|