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:
52
CLAUDE.md
52
CLAUDE.md
@@ -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.
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ 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 |
|
||||||
| `embedded-hal` | 1.0.0 | Hardware abstraction traits (I2C, GPIO, delay) |
|
| `embedded-hal` | 1.0.0 | Hardware abstraction traits (I2C, GPIO, delay) |
|
||||||
|
|
||||||
When porting to bare SAMD21E: replace `xiao_m0` with `atsamd-hal = { version = "0.17", features = ["samd21e"] }` and add `cortex-m-rt = "0.7"`.
|
When porting to bare SAMD21E: replace `xiao_m0` with `atsamd-hal = { version = "0.17", features = ["samd21e"] }` and add `cortex-m-rt = "0.7"`.
|
||||||
|
|
||||||
@@ -68,22 +68,22 @@ 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 |
|
||||||
|
|
||||||
## NTAG5Link I2C Register Map (MCU-side)
|
## NTAG5Link I2C Register Map (MCU-side)
|
||||||
|
|
||||||
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 |
|
||||||
| SRAM | 0xF8-0xFF | 256 bytes | 64 x 4-byte blocks, volatile |
|
| SRAM | 0xF8-0xFF | 256 bytes | 64 x 4-byte blocks, volatile |
|
||||||
| EEPROM user memory | blocks 0x00-0x1FF | 2048 bytes | 512 x 4-byte blocks |
|
| EEPROM user memory | blocks 0x00-0x1FF | 2048 bytes | 512 x 4-byte blocks |
|
||||||
|
|
||||||
Key config constants (from `../ntag5sensor/vicinity/ntag5link.py`):
|
Key config constants (from `../ntag5sensor/vicinity/ntag5link.py`):
|
||||||
- `CONFIG_1_USE_CASE_CONF_I2C_SLAVE = 0 << 4` — NTAG5 as I2C slave (our mode)
|
- `CONFIG_1_USE_CASE_CONF_I2C_SLAVE = 0 << 4` — NTAG5 as I2C slave (our mode)
|
||||||
@@ -93,13 +93,13 @@ 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 |
|
||||||
| TBD (EIC) | Hall sensor output | EIC wake + wired-AND to LP5562 EN line |
|
| TBD (EIC) | Hall sensor output | EIC wake + wired-AND to LP5562 EN line |
|
||||||
| TBD (EIC) | NTAG5 FD pin | Field detect / SRAM write indication (EIC wake) |
|
| TBD (EIC) | NTAG5 FD pin | Field detect / SRAM write indication (EIC wake) |
|
||||||
|
|
||||||
**LP5562 EN wired-AND**: D0/A0 (open-drain) and hall sensor (open-drain, active-low) both connect to LP5562 EN with a 1M pull-up. Either can force EN low. Magnet kills LEDs at hardware level regardless of MCU state.
|
**LP5562 EN wired-AND**: D0/A0 (open-drain) and hall sensor (open-drain, active-low) both connect to LP5562 EN with a 1M pull-up. Either can force EN low. Magnet kills LEDs at hardware level regardless of MCU state.
|
||||||
|
|
||||||
@@ -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`)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# xblink
|
# xBlink
|
||||||
|
|
||||||
NFC-powered LED implant using NTAG5Link, SAMD21E, and LP5562.
|
NFC-powered LED implant using NTAG5Link, SAMD21E, and LP5562.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user