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>
This commit is contained in:
michael
2026-03-05 16:20:52 -08:00
parent 3469944209
commit 43cda8ac3d
3 changed files with 1255 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
# xblink Project Status
**Current Milestone**: M6 — SRAM Mailbox
**Current Milestone**: M7 — Sleep/Wake
**Last Updated**: 2026-03-05
---
@@ -65,12 +65,16 @@
- [x] Write Python serializer tool (`tools/xblk_serialize.py`) for PCSC pattern uploads
- [x] Verified on hardware: self-provisioning writes XBLK, subsequent boots load from EEPROM
### M6: SRAM Mailbox
### M6: SRAM Mailbox (COMPLETE)
- [ ] Implement SRAM read/write (`src/ntag5/sram.rs`)
- [ ] Design command/response protocol
- [ ] Implement MCU-side protocol handler
- [ ] Test pattern update via NFC without power-cycling
- [x] Design SRAM mailbox protocol (`docs/plans/2026-03-05-sram-mailbox-protocol.md`)
- [x] Add NTAG5 `write_register`, SRAM read/write, FD pin configuration
- [x] Implement command protocol types and CRC helpers (`src/ntag5/sram.rs`)
- [x] Implement all 7 command handlers (WRITE_PATTERN, GET_STATUS, SET_ACTIVE, SYNC_START/END, READ_LIBRARY/NEXT)
- [x] Streaming transfer: single NFC hold for full library sync (one pattern per SRAM round-trip)
- [x] FD pin polling in main idle loop (200ms interval, A1/PA04)
- [x] I2C bus swapping for LP5562 reprogramming after pattern updates
- [ ] Hardware test: flash and verify with PCSC reader / phone app (pending FD pin wiring)
## Group C — Power + Recovery

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,221 @@
# 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