| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- #!/usr/bin/env python3
- """PN5180 NFC reader diagnostic script.
- Connects to a PN5180 over SPI on a Raspberry Pi and reads
- hardware status, version info, and register state.
- Wiring (from spoolbuddy/README.md):
- PN5180 VCC -> Pi Pin 1 (3.3V)
- PN5180 GND -> Pi Pin 20 (GND)
- PN5180 SCK -> Pi Pin 23 (GPIO11)
- PN5180 MISO -> Pi Pin 21 (GPIO9)
- PN5180 MOSI -> Pi Pin 19 (GPIO10)
- PN5180 NSS -> Pi Pin 24 (GPIO8 / CE0)
- PN5180 BUSY -> Pi Pin 22 (GPIO25)
- PN5180 RST -> Pi Pin 18 (GPIO24)
- """
- import sys
- import time
- import gpiod
- import spidev
- # ---------------------------------------------------------------------------
- # Pin assignments (BCM numbering)
- # ---------------------------------------------------------------------------
- BUSY_PIN = 25 # Pin 22
- RST_PIN = 24 # Pin 18
- # ---------------------------------------------------------------------------
- # SPI command instruction codes (NXP PN5180 datasheet Table 5)
- # ---------------------------------------------------------------------------
- CMD_WRITE_REGISTER = 0x00
- CMD_WRITE_REGISTER_OR_MASK = 0x01
- CMD_WRITE_REGISTER_AND_MASK = 0x02
- CMD_READ_REGISTER = 0x04
- CMD_READ_REGISTER_MULTIPLE = 0x05
- CMD_WRITE_EEPROM = 0x06
- CMD_READ_EEPROM = 0x07
- CMD_SEND_DATA = 0x09
- CMD_READ_DATA = 0x0A
- CMD_LOAD_RF_CONFIG = 0x11
- CMD_RF_ON = 0x16
- CMD_RF_OFF = 0x17
- # ---------------------------------------------------------------------------
- # Register addresses (32-bit each)
- # ---------------------------------------------------------------------------
- REG_SYSTEM_CONFIG = 0x00
- REG_IRQ_ENABLE = 0x01
- REG_IRQ_STATUS = 0x02
- REG_IRQ_CLEAR = 0x03
- REG_TRANSCEIVE_CONTROL = 0x04
- REG_TIMER1_RELOAD = 0x0C
- REG_TIMER1_CONFIG = 0x0F
- REG_RX_WAIT_CONFIG = 0x11
- REG_CRC_RX_CONFIG = 0x12
- REG_RX_STATUS = 0x13
- REG_CRC_TX_CONFIG = 0x19
- REG_RF_STATUS = 0x1D
- REG_SYSTEM_STATUS = 0x24
- REG_TEMP_CONTROL = 0x25
- REGISTER_NAMES = {
- REG_SYSTEM_CONFIG: "SYSTEM_CONFIG",
- REG_IRQ_ENABLE: "IRQ_ENABLE",
- REG_IRQ_STATUS: "IRQ_STATUS",
- REG_IRQ_CLEAR: "IRQ_CLEAR",
- REG_TRANSCEIVE_CONTROL: "TRANSCEIVE_CONTROL",
- REG_TIMER1_RELOAD: "TIMER1_RELOAD",
- REG_TIMER1_CONFIG: "TIMER1_CONFIG",
- REG_RX_WAIT_CONFIG: "RX_WAIT_CONFIG",
- REG_CRC_RX_CONFIG: "CRC_RX_CONFIG",
- REG_RX_STATUS: "RX_STATUS",
- REG_CRC_TX_CONFIG: "CRC_TX_CONFIG",
- REG_RF_STATUS: "RF_STATUS",
- REG_SYSTEM_STATUS: "SYSTEM_STATUS",
- REG_TEMP_CONTROL: "TEMP_CONTROL",
- }
- # ---------------------------------------------------------------------------
- # EEPROM addresses
- # ---------------------------------------------------------------------------
- EEPROM_DIE_IDENTIFIER = 0x00 # 16 bytes
- EEPROM_PRODUCT_VERSION = 0x10 # 2 bytes
- EEPROM_FIRMWARE_VERSION = 0x12 # 2 bytes
- EEPROM_EEPROM_VERSION = 0x14 # 2 bytes
- EEPROM_IRQ_PIN_CONFIG = 0x1A # 1 byte
- def _find_gpio_chip():
- """Find the right gpiochip for Raspberry Pi GPIO pins.
- RPi 5 uses gpiochip4, RPi 4 uses gpiochip0.
- """
- for path in ["/dev/gpiochip4", "/dev/gpiochip0"]:
- try:
- chip = gpiod.Chip(path)
- info = chip.get_info()
- # RPi 4: pinctrl-bcm2711, RPi 5: pinctrl-rp1
- if "pinctrl" in info.label:
- return chip
- chip.close()
- except (FileNotFoundError, PermissionError, OSError):
- continue
- raise RuntimeError("Could not find Raspberry Pi GPIO chip")
- class PN5180:
- """Low-level driver for the PN5180 NFC frontend over SPI."""
- def __init__(self, spi_bus=0, spi_device=0, spi_speed_hz=1_000_000, busy_pin=BUSY_PIN, rst_pin=RST_PIN):
- # GPIO setup via libgpiod
- self._chip = _find_gpio_chip()
- self._busy_line = self._chip.request_lines(
- consumer="pn5180-diag",
- config={busy_pin: gpiod.LineSettings(direction=gpiod.line.Direction.INPUT)},
- )
- self._rst_line = self._chip.request_lines(
- consumer="pn5180-diag",
- config={
- rst_pin: gpiod.LineSettings(
- direction=gpiod.line.Direction.OUTPUT,
- output_value=gpiod.line.Value.ACTIVE,
- )
- },
- )
- self._busy_pin = busy_pin
- self._rst_pin = rst_pin
- # SPI setup – mode 0 (CPOL=0, CPHA=0), MSB first
- self._spi = spidev.SpiDev()
- self._spi.open(spi_bus, spi_device)
- self._spi.max_speed_hz = spi_speed_hz
- self._spi.mode = 0b00
- self._spi.bits_per_word = 8
- def close(self):
- self._spi.close()
- self._busy_line.release()
- self._rst_line.release()
- self._chip.close()
- # -- low-level helpers --------------------------------------------------
- def _busy_is_high(self):
- return self._busy_line.get_value(self._busy_pin) == gpiod.line.Value.ACTIVE
- def _wait_busy(self, timeout_s=1.0):
- """Block until BUSY goes LOW (PN5180 ready)."""
- deadline = time.monotonic() + timeout_s
- while self._busy_is_high():
- if time.monotonic() > deadline:
- raise TimeoutError("PN5180 BUSY line did not go low")
- time.sleep(0.001)
- def _send_command(self, tx_data, rx_len=0):
- """Send an SPI command frame and optionally read a response frame.
- The PN5180 SPI protocol is half-duplex:
- 1. Send command frame (NSS held low for entire frame).
- 2. Wait for BUSY high then low (command processed).
- 3. If a response is expected, clock out rx_len bytes in a second frame.
- """
- self._wait_busy()
- # Transmit command
- self._spi.xfer2(list(tx_data))
- if rx_len == 0:
- # Write-only command – wait for processing
- time.sleep(0.001)
- self._wait_busy()
- return None
- # Wait for PN5180 to process command (BUSY goes high then low)
- time.sleep(0.001)
- self._wait_busy()
- # Read response
- rx = self._spi.xfer2([0xFF] * rx_len)
- time.sleep(0.001)
- self._wait_busy()
- return bytes(rx)
- # -- register operations ------------------------------------------------
- def read_register(self, addr):
- """Read a 32-bit register. Returns int."""
- resp = self._send_command([CMD_READ_REGISTER, addr], rx_len=4)
- return int.from_bytes(resp, "little")
- def write_register(self, addr, value):
- """Write a 32-bit value to a register."""
- self._send_command(
- [
- CMD_WRITE_REGISTER,
- addr,
- value & 0xFF,
- (value >> 8) & 0xFF,
- (value >> 16) & 0xFF,
- (value >> 24) & 0xFF,
- ]
- )
- def write_register_or_mask(self, addr, mask):
- self._send_command(
- [
- CMD_WRITE_REGISTER_OR_MASK,
- addr,
- mask & 0xFF,
- (mask >> 8) & 0xFF,
- (mask >> 16) & 0xFF,
- (mask >> 24) & 0xFF,
- ]
- )
- def write_register_and_mask(self, addr, mask):
- self._send_command(
- [
- CMD_WRITE_REGISTER_AND_MASK,
- addr,
- mask & 0xFF,
- (mask >> 8) & 0xFF,
- (mask >> 16) & 0xFF,
- (mask >> 24) & 0xFF,
- ]
- )
- # -- EEPROM operations --------------------------------------------------
- def read_eeprom(self, addr, length):
- """Read `length` bytes from EEPROM starting at `addr`."""
- return self._send_command([CMD_READ_EEPROM, addr, length], rx_len=length)
- # -- reset --------------------------------------------------------------
- def reset(self):
- """Hardware-reset the PN5180 via the RST pin."""
- self._rst_line.set_value(self._rst_pin, gpiod.line.Value.INACTIVE)
- time.sleep(0.01)
- self._rst_line.set_value(self._rst_pin, gpiod.line.Value.ACTIVE)
- time.sleep(0.05)
- self._wait_busy(timeout_s=2.0)
- # Clear all IRQ flags
- self.write_register(REG_IRQ_CLEAR, 0xFFFFFFFF)
- # -- version / identity -------------------------------------------------
- def get_product_version(self):
- data = self.read_eeprom(EEPROM_PRODUCT_VERSION, 2)
- return f"{data[1]}.{data[0]}"
- def get_firmware_version(self):
- data = self.read_eeprom(EEPROM_FIRMWARE_VERSION, 2)
- return f"{data[1]}.{data[0]}"
- def get_eeprom_version(self):
- data = self.read_eeprom(EEPROM_EEPROM_VERSION, 2)
- return f"{data[1]}.{data[0]}"
- def get_die_identifier(self):
- data = self.read_eeprom(EEPROM_DIE_IDENTIFIER, 16)
- return data.hex()
- def run_diagnostics():
- print("=" * 60)
- print("PN5180 NFC Reader Diagnostics")
- print("=" * 60)
- nfc = PN5180()
- try:
- # Reset
- print("\n[1] Hardware reset...")
- nfc.reset()
- print(" Reset OK")
- # Version info
- print("\n[2] Version info (EEPROM)")
- print(f" Product version : {nfc.get_product_version()}")
- print(f" Firmware version : {nfc.get_firmware_version()}")
- print(f" EEPROM version : {nfc.get_eeprom_version()}")
- print(f" Die identifier : {nfc.get_die_identifier()}")
- # Register dump
- print("\n[3] Register dump")
- for addr, name in sorted(REGISTER_NAMES.items()):
- val = nfc.read_register(addr)
- print(f" 0x{addr:02X} {name:<24s} = 0x{val:08X}")
- # IRQ status breakdown
- irq = nfc.read_register(REG_IRQ_STATUS)
- print(f"\n[4] IRQ status flags (0x{irq:08X})")
- irq_flags = [
- (0, "RX_IRQ"),
- (1, "TX_IRQ"),
- (2, "IDLE_IRQ"),
- (3, "MODE_DETECTED_IRQ"),
- (4, "CARD_ACTIVATED_IRQ"),
- (5, "STATE_CHANGE_IRQ"),
- (6, "RFOFF_DET_IRQ"),
- (7, "RFON_DET_IRQ"),
- (8, "TX_RFOFF_IRQ"),
- (9, "TX_RFON_IRQ"),
- (10, "RF_ACTIVE_ERROR_IRQ"),
- (14, "LPCD_IRQ"),
- ]
- for bit, name in irq_flags:
- state = "SET" if irq & (1 << bit) else "---"
- print(f" bit {bit:2d}: {name:<28s} [{state}]")
- # RF status
- rf = nfc.read_register(REG_RF_STATUS)
- print(f"\n[5] RF status (0x{rf:08X})")
- tx_rf_on = bool(rf & (1 << 0))
- rx_en = bool(rf & (1 << 1))
- print(f" TX RF active : {tx_rf_on}")
- print(f" RX enabled : {rx_en}")
- # System status
- sys_stat = nfc.read_register(REG_SYSTEM_STATUS)
- print(f"\n[6] System status (0x{sys_stat:08X})")
- # Temperature
- temp_ctrl = nfc.read_register(REG_TEMP_CONTROL)
- print(f"\n[7] Temp control register (0x{temp_ctrl:08X})")
- print("\n" + "=" * 60)
- print("Diagnostics complete - PN5180 is responding over SPI.")
- print("=" * 60)
- except TimeoutError as e:
- print(f"\nERROR: {e}")
- print("Check wiring and ensure SPI is enabled (dtparam=spi=on in /boot/firmware/config.txt)")
- sys.exit(1)
- except Exception as e:
- print(f"\nERROR: {e}")
- sys.exit(1)
- finally:
- nfc.close()
- if __name__ == "__main__":
- run_diagnostics()
|