Tested retry-based relay and spin-wait approaches. Relay works mechanically via retry but is unreliable (reader-dependent). Spin-wait fails due to USB stack latency. Table is the only viable mechanism for 15693 custom commands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4.2 KiB
ISO 15693 Relay Timing Findings
Date: 2026-03-17
Hardware: PM3 Easy, firmware branch sim-relay
Test script: test_relay_timing.py in sim-framework worktree
Summary
The 15693 retry-based relay works mechanically but is not reliable enough for production use. The response table (sim_table) is the correct approach for custom/vendor commands.
How the relay works
- Firmware receives unknown command from reader
- Firmware does NOT respond (reader times out at ~311µs FDT)
- Firmware relays command to Python over USB
- Python computes response, sends back over USB
- Firmware caches the response
- If the reader retries the same command, firmware serves the cached response
Timing measurements
| Metric | Value |
|---|---|
| Python roundtrip (receive relay → send response) | 0.11 - 0.25 ms |
| ISO 15693 FDT (frame delay time) | 0.311 ms (311 µs) |
| Python roundtrip vs FDT | Under budget on Python side |
| First-attempt response | NO — firmware cannot respond in time |
| Retry-based response | YES — works when reader retries |
Why it doesn't work in practice
The Python-side timing (0.11-0.25ms) is under the 311µs FDT budget, but this only measures the USB round-trip. The firmware's total path (receive RF frame → decode → relay over USB → receive response → encode RF → transmit) exceeds FDT on the first attempt. The firmware stays silent, and the reader must retry.
The problem is retry behavior:
- Retry is not guaranteed by ISO 15693 — it's reader-dependent
- Some readers retry, some don't
- Some NFC stacks (Android) retry certain commands but not others
- The number of retries varies (1-3 typically, but not specified)
- A reader that doesn't retry will see "no response" and may drop the tag
Observed behavior with Android phone:
- Standard commands (inventory, read block): handled by firmware natively, no relay needed
- Custom command
0xFE(fabricated): reader retried, cached response served successfully - NXP READ_CONFIG
0xA1: reader did NOT retry (may have been dropped by silence check in earlier firmware — inconclusive)
What works instead
Stateful response table (sim_table.c):
- Pre-computed responses uploaded to firmware BigBuf before sim starts
- Firmware does linear scan lookup (~2µs on 48MHz ARM7)
- Response served within FDT on first attempt — no retry needed
- Supports: static responses, EML read/write, AES-CMAC computation, group-based state machine
- All NXP custom commands (READ/WRITE_CONFIG, GET_RANDOM, SET_PASSWORD) handled via table
- TAM/MAM authentication handled via table's AES-CMAC action (~100-150µs)
The relay remains as a fallback for truly unknown commands that aren't in the table, but it should not be relied upon for correct operation.
Recommendations for future work
-
Do not design features that depend on relay timing. Always use the response table for commands that must work reliably.
-
The relay is useful for debugging/development — it lets you see what commands a reader sends without pre-configuring table entries.
-
If relay reliability is ever needed, potential improvements:
- DMA-based USB transfer (current
reply_ngis polled) - Dedicated USB endpoint for relay traffic (avoid contention with debug/trace)
- Firmware-side response prediction (start encoding a likely response while waiting for USB)
- None of these are guaranteed to meet 311µs FDT consistently
- DMA-based USB transfer (current
-
Spin-wait approach tested and failed. We modified the firmware to spin-wait on
data_available()for ~283µs after relaying, hoping to catch the Python response before FDT deadline. Results:- Python reports sending response in 0.14-0.38ms
- But USB stack latency means
data_available()doesn't see it in time - The spin-wait also broke the retry path (cached responses stopped being served)
- Reverted. The USB round-trip to/from the ARM7 is fundamentally too slow for inline relay.
-
For 14443-A Layer 4 (ISO-DEP), the relay IS viable via S(WTX) — the card requests a waiting time extension and the reader must wait. This gives seconds, not microseconds. The 15693 timing constraint does not apply there.
Final verdict
Table for everything. Relay is a debug/fallback tool, not a production mechanism.