Files
xblink/src/ntag5/mod.rs
michael 8dfe9117af Add SRAM mailbox protocol with all 7 command handlers
Implements the NFC-to-MCU SRAM mailbox protocol in src/ntag5/sram.rs:
- Command parsing with CRC-16/CCITT-FALSE verification
- Response building with 4-byte-aligned SRAM writes
- WRITE_PATTERN: stores 112-byte pattern entries to EEPROM
- GET_STATUS: returns firmware version + library summary
- SET_ACTIVE: changes the active pattern index in EEPROM header
- SYNC_START/SYNC_END: bulk upload with deferred header CRC
- READ_LIBRARY/READ_NEXT: iterative pattern readback
- MailboxState struct for cross-command state tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:16:12 -08:00

369 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// NTAG5Link I2C slave driver for NTP53x2.
///
/// Provides session register reads, EEPROM block read/write,
/// and NDEF Type 5 text record writing.
///
/// I2C protocol (Section 8.3.1.4 of NTP53x2 datasheet):
/// - READ MEMORY: write [BL_AD1, BL_AD0], read N bytes
/// - WRITE MEMORY: write [BL_AD1, BL_AD0, D0, D1, D2, D3]
/// - READ REGISTER: write [BL_AD1, BL_AD0, REGA], read 1 byte
/// - WRITE REGISTER: write [BL_AD1, BL_AD0, REGA, MASK, REGDATA]
pub mod sram;
use embedded_hal::i2c::I2c;
pub const DEFAULT_ADDRESS: u8 = 0x54;
// Session register I2C block addresses (16-bit)
pub const SESSION_CONFIG_REG: u16 = 0x10A1; // CONFIG_0_REG, CONFIG_1_REG, CONFIG_2_REG, RFU
pub const SESSION_EH_CONFIG_REG: u16 = 0x10A7; // EH_CONFIG_REG, RFU
pub const SESSION_I2C_SLAVE_REG: u16 = 0x10A9; // I2C_SLAVE_ADDR_REG, I2C_SLAVE_CONFIG_REG
// SRAM I2C block address range (64 blocks × 4 bytes = 256 bytes)
pub const SRAM_BASE_BLOCK: u16 = 0x10F8;
pub const SRAM_BLOCK_COUNT: u16 = 64;
pub const SRAM_SIZE: usize = 256;
// Session register for ED/FD pin configuration
pub const SESSION_ED_FD_PIN_CFG: u16 = 0x10A3;
// FD pin mode: active on SRAM write by RF, cleared on I2C read
pub const FD_MODE_SRAM_RF_WRITE: u8 = 0x04;
// EEPROM user memory I2C block addresses
// Block 0 = CC (capability container), blocks 1+ = NDEF data
pub const EEPROM_BLOCK_0: u16 = 0x0000;
// Expected config values for xblink
// CONFIG_0: EH_MODE = low field strength (bits 3:2 = 10b)
pub const EXPECTED_CONFIG_0: u8 = 0x08;
// CONFIG_1: SRAM_ENABLE (bit 1) + ARBITER_MODE passthrough (bits 3:2 = 10b) + USE_CASE I2C slave (bits 5:4 = 00b)
pub const EXPECTED_CONFIG_1: u8 = 0x0A;
// EH_CONFIG: skip for now — EH is disabled (0x00) when powered externally.
// Set to 0x75 (EH_ENABLE + 3.0V + 12.5mA) when running from energy harvesting.
pub const EXPECTED_EH_CONFIG: u8 = 0x00;
// Masks for checking — only check the bits we care about
// CONFIG_0: bits 3:2 (EH_MODE) — ignore SRAM_COPY_EN, AUTO_STANDBY, LOCK_SESSION
pub const CONFIG_0_MASK: u8 = 0x0C;
// CONFIG_1: bits 5:4 (USE_CASE) + bits 3:2 (ARBITER) + bit 1 (SRAM_EN)
pub const CONFIG_1_MASK: u8 = 0x3E;
// EH_CONFIG: mask 0x00 — don't check EH config during external power testing
pub const EH_CONFIG_MASK: u8 = 0x00;
#[derive(Debug)]
pub enum Error<E> {
I2c(E),
/// EEPROM write-verify failed: read-back didn't match written data
VerifyFailed,
}
impl<E> From<E> for Error<E> {
fn from(e: E) -> Self {
Error::I2c(e)
}
}
pub struct Ntag5Link<I2C> {
i2c: I2C,
addr: u8,
}
/// Config check result for a single register
pub struct RegCheck {
/// Register name abbreviation (e.g., "C0", "C1", "EH")
pub name: [u8; 2],
/// Actual value read
pub actual: u8,
/// Expected value (after masking)
pub expected: u8,
/// Whether it matches
pub ok: bool,
}
/// Full config check result
pub struct ConfigResult {
pub checks: [RegCheck; 3],
/// True if all checks passed
pub all_ok: bool,
}
impl<I2C, E> Ntag5Link<I2C>
where
I2C: I2c<Error = E>,
{
pub fn new(i2c: I2C, addr: u8) -> Self {
Self { i2c, addr }
}
/// Consume the driver and return the I2C bus.
pub fn release(self) -> I2C {
self.i2c
}
// ---- Low-level I2C commands ----
/// Read N bytes from a 16-bit block address (READ MEMORY command).
/// Used for EEPROM and config memory.
pub fn read_memory(&mut self, block: u16, buf: &mut [u8]) -> Result<(), E> {
let addr_bytes = block.to_be_bytes();
self.i2c.write_read(self.addr, &addr_bytes, buf)
}
/// Write 4 bytes to a 16-bit block address (WRITE MEMORY command).
/// Used for EEPROM writes. Each EEPROM block is 4 bytes.
pub fn write_memory_block(&mut self, block: u16, data: &[u8; 4]) -> Result<(), E> {
let addr_bytes = block.to_be_bytes();
let mut buf = [0u8; 6];
buf[0] = addr_bytes[0];
buf[1] = addr_bytes[1];
buf[2] = data[0];
buf[3] = data[1];
buf[4] = data[2];
buf[5] = data[3];
self.i2c.write(self.addr, &buf)
}
/// Write 4 bytes then read back and verify. Returns Error::VerifyFailed
/// if the read-back doesn't match.
pub fn write_verify_block(
&mut self,
block: u16,
data: &[u8; 4],
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<(), Error<E>> {
self.write_memory_block(block, data)?;
delay.delay_ms(5); // EEPROM write cycle ~5ms
let mut readback = [0u8; 4];
self.read_memory(block, &mut readback)?;
if readback != *data {
return Err(Error::VerifyFailed);
}
Ok(())
}
/// Read a single session register byte (READ REGISTER command).
/// Protocol: write [BL_AD1, BL_AD0, REGA], then read 1 byte.
pub fn read_register(&mut self, block: u16, reg_addr: u8) -> Result<u8, E> {
let addr_bytes = block.to_be_bytes();
let mut buf = [0u8; 1];
self.i2c.write_read(
self.addr,
&[addr_bytes[0], addr_bytes[1], reg_addr],
&mut buf,
)?;
Ok(buf[0])
}
/// Write a single session register byte (WRITE REGISTER command).
/// Protocol: write [BL_AD1, BL_AD0, REGA, MASK, REGDATA].
/// The MASK selects which bits to modify (1 = modify, 0 = keep).
pub fn write_register(&mut self, block: u16, reg_addr: u8, mask: u8, data: u8) -> Result<(), E> {
let addr_bytes = block.to_be_bytes();
self.i2c.write(
self.addr,
&[addr_bytes[0], addr_bytes[1], reg_addr, mask, data],
)
}
// ---- SRAM access ----
/// Read N bytes from SRAM starting at byte offset 0.
/// SRAM is at I2C blocks 0x10F8-0x10FF (64 blocks × 4 bytes = 256 bytes).
/// Always reads from the start of SRAM (block-aligned).
pub fn read_sram(&mut self, buf: &mut [u8]) -> Result<(), E> {
self.read_memory(SRAM_BASE_BLOCK, buf)
}
/// Write data to SRAM starting at block offset from SRAM base.
/// Data length must be a multiple of 4 (NTAG5 block size).
pub fn write_sram_blocks(&mut self, block_offset: u16, data: &[u8]) -> Result<(), E> {
for (i, chunk) in data.chunks(4).enumerate() {
let mut block_data = [0u8; 4];
for (j, &b) in chunk.iter().enumerate() {
block_data[j] = b;
}
self.write_memory_block(SRAM_BASE_BLOCK + block_offset + i as u16, &block_data)?;
}
Ok(())
}
/// Configure FD pin for SRAM-write-by-RF indication.
/// FD goes low when phone writes to SRAM, returns high when MCU reads SRAM.
pub fn configure_fd_sram_write(&mut self) -> Result<(), E> {
// ED_FD_PIN_CFG register index 1 within the session block
// Bits 2:0 control FD output mode
self.write_register(SESSION_ED_FD_PIN_CFG, 0x01, 0x07, FD_MODE_SRAM_RF_WRITE)
}
// ---- Config check ----
/// Read session registers and compare against expected xblink config.
/// Session registers are always readable from I2C, even if config
/// memory is password/AES protected.
pub fn check_config(&mut self) -> Result<ConfigResult, E> {
// Read CONFIG_REG session register: bytes 0,1,2 = CONFIG_0, CONFIG_1, CONFIG_2
let c0 = self.read_register(SESSION_CONFIG_REG, 0)?;
let c1 = self.read_register(SESSION_CONFIG_REG, 1)?;
// Read EH_CONFIG_REG session register: byte 0 = EH_CONFIG
let eh = self.read_register(SESSION_EH_CONFIG_REG, 0)?;
let c0_ok = (c0 & CONFIG_0_MASK) == (EXPECTED_CONFIG_0 & CONFIG_0_MASK);
let c1_ok = (c1 & CONFIG_1_MASK) == (EXPECTED_CONFIG_1 & CONFIG_1_MASK);
let eh_ok = (eh & EH_CONFIG_MASK) == (EXPECTED_EH_CONFIG & EH_CONFIG_MASK);
let all_ok = c0_ok && c1_ok && eh_ok;
Ok(ConfigResult {
checks: [
RegCheck { name: *b"C0", actual: c0, expected: EXPECTED_CONFIG_0, ok: c0_ok },
RegCheck { name: *b"C1", actual: c1, expected: EXPECTED_CONFIG_1, ok: c1_ok },
RegCheck { name: *b"EH", actual: eh, expected: EXPECTED_EH_CONFIG, ok: eh_ok },
],
all_ok,
})
}
// ---- NDEF Type 5 text record ----
/// Write an NDEF Type 5 text record to EEPROM user memory.
///
/// Layout (NFC Forum Type 5 Tag):
/// - Block 0: CC (Capability Container) — 4 bytes
/// - Block 1+: TLV wrapper + NDEF message
///
/// The text record uses "en" language code, UTF-8 encoding.
pub fn write_ndef_text(&mut self, text: &[u8], delay: &mut impl embedded_hal::delay::DelayNs) -> Result<(), Error<E>> {
// CC (block 0) is already present from factory/provisioning — don't overwrite.
// NDEF message TLV:
// [0] 03 = NDEF message TLV type
// [1] len = total NDEF message length
// NDEF record header:
// [2] D1 = MB=1, ME=1, CF=0, SR=1, IL=0, TNF=01 (well-known)
// [3] 01 = type length (1 byte: "T")
// [4] payload_len = 3 + text.len() (status byte + "en" + text)
// [5] 54 = type: "T" (text record)
// NDEF payload:
// [6] 02 = status byte: UTF-8 (bit 7=0), language code length=2
// [7] 65 = 'e'
// [8] 6E = 'n'
// [9..] = text bytes
// After message:
// FE = terminator TLV
let payload_len = 3 + text.len(); // status + "en" + text
let ndef_len = 4 + payload_len; // header(3) + type(1) + payload
let tlv_len = 2 + ndef_len; // TLV type + TLV len + NDEF message
let total_bytes = tlv_len + 1; // + terminator TLV (FE)
// Build the message into a buffer (max ~80 bytes for our use)
let mut msg = [0u8; 80];
if total_bytes > msg.len() {
// Text too long, truncate silently — shouldn't happen for our short messages
return Ok(());
}
let mut i = 0;
msg[i] = 0x03; i += 1; // NDEF TLV type
msg[i] = ndef_len as u8; i += 1; // NDEF TLV length
msg[i] = 0xD1; i += 1; // NDEF record header: MB|ME|SR, TNF=well-known
msg[i] = 0x01; i += 1; // Type length = 1
msg[i] = payload_len as u8; i += 1; // Payload length
msg[i] = b'T'; i += 1; // Type = "T" (text)
msg[i] = 0x02; i += 1; // Status: UTF-8, lang len = 2
msg[i] = b'e'; i += 1;
msg[i] = b'n'; i += 1;
for &b in text {
msg[i] = b;
i += 1;
}
msg[i] = 0xFE; i += 1; // Terminator TLV
// Write to EEPROM in 4-byte blocks starting at block 1
let mut block = 1u16;
let mut offset = 0;
while offset < i {
let mut data = [0u8; 4];
for j in 0..4 {
if offset + j < i {
data[j] = msg[offset + j];
}
}
self.write_verify_block(block, &data, delay)?;
block += 1;
offset += 4;
}
Ok(())
}
/// Format the config check result as a human-readable NDEF text record.
/// Returns the number of bytes written to `buf`.
pub fn format_config_result(result: &ConfigResult, buf: &mut [u8]) -> usize {
let mut i = 0;
// Helper: append a byte slice
fn append(buf: &mut [u8], i: &mut usize, data: &[u8]) {
for &b in data {
if *i < buf.len() {
buf[*i] = b;
*i += 1;
}
}
}
// Helper: append hex byte as 2 ASCII chars
fn hex(buf: &mut [u8], i: &mut usize, val: u8) {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
if *i + 1 < buf.len() {
buf[*i] = HEX[(val >> 4) as usize];
*i += 1;
buf[*i] = HEX[(val & 0x0F) as usize];
*i += 1;
}
}
append(buf, &mut i, b"xblink cfg:");
if result.all_ok {
append(buf, &mut i, b" OK ");
} else {
append(buf, &mut i, b" BAD ");
}
for check in &result.checks {
append(buf, &mut i, &check.name);
append(buf, &mut i, b":");
hex(buf, &mut i, check.actual);
if check.ok {
append(buf, &mut i, b"ok ");
} else {
append(buf, &mut i, b"!=");
hex(buf, &mut i, check.expected);
append(buf, &mut i, b" ");
}
}
i
}
/// Run config check and write result as NDEF text record.
/// Returns Ok(true) if config matches, Ok(false) if mismatch,
/// Err(VerifyFailed) if EEPROM write-verify failed,
/// Err(I2c(e)) if I2C communication failed.
pub fn check_and_write_ndef(&mut self, delay: &mut impl embedded_hal::delay::DelayNs) -> Result<bool, Error<E>> {
let result = self.check_config()?;
let all_ok = result.all_ok;
let mut buf = [0u8; 64];
let len = Self::format_config_result(&result, &mut buf);
self.write_ndef_text(&buf[..len], delay)?;
Ok(all_ok)
}
}