docs: ISO 15693 relay timing findings
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>
This commit is contained in:
80
docs/RELAY_TIMING_FINDINGS.md
Normal file
80
docs/RELAY_TIMING_FINDINGS.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
1. Firmware receives unknown command from reader
|
||||||
|
2. Firmware does NOT respond (reader times out at ~311µs FDT)
|
||||||
|
3. Firmware relays command to Python over USB
|
||||||
|
4. Python computes response, sends back over USB
|
||||||
|
5. Firmware caches the response
|
||||||
|
6. 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
|
||||||
|
|
||||||
|
1. **Do not design features that depend on relay timing.** Always use the response table for commands that must work reliably.
|
||||||
|
|
||||||
|
2. **The relay is useful for debugging/development** — it lets you see what commands a reader sends without pre-configuring table entries.
|
||||||
|
|
||||||
|
3. **If relay reliability is ever needed**, potential improvements:
|
||||||
|
- DMA-based USB transfer (current `reply_ng` is 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
|
||||||
|
|
||||||
|
4. **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.
|
||||||
|
|
||||||
|
5. **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.**
|
||||||
Reference in New Issue
Block a user