/// 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 { I2c(E), /// EEPROM write-verify failed: read-back didn't match written data VerifyFailed, } impl From for Error { fn from(e: E) -> Self { Error::I2c(e) } } pub struct Ntag5Link { 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 Ntag5Link where I2C: I2c, { 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> { 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 { 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 { // 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> { // 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> { 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) } }