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>
This commit is contained in:
michael
2026-03-05 17:40:56 -08:00
parent 43cda8ac3d
commit f437e4e281
4 changed files with 45 additions and 101 deletions

View File

@@ -1,4 +1,4 @@
# xblink Development Guide # xBlink Development Guide
Embedded Rust firmware for a battery-free, NFC-powered LED implant. The NTAG5Link harvests energy from an NFC field and provides EEPROM storage for LED patterns. A SAMD21E MCU reads patterns from EEPROM, programs LP5562 execution engines, then sleeps while the LP5562 autonomously drives RGBW LEDs. Embedded Rust firmware for a battery-free, NFC-powered LED implant. The NTAG5Link harvests energy from an NFC field and provides EEPROM storage for LED patterns. A SAMD21E MCU reads patterns from EEPROM, programs LP5562 execution engines, then sleeps while the LP5562 autonomously drives RGBW LEDs.
@@ -25,7 +25,7 @@ cargo hf2 --release
## Dependencies ## Dependencies
| Crate | Version | Purpose | | Crate | Version | Purpose |
|-------|---------|---------| | -------------- | ------- | -------------------------------------------------------- |
| `xiao_m0` | 0.13 | Board support package (dev board phase) | | `xiao_m0` | 0.13 | Board support package (dev board phase) |
| `panic-halt` | 0.2 | Minimal panic handler for `no_std` | | `panic-halt` | 0.2 | Minimal panic handler for `no_std` |
| `cortex-m` | 0.7 | Cortex-M runtime, `critical-section-single-core` feature | | `cortex-m` | 0.7 | Cortex-M runtime, `critical-section-single-core` feature |
@@ -69,7 +69,7 @@ The LP5562 driver does NOT enforce timing delays internally — the caller is re
Both devices share the same I2C bus (A4/A5). Non-conflicting addresses — no bus arbitration issues. Both devices share the same I2C bus (A4/A5). Non-conflicting addresses — no bus arbitration issues.
| Device | Address | Notes | | Device | Address | Notes |
|--------|---------|-------| | --------- | ------- | ------------------------------------------ |
| LP5562 | 0x30 | ADDR_SEL pins both low (LP5562EVM default) | | LP5562 | 0x30 | ADDR_SEL pins both low (LP5562EVM default) |
| NTAG5Link | 0x54 | Default NTP53x2 I2C slave address | | NTAG5Link | 0x54 | Default NTP53x2 I2C slave address |
@@ -78,7 +78,7 @@ Both devices share the same I2C bus (A4/A5). Non-conflicting addresses — no bu
The MCU accesses NTAG5Link as a standard I2C slave — plain register read/write, NOT the NFC-side ISO15693 custom commands used by the Python ntag5sensor tooling. The MCU accesses NTAG5Link as a standard I2C slave — plain register read/write, NOT the NFC-side ISO15693 custom commands used by the Python ntag5sensor tooling.
| Region | Address Range | Size | Notes | | Region | Address Range | Size | Notes |
|--------|--------------|------|-------| | ------------------ | ----------------- | ---------- | --------------------------------- |
| Session registers | 0x00-0x06 | 7 bytes | Volatile, runtime config | | Session registers | 0x00-0x06 | 7 bytes | Volatile, runtime config |
| Configuration | 0x37-0x3F | Varies | Persistent, EEPROM-backed | | Configuration | 0x37-0x3F | Varies | Persistent, EEPROM-backed |
| EH config | 0x3D | 1 byte | Energy harvesting voltage/current | | EH config | 0x3D | 1 byte | Energy harvesting voltage/current |
@@ -94,7 +94,7 @@ Key config constants (from `../ntag5sensor/vicinity/ntag5link.py`):
## Hardware Wiring (Dev Board) ## Hardware Wiring (Dev Board)
| XIAO Pin | Connection | Function | | XIAO Pin | Connection | Function |
|----------|------------|----------| | --------- | --------------------------- | ----------------------------------------------- |
| A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data | | A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data |
| A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock | | A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock |
| D0/A0 | LP5562 EN line (open-drain) | Wired-AND with hall sensor, pull-up to VCC | | D0/A0 | LP5562 EN line (open-drain) | Wired-AND with hall sensor, pull-up to VCC |
@@ -112,5 +112,5 @@ Key config constants (from `../ntag5sensor/vicinity/ntag5link.py`):
## Sibling Projects ## Sibling Projects
- `../ntag5-samd21-lp562/` — Original LP5562 driver + smoke test (xblink's `src/led/lp5562.rs` is synced from here) - `../ntag5-samd21-lp562/` — Original LP5562 driver + smoke test (xBlink's `src/led/lp5562.rs` is synced from here)
- `../ntag5sensor/` — NTAG5Link command reference (`vicinity/ntag5link.py`), I2C patterns (`vicinity/i2cbase.py`) - `../ntag5sensor/` — NTAG5Link command reference (`vicinity/ntag5link.py`), I2C patterns (`vicinity/i2cbase.py`)

View File

@@ -1,4 +1,4 @@
# xblink # xBlink
NFC-powered LED implant using NTAG5Link, SAMD21E, and LP5562. NFC-powered LED implant using NTAG5Link, SAMD21E, and LP5562.

View File

@@ -35,20 +35,20 @@ Byte Field
0 Command ID (0x01-0x7F) 0 Command ID (0x01-0x7F)
1 Sequence number (0-255, incremented per packet) 1 Sequence number (0-255, incremented per packet)
2-3 Payload length (u16 LE) 2-3 Payload length (u16 LE)
4-5 CRC-16 over bytes 0-3 + payload 4-255 Payload (up to 252 bytes)
6-255 Payload (up to 250 bytes)
``` ```
No application-layer CRC — ISO15693 already provides CRC-16 at the transport layer.
**MCU → Phone (written back to SRAM):** **MCU → Phone (written back to SRAM):**
``` ```
Byte Field Byte Field
0 0xFF (response marker — distinguishes from commands) 0 0xFF (response marker — distinguishes from commands)
1 Echo sequence number 1 Echo sequence number
2 Status (0x00=OK, 0x01=BAD_CRC, 0x02=BAD_CMD, 0x03=EEPROM_FAIL, 0x04=INVALID_INDEX) 2 Status (0x00=OK, 0x02=BAD_CMD, 0x03=EEPROM_FAIL, 0x04=INVALID_INDEX)
3 Payload length 3 Payload length
4-5 CRC-16 over bytes 0-3 + payload 4-255 Response 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). 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).
@@ -143,13 +143,12 @@ loop {
### Command Processing ### Command Processing
1. Read SRAM bytes 0-5 (header) 1. Read SRAM bytes 0-3 (header)
2. Validate command ID (0x01-0x08) 2. Validate command ID (0x01-0x08)
3. CRC-16 check over header + payload — if bad, write `BAD_CRC` response 3. Read remaining payload bytes based on length field
4. Read remaining payload bytes based on length field 4. Execute command (see per-command logic below)
5. Execute command (see per-command logic below) 5. Write response to SRAM
6. Write response to SRAM 6. Return to polling
7. Return to polling
### Per-Command Logic ### Per-Command Logic
@@ -196,12 +195,12 @@ No special handling required. LP5562 engines run autonomously on the chip's inte
## Error Handling ## Error Handling
- **Bad CRC**: Respond with `BAD_CRC` (0x01), discard command
- **Unknown command**: Respond with `BAD_CMD` (0x02) - **Unknown command**: Respond with `BAD_CMD` (0x02)
- **EEPROM write failure**: Respond with `EEPROM_FAIL` (0x03), library may be in partial state - **EEPROM write failure**: Respond with `EEPROM_FAIL` (0x03), library may be in partial state
- **Invalid pattern index**: Respond with `INVALID_INDEX` (0x04) - **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. - **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. - **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 ## Phone-Side Polling
@@ -215,7 +214,7 @@ After writing a command to SRAM, the phone polls byte 0 via ISO15693 READ SINGLE
1. **NTAG5 SRAM driver** `src/ntag5/sram.rs`: block read/write at 0xF8-0xFF 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 2. **FD pin setup** Session register config at boot, GPIO input on A1
3. **Command parser** Read SRAM, validate header/CRC, dispatch 3. **Command parser** Read SRAM, validate header, dispatch
4. **Command handlers** WRITE_PATTERN, GET_STATUS, SET_ACTIVE first 4. **Command handlers** WRITE_PATTERN, GET_STATUS, SET_ACTIVE first
5. **Streaming sync** SYNC_START/END, READ_LIBRARY/NEXT 5. **Streaming sync** SYNC_START/END, READ_LIBRARY/NEXT
6. **Testing** PCSC reader with Python test script, then VivoKey RawNFC app 6. **Testing** PCSC reader with Python test script, then VivoKey RawNFC app

View File

@@ -29,16 +29,15 @@ pub const RESPONSE_MARKER: u8 = 0xFF;
// Status codes // Status codes
pub const STATUS_OK: u8 = 0x00; pub const STATUS_OK: u8 = 0x00;
pub const STATUS_BAD_CRC: u8 = 0x01;
pub const STATUS_BAD_CMD: u8 = 0x02; pub const STATUS_BAD_CMD: u8 = 0x02;
pub const STATUS_EEPROM_FAIL: u8 = 0x03; pub const STATUS_EEPROM_FAIL: u8 = 0x03;
pub const STATUS_INVALID_INDEX: u8 = 0x04; pub const STATUS_INVALID_INDEX: u8 = 0x04;
/// Command header size (cmd + seq + payload_len_u16 + crc_u16). /// Command header size (cmd + seq + payload_len_u16).
pub const CMD_HEADER_SIZE: usize = 6; pub const CMD_HEADER_SIZE: usize = 4;
/// Response header size (marker + seq + status + payload_len + crc_u16). /// Response header size (marker + seq + status + payload_len).
pub const RSP_HEADER_SIZE: usize = 6; pub const RSP_HEADER_SIZE: usize = 4;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mailbox state // Mailbox state
@@ -73,34 +72,11 @@ impl MailboxState {
} }
} }
// ---------------------------------------------------------------------------
// CRC helpers
// ---------------------------------------------------------------------------
/// Compute CRC-16/CCITT-FALSE over the given data, starting from an
/// initial CRC value. This allows continuing a CRC computation across
/// multiple buffers.
fn crc16_continue(init: u16, data: &[u8]) -> u16 {
let mut crc = init;
for &b in data {
crc ^= (b as u16) << 8;
for _ in 0..8 {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
crc &= 0xFFFF;
}
}
crc
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Protocol parse / build // Protocol parse / build
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Parse the 6-byte command header. Returns (cmd, seq, payload_len) or None /// Parse the 4-byte command header. Returns (cmd, seq, payload_len) or None
/// if the buffer is too short or the command ID is out of range. /// if the buffer is too short or the command ID is out of range.
fn parse_header(buf: &[u8]) -> Option<(u8, u8, u16)> { fn parse_header(buf: &[u8]) -> Option<(u8, u8, u16)> {
if buf.len() < CMD_HEADER_SIZE { if buf.len() < CMD_HEADER_SIZE {
@@ -115,20 +91,6 @@ fn parse_header(buf: &[u8]) -> Option<(u8, u8, u16)> {
Some((cmd, seq, payload_len)) Some((cmd, seq, payload_len))
} }
/// Verify CRC over the command packet.
/// CRC is computed over bytes 0-3 (header minus CRC) + payload, then
/// compared to the big-endian CRC in bytes 4-5.
fn verify_crc(buf: &[u8], payload_len: u16) -> bool {
let total = CMD_HEADER_SIZE + payload_len as usize;
if buf.len() < total {
return false;
}
let expected = ((buf[4] as u16) << 8) | buf[5] as u16;
let crc = crc16_continue(0xFFFF, &buf[0..4]);
let crc = crc16_continue(crc, &buf[CMD_HEADER_SIZE..total]);
crc == expected
}
/// Build a response packet into `buf`. Returns the total number of bytes /// Build a response packet into `buf`. Returns the total number of bytes
/// written (padded to a multiple of 4 for SRAM block alignment). /// written (padded to a multiple of 4 for SRAM block alignment).
fn build_response(buf: &mut [u8], seq: u8, status: u8, payload: &[u8]) -> usize { fn build_response(buf: &mut [u8], seq: u8, status: u8, payload: &[u8]) -> usize {
@@ -137,18 +99,10 @@ fn build_response(buf: &mut [u8], seq: u8, status: u8, payload: &[u8]) -> usize
buf[1] = seq; buf[1] = seq;
buf[2] = status; buf[2] = status;
buf[3] = plen as u8; buf[3] = plen as u8;
// CRC placeholder at [4..6], computed below
// Copy payload
buf[RSP_HEADER_SIZE..RSP_HEADER_SIZE + plen].copy_from_slice(payload); buf[RSP_HEADER_SIZE..RSP_HEADER_SIZE + plen].copy_from_slice(payload);
// Compute CRC over bytes 0-3 + payload
let crc = crc16_continue(0xFFFF, &buf[0..4]);
let crc = crc16_continue(crc, payload);
buf[4] = (crc >> 8) as u8;
buf[5] = crc as u8;
// Pad total length to multiple of 4 // Pad total length to multiple of 4
let raw_len = RSP_HEADER_SIZE + plen; let raw_len = RSP_HEADER_SIZE + plen;
let padded = (raw_len + 3) & !3; let padded = (raw_len + 3) & !3;
// Zero padding bytes
for b in buf[raw_len..padded].iter_mut() { for b in buf[raw_len..padded].iter_mut() {
*b = 0; *b = 0;
} }
@@ -522,15 +476,6 @@ where
return Ok(false); return Ok(false);
} }
// Verify CRC
if !verify_crc(&sram, payload_len) {
// Write BAD_CRC response
let mut rsp = [0u8; SRAM_SIZE];
let len = build_response(&mut rsp, seq, STATUS_BAD_CRC, &[]);
ntag.write_sram_blocks(0, &rsp[..len])?;
return Ok(true);
}
let payload = &sram[CMD_HEADER_SIZE..total]; let payload = &sram[CMD_HEADER_SIZE..total];
// Dispatch // Dispatch