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>
369 lines
13 KiB
Rust
369 lines
13 KiB
Rust
/// 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)
|
||
}
|
||
}
|