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:
michael
2026-03-05 15:03:31 -08:00
parent e7bc8834b2
commit acb48902be
6 changed files with 1232 additions and 114 deletions

314
src/ntag5/mod.rs Normal file
View 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)
}
}