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

View File

@@ -66,6 +66,8 @@ The LP5562 driver does NOT enforce timing delays internally — the caller is re
## Key I2C Addresses ## Key I2C Addresses
Both devices share the same I2C bus (A4/A5). Non-conflicting addresses — no bus arbitration issues.
| Device | Address | Notes | | Device | Address | Notes |
|--------|---------|-------| |--------|---------|-------|
| LP5562 | 0x30 | ADDR_SEL pins both low (LP5562EVM default) | | LP5562 | 0x30 | ADDR_SEL pins both low (LP5562EVM default) |
@@ -95,9 +97,11 @@ Key config constants (from `../ntag5sensor/vicinity/ntag5link.py`):
|----------|------------|----------| |----------|------------|----------|
| A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data | | A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data |
| A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock | | A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock |
| D0/A0 | LP5562 EN | Hardware enable (GPIO, active high) | | D0/A0 | LP5562 EN line (open-drain) | Wired-AND with hall sensor, pull-up to VCC |
| TBD | NTAG5 FD pin | Field detect / SRAM write indication (EIC wake) | | TBD (EIC) | Hall sensor output | EIC wake + wired-AND to LP5562 EN line |
| TBD | Hall sensor | Recovery mode trigger (EIC wake) | | TBD (EIC) | NTAG5 FD pin | Field detect / SRAM write indication (EIC wake) |
**LP5562 EN wired-AND**: D0/A0 (open-drain) and hall sensor (open-drain, active-low) both connect to LP5562 EN with a 1M pull-up. Either can force EN low. Magnet kills LEDs at hardware level regardless of MCU state.
## Testing ## Testing

View File

@@ -10,26 +10,29 @@ A battery-free subdermal LED implant that harvests energy from an NFC field. An
I2C (0x30) I2C (0x30)
Phone/Reader ))) NFC ((( NTAG5Link ---------> LP5562 -----> RGBW LEDs Phone/Reader ))) NFC ((( NTAG5Link ---------> LP5562 -----> RGBW LEDs
| | | | | |
| VOUT | FD pin | EN | VOUT | FD pin | EN (wired-AND)
| (EH) | | | (EH) | | |
v v | v v | |
+--SAMD21E--+ | +--SAMD21E--+ | |
| D0/A0 ----------------+ (GPIO enable) | D0/A0 (open-drain) ---+ |
| A4/A5 (I2C bus) ------+ | A4/A5 (I2C bus) ------+ |
| GPIO <--- Hall Sensor | EIC <--- Hall Sensor ---+ (open-drain)
+----------+ +----------+ |
Rpull-up (1M)
|
VCC
``` ```
All power comes from NTAG5Link energy harvesting — no battery. All power comes from NTAG5Link energy harvesting — no battery.
## Components ## Components
| Component | Part | Role | I2C Addr | Key Specs | | Component | Part | Role | I2C Addr | Key Specs |
|-----------|------|------|----------|-----------| | --------------- | ------------------------------------ | --------------------------------------------------- | ------------ | ---------------------------------------------------------- |
| NFC + Power | NXP NTAG5Link (NTP53x2) | NFC interface, energy harvesting, pattern storage | 0x54 (slave) | 2048B EEPROM, 256B SRAM, ISO15693, 1.8/2.4/3.0V EH output | | NFC + Power | NXP NTAG5Link (NTP53x2) | NFC interface, energy harvesting, pattern storage | 0x54 (slave) | 2048B EEPROM, 256B SRAM, ISO15693, 1.8/2.4/3.0V EH output |
| MCU | SAMD21E18A | Firmware (Rust), pattern loading, sleep/wake | — | 256KB flash, 32KB RAM, ARM Cortex-M0+, 32-pin QFN | | MCU | SAMD21E18A | Firmware (Rust), pattern loading, sleep/wake | — | 256KB flash, 32KB RAM, ARM Cortex-M0+, 32-pin QFN |
| LED Driver | TI LP5562 | RGBW LED control with autonomous patterns | 0x30 | 4ch RGBW, 3 execution engines, 16 cmds/engine, 0-25.5mA/ch | | LED Driver | TI LP5562 | RGBW LED control with autonomous patterns | 0x30 | 4ch RGBW, 3 execution engines, 16 cmds/engine, 0-25.5mA/ch |
| Recovery | Hall effect sensor | Safe mode / recovery trigger | — | GPIO input with EIC wake from sleep | | Recovery + Swap | Hall effect sensor (e.g. TI DRV5032) | Pattern cycling, recovery trigger, hardware EN kill | — | Open-drain, active-low, omnipolar, <2µA quiescent |
**Dev board**: Seeed XIAO M0 (SAMD21G18A) + LP5562EVM + MikroElektronika NTAG5 Link Click **Dev board**: Seeed XIAO M0 (SAMD21G18A) + LP5562EVM + MikroElektronika NTAG5 Link Click
@@ -38,13 +41,14 @@ All power comes from NTAG5Link energy harvesting — no battery.
### Boot Sequence ### Boot Sequence
1. NFC field detected by NTAG5Link 1. NFC field detected by NTAG5Link
2. VOUT powers SAMD21E and LP5562 (MCU drives EN pin high on D0/A0) 2. VOUT powers SAMD21E; pull-up asserts LP5562 EN (unless hall sensor is active)
3. SAMD21E boots, checks hall sensor GPIO (asserted = recovery mode) 3. SAMD21E boots, reads hall sensor GPIO
4. Normal boot: reads LED pattern from NTAG5Link EEPROM via I2C 4. If hall asserted **boot-time recovery** (see Recovery Mode below)
5. Programs LP5562 execution engines with pattern data 5. Normal boot: reads LED pattern from NTAG5Link EEPROM via I2C
6. Enters STANDBY sleep (~5uA) 6. Programs LP5562 execution engines with pattern data
7. LP5562 runs patterns autonomously using internal oscillator 7. Enters STANDBY sleep (~5µA)
8. NFC field drops → VOUT drops → clean power-off 8. LP5562 runs patterns autonomously using internal oscillator
9. NFC field drops VOUT drops clean power-off
### Pattern Update Sequence ### Pattern Update Sequence
@@ -57,25 +61,37 @@ All power comes from NTAG5Link energy harvesting — no battery.
7. Reprograms LP5562 engines with new pattern 7. Reprograms LP5562 engines with new pattern
8. Returns to STANDBY sleep 8. Returns to STANDBY sleep
### Recovery Mode ### Hall Sensor Interaction
If the hall sensor is asserted at boot (magnet held near implant): The hall sensor serves three purposes via a wired-AND circuit: the hall sensor (open-drain, active-low) and MCU GPIO (open-drain) share the LP5562 EN line with a pull-up resistor. When the magnet is present, EN is forced low at the hardware level independent of MCU state.
**Pattern Swap (tap <1s):**
1. Magnet near hall pulls EN low LEDs off instantly (hardware)
2. MCU wakes via EIC interrupt, starts timer
3. Magnet removed MCU re-asserts EN (pin high-Z, pull-up takes over)
4. Loads next stored pattern LEDs back on
**Boot-time Recovery:**
If the hall sensor is asserted when the MCU powers up (magnet was present before NFC field arrived):
- LP5562 EN held low by hardware (LEDs never turn on)
- Loads a hardcoded default pattern (solid white, low brightness) - Loads a hardcoded default pattern (solid white, low brightness)
- Ignores EEPROM pattern data - Ignores EEPROM pattern data
- Sets recovery flag in SRAM for phone app detection - Sets recovery flag in SRAM for phone app detection
- Remains awake for firmware update if supported - Remains awake for firmware update if supported
**Hardware Safety:** Even if the MCU firmware crashes, holding a magnet near the implant forces the LEDs off at the circuit level.
## Power Budget ## Power Budget
All power from NTAG5Link energy harvesting. Budget is constrained and TBD pending prototype measurements. All power from NTAG5Link energy harvesting. Budget is constrained and TBD pending prototype measurements.
| Parameter | Value | Notes | | Parameter | Value | Notes |
|-----------|-------|-------| | ------------------ | -------- | ------------------------------------------------------- |
| EH max current | ~12.5mA | Highest threshold setting | | EH max current | ~12.5mA | Highest threshold setting |
| EH voltage | 3.0V | Required for LP5562 (2.7-5.5V) and SAMD21E (1.62-3.63V) | | EH voltage | 3.0V | Required for LP5562 (2.7-5.5V) and SAMD21E (1.62-3.63V) |
| LP5562 per channel | 0-25.5mA | Must limit to ~2-5mA/ch for EH budget | | LP5562 per channel | 0-25.5mA | Must limit to ~2-5mA/ch for EH budget |
| SAMD21E active | ~3-5mA | During boot and pattern programming | | SAMD21E active | ~3-5mA | During boot and pattern programming |
| SAMD21E standby | ~5uA | After LP5562 engines are running | | SAMD21E standby | ~5uA | After LP5562 engines are running |
## Getting Started ## Getting Started
@@ -112,13 +128,16 @@ cargo hf2 --release
Current prototype (XIAO M0 + LP5562EVM): Current prototype (XIAO M0 + LP5562EVM):
| XIAO Pin | LP5562EVM | Function | | XIAO Pin | Connection | Function |
|----------|-----------|----------| | --------- | --------------------------- | ------------------------------------------- |
| A4 (SDA) | SDA | I2C data | | A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data |
| A5 (SCL) | SCL | I2C clock | | A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock |
| D0/A0 | EN | Hardware enable (GPIO) | | D0/A0 | LP5562 EN line (open-drain) | Hardware enable, wired-AND with hall sensor |
| 3V3 | VCC | Power | | TBD (EIC) | Hall sensor output | Interrupt/wake + wired-AND to EN line |
| GND | GND | Ground | | 3V3 | VCC | Power |
| GND | GND | Ground |
LP5562 EN line has a 1M pull-up resistor to VCC. Both D0/A0 (open-drain) and the hall sensor (open-drain, active-low) connect to this line either can pull EN low.
## Project Status ## Project Status

View File

@@ -80,6 +80,12 @@ This gives us real pattern data to inform the EEPROM storage format later (M5),
**Important**: The MCU accesses NTAG5Link as a standard I2C slave at address 0x54 — plain register read/write. This is NOT the NFC-side ISO15693 custom commands used by the Python ntag5sensor tooling. **Important**: The MCU accesses NTAG5Link as a standard I2C slave at address 0x54 — plain register read/write. This is NOT the NFC-side ISO15693 custom commands used by the Python ntag5sensor tooling.
**I2C bus management**: LP5562 (0x30) and NTAG5Link (0x54) share the same I2C bus (A4/A5). Both addresses are used simultaneously — the MCU talks to each device by its address. Key considerations:
- Both are I2C slave devices with non-conflicting addresses
- LP5562 engines run autonomously after programming — no ongoing I2C traffic needed
- Pause LP5562 engines (set exec to Hold) before lengthy NTAG5 I2C transactions to avoid any bus contention edge cases
- NTAG5Link's use_case must be set to `i2c_slave` (not `i2c_master`) so it doesn't drive the bus
**Tasks**: **Tasks**:
1. Write `src/ntag5/mod.rs``Ntag5Link<I2C>` struct, generic over `embedded_hal::i2c::I2c` 1. Write `src/ntag5/mod.rs``Ntag5Link<I2C>` struct, generic over `embedded_hal::i2c::I2c`
2. Write `src/ntag5/registers.rs` — register map constants from `ntag5link.py` 2. Write `src/ntag5/registers.rs` — register map constants from `ntag5link.py`
@@ -182,24 +188,37 @@ Typical 3-engine pattern: 16 + 3×34 + 4 = **122 bytes** (fits easily in 2048B E
| LP5562 quiescent | ~0.5mA | ~0.5mA | | LP5562 quiescent | ~0.5mA | ~0.5mA |
| LP5562 LEDs (4ch @ 3mA) | ~12mA | ~12mA | | LP5562 LEDs (4ch @ 3mA) | ~12mA | ~12mA |
| NTAG5Link | ~100µA | ~1µA | | NTAG5Link | ~100µA | ~1µA |
| Hall sensor (DRV5032) | ~1.6µA | ~1.6µA |
| EN pull-up (1M @ 3V) | ~0 | ~0 (3µA only when EN low) |
| **Total** | **~17mA** | **~12.5mA** | | **Total** | **~17mA** | **~12.5mA** |
Key insight: LED current dominates. MCU sleep saves ~4mA. Total must stay under EH max (~12.5mA), so LED current must be limited. Key insight: LED current dominates. MCU sleep saves ~4mA. Total must stay under EH max (~12.5mA), so LED current must be limited.
**Done when**: MCU sleeps after boot, LP5562 runs patterns, MCU wakes on NFC to accept new pattern. **Done when**: MCU sleeps after boot, LP5562 runs patterns, MCU wakes on NFC to accept new pattern.
### M8: Recovery Mode ### M8: Recovery Mode + Pattern Swap
**Goal**: Hall sensor provides safe mode entry for post-implant recovery. **Goal**: Hall sensor provides pattern cycling (tap) and safe mode entry (hold + confirm).
**Circuit**: Wired-AND on LP5562 EN — hall sensor (open-drain) + MCU GPIO (open-drain) + pull-up resistor. Magnet presence forces EN low at hardware level, independent of MCU state. Hall output also connected to MCU EIC pin for interrupt/wake. See `docs/plans/2026-03-03-hall-en-design.md`.
**Interaction model**:
- **Boot-time recovery**: Hall asserted at power-on → EN held low (hardware), MCU skips EEPROM, loads default pattern, stays awake
- **Tap (<1s)**: LEDs off instantly (hardware EN drop) → magnet removed → MCU loads next pattern → LEDs back
- **Hold (>3s + confirm)**: LEDs off (hardware) → magnet removed → MCU blinks warning (3x red) → 2s window for confirmation tap → if confirmed, enters recovery mode. No second tap → resumes normal operation
- **MCU crashed + magnet**: Hall still kills EN (hardware safety)
**Tasks**: **Tasks**:
1. Wire hall sensor to EIC-capable GPIO pin 1. Wire hall sensor: open-drain output to EN line (wired-AND) + separate wire to EIC pin
2. Boot-time check: if hall sensor asserted → recovery mode 2. Add pull-up resistor (100k-1M) on EN line
3. Recovery behavior: load hardcoded solid white (low brightness), skip EEPROM, stay awake 3. Configure MCU GPIO (D0/A0) as open-drain for EN control
4. Set recovery flag in SRAM so phone app can detect it 4. Implement EIC interrupt handler for hall sensor pin
5. Add hall sensor as runtime EIC wake source (e.g., cycle patterns) 5. Implement boot-time recovery: read hall GPIO at startup, if asserted → default pattern, skip EEPROM
6. Implement runtime state machine: tap detection, hold timing, warning blink, recovery confirmation
7. Recovery behavior: load hardcoded solid white (low brightness), skip EEPROM, stay awake
8. Set recovery flag in SRAM so phone app can detect it
**Done when**: Holding magnet near hall sensor at boot triggers recovery mode with default LED pattern. **Done when**: Boot-time recovery works (magnet at power-on), tap cycles patterns at runtime, hold + confirm enters runtime recovery, hardware EN kill works even if MCU is unresponsive.
### M9: Power Characterization ### M9: Power Characterization
@@ -272,7 +291,7 @@ Deferred until Groups A-D are solid.
| Risk | Severity | Mitigation | | Risk | Severity | Mitigation |
|------|----------|------------| |------|----------|------------|
| Power budget too tight for 4ch LED | High | Limit current to 2-3mA/ch, use pulsed patterns, run fewer channels | | Power budget too tight for 4ch LED | High | Limit current to 2-3mA/ch, use pulsed patterns, run fewer channels |
| I2C bus contention (LP5562 + NTAG5) | Medium | Pause LP5562 engines before NTAG5 I2C access, resume after | | I2C bus contention (LP5562 + NTAG5) | Medium | Non-conflicting addresses (0x30 vs 0x54). Pause LP5562 engines during NTAG5 I2C bursts. Verify NTAG5 is in i2c_slave mode (not i2c_master) |
| EEPROM write endurance (~100K cycles) | Low | SRAM for transient comms, EEPROM only for pattern persistence | | EEPROM write endurance (~100K cycles) | Low | SRAM for transient comms, EEPROM only for pattern persistence |
| Boot time too slow | Low | Estimated ~15-20ms appears instant to user | | Boot time too slow | Low | Estimated ~15-20ms appears instant to user |
| NFC coupling distance too short | Medium | Optimize antenna, low field strength EH mode, accept 1-3cm | | NFC coupling distance too short | Medium | Optimize antenna, low field strength EH mode, accept 1-3cm |

View File

@@ -131,6 +131,9 @@
| Firmware update strategy | Deferred | UF2 for dev, NFC OTA evaluated later | | Firmware update strategy | Deferred | UF2 for dev, NFC OTA evaluated later |
| NTAG5Link I2C slave address | Assumed 0x54 | Verify in M4 with Click board | | NTAG5Link I2C slave address | Assumed 0x54 | Verify in M4 with Click board |
| Pattern binary format | Deferred to M5 | Design after M2-M3 engine experience | | Pattern binary format | Deferred to M5 | Design after M2-M3 engine experience |
| Hall sensor + EN circuit | Designed | Wired-AND: hall + MCU open-drain on EN with 1M pull-up. See `docs/plans/2026-03-03-hall-en-design.md` |
| Hall sensor part selection | Decided | DRV5032FB (SOT-23, 8.4mT, prototype) → DRV5032FE (X2SON 1x1mm, final PCB) |
| EN pull-up value | Decided | 1M — zero steady-state draw, 3µA when EN low, ~10µs rise time |
## Hardware Inventory ## Hardware Inventory
@@ -140,6 +143,6 @@
| LP5562EVM | Available | TI eval module, RGBW LEDs, I2C addr 0x30 | | LP5562EVM | Available | TI eval module, RGBW LEDs, I2C addr 0x30 |
| Mini-USB cable | Not on hand | Needed to flash XIAO M0 | | Mini-USB cable | Not on hand | Needed to flash XIAO M0 |
| NTAG5 Link Click | Available, partially wired | Missing I2C jumper to XIAO | | NTAG5 Link Click | Available, partially wired | Missing I2C jumper to XIAO |
| Hall effect sensor | Not available | Need to source | | Hall effect sensor | Not available | Need to source — TI DRV5032FB (SOT-23) for prototype |
| ACR1552 PCSC reader | Available | For ntag5sensor Python tooling | | ACR1552 PCSC reader | Available | For ntag5sensor Python tooling |
| Multimeter | Available | For power budget measurements | | Multimeter | Available | For power budget measurements |

View File

@@ -0,0 +1,159 @@
# Hall Sensor + LP5562 EN Wired-AND Circuit Design
**Date**: 2026-03-03
**Status**: Design in progress
## Problem
The implant needs a hall sensor for three functions:
1. **Pattern swap** — quick magnet tap cycles to the next stored pattern
2. **Runtime recovery** — long hold + confirmation tap enters safe mode
3. **Boot-time recovery** — magnet present at power-on skips EEPROM, loads default
4. **Hardware safety** — LEDs forced off by magnet even if MCU firmware has crashed
These must work within extreme space and power constraints (energy-harvested implant).
## Circuit: Wired-AND on LP5562 EN
Zero extra active components. Hall sensor and MCU share the EN line via open-drain outputs.
```
VCC (3.0V from NTAG5Link EH)
|
1M (pull-up resistor)
|
EN ------+------- MCU D0/A0 (open-drain)
|
+------- Hall sensor OUT (open-drain, active-low)
|
LP5562 EN pin
Hall sensor OUT ---- MCU EIC pin (separate connection for interrupt/wake)
```
### How it works
| State | MCU pin | Hall sensor | EN line | LP5562 |
|-------|---------|-------------|---------|--------|
| Normal operation | High-Z | High-Z (no magnet) | High (pull-up) | On |
| Magnet present | High-Z | Driving low | Low | Off |
| MCU disabling LP5562 | Driving low | High-Z | Low | Off |
| Magnet + MCU low | Driving low | Driving low | Low | Off |
### Pull-up resistor: 1M
- Steady-state current: ~0 (both open-drains high-Z, no path to ground)
- Current when EN low: 3V / 1M = 3 µA (only during magnet or MCU-driven low)
- Rise time: ~10 µs (1M x ~10pF parasitic) — instant from human perspective
- LP5562 EN input is high-impedance CMOS — no DC loading
## Hall Sensor: TI DRV5032
| Variant | Use | Iq | Threshold | Output | Package |
|---------|-----|-----|-----------|--------|---------|
| **DRV5032FB** | Prototype | 1.6 µA | 8.4 mT (omnipolar) | Open-drain | SOT-23 |
| **DRV5032FE** | Final PCB | 0.55 µA | 4.5 mT (omnipolar) | Open-drain | X2SON (1x1mm) |
- **Omnipolar**: Triggers on either magnetic pole (user doesn't need to orient magnet)
- **8.4 mT threshold (FB)**: Higher threshold = harder to accidentally trigger. Requires deliberate, close magnet interaction. Good for "don't accidentally enter recovery" requirement.
- **Open-drain, active-low**: Required for wired-AND topology
- **SOT-23 for prototyping**: Easy to hand-solder; X2SON for final implant PCB
## Interaction Model
### Pattern Swap (tap <1s)
1. Magnet near → hall pulls EN low → LEDs off instantly (hardware, zero MCU latency)
2. MCU wakes via EIC interrupt on hall sensor pin, starts timer
3. Magnet removed (hall goes high-Z, pull-up restores EN)
4. MCU detects short press (<1s) loads next stored pattern LEDs back on
### Runtime Recovery (hold >3s + confirm)
1. Magnet near EN low (hardware), MCU wakes via EIC
2. MCU counts hold duration while hall stays asserted
3. Magnet removed after >3s → MCU notes "long hold detected"
4. MCU re-enables LP5562 → blinks warning pattern (3x red flash)
5. MCU waits 2s for confirmation tap
6. Confirmation tap received → enters recovery mode
7. No confirmation → resumes normal operation (reloads previous pattern)
### Boot-time Recovery
1. NFC field arrives, VOUT powers system
2. Hall sensor already active (magnet was present before power) → EN held low by hardware
3. MCU boots, reads hall GPIO → asserted → boot-time recovery
4. Loads hardcoded default pattern (solid white, low brightness)
5. Skips EEPROM pattern data
6. Sets recovery flag in SRAM for phone app detection
7. When magnet removed, EN goes high, default pattern runs
### Hardware Safety (MCU crashed)
- Holding magnet forces EN low at circuit level regardless of MCU state
- No firmware dependency — pure hardware path
- Magnet removal restores EN via pull-up (LP5562 resumes last-programmed pattern)
## Firmware State Machine (M8)
```
┌─────────────┐
power-on -->│ BOOT_CHECK │
└──────┬──────┘
hall low? │
┌───yes─────┼────no───┐
v │ v
┌──────────┐ │ ┌─────────────┐
│ RECOVERY │ │ │ RUNNING │<──────────────┐
└──────────┘ │ └──────┬──────┘ │
│ hall EIC │ │
│ interrupt│ │
│ v │
│ ┌─────────────┐ │
│ │ HALL_ACTIVE │ │
│ └──────┬──────┘ │
│ hall released │
│ │ │ │
│ <1s >3s │
│ │ │ │
│ v v │
│ ┌──────┐ ┌──────────────┐ │
│ │ SWAP │ │ WARN_BLINK │ │
│ └──┬───┘ └──────┬───────┘ │
│ │ hall tap │ no tap │
│ │ in 2s? │ │
│ │ ┌──yes ┌──no │
│ │ v v │
│ │ ┌────────┐ │ │
│ │ │RECOVERY│ │ │
│ │ └────────┘ │ │
│ └──────────────┴──────────┘
```
States:
- **BOOT_CHECK**: Read hall GPIO. If asserted → RECOVERY. Else → RUNNING.
- **RUNNING**: LP5562 engines active, MCU in STANDBY sleep. Wakes on hall EIC.
- **HALL_ACTIVE**: Magnet detected. Timer running. Waiting for release.
- **SWAP**: Load next pattern, re-enable LP5562 → RUNNING.
- **WARN_BLINK**: Blink 3x red. Wait 2s for confirmation tap.
- **RECOVERY**: Default pattern, skip EEPROM, stay awake, set SRAM flag.
## Power Impact
| Component | Added draw | When |
|-----------|-----------|------|
| Hall sensor (DRV5032FB) | 1.6 µA | Always (quiescent) |
| Pull-up resistor (1M) | 3 µA | Only when EN is low (magnet or MCU-driven) |
| Pull-up resistor (1M) | ~0 | Steady state (EN high, no current path) |
Total added quiescent: **~1.6 µA** — negligible vs LP5562 LED current (~12 mA).
## Open Questions
| Question | Notes |
|----------|-------|
| Debounce needed? | Hall sensors are usually clean (no mechanical bounce), but verify with DRV5032 |
| EIC pin selection on SAMD21E | Must be EIC-capable for wake from STANDBY. Check SAMD21E pinout. |
| Magnet strength/distance | What N52 neodymium size triggers DRV5032FB at implant depth (~3-5mm subdermal)? |
| Multiple stored patterns | How many patterns to cycle through? Currently TBD (depends on EEPROM format, M5) |

View File

@@ -0,0 +1,90 @@
# NTAG5Link Config Verification via uFR Zero
**Date**: 2026-03-03
**Status**: Tool written, reader connection blocked
## Goal
Read-only verification that the NTAG5Link Click board's configuration matches xblink requirements, using the Digital Logic uFR Zero Multi-ISO reader.
## Tool
`tools/ntag5_config_check.py` — Python script using uFCoder library (ctypes) in ISO15693 transparent mode.
Features:
- ISO15693 INVENTORY to discover all tags by UID
- Addressed mode for all config reads (supports multiple tags in field)
- Reads CONFIG block (0x37) and EH/ED CONFIG block (0x3D)
- NXP system info for interface type verification
- Checks against xblink expected config values
## Expected Config
| Setting | Expected | Config Location |
|---------|----------|----------------|
| EH mode | low_field_strength | CONFIG byte 0, bits 3:2 = 10 |
| Use case | i2c_slave | CONFIG byte 1, bits 5:4 = 00 |
| SRAM enabled | true | CONFIG byte 1, bit 1 = 1 |
| Arbiter mode | sram_passthrough | CONFIG byte 1, bits 3:2 = 10 |
| EH enable | true | EH_CONFIG byte 0, bit 0 = 1 |
| EH voltage | 3.0V | EH_CONFIG byte 0, bits 2:1 = 10 |
## Reader Connection Issue
**Problem**: uFCoder library returns `UFR_TIMEOUT_ERR` (0x1002) for all `ReaderOpenEx` parameter combinations.
**USB device**: `10c4:ea60` — Silicon Labs CP2102 USB to UART Bridge Controller, iSerial "0001". Generic descriptor, no D-Logic branding in USB strings.
**Raw serial investigation**:
- At 1 Mbps (expected uFR baud): responds with `00 80` (2 bytes) — not a valid uFR protocol frame (expected 6+ bytes with `0x55...0xAA` framing)
- At 115200 bps: returns ASCII shell commands mentioning "systemctl", "proxmark3" paths
- At 250 Kbps: returns structured but unrecognized data
**Possible causes**:
1. Device may not be a uFR Zero — the CP210x bridge has generic descriptors
2. Reader firmware may need updating
3. Reader may be in a non-standard mode
4. Different hardware variant requiring different protocol
**Next steps**:
1. Physically verify the device is indeed the uFR Zero (check labels, LEDs)
2. Try the D-Logic `ufr_online` Windows utility to confirm reader identity
3. If confirmed as uFR Zero, check firmware version and update if needed
4. Alternative: use the ACR1552 PCSC reader with ntag5sensor directly (requires porting the reader to share the field with NTAG5 Click)
## Fallback Plan: ntag5sensor + ACR1552
If the uFR Zero connection cannot be resolved, use the existing ntag5sensor Python tooling with the ACR1552 PCSC reader:
```python
# From ntag5sensor — already has full config read/write support
chip = NTAG5Link(reader)
config = chip.get_config_info()
eh_config = chip.get_eh_ed_config_info()
```
The ACR1552 is already proven with ntag5sensor. Only change needed: physically position the ACR1552 over the NTAG5 Click board instead of the uFR Zero.
## Config Change Plan (if mismatches found)
Use ntag5sensor's write functions (via ACR1552 or adapted for uFR Zero):
```python
# Set I2C slave mode + SRAM passthrough + SRAM enable
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
)
# Set EH: 3.0V, enabled, current limit TBD
chip.write_eh_ed_config(
enable=True,
voltage=NXP_EH_CONFIG_EH_VOUT_V_SEL_3_0,
current=NXP_EH_CONFIG_EH_VOUT_I_SEL_12_5, # max for testing
ed_config=NXP_ED_CONFIG_NFC_FIELD_DETECT
)
# Set EH mode in CONFIG byte 0
chip.write_config0(eh_mode=NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH)
```

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()