diff --git a/CLAUDE.md b/CLAUDE.md index d6ebb48..0aa4b49 100644 --- a/CLAUDE.md +++ b/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. @@ -24,12 +24,12 @@ cargo hf2 --release ## Dependencies -| Crate | Version | Purpose | -|-------|---------|---------| -| `xiao_m0` | 0.13 | Board support package (dev board phase) | -| `panic-halt` | 0.2 | Minimal panic handler for `no_std` | -| `cortex-m` | 0.7 | Cortex-M runtime, `critical-section-single-core` feature | -| `embedded-hal` | 1.0.0 | Hardware abstraction traits (I2C, GPIO, delay) | +| Crate | Version | Purpose | +| -------------- | ------- | -------------------------------------------------------- | +| `xiao_m0` | 0.13 | Board support package (dev board phase) | +| `panic-halt` | 0.2 | Minimal panic handler for `no_std` | +| `cortex-m` | 0.7 | Cortex-M runtime, `critical-section-single-core` feature | +| `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"`. @@ -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. -| Device | Address | Notes | -|--------|---------|-------| -| LP5562 | 0x30 | ADDR_SEL pins both low (LP5562EVM default) | -| NTAG5Link | 0x54 | Default NTP53x2 I2C slave address | +| Device | Address | Notes | +| --------- | ------- | ------------------------------------------ | +| LP5562 | 0x30 | ADDR_SEL pins both low (LP5562EVM default) | +| NTAG5Link | 0x54 | Default NTP53x2 I2C slave address | ## 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. -| Region | Address Range | Size | Notes | -|--------|--------------|------|-------| -| Session registers | 0x00-0x06 | 7 bytes | Volatile, runtime config | -| Configuration | 0x37-0x3F | Varies | Persistent, EEPROM-backed | -| EH config | 0x3D | 1 byte | Energy harvesting voltage/current | -| SRAM | 0xF8-0xFF | 256 bytes | 64 x 4-byte blocks, volatile | -| EEPROM user memory | blocks 0x00-0x1FF | 2048 bytes | 512 x 4-byte blocks | +| Region | Address Range | Size | Notes | +| ------------------ | ----------------- | ---------- | --------------------------------- | +| Session registers | 0x00-0x06 | 7 bytes | Volatile, runtime config | +| Configuration | 0x37-0x3F | Varies | Persistent, EEPROM-backed | +| EH config | 0x3D | 1 byte | Energy harvesting voltage/current | +| SRAM | 0xF8-0xFF | 256 bytes | 64 x 4-byte blocks, volatile | +| EEPROM user memory | blocks 0x00-0x1FF | 2048 bytes | 512 x 4-byte blocks | Key config constants (from `../ntag5sensor/vicinity/ntag5link.py`): - `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) -| XIAO Pin | Connection | Function | -|----------|------------|----------| -| A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data | -| 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 | -| 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) | +| XIAO Pin | Connection | Function | +| --------- | --------------------------- | ----------------------------------------------- | +| A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data | +| 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 | +| 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) | **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 -- `../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`) diff --git a/README.md b/README.md index 02d37ef..73f6f8b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# xblink +# xBlink NFC-powered LED implant using NTAG5Link, SAMD21E, and LP5562. diff --git a/docs/plans/2026-03-05-sram-mailbox-protocol.md b/docs/plans/2026-03-05-sram-mailbox-protocol.md index f3e9c78..5b2d3c4 100644 --- a/docs/plans/2026-03-05-sram-mailbox-protocol.md +++ b/docs/plans/2026-03-05-sram-mailbox-protocol.md @@ -35,20 +35,20 @@ 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) +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, 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 -4-5 CRC-16 over bytes 0-3 + payload -6-255 Response payload +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). @@ -143,13 +143,12 @@ loop { ### Command Processing -1. Read SRAM bytes 0-5 (header) +1. Read SRAM bytes 0-3 (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 +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 @@ -196,12 +195,12 @@ No special handling required. LP5562 engines run autonomously on the chip's inte ## 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. +- **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 @@ -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 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 5. **Streaming sync** — SYNC_START/END, READ_LIBRARY/NEXT 6. **Testing** — PCSC reader with Python test script, then VivoKey RawNFC app diff --git a/src/ntag5/sram.rs b/src/ntag5/sram.rs index 496801c..248398e 100644 --- a/src/ntag5/sram.rs +++ b/src/ntag5/sram.rs @@ -29,16 +29,15 @@ pub const RESPONSE_MARKER: u8 = 0xFF; // Status codes pub const STATUS_OK: u8 = 0x00; -pub const STATUS_BAD_CRC: u8 = 0x01; pub const STATUS_BAD_CMD: u8 = 0x02; pub const STATUS_EEPROM_FAIL: u8 = 0x03; pub const STATUS_INVALID_INDEX: u8 = 0x04; -/// Command header size (cmd + seq + payload_len_u16 + crc_u16). -pub const CMD_HEADER_SIZE: usize = 6; +/// Command header size (cmd + seq + payload_len_u16). +pub const CMD_HEADER_SIZE: usize = 4; -/// Response header size (marker + seq + status + payload_len + crc_u16). -pub const RSP_HEADER_SIZE: usize = 6; +/// Response header size (marker + seq + status + payload_len). +pub const RSP_HEADER_SIZE: usize = 4; // --------------------------------------------------------------------------- // 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 // --------------------------------------------------------------------------- -/// 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. fn parse_header(buf: &[u8]) -> Option<(u8, u8, u16)> { if buf.len() < CMD_HEADER_SIZE { @@ -115,20 +91,6 @@ fn parse_header(buf: &[u8]) -> Option<(u8, u8, u16)> { 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 /// written (padded to a multiple of 4 for SRAM block alignment). 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[2] = status; 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); - // 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 let raw_len = RSP_HEADER_SIZE + plen; let padded = (raw_len + 3) & !3; - // Zero padding bytes for b in buf[raw_len..padded].iter_mut() { *b = 0; } @@ -522,15 +476,6 @@ where 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]; // Dispatch