Pivot from LP5562 to GPIO-direct PWM LEDs, add NTAG5 EH provisioning
LP5562 requires 2.7V min but NFC energy harvesting produces only 1.8V. New architecture: SAMD21 drives 6 red LEDs directly via TCC0/TCC1 hardware PWM through current-limiting resistors. Key changes: - Add TCC PWM driver (src/led/pwm.rs) for 6 GPIO-direct LED channels - Rewrite pattern engine: software waveform LUTs (sine/triangle/square/ heartbeat) replace LP5562 hardware execution engines - Add XBLK v2 EEPROM format with 16-byte pattern entries + playlist - Add TC4 50Hz ISR for animation, power governor for current budget - Add NTAG5 EH provisioning: persistent config + session trigger - Critical finding: SRAM passthrough in persistent EEPROM blocks all NFC access when MCU unpowered — CONFIG_1 must be session-only - Add provision_eh.py PCSC tool for ACR1552 reader - Update CLAUDE.md and STATUS.md for new architecture Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
166
tools/provision_eh.py
Normal file
166
tools/provision_eh.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Configure NTAG5Link energy harvesting for 1.8V automatic VOUT.
|
||||
|
||||
Writes EH_CONFIG to persistent EEPROM (block 0x3D) so the NTAG5
|
||||
automatically outputs 1.8V when an NFC field is present — powering
|
||||
the SAMD21 MCU without any firmware intervention.
|
||||
|
||||
Also sets ED_CONFIG (FD pin) for NFC-to-I2C SRAM pass-through so
|
||||
the MCU can detect SRAM writes from the phone.
|
||||
|
||||
Usage:
|
||||
# Read current EH config:
|
||||
python provision_eh.py --read
|
||||
|
||||
# Write 1.8V / 6.5mA EH config:
|
||||
python provision_eh.py --write
|
||||
|
||||
# Write with custom current limit:
|
||||
python provision_eh.py --write --current 4.0
|
||||
|
||||
# Also configure CONFIG_0 and CONFIG_1 for xblink:
|
||||
python provision_eh.py --write --full
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add ntag5sensor to path
|
||||
ntag5sensor_path = os.path.join(os.path.dirname(__file__), "..", "..", "ntag5sensor")
|
||||
sys.path.insert(0, ntag5sensor_path)
|
||||
|
||||
from reader.acr1552 import ACR1552
|
||||
from vicinity.iso15693 import ISO15693
|
||||
from vicinity.ntag5link import (
|
||||
Ntag5Link,
|
||||
NXP_EH_CONFIG_EH_VOUT_V_SEL_1_8,
|
||||
NXP_EH_CONFIG_EH_VOUT_V_SEL_2_4,
|
||||
NXP_EH_CONFIG_EH_VOUT_V_SEL_3_0,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_0_4,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_0_6,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_1_4,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_2_7,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_4_0,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_6_5,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_9_0,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_12_5,
|
||||
NXP_ED_CONFIG_NFC_TO_I2C_PASS_THROUGH,
|
||||
NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH,
|
||||
NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH,
|
||||
NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE,
|
||||
)
|
||||
|
||||
# Current limit lookup: string -> constant
|
||||
CURRENT_MAP = {
|
||||
"0.4": NXP_EH_CONFIG_EH_VOUT_I_SEL_0_4,
|
||||
"0.6": NXP_EH_CONFIG_EH_VOUT_I_SEL_0_6,
|
||||
"1.4": NXP_EH_CONFIG_EH_VOUT_I_SEL_1_4,
|
||||
"2.7": NXP_EH_CONFIG_EH_VOUT_I_SEL_2_7,
|
||||
"4.0": NXP_EH_CONFIG_EH_VOUT_I_SEL_4_0,
|
||||
"6.5": NXP_EH_CONFIG_EH_VOUT_I_SEL_6_5,
|
||||
"9.0": NXP_EH_CONFIG_EH_VOUT_I_SEL_9_0,
|
||||
"12.5": NXP_EH_CONFIG_EH_VOUT_I_SEL_12_5,
|
||||
}
|
||||
|
||||
|
||||
def read_config(chip):
|
||||
"""Read and display current EH and general config."""
|
||||
print("=== NTAG5 Configuration ===\n")
|
||||
|
||||
info = chip.get_system_info()
|
||||
print(f"UID: {info['uid'].hex()}")
|
||||
|
||||
config = chip.get_config_info()
|
||||
print(f"\nCONFIG_0:")
|
||||
print(f" EH mode: {config.get('energy_harvesting_mode', '?')}")
|
||||
print(f" SRAM copy: {config.get('sram_copy_enabled', '?')}")
|
||||
print(f" Auto standby: {config.get('auto_standby_mode', '?')}")
|
||||
|
||||
print(f"\nCONFIG_1:")
|
||||
print(f" SRAM enable: {config.get('sram_enabled', '?')}")
|
||||
print(f" Arbiter mode: {config.get('arbiter_mode', '?')}")
|
||||
print(f" Use case: {config.get('use_case', '?')}")
|
||||
print(f" EH arbiter: {config.get('eh_arbiter_mode_enabled', '?')}")
|
||||
|
||||
eh = chip.get_eh_ed_config_info()
|
||||
print(f"\nEH_CONFIG (block 0x3D):")
|
||||
print(f" EH enable: {eh.get('eh_enable', '?')}")
|
||||
print(f" VOUT voltage: {eh.get('eh_vout_v_sel', '?')}V")
|
||||
print(f" VOUT current: {eh.get('eh_vout_i_sel', '?')}mA")
|
||||
print(f" Power check disabled: {eh.get('disable_power_check', '?')}")
|
||||
print(f" ED/FD config: {eh.get('ed_config', '?')}")
|
||||
|
||||
|
||||
def write_eh(chip, current_sel, full_config=False):
|
||||
"""Write EH config for 1.8V automatic come-up."""
|
||||
|
||||
print("Writing EH config: 1.8V, current limit = "
|
||||
f"{[k for k,v in CURRENT_MAP.items() if v == current_sel][0]}mA")
|
||||
print(f" ED/FD pin: NFC-to-I2C pass-through (SRAM write detect)")
|
||||
|
||||
chip.write_eh_ed_config(
|
||||
enable=True,
|
||||
disable_power_check=False,
|
||||
current=current_sel,
|
||||
voltage=NXP_EH_CONFIG_EH_VOUT_V_SEL_1_8,
|
||||
ed_config=NXP_ED_CONFIG_NFC_TO_I2C_PASS_THROUGH,
|
||||
)
|
||||
print(" EH_CONFIG written.")
|
||||
|
||||
if full_config:
|
||||
print("\nWriting CONFIG_0: EH mode = low field strength")
|
||||
chip.write_config0(
|
||||
eh_mode=NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH,
|
||||
)
|
||||
print(" CONFIG_0 written.")
|
||||
|
||||
print("Writing CONFIG_1: SRAM enable, arbiter=passthrough, use_case=I2C slave")
|
||||
chip.write_config1(
|
||||
sram_enable=True,
|
||||
arbiter_mode=NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH,
|
||||
use_case=NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE,
|
||||
)
|
||||
print(" CONFIG_1 written.")
|
||||
|
||||
# Verify
|
||||
print("\n--- Verify ---")
|
||||
read_config(chip)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Configure NTAG5Link energy harvesting for 1.8V")
|
||||
parser.add_argument("--read", action="store_true",
|
||||
help="Read current config (no writes)")
|
||||
parser.add_argument("--write", action="store_true",
|
||||
help="Write EH config for 1.8V automatic VOUT")
|
||||
parser.add_argument("--current", default="6.5",
|
||||
choices=list(CURRENT_MAP.keys()),
|
||||
help="VOUT current limit in mA (default: 6.5)")
|
||||
parser.add_argument("--full", action="store_true",
|
||||
help="Also write CONFIG_0 and CONFIG_1 for xblink")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.read and not args.write:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
reader = ACR1552()
|
||||
reader.connect()
|
||||
iso = ISO15693(reader)
|
||||
chip = Ntag5Link(iso)
|
||||
|
||||
if args.read:
|
||||
read_config(chip)
|
||||
|
||||
if args.write:
|
||||
current_sel = CURRENT_MAP[args.current]
|
||||
write_eh(chip, current_sel, full_config=args.full)
|
||||
|
||||
reader.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user