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:
10
CLAUDE.md
10
CLAUDE.md
@@ -66,6 +66,8 @@ The LP5562 driver does NOT enforce timing delays internally — the caller is re
|
||||
|
||||
## Key I2C Addresses
|
||||
|
||||
Both devices share the same I2C bus (A4/A5). Non-conflicting addresses — no bus arbitration issues.
|
||||
|
||||
| Device | Address | Notes |
|
||||
|--------|---------|-------|
|
||||
| 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 |
|
||||
| A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock |
|
||||
| D0/A0 | LP5562 EN | Hardware enable (GPIO, active high) |
|
||||
| TBD | NTAG5 FD pin | Field detect / SRAM write indication (EIC wake) |
|
||||
| TBD | Hall sensor | Recovery mode trigger (EIC wake) |
|
||||
| D0/A0 | LP5562 EN line (open-drain) | Wired-AND with hall sensor, pull-up to VCC |
|
||||
| TBD (EIC) | Hall sensor output | EIC wake + wired-AND to LP5562 EN line |
|
||||
| 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
|
||||
|
||||
|
||||
69
README.md
69
README.md
@@ -10,14 +10,17 @@ A battery-free subdermal LED implant that harvests energy from an NFC field. An
|
||||
I2C (0x30)
|
||||
Phone/Reader ))) NFC ((( NTAG5Link ---------> LP5562 -----> RGBW LEDs
|
||||
| | |
|
||||
| VOUT | FD pin | EN
|
||||
| (EH) | |
|
||||
v v |
|
||||
+--SAMD21E--+ |
|
||||
| D0/A0 ----------------+ (GPIO enable)
|
||||
| A4/A5 (I2C bus) ------+
|
||||
| GPIO <--- Hall Sensor
|
||||
+----------+
|
||||
| VOUT | FD pin | EN (wired-AND)
|
||||
| (EH) | | |
|
||||
v v | |
|
||||
+--SAMD21E--+ | |
|
||||
| D0/A0 (open-drain) ---+ |
|
||||
| A4/A5 (I2C bus) ------+ |
|
||||
| EIC <--- Hall Sensor ---+ (open-drain)
|
||||
+----------+ |
|
||||
Rpull-up (1M)
|
||||
|
|
||||
VCC
|
||||
```
|
||||
|
||||
All power comes from NTAG5Link energy harvesting — no battery.
|
||||
@@ -25,11 +28,11 @@ All power comes from NTAG5Link energy harvesting — no battery.
|
||||
## Components
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
@@ -38,13 +41,14 @@ All power comes from NTAG5Link energy harvesting — no battery.
|
||||
### Boot Sequence
|
||||
|
||||
1. NFC field detected by NTAG5Link
|
||||
2. VOUT powers SAMD21E and LP5562 (MCU drives EN pin high on D0/A0)
|
||||
3. SAMD21E boots, checks hall sensor GPIO (asserted = recovery mode)
|
||||
4. Normal boot: reads LED pattern from NTAG5Link EEPROM via I2C
|
||||
5. Programs LP5562 execution engines with pattern data
|
||||
6. Enters STANDBY sleep (~5uA)
|
||||
7. LP5562 runs patterns autonomously using internal oscillator
|
||||
8. NFC field drops → VOUT drops → clean power-off
|
||||
2. VOUT powers SAMD21E; pull-up asserts LP5562 EN (unless hall sensor is active)
|
||||
3. SAMD21E boots, reads hall sensor GPIO
|
||||
4. If hall asserted → **boot-time recovery** (see Recovery Mode below)
|
||||
5. Normal boot: reads LED pattern from NTAG5Link EEPROM via I2C
|
||||
6. Programs LP5562 execution engines with pattern data
|
||||
7. Enters STANDBY sleep (~5µA)
|
||||
8. LP5562 runs patterns autonomously using internal oscillator
|
||||
9. NFC field drops → VOUT drops → clean power-off
|
||||
|
||||
### Pattern Update Sequence
|
||||
|
||||
@@ -57,20 +61,32 @@ All power comes from NTAG5Link energy harvesting — no battery.
|
||||
7. Reprograms LP5562 engines with new pattern
|
||||
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)
|
||||
- Ignores EEPROM pattern data
|
||||
- Sets recovery flag in SRAM for phone app detection
|
||||
- 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
|
||||
|
||||
All power from NTAG5Link energy harvesting. Budget is constrained and TBD pending prototype measurements.
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|-------|-------|
|
||||
| ------------------ | -------- | ------------------------------------------------------- |
|
||||
| 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) |
|
||||
| LP5562 per channel | 0-25.5mA | Must limit to ~2-5mA/ch for EH budget |
|
||||
@@ -112,14 +128,17 @@ cargo hf2 --release
|
||||
|
||||
Current prototype (XIAO M0 + LP5562EVM):
|
||||
|
||||
| XIAO Pin | LP5562EVM | Function |
|
||||
|----------|-----------|----------|
|
||||
| A4 (SDA) | SDA | I2C data |
|
||||
| A5 (SCL) | SCL | I2C clock |
|
||||
| D0/A0 | EN | Hardware enable (GPIO) |
|
||||
| XIAO Pin | Connection | Function |
|
||||
| --------- | --------------------------- | ------------------------------------------- |
|
||||
| A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data |
|
||||
| A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock |
|
||||
| D0/A0 | LP5562 EN line (open-drain) | Hardware enable, wired-AND with hall sensor |
|
||||
| TBD (EIC) | Hall sensor output | Interrupt/wake + wired-AND to EN line |
|
||||
| 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
|
||||
|
||||
See [docs/STATUS.md](docs/STATUS.md) for current progress.
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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**:
|
||||
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`
|
||||
@@ -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 LEDs (4ch @ 3mA) | ~12mA | ~12mA |
|
||||
| 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** |
|
||||
|
||||
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.
|
||||
|
||||
### 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**:
|
||||
1. Wire hall sensor to EIC-capable GPIO pin
|
||||
2. Boot-time check: if hall sensor asserted → recovery mode
|
||||
3. Recovery behavior: load hardcoded solid white (low brightness), skip EEPROM, stay awake
|
||||
4. Set recovery flag in SRAM so phone app can detect it
|
||||
5. Add hall sensor as runtime EIC wake source (e.g., cycle patterns)
|
||||
1. Wire hall sensor: open-drain output to EN line (wired-AND) + separate wire to EIC pin
|
||||
2. Add pull-up resistor (100k-1M) on EN line
|
||||
3. Configure MCU GPIO (D0/A0) as open-drain for EN control
|
||||
4. Implement EIC interrupt handler for hall sensor pin
|
||||
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
|
||||
|
||||
@@ -272,7 +291,7 @@ Deferred until Groups A-D are solid.
|
||||
| Risk | Severity | Mitigation |
|
||||
|------|----------|------------|
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -131,6 +131,9 @@
|
||||
| Firmware update strategy | Deferred | UF2 for dev, NFC OTA evaluated later |
|
||||
| NTAG5Link I2C slave address | Assumed 0x54 | Verify in M4 with Click board |
|
||||
| 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
|
||||
|
||||
@@ -140,6 +143,6 @@
|
||||
| LP5562EVM | Available | TI eval module, RGBW LEDs, I2C addr 0x30 |
|
||||
| Mini-USB cable | Not on hand | Needed to flash XIAO M0 |
|
||||
| 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 |
|
||||
| Multimeter | Available | For power budget measurements |
|
||||
|
||||
159
docs/plans/2026-03-03-hall-en-design.md
Normal file
159
docs/plans/2026-03-03-hall-en-design.md
Normal 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) |
|
||||
90
docs/plans/2026-03-03-ntag5-config-check.md
Normal file
90
docs/plans/2026-03-03-ntag5-config-check.md
Normal 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
BIN
tools/libuFCoder-x86_64.so
Executable file
Binary file not shown.
530
tools/ntag5_config_check.py
Normal file
530
tools/ntag5_config_check.py
Normal 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()
|
||||
Reference in New Issue
Block a user