Skip to content

nfc_driver_matrix

moggieuk edited this page Jul 30, 2026 · 1 revision

NFC/RFID reader support matrix

Derived from the code on private_rfid2_poll, not from datasheets. Everything below is what the drivers in extras/mmu/unit/nfc/ actually implement; several of these chips speak more transports and protocols in silicon than we drive (reader_factory.SUPPORTED_INTERFACES says as much in its comment).

Key files:

  • extras/mmu/unit/nfc/reader_factory.py — transport selection, wiring validation
  • extras/mmu/unit/nfc/mmu_nfc_reader.py — per-instance facade, probe contract + shim
  • extras/mmu/unit/mmu_nfc_manager.py — poll orchestration, homing poll cadence
  • extras/mmu/unit/nfc/mmu_nfc_endstop.py — reader-as-endstop
  • extras/mmu/mmu_filament_movement.py — the homing moves that use it

A. Chips, transports, wiring cost

Chip Transports implemented Default Required config Extra MCU GPIO per reader Multiple readers Maturity
PN532 i2c, uart, spi i2c i2c_address (0x24) 0 on a hardware bus One per bus. Address is fixed at 0x24, so a 2nd PN532 needs its own bus Default chip (DEFAULT_READER_TYPE); homing path still wants a bench check — see §C
PN532 (uart / HSU) serial: (host device path), baud 0 (not MCU-attached) One reader per tty, enforced (_validate_serial_port) Supported
PN532 (spi) cs_pin 1 (cs_pin) on a shared SPI bus Fine — one CS per reader UNTESTED — warns at startup in create_reader()
PN7160 i2c i2c i2c_address (0x28–0x2B) 0 base; +1 for irq_pin (needed for probe homing); optional +1 ven_pin Up to 4 per bus (four addresses); _software_i2c_buses explicitly allows several on one bit-banged bus Working; probe path is IRQ-gated
PN5180 spi spi cs_pin, busy_pin, reset_pin (both required, no defaults) 3 Fine on shared SPI (one CS each) Working reads; no presence probe
RC522 spi spi cs_pin 1 Fine on shared SPI Working; "Confirmed viable" for homing per the endstop header

Software (bit-banged) I2C, if you need more of the same chip than the addressing allows: 2 GPIO per bus plus its own pull-ups. reader_factory validates the pins and catches bus/address collisions, because Klipper does not — two readers handed the same pin pair silently share one bus.

Per-gate GPIO cost, cheapest first: RC522 (1) → PN532/SPI (1, untested) → PN7160 (0–1 shared bus, +1 for IRQ) → PN5180 (3) → PN532/I2C (2 per gate, software bus). PN532/UART can't scale per-gate at all without one USB adapter per gate.


B. Read functionality

Capability PN532 (i2c/uart/spi) PN7160 PN5180 RC522
Presence probe (non-blocking, for homing) Yes, all three transports Only with irq_pin No (shim) Yes
UID read (read_tag) Yes Yes Yes Yes
Target info (read_target: SAK/ATQA/UID len) Yes Yes Yes Yes
ISO14443A (MIFARE / NTAG) Yes Yes Yes Yes
ISO15693 / NFC Type-5 No Yes Yes No
NTAG/Type-2 deep read Yes, NDEF-aware (ntag_read_ndef_user_memory — stops at the TLV end) Yes, NDEF-aware Fixed page span only (no NDEF-aware helper, so _capture_ntag falls back to tag_max_pages pages) Yes, NDEF-aware
MIFARE Classic authenticated read Yes Yes Yes Yes
Vendor formats reachable (via tag_parser) Bambu, Creality, QIDI, Elegoo, Anycubic, NDEF-based (OpenTag3D, OpenSpool, TigerTag, …) same, plus anything on a Type-5 carrier same, plus Type-5 same as PN532

Notes:

  • The PN532 UART/SPI drivers look sparse if you grep them — read_target, ntag_* and mifare_* all live in _PN532Base (pn532_driver.py:213), above PN532Driver/PN532SPIDriver. All three transports share one capability surface. Verified by position, not assumed.
  • Deep read is only performed when Spoolman auto-create needs it, and never during a homing moveread_gate_after_home() does it after movequeue_wait(), with the machine stationary. Deep-read cost is therefore irrelevant to homing accuracy.
  • ISO15693 only matters if you actually hold Type-5 tags. tag_parser is carrier-agnostic: the NDEF-based formats parse identically whether the bytes came off an NTAG or off a Type-5 tag (_type5_parser_memory strips the CC first).
  • PN5180 with pn5180_tag_format: auto walks both protocols on every read — pin it to ntag or iso15693 if you know which you have; it halves the miss cost.

C. Homing behaviour (the polling presence test)

Cadence, from mmu_nfc_manager.py:55: NFC_HOMING_POLL_INTERVAL = 0.020 for drivers implementing the probe contract, NFC_HOMING_POLL_INTERVAL_SHIM = 0.050 for the blocking shim, whose per-tick read is bounded at PROBE_SHIM_TIMEOUT = 0.050.

Chip / mode Detector Pins needed for full-rate probe Cost of a "no tag" tick Worst-case detection latency ≈ overshoot @100 mm/s
PN532 (i2c/uart/spi) Continuous — InListPassiveTarget stays in flight and retries activation forever, so the chip itself is the detector none one 1-byte status read ~1 tick ≈ 20 ms ~2 mm
PN7160 + irq_pin Continuous — NCI RF discovery running; IRQ line signals a pending frame irq_pin (real wired GPIO) zero bus I/O — just reads the cached irq_state ~20 ms + ≤2 ms button sampling ~2 mm
RC522 Discrete — one WUPA exchange per cycle, started on one tick and collected on the next none (it polls _ComIrqReg over SPI — no physical IRQ pin) a few register reads ~2 ticks ≈ 40 ms ~4 mm
PN7160, no irq_pin Shim — probe_supported() declines a bounded blocking read_target() 50 ms interval + ≤50 ms read ≈ 100 ms ~10 mm
PN5180 Shim — probe deliberately not implemented (pn5180_driver.py, "deliberately NOT implemented") bounded blocking read_target(); auto format walks 2 protocols 100–150 ms ~10–15 mm

Two different things get called "IRQ" in the comments and they are not the same: RC522 reads an interrupt register over the SPI bus it already has, costing no pins; PN7160 needs a real wired interrupt line, and without it there is no way to ask "is a frame waiting?" without blocking, so it falls back to the shim. RC522 is the only chip that gets full-rate probing at one pin per gate.

The endstop module still carries a PROTOTYPE header: "Confirmed viable on RC522 (SPI); PN532/PN7160 (I2C) need a bench check of per-poll blocking time against the drip budget (~50 ms safe)." So RC522 is the only one of these measured in practice — the PN532 and PN7160 numbers above are what the code should cost per tick, not what anyone has scoped.

Latency figures are code-derived upper bounds on detection only. They exclude the deceleration ramp (speed/accel dependent) and, more importantly, RF coupling range — see the caveat at the end.


D. How the probe works, per chip

The contract is probe_start() / probe_poll() / probe_stop(), driven by MmuNfcManager._homing_poll on a reactor timer. probe_poll() returns True (tag present), False (this scan finished with nothing there — the manager restarts it) or None (still in flight — ask again). It reports presence only, never a UID: the endstop needs a boolean, and the UID comes from the stationary post-move read.

  • PN532InListPassiveTarget as an explicit 3-stage state machine (send → collect ACK → collect response), one bus transaction per tick. The in-flight scan is the detector, which is why detection is one tick rather than one scan. The 2 s watchdog exists to recover a wedged chip, not to bound a healthy scan.
  • PN7160start_discovery() once, then each tick collects at most one pending NCI frame if irq_state says one is waiting. Presence = RF_DISCOVER_NTF or RF_INTF_ACTIVATED_NTF; it deliberately does not select/activate, because select_discovered_endpoint() carries a 1 s NCI timeout. One caveat: the tick that does read a frame ends in _wait_for_irq_release(), bounded at 50 ms — right at the ~50 ms drip budget the endstop header flags for a bench check. It only happens on the detection tick, when the move is stopping anyway.
  • RC522_transceive() split at its tag-wait sleep: probe_start loads WUPA with 7-bit framing and hits StartSend; probe_poll reads _ComIrqReg and decides. A single WUPA, no anticollision, no SELECT.
  • PN5180 / PN7160-without-IRQMmuNfcReader.probe_poll()'s shim: one bounded read_target() per tick. Still blocking, so the manager polls it at the slower cadence rather than making things worse by ticking a blocking read faster.

E. Consequences of homing to a tag

What is not a problem:

  • Overshoot does not corrupt the positional datum. _home_gate_with_nfc() already treats the NFC stop as imprecise: after an NFC-first trigger it continues to the gate switch, and after a jog-scan hit it re-homes backward to the gate switch ("Virtual-NFC stop overshoots more than the MCU gate switch"). The reference always comes from the real MCU endstop. So the poll-interval overshoot costs filament travel and time, not accuracy of the gate datum.
  • Reactor stalls during the move. The probe split is exactly what removed them. The old path issued a blocking read whose most expensive answer was "nothing there" — ~50 pause/resume cycles per poll, and it abandoned a command still running on the chip, whose late reply the next command's ACK wait then read as garbage.

Real limitations:

  1. Presence only — no identity during the move. You cannot verify which tag stopped the move. The UID arrives afterwards from read_gate_after_home(), and that read can legitimately come back empty: the tag keeps travelling through the deceleration ramp and may no longer be coupled to the antenna. The code logs it and moves on — it deliberately does not jog to chase it. So "homed to tag" and "read the tag" are separate outcomes, and _home_gate_with_nfc returns them as separate flags (homed, tag_read).
  2. It is a host software endstop. Trigger time is estimated_print_time(eventtime) from a reactor timer, so host scheduling jitter adds to the poll interval. mmu_filament_movement also has to force gear_homing_speed for the NFC-only chase move, because a virtual endstop otherwise defaults to the much slower virtual_sensor_homing_speed.
  3. Compounding requires a real MCU gate switch. _build_nfc_compound_endstop() refuses to compound the reader with a virtual gate endstop — that would host-poll two virtuals in one drip move and double the stall risk. On such a machine you fall back to plain homing and the tag is not read during the move.
  4. The drain after a hit can block. probe_stop() is real work: PN532's InRelease is bounded at 200 ms (it ignores the caller's timeout), PN7160 honours a 25 ms RF_DEACTIVATE guard, RC522 drops and restores the RF field (~4 ms). _homing_poll deliberately does not do this at the moment of detection — _drain_probe() runs at the next stationary point. Correct, but it means the chip is briefly in an undefined-ish state while the move decelerates.
  5. RC522's handoff is subtle. Its probe wakes tags with WUPA, leaving them in READY, and the following read_target() starts with REQA, which READY tags ignore. probe_stop() drops the field to reset PICCs back to IDLE. If that field-drop fails, the post-move read fails on the very tag the probe just found.
  6. One homing move at a time. A single timer and a single _homing_endstop. Fine as Happy Hare homes one gate at a time — but note a shared reader listed for several gates yields distinct endstop objects over one reader object, so those homes are strictly serialized.
  7. A disabled reader refuses to arm (start_homing_poll logs and returns) rather than letting the move run its full length and fail with an opaque "no trigger".
  8. RF range dominates repeatability. Detection happens over a volume, not a plane; where in that volume the tag first answers depends on tag size, antenna geometry, orientation and what's behind the tag (metal, wet filament, spool core). None of that is in the code, and it very plausibly exceeds the 2–4 mm of poll-interval overshoot on the good chips. Assume the poll interval stops being the limiting factor once you're on a probe-capable driver — which is another way of saying the difference between PN532 (2 mm) and RC522 (4 mm) is probably noise, while the difference to the shim (10–15 mm) is not.

F. "Home OFF a tag" — what it would take

The user-visible answer: not currently possible, and the reason is three specific things, not general immaturity.

  1. The endstop cannot express trigger-on-absence. MmuNfcEndstop.home_start() pins triggered=True unconditionally — "A tag detection is ALWAYS the trigger, independent of move direction" — and pre-resets state with note_filament_present(print_time, False). Both bake in trigger-on-present.
  2. The poll loop has no absence path. _homing_poll triggers on truthy found; found is False means "that scan completed empty — start the next one". Absence is currently the loop's idle state, so it cannot also be its trigger. Making it one needs a distinct return value (e.g. 'absent') plus hysteresis — N consecutive confirmed absences — because a single missed exchange is not the same as a tag having left the field.
  3. Most drivers don't produce a trustworthy absence signal at all. Per chip:
Chip Absence signal available?
PN532 No. The in-flight InListPassiveTarget retries activation forever (MxRtyPassiveActivation = 0xFF), so a healthy no-tag scan returns None indefinitely. The only False is an error or the 2 s watchdog — which the code treats as a wedge. Getting absence means running activation with a finite retry count, or deliberately using the abort path as a bounded scan.
RC522 Best of the probe drivers. An unanswered WUPA is positive evidence of absence, ~every 20 ms, already distinguished in probe_poll (TimerIRq set, RxIRq/IdleIRq clear). Caveat: within a run WUPA keeps working from READY, so a woken tag still answers — good, that's what makes "it stopped answering" meaningful.
PN7160 Tractable but not implemented. is_tag_present_ntf() covers only RF_DISCOVER_NTF (oid 0x03) and RF_INTF_ACTIVATED_NTF (oid 0x05). NCI does signal deactivation — the driver already knows gid RF/oid 0x06 as RF_DEACTIVATE and sends NCI_RF_DEACTIVATE_IDLE_CMD — so a sibling is_tag_removed_ntf() predicate is the missing piece, plus deciding whether to run a presence check while activated.
PN5180 Ironically already correct, just slow. The shim's False means "a bounded read_target() found nothing" — a genuine absence assertion, unlike the probe drivers' False. At ~100–150 ms per confirmation it's too coarse for accurate homing, but it's the semantically right signal.

So the cheapest real path to homing-off-a-tag is RC522 (or PN7160 once a removal predicate exists), with the endstop and poll loop taught a three-state present/absent/unknown vocabulary.


G. Picking a chip

Best shared reader (one reader, user presents tags by hand — the manager polls it at NFC_CHECK_INTERVAL = 1.0 s with a 0.1 s read, and homing accuracy is irrelevant):

  • PN532 over UART/HSU. Zero MCU pins, plugs into the Pi, exclusive tty enforced, and a missing adapter doesn't stop klippy — just reports not-alive until MMU_RFID_INIT. Doesn't scale per-gate, which doesn't matter here.
  • PN5180 if you need ISO15693/Type-5 and don't need homing. Its 3 pins are paid once, and the shim's latency is a non-issue for a hand-presented reader.
  • PN7160 if you want Type-5 and the option of reusing the same chip per-gate.

Best per-gate reader (reader per lane, used as a homing endstop):

  • RC522 — 1 pin per gate on a shared SPI bus, full-rate probing with no extra wiring, ~4 mm detection window, and the only driver the endstop header calls "confirmed viable". The clear default for a reader-per-gate build.
  • PN7160 — pick this when you need ISO15693 per-gate or want to run up to 4 readers on one I2C bus. Budget irq_pin per gate; without it you drop to the shim and lose 5× on detection latency. Bench-check the 50 ms _wait_for_irq_release() on the detection tick.
  • PN532 over software I2C — works, but 2 pins per gate plus pull-ups per bus, because 0x24 is fixed. Best detection latency of the lot (~2 mm) if pin count isn't your constraint. PN532/SPI would be 1 pin per gate and is fully written — but it is untested against hardware and warns as such at startup.
  • PN5180 — avoid per-gate for homing. 3 required pins per gate and no probe. Its place is as a shared Type-5 reader.

If you need ISO15693/Type-5 at all: PN7160 or PN5180 only. PN532 and RC522 cannot read those tags in this codebase.

*** NEW V4 DOC IS HERE ***

 1. Introduction
 2. Installation
 3. Essential Configuration
 4. Calibration
 5. Operation

-- YOUR MMU IS READY TO PLAY WITH --

 6. Slicer-MMU Setup

-- NOW YOU ARE READY TO PRINT! --

 7. Tuning
 8. Optional Feature Setup
 9. Advanced Configuration
 10. Advanced Concepts
11. Quick References

12. Troubleshooting
13. FAQ
14. MCU Board Reference 🆕
15. Change Log
Happy Hare Discord

Clone this wiki locally