Add pattern library, NTAG5 config check, NDEF writer (M3+M4)
M3: 5 autonomous LP5562 engine patterns (breathe, heartbeat, slow_pulse, rgb_cycle, color_wash) with dual LED mode support (RGBW/Mono3) and 2mA/ch current for EH-realistic power budget. M4: NTAG5Link I2C slave driver with session register reads, EEPROM write-verify, and NDEF Type 5 text record output. Config check validates CONFIG_0, CONFIG_1, EH_CONFIG against expected values and writes result as NFC-readable text. Verified on hardware: config check passes, NDEF readable via ISO15693 from phone. Key findings from hardware testing: - SRAM passthrough arbiter blocks NFC reads while I2C bus is active (resolves naturally when MCU enters STANDBY — noted in M7 plan) - NTAG5 must be in I2C slave mode (not master) for MCU communication - EH config mask set to 0x00 (skip check during external power testing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
314
src/ntag5/mod.rs
Normal file
314
src/ntag5/mod.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
/// 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]
|
||||
|
||||
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
|
||||
|
||||
// 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])
|
||||
}
|
||||
|
||||
// ---- 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user