Add hall sensor circuit design, NTAG5 config check tool, I2C bus docs

- Hall sensor + LP5562 EN wired-AND circuit design (DRV5032FB/FE,
  1M pull-up, open-drain topology) with three interaction modes:
  boot-time recovery, tap-to-swap, hold-to-confirm recovery
- NTAG5 config check tool (tools/ntag5_config_check.py) using
  uFCoder library for ISO15693 transparent mode via uFR Zero reader.
  Supports inventory + addressed mode for multi-tag fields.
- Updated DEVELOPMENT_PLAN with I2C bus management notes for
  LP5562 (0x30) + NTAG5Link (0x54) shared bus
- Updated README with wired-AND hardware diagram, hall sensor
  interaction section, and updated wiring table
- Updated CLAUDE.md with LP5562 EN wired-AND topology docs
- Updated STATUS.md with hall sensor decisions and hardware inventory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-03 14:06:11 -08:00
parent e4195d2583
commit db492050e3
8 changed files with 874 additions and 50 deletions

BIN
tools/libuFCoder-x86_64.so Executable file

Binary file not shown.

530
tools/ntag5_config_check.py Normal file
View File

@@ -0,0 +1,530 @@
#!/usr/bin/env python3
"""Read NTAG5Link configuration via uFR Zero reader (read-only).
Uses the Digital Logic uFCoder library in ISO15693 transparent mode
to send NXP custom commands for reading config registers.
Supports multiple tags in the field: runs INVENTORY first, then
reads each tag using addressed mode.
Usage: python3 ntag5_config_check.py [/dev/ttyUSBx]
"""
import ctypes
import sys
import os
# ---------------------------------------------------------------------------
# Load uFCoder library
# ---------------------------------------------------------------------------
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LIB_PATH = os.path.join(SCRIPT_DIR, "libuFCoder-x86_64.so")
if not os.path.exists(LIB_PATH):
print(f"ERROR: uFCoder library not found at {LIB_PATH}")
sys.exit(1)
ufr = ctypes.cdll.LoadLibrary(LIB_PATH)
# ---------------------------------------------------------------------------
# uFCoder function signatures
# ---------------------------------------------------------------------------
ufr.ReaderOpenEx.argtypes = [ctypes.c_uint32, ctypes.c_char_p, ctypes.c_uint32, ctypes.c_char_p]
ufr.ReaderOpenEx.restype = ctypes.c_uint32
ufr.ReaderClose.argtypes = []
ufr.ReaderClose.restype = ctypes.c_uint32
ufr.card_transceive_mode_start.argtypes = [ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32]
ufr.card_transceive_mode_start.restype = ctypes.c_uint32
ufr.card_transceive_mode_stop.argtypes = []
ufr.card_transceive_mode_stop.restype = ctypes.c_uint32
ufr.uart_transceive.argtypes = [
ctypes.POINTER(ctypes.c_uint8), # send_data
ctypes.c_uint8, # send_len
ctypes.POINTER(ctypes.c_uint8), # rcv_data
ctypes.c_uint32, # bytes_to_receive
ctypes.POINTER(ctypes.c_uint32), # rcv_len
]
ufr.uart_transceive.restype = ctypes.c_uint32
# UFR status codes
UFR_OK = 0
# ---------------------------------------------------------------------------
# ISO15693 flags and commands
# ---------------------------------------------------------------------------
# Request flags
ISO_FLAG_SUB_CARRIER = (1 << 0)
ISO_FLAG_DATA_RATE = (1 << 1)
ISO_FLAG_INVENTORY = (1 << 2)
ISO_FLAG_PROTOCOL_EXT = (1 << 3)
# When INVENTORY flag is NOT set:
ISO_FLAG_SELECT = (1 << 4)
ISO_FLAG_ADDRESS = (1 << 5)
ISO_FLAG_OPTION = (1 << 6)
# When INVENTORY flag IS set:
ISO_FLAG_AFI = (1 << 4)
ISO_FLAG_NB_SLOTS_1 = (1 << 5) # single slot
# Standard ISO15693 commands
ISO_CMD_INVENTORY = 0x01
ISO_CMD_STAY_QUIET = 0x02
ISO_CMD_SYSTEM_INFO = 0x2B
# NXP custom commands
NXP_CMD_SYSTEM_INFO = 0xAB
NXP_CMD_READ_CONFIG = 0xC0
NXP_CMD_MANUF_CODE_NXP = 0x04
# ---------------------------------------------------------------------------
# NXP NTAG5Link config constants (from ntag5sensor)
# ---------------------------------------------------------------------------
# Config addresses
NXP_CONFIG_ADDR_CONFIG = 0x37
NXP_CONFIG_ADDR_EH_CONFIG = 0x3D
# Config byte 0 flags
NXP_CONFIG_0_AUTO_STANDBY_MODE_EN = (1 << 0)
NXP_CONFIG_0_LOCK_SESSION_REG = (1 << 1)
NXP_CONFIG_0_EH_MODE_MASK = (3 << 2)
NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH = (2 << 2)
NXP_CONFIG_0_EH_MODE_HIGH_FIELD_STRENGTH = (3 << 2)
NXP_CONFIG_0_SRAM_COPY_EN = (1 << 7)
# Config byte 1 flags
NXP_CONFIG_1_PT_TRANSFER_DIR = (1 << 0)
NXP_CONFIG_1_SRAM_ENABLE = (1 << 1)
NXP_CONFIG_1_ARBITER_MASK = (3 << 2)
NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH = (2 << 2)
NXP_CONFIG_1_USE_CASE_MASK = (3 << 4)
NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE = (0 << 4)
NXP_CONFIG_1_EH_ARBITER_MODE_EN = (1 << 7)
# Config byte 2 flags
NXP_CONFIG_2_GPIO0_SLEW_RATE = (1 << 0)
NXP_CONFIG_2_GPIO1_SLEW_RATE = (1 << 1)
NXP_CONFIG_2_LOCK_BLOCK_COMMAND_SUPPORTED = (1 << 2)
NXP_CONFIG_2_EXTENDED_COMMANDS_SUPPORTED = (1 << 3)
NXP_CONFIG_2_GPIO0_PAD_MASK = (3 << 4)
NXP_CONFIG_2_GPIO1_PAD_MASK = (3 << 6)
# EH config flags
NXP_EH_CONFIG_EH_ENABLE = (1 << 0)
NXP_EH_CONFIG_VOUT_V_MASK = (3 << 1)
NXP_EH_CONFIG_DISABLE_POWER_CHECK = (1 << 3)
NXP_EH_CONFIG_VOUT_I_MASK = (7 << 4)
# Lookup tables
EH_V_SEL = {0: "1.8V", 1: "2.4V", 2: "3.0V", 3: "RFU"}
EH_I_SEL = {0: "0.4mA", 1: "0.6mA", 2: "1.4mA", 3: "2.7mA",
4: "4.0mA", 5: "6.5mA", 6: "9.0mA", 7: "12.5mA"}
ARBITER_MAP = {0: "normal", 1: "sram_mirror", 2: "sram_passthrough", 3: "sram_phdc"}
USE_CASE_MAP = {0: "i2c_slave", 1: "i2c_master", 2: "gpio_pwm", 3: "tristate"}
GPIO_PAD_MAP = {0: "disabled", 1: "plain_pullup", 2: "plain", 3: "plain_pulldown"}
EH_MODE_MAP = {0: "rfu0", 1: "rfu1", 2: "low_field_strength", 3: "high_field_strength"}
ED_CONFIG_MAP = {0: "disable", 1: "nfc_field_detect", 2: "pwm",
3: "i2c_to_nfc_passthrough", 4: "nfc_to_i2c_passthrough",
5: "arbiter_lock", 6: "ndef_msg_tlv_length", 7: "standby_mode",
8: "write_cmd_indication", 9: "read_cmd_indication",
10: "start_of_command_indication", 11: "read_from_synch_block",
12: "write_to_synch_block", 13: "software_interrupt"}
INTERFACE_MAP = {0: "nfc_only", 1: "gpio", 2: "rfu", 3: "gpio_i2c"}
# ---------------------------------------------------------------------------
# xblink expected configuration
# ---------------------------------------------------------------------------
EXPECTED = {
"eh_mode": "low_field_strength",
"use_case": "i2c_slave",
"sram_enabled": True,
"arbiter_mode": "sram_passthrough",
"eh_enable": True,
"eh_voltage": "3.0V",
}
# ---------------------------------------------------------------------------
# Low-level transceive
# ---------------------------------------------------------------------------
def iso15693_transceive(cmd_bytes, expected_response_len=32):
"""Send raw ISO15693 command and return response bytes (flags stripped)."""
cmd = (ctypes.c_uint8 * len(cmd_bytes))(*cmd_bytes)
rcv = (ctypes.c_uint8 * 256)()
rcv_len = ctypes.c_uint32(0)
status = ufr.uart_transceive(cmd, len(cmd_bytes), rcv, expected_response_len, ctypes.byref(rcv_len))
if status != UFR_OK:
if rcv_len.value > 0:
data = bytes(rcv[:rcv_len.value])
if data[0] == 0x00: # Response flags OK
return data[1:]
else:
print(f" WARNING: Response flags = 0x{data[0]:02X} (error)")
return data[1:]
print(f" ERROR: uart_transceive failed, status=0x{status:04X}, rcv_len={rcv_len.value}")
return None
data = bytes(rcv[:rcv_len.value])
if len(data) > 0 and data[0] != 0x00:
print(f" WARNING: Response flags = 0x{data[0]:02X}")
return data[1:] if len(data) > 1 else data
# ---------------------------------------------------------------------------
# ISO15693 inventory
# ---------------------------------------------------------------------------
def inventory():
"""Run ISO15693 INVENTORY to discover all tags in the field.
Returns list of 8-byte UIDs (as bytes, LSB first as received on air).
"""
# Single-slot inventory: flags = data_rate | inventory | single_slot
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_INVENTORY | ISO_FLAG_NB_SLOTS_1
cmd = [flags, ISO_CMD_INVENTORY, 0x00] # mask_length=0 (no mask)
# Response: flags(1) + DSFID(1) + UID(8) = 10 bytes
data = iso15693_transceive(cmd, 10)
if data is None:
return []
uids = []
if len(data) >= 9:
dsfid = data[0]
uid = data[1:9] # 8 bytes, LSB first
uids.append(uid)
# TODO: for multi-slot (16 slots), would iterate and collect multiple UIDs.
# Single-slot works when only one tag is present; with multiple tags,
# collisions occur and we'd need anti-collision. For now this covers
# the primary use case (one NTAG5 Click on the reader).
return uids
# ---------------------------------------------------------------------------
# Addressed config reads
# ---------------------------------------------------------------------------
def read_config_block_addressed(uid, address, num_blocks=1):
"""Read NTAG5Link config block via NXP READ_CONFIG in addressed mode.
Args:
uid: 8-byte UID (LSB first, as returned by inventory)
address: config block address (e.g. 0x37)
num_blocks: number of 4-byte blocks to read
"""
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS
cmd = [flags, NXP_CMD_READ_CONFIG, NXP_CMD_MANUF_CODE_NXP]
cmd.extend(uid) # 8-byte UID
cmd.extend([address, num_blocks - 1])
# Response: 1 byte flags + num_blocks * 4 bytes data
return iso15693_transceive(cmd, 1 + num_blocks * 4)
def read_config_block_nonaddressed(address, num_blocks=1):
"""Read NTAG5Link config block in non-addressed mode (single tag only)."""
cmd = [ISO_FLAG_DATA_RATE, NXP_CMD_READ_CONFIG, NXP_CMD_MANUF_CODE_NXP,
address, num_blocks - 1]
return iso15693_transceive(cmd, 1 + num_blocks * 4)
def get_system_info_addressed(uid):
"""Get ISO15693 system info in addressed mode."""
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS
cmd = [flags, ISO_CMD_SYSTEM_INFO]
cmd.extend(uid)
# Response varies; request generous buffer
return iso15693_transceive(cmd, 32)
def get_nxp_system_info_addressed(uid):
"""Get NXP-specific system info in addressed mode."""
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS
cmd = [flags, NXP_CMD_SYSTEM_INFO, NXP_CMD_MANUF_CODE_NXP]
cmd.extend(uid)
return iso15693_transceive(cmd, 32)
# ---------------------------------------------------------------------------
# Decoders
# ---------------------------------------------------------------------------
def decode_uid(uid_bytes):
"""Format UID for display (MSB first, colon-separated)."""
return ":".join(f"{b:02X}" for b in reversed(uid_bytes))
def decode_system_info(data):
"""Decode ISO15693 GET_SYSTEM_INFO response."""
if data is None or len(data) < 9:
return None
info_flags = data[0]
uid = data[1:9][::-1] # reverse to MSB first
res = {"uid": uid, "uid_str": ":".join(f"{b:02X}" for b in uid)}
idx = 9
if info_flags & 0x01 and idx < len(data): # DSFID
res["dsfid"] = data[idx]
idx += 1
if info_flags & 0x02 and idx < len(data): # AFI
res["afi"] = data[idx]
idx += 1
if info_flags & 0x04 and idx + 1 < len(data): # Memory size
res["num_blocks"] = data[idx] + 1
res["block_size"] = (data[idx + 1] & 0x1F) + 1
res["memory_bytes"] = res["num_blocks"] * res["block_size"]
idx += 2
if info_flags & 0x08 and idx < len(data): # IC reference
res["ic_ref"] = data[idx]
idx += 1
return res
def decode_nxp_system_info(data):
"""Decode NXP GET_NXP_SYSTEM_INFO response."""
if data is None or len(data) < 7:
return None
res = {
"pp_pointer": data[0],
"lock_bits_raw": data[2],
}
if len(data) >= 7:
b3 = data[6]
res["interface"] = INTERFACE_MAP.get((b3 >> 5) & 0x03, "unknown")
res["num_keys"] = b3 & 0x0F
return res
def decode_config(data):
"""Decode CONFIG block (address 0x37) — 4 bytes."""
if data is None or len(data) < 3:
return None
b0, b1, b2 = data[0], data[1], data[2]
return {
"raw": [f"0x{b:02X}" for b in data[:4]] if len(data) >= 4 else [f"0x{b:02X}" for b in data],
"auto_standby": bool(b0 & NXP_CONFIG_0_AUTO_STANDBY_MODE_EN),
"lock_session_reg": bool(b0 & NXP_CONFIG_0_LOCK_SESSION_REG),
"eh_mode": EH_MODE_MAP.get((b0 >> 2) & 0x03, "unknown"),
"sram_copy_en": bool(b0 & NXP_CONFIG_0_SRAM_COPY_EN),
"pt_transfer_dir": "reader_to_tag" if (b1 & NXP_CONFIG_1_PT_TRANSFER_DIR) else "tag_to_reader",
"sram_enabled": bool(b1 & NXP_CONFIG_1_SRAM_ENABLE),
"arbiter_mode": ARBITER_MAP.get((b1 >> 2) & 0x03, "unknown"),
"use_case": USE_CASE_MAP.get((b1 >> 4) & 0x03, "unknown"),
"eh_arbiter_mode_en": bool(b1 & NXP_CONFIG_1_EH_ARBITER_MODE_EN),
"gpio0_slew_fast": bool(b2 & NXP_CONFIG_2_GPIO0_SLEW_RATE),
"gpio1_slew_fast": bool(b2 & NXP_CONFIG_2_GPIO1_SLEW_RATE),
"lock_block_supported": bool(b2 & NXP_CONFIG_2_LOCK_BLOCK_COMMAND_SUPPORTED),
"ext_commands_supported": bool(b2 & NXP_CONFIG_2_EXTENDED_COMMANDS_SUPPORTED),
"gpio0_pad_in": GPIO_PAD_MAP.get((b2 >> 4) & 0x03, "unknown"),
"gpio1_pad_in": GPIO_PAD_MAP.get((b2 >> 6) & 0x03, "unknown"),
}
def decode_eh_config(data):
"""Decode EH/ED CONFIG block (address 0x3D) — 4 bytes."""
if data is None or len(data) < 1:
return None
eh = data[0]
ed = data[2] if len(data) > 2 else 0
return {
"raw": [f"0x{b:02X}" for b in data[:4]] if len(data) >= 4 else [f"0x{b:02X}" for b in data],
"eh_enable": bool(eh & NXP_EH_CONFIG_EH_ENABLE),
"eh_voltage": EH_V_SEL.get((eh >> 1) & 0x03, "unknown"),
"disable_power_check": bool(eh & NXP_EH_CONFIG_DISABLE_POWER_CHECK),
"eh_current": EH_I_SEL.get((eh >> 4) & 0x07, "unknown"),
"ed_config": ED_CONFIG_MAP.get(ed & 0x0F, "unknown"),
}
# ---------------------------------------------------------------------------
# Verification
# ---------------------------------------------------------------------------
def check_against_expected(config, eh_config):
"""Compare config against xblink expected values."""
print("\n--- xblink Config Check ---")
checks = [
("EH mode", config.get("eh_mode"), EXPECTED["eh_mode"]),
("Use case", config.get("use_case"), EXPECTED["use_case"]),
("SRAM enabled", config.get("sram_enabled"), EXPECTED["sram_enabled"]),
("Arbiter mode", config.get("arbiter_mode"), EXPECTED["arbiter_mode"]),
("EH enable", eh_config.get("eh_enable"), EXPECTED["eh_enable"]),
("EH voltage", eh_config.get("eh_voltage"), EXPECTED["eh_voltage"]),
]
all_ok = True
for name, actual, expected in checks:
match = actual == expected
symbol = "OK" if match else "MISMATCH"
if not match:
all_ok = False
print(f" [{symbol:8s}] {name:20s}: {actual} (expected: {expected})")
if all_ok:
print("\n All config values match xblink requirements!")
else:
print("\n Some values need updating. Use ntag5sensor tooling to reconfigure.")
return all_ok
def read_and_check_tag(uid):
"""Read all config from one tag and verify against xblink requirements.
Args:
uid: 8-byte UID (LSB first) or None for non-addressed mode
"""
use_addressed = uid is not None
uid_str = decode_uid(uid) if uid else "(non-addressed)"
# --- ISO15693 system info ---
if use_addressed:
print(f"\nISO15693 System Info...")
sys_data = get_system_info_addressed(uid)
sys_info = decode_system_info(sys_data)
if sys_info:
print(f" Memory: {sys_info.get('memory_bytes', '?')} bytes "
f"({sys_info.get('num_blocks', '?')} blocks x {sys_info.get('block_size', '?')} bytes)")
if "ic_ref" in sys_info:
print(f" IC ref: 0x{sys_info['ic_ref']:02X}")
else:
print(" Failed to read system info.")
# --- NXP system info ---
if use_addressed:
print(f"\nNXP System Info...")
nxp_data = get_nxp_system_info_addressed(uid)
nxp_info = decode_nxp_system_info(nxp_data)
if nxp_info:
print(f" Interface: {nxp_info.get('interface', '?')}")
print(f" Num keys: {nxp_info.get('num_keys', '?')}")
else:
print(" Failed to read NXP system info (may not be an NXP tag).")
# --- CONFIG block (0x37) ---
print(f"\nCONFIG block (0x37)...")
if use_addressed:
config_data = read_config_block_addressed(uid, NXP_CONFIG_ADDR_CONFIG)
else:
config_data = read_config_block_nonaddressed(NXP_CONFIG_ADDR_CONFIG)
config = decode_config(config_data)
if config:
print(f" Raw bytes: {config['raw']}")
print(f" EH mode: {config['eh_mode']}")
print(f" Use case: {config['use_case']}")
print(f" SRAM enabled: {config['sram_enabled']}")
print(f" Arbiter mode: {config['arbiter_mode']}")
print(f" Auto standby: {config['auto_standby']}")
print(f" SRAM copy: {config['sram_copy_en']}")
print(f" PT transfer dir: {config['pt_transfer_dir']}")
print(f" EH arbiter mode: {config['eh_arbiter_mode_en']}")
print(f" GPIO0 pad: {config['gpio0_pad_in']}")
print(f" GPIO1 pad: {config['gpio1_pad_in']}")
print(f" Lock block: {config['lock_block_supported']}")
print(f" Ext commands: {config['ext_commands_supported']}")
else:
print(" Failed to read config block.")
# --- EH/ED CONFIG block (0x3D) ---
print(f"\nEH/ED CONFIG block (0x3D)...")
if use_addressed:
eh_data = read_config_block_addressed(uid, NXP_CONFIG_ADDR_EH_CONFIG)
else:
eh_data = read_config_block_nonaddressed(NXP_CONFIG_ADDR_EH_CONFIG)
eh_config = decode_eh_config(eh_data)
if eh_config:
print(f" Raw bytes: {eh_config['raw']}")
print(f" EH enable: {eh_config['eh_enable']}")
print(f" EH voltage: {eh_config['eh_voltage']}")
print(f" EH current limit: {eh_config['eh_current']}")
print(f" Power check: {'disabled' if eh_config['disable_power_check'] else 'enabled'}")
print(f" ED config: {eh_config['ed_config']}")
else:
print(" Failed to read EH config block.")
# --- Check against xblink expected values ---
if config and eh_config:
return check_against_expected(config, eh_config)
return False
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
print(f"Connecting to uFR Zero on {port}...")
# Open reader: reader_type=1 (uFR @ 1Mbps), port_interface=1 (serial/CP210x)
status = ufr.ReaderOpenEx(1, port.encode(), 1, None)
if status != UFR_OK:
print(f"ERROR: Failed to open reader, status=0x{status:04X}")
sys.exit(1)
import time
time.sleep(0.5)
print("Reader connected.")
# Enter ISO15693 transparent mode (tx_crc=1, rx_crc=1, rf_timeout=10000us, uart_timeout=500ms)
status = ufr.card_transceive_mode_start(1, 1, 10000, 500)
if status != UFR_OK:
print(f"ERROR: Failed to enter transceive mode, status=0x{status:04X}")
ufr.ReaderClose()
sys.exit(1)
print("ISO15693 transparent mode active.")
# --- Step 1: Inventory ---
print("\n=== ISO15693 Inventory ===")
uids = inventory()
if not uids:
print(" No tags found. Trying non-addressed mode as fallback...")
print("\n=== Tag (non-addressed) ===")
read_and_check_tag(None)
else:
print(f" Found {len(uids)} tag(s):")
for i, uid in enumerate(uids):
print(f" [{i}] UID: {decode_uid(uid)}")
# --- Step 2: Read each tag ---
all_ok = True
for i, uid in enumerate(uids):
print(f"\n{'='*50}")
print(f"=== Tag {i}: {decode_uid(uid)} ===")
print(f"{'='*50}")
ok = read_and_check_tag(uid)
if not ok:
all_ok = False
if len(uids) > 1:
print(f"\n{'='*50}")
if all_ok:
print("All tags pass xblink config check.")
else:
print("Some tags have config mismatches.")
# Cleanup
ufr.card_transceive_mode_stop()
ufr.ReaderClose()
print("\nReader closed.")
if __name__ == "__main__":
main()