pn5180_diag.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. #!/usr/bin/env python3
  2. """PN5180 NFC reader diagnostic script.
  3. Connects to a PN5180 over SPI on a Raspberry Pi and reads
  4. hardware status, version info, and register state.
  5. Wiring (from spoolbuddy/README.md):
  6. PN5180 VCC -> Pi Pin 1 (3.3V)
  7. PN5180 GND -> Pi Pin 20 (GND)
  8. PN5180 SCK -> Pi Pin 23 (GPIO11)
  9. PN5180 MISO -> Pi Pin 21 (GPIO9)
  10. PN5180 MOSI -> Pi Pin 19 (GPIO10)
  11. PN5180 NSS -> Pi Pin 16 (GPIO23, manual CS)
  12. PN5180 BUSY -> Pi Pin 22 (GPIO25)
  13. PN5180 RST -> Pi Pin 18 (GPIO24)
  14. """
  15. import sys
  16. import time
  17. import gpiod
  18. import spidev
  19. # ---------------------------------------------------------------------------
  20. # Pin assignments (BCM numbering)
  21. # ---------------------------------------------------------------------------
  22. BUSY_PIN = 25 # Pin 22
  23. RST_PIN = 24 # Pin 18
  24. NSS_PIN = 23 # Pin 16 (manual CS)
  25. # ---------------------------------------------------------------------------
  26. # SPI command instruction codes (NXP PN5180 datasheet Table 5)
  27. # ---------------------------------------------------------------------------
  28. CMD_WRITE_REGISTER = 0x00
  29. CMD_WRITE_REGISTER_OR_MASK = 0x01
  30. CMD_WRITE_REGISTER_AND_MASK = 0x02
  31. CMD_READ_REGISTER = 0x04
  32. CMD_READ_REGISTER_MULTIPLE = 0x05
  33. CMD_WRITE_EEPROM = 0x06
  34. CMD_READ_EEPROM = 0x07
  35. CMD_SEND_DATA = 0x09
  36. CMD_READ_DATA = 0x0A
  37. CMD_LOAD_RF_CONFIG = 0x11
  38. CMD_RF_ON = 0x16
  39. CMD_RF_OFF = 0x17
  40. # ---------------------------------------------------------------------------
  41. # Register addresses (32-bit each)
  42. # ---------------------------------------------------------------------------
  43. REG_SYSTEM_CONFIG = 0x00
  44. REG_IRQ_ENABLE = 0x01
  45. REG_IRQ_STATUS = 0x02
  46. REG_IRQ_CLEAR = 0x03
  47. REG_TRANSCEIVE_CONTROL = 0x04
  48. REG_TIMER1_RELOAD = 0x0C
  49. REG_TIMER1_CONFIG = 0x0F
  50. REG_RX_WAIT_CONFIG = 0x11
  51. REG_CRC_RX_CONFIG = 0x12
  52. REG_RX_STATUS = 0x13
  53. REG_CRC_TX_CONFIG = 0x19
  54. REG_RF_STATUS = 0x1D
  55. REG_SYSTEM_STATUS = 0x24
  56. REG_TEMP_CONTROL = 0x25
  57. REGISTER_NAMES = {
  58. REG_SYSTEM_CONFIG: "SYSTEM_CONFIG",
  59. REG_IRQ_ENABLE: "IRQ_ENABLE",
  60. REG_IRQ_STATUS: "IRQ_STATUS",
  61. REG_IRQ_CLEAR: "IRQ_CLEAR",
  62. REG_TRANSCEIVE_CONTROL: "TRANSCEIVE_CONTROL",
  63. REG_TIMER1_RELOAD: "TIMER1_RELOAD",
  64. REG_TIMER1_CONFIG: "TIMER1_CONFIG",
  65. REG_RX_WAIT_CONFIG: "RX_WAIT_CONFIG",
  66. REG_CRC_RX_CONFIG: "CRC_RX_CONFIG",
  67. REG_RX_STATUS: "RX_STATUS",
  68. REG_CRC_TX_CONFIG: "CRC_TX_CONFIG",
  69. REG_RF_STATUS: "RF_STATUS",
  70. REG_SYSTEM_STATUS: "SYSTEM_STATUS",
  71. REG_TEMP_CONTROL: "TEMP_CONTROL",
  72. }
  73. # ---------------------------------------------------------------------------
  74. # EEPROM addresses
  75. # ---------------------------------------------------------------------------
  76. EEPROM_DIE_IDENTIFIER = 0x00 # 16 bytes
  77. EEPROM_PRODUCT_VERSION = 0x10 # 2 bytes
  78. EEPROM_FIRMWARE_VERSION = 0x12 # 2 bytes
  79. EEPROM_EEPROM_VERSION = 0x14 # 2 bytes
  80. EEPROM_IRQ_PIN_CONFIG = 0x1A # 1 byte
  81. def _find_gpio_chip():
  82. """Find the right gpiochip for Raspberry Pi GPIO pins.
  83. RPi 5 uses gpiochip4, RPi 4 uses gpiochip0.
  84. """
  85. for path in ["/dev/gpiochip4", "/dev/gpiochip0"]:
  86. try:
  87. chip = gpiod.Chip(path)
  88. info = chip.get_info()
  89. # RPi 4: pinctrl-bcm2711, RPi 5: pinctrl-rp1
  90. if "pinctrl" in info.label:
  91. return chip
  92. chip.close()
  93. except (FileNotFoundError, PermissionError, OSError):
  94. continue
  95. raise RuntimeError("Could not find Raspberry Pi GPIO chip")
  96. class PN5180:
  97. """Low-level driver for the PN5180 NFC frontend over SPI."""
  98. def __init__(
  99. self,
  100. spi_bus=0,
  101. spi_device=0,
  102. spi_speed_hz=500_000,
  103. busy_pin=BUSY_PIN,
  104. rst_pin=RST_PIN,
  105. nss_pin=NSS_PIN,
  106. ):
  107. # GPIO setup via libgpiod
  108. self._chip = _find_gpio_chip()
  109. try:
  110. self._busy_line = self._chip.request_lines(
  111. consumer="pn5180-diag",
  112. config={busy_pin: gpiod.LineSettings(direction=gpiod.line.Direction.INPUT)},
  113. )
  114. self._rst_line = self._chip.request_lines(
  115. consumer="pn5180-diag",
  116. config={
  117. rst_pin: gpiod.LineSettings(
  118. direction=gpiod.line.Direction.OUTPUT,
  119. output_value=gpiod.line.Value.ACTIVE,
  120. )
  121. },
  122. )
  123. self._nss_line = self._chip.request_lines(
  124. consumer="pn5180-diag",
  125. config={
  126. nss_pin: gpiod.LineSettings(
  127. direction=gpiod.line.Direction.OUTPUT,
  128. output_value=gpiod.line.Value.ACTIVE,
  129. )
  130. },
  131. )
  132. except OSError as e:
  133. self._chip.close()
  134. if getattr(e, "errno", None) == 16:
  135. raise RuntimeError(
  136. "GPIO line is busy (another process owns PN5180 pins). "
  137. "Stop spoolbuddy service before running diagnostics: "
  138. "sudo systemctl stop spoolbuddy"
  139. ) from e
  140. raise
  141. self._busy_pin = busy_pin
  142. self._rst_pin = rst_pin
  143. self._nss_pin = nss_pin
  144. # SPI setup - mode 0 (CPOL=0, CPHA=0), MSB first
  145. self._spi = spidev.SpiDev()
  146. self._spi.open(spi_bus, spi_device)
  147. self._spi.max_speed_hz = spi_speed_hz
  148. self._spi.mode = 0b00
  149. self._spi.bits_per_word = 8
  150. self._spi.no_cs = True
  151. def close(self):
  152. self._spi.close()
  153. self._busy_line.release()
  154. self._rst_line.release()
  155. self._nss_line.release()
  156. self._chip.close()
  157. # -- low-level helpers --------------------------------------------------
  158. def _busy_is_high(self):
  159. return self._busy_line.get_value(self._busy_pin) == gpiod.line.Value.ACTIVE
  160. def _wait_busy(self, timeout_s=1.0):
  161. """Block until BUSY goes LOW (PN5180 ready)."""
  162. deadline = time.monotonic() + timeout_s
  163. while self._busy_is_high():
  164. if time.monotonic() > deadline:
  165. raise TimeoutError("PN5180 BUSY line did not go low")
  166. time.sleep(0.001)
  167. def _cs_low(self):
  168. self._nss_line.set_value(self._nss_pin, gpiod.line.Value.INACTIVE)
  169. time.sleep(0.000005)
  170. def _cs_high(self):
  171. self._nss_line.set_value(self._nss_pin, gpiod.line.Value.ACTIVE)
  172. time.sleep(0.000100)
  173. def _send_command(self, tx_data, rx_len=0):
  174. """Send an SPI command frame and optionally read a response frame.
  175. The PN5180 SPI protocol is half-duplex:
  176. 1. Send command frame (NSS held low for entire frame).
  177. 2. Wait for BUSY high then low (command processed).
  178. 3. If a response is expected, clock out rx_len bytes in a second frame.
  179. """
  180. self._wait_busy()
  181. # Transmit command (manual CS)
  182. self._cs_low()
  183. self._spi.xfer2(list(tx_data))
  184. self._cs_high()
  185. if rx_len == 0:
  186. # Write-only command - wait for processing
  187. time.sleep(0.001)
  188. self._wait_busy()
  189. return None
  190. # Wait for PN5180 to process command (BUSY goes high then low)
  191. time.sleep(0.001)
  192. self._wait_busy()
  193. # Read response (manual CS)
  194. self._cs_low()
  195. rx = self._spi.xfer2([0xFF] * rx_len)
  196. self._cs_high()
  197. time.sleep(0.001)
  198. self._wait_busy()
  199. return bytes(rx)
  200. # -- register operations ------------------------------------------------
  201. def read_register(self, addr):
  202. """Read a 32-bit register. Returns int."""
  203. resp = self._send_command([CMD_READ_REGISTER, addr], rx_len=4)
  204. return int.from_bytes(resp, "little")
  205. def write_register(self, addr, value):
  206. """Write a 32-bit value to a register."""
  207. self._send_command(
  208. [
  209. CMD_WRITE_REGISTER,
  210. addr,
  211. value & 0xFF,
  212. (value >> 8) & 0xFF,
  213. (value >> 16) & 0xFF,
  214. (value >> 24) & 0xFF,
  215. ]
  216. )
  217. def write_register_or_mask(self, addr, mask):
  218. self._send_command(
  219. [
  220. CMD_WRITE_REGISTER_OR_MASK,
  221. addr,
  222. mask & 0xFF,
  223. (mask >> 8) & 0xFF,
  224. (mask >> 16) & 0xFF,
  225. (mask >> 24) & 0xFF,
  226. ]
  227. )
  228. def write_register_and_mask(self, addr, mask):
  229. self._send_command(
  230. [
  231. CMD_WRITE_REGISTER_AND_MASK,
  232. addr,
  233. mask & 0xFF,
  234. (mask >> 8) & 0xFF,
  235. (mask >> 16) & 0xFF,
  236. (mask >> 24) & 0xFF,
  237. ]
  238. )
  239. # -- EEPROM operations --------------------------------------------------
  240. def read_eeprom(self, addr, length):
  241. """Read `length` bytes from EEPROM starting at `addr`."""
  242. return self._send_command([CMD_READ_EEPROM, addr, length], rx_len=length)
  243. # -- reset --------------------------------------------------------------
  244. def reset(self):
  245. """Hardware-reset the PN5180 via the RST pin."""
  246. self._rst_line.set_value(self._rst_pin, gpiod.line.Value.INACTIVE)
  247. time.sleep(0.01)
  248. self._rst_line.set_value(self._rst_pin, gpiod.line.Value.ACTIVE)
  249. time.sleep(0.05)
  250. self._wait_busy(timeout_s=2.0)
  251. # Clear all IRQ flags
  252. self.write_register(REG_IRQ_CLEAR, 0xFFFFFFFF)
  253. # -- version / identity -------------------------------------------------
  254. def get_product_version(self):
  255. data = self.read_eeprom(EEPROM_PRODUCT_VERSION, 2)
  256. return f"{data[1]}.{data[0]}"
  257. def get_firmware_version(self):
  258. data = self.read_eeprom(EEPROM_FIRMWARE_VERSION, 2)
  259. return f"{data[1]}.{data[0]}"
  260. def get_eeprom_version(self):
  261. data = self.read_eeprom(EEPROM_EEPROM_VERSION, 2)
  262. return f"{data[1]}.{data[0]}"
  263. def get_die_identifier(self):
  264. data = self.read_eeprom(EEPROM_DIE_IDENTIFIER, 16)
  265. return data.hex()
  266. def run_diagnostics():
  267. print("=" * 60)
  268. print("PN5180 NFC Reader Diagnostics")
  269. print("=" * 60)
  270. nfc = None
  271. try:
  272. nfc = PN5180()
  273. # Reset
  274. print("\n[1] Hardware reset...")
  275. nfc.reset()
  276. print(" Reset OK")
  277. # Version info
  278. print("\n[2] Version info (EEPROM)")
  279. print(f" Product version : {nfc.get_product_version()}")
  280. print(f" Firmware version : {nfc.get_firmware_version()}")
  281. print(f" EEPROM version : {nfc.get_eeprom_version()}")
  282. print(f" Die identifier : {nfc.get_die_identifier()}")
  283. # Register dump
  284. print("\n[3] Register dump")
  285. for addr, name in sorted(REGISTER_NAMES.items()):
  286. val = nfc.read_register(addr)
  287. print(f" 0x{addr:02X} {name:<24s} = 0x{val:08X}")
  288. # IRQ status breakdown
  289. irq = nfc.read_register(REG_IRQ_STATUS)
  290. print(f"\n[4] IRQ status flags (0x{irq:08X})")
  291. irq_flags = [
  292. (0, "RX_IRQ"),
  293. (1, "TX_IRQ"),
  294. (2, "IDLE_IRQ"),
  295. (3, "MODE_DETECTED_IRQ"),
  296. (4, "CARD_ACTIVATED_IRQ"),
  297. (5, "STATE_CHANGE_IRQ"),
  298. (6, "RFOFF_DET_IRQ"),
  299. (7, "RFON_DET_IRQ"),
  300. (8, "TX_RFOFF_IRQ"),
  301. (9, "TX_RFON_IRQ"),
  302. (10, "RF_ACTIVE_ERROR_IRQ"),
  303. (14, "LPCD_IRQ"),
  304. ]
  305. for bit, name in irq_flags:
  306. state = "SET" if irq & (1 << bit) else "---"
  307. print(f" bit {bit:2d}: {name:<28s} [{state}]")
  308. # RF status
  309. rf = nfc.read_register(REG_RF_STATUS)
  310. print(f"\n[5] RF status (0x{rf:08X})")
  311. tx_rf_on = bool(rf & (1 << 0))
  312. rx_en = bool(rf & (1 << 1))
  313. print(f" TX RF active : {tx_rf_on}")
  314. print(f" RX enabled : {rx_en}")
  315. # System status
  316. sys_stat = nfc.read_register(REG_SYSTEM_STATUS)
  317. print(f"\n[6] System status (0x{sys_stat:08X})")
  318. # Temperature
  319. temp_ctrl = nfc.read_register(REG_TEMP_CONTROL)
  320. print(f"\n[7] Temp control register (0x{temp_ctrl:08X})")
  321. print("\n" + "=" * 60)
  322. print("Diagnostics complete - PN5180 is responding over SPI.")
  323. print("=" * 60)
  324. except TimeoutError as e:
  325. print(f"\nERROR: {e}")
  326. print("Check wiring and ensure SPI is enabled (dtparam=spi=on in /boot/firmware/config.txt)")
  327. sys.exit(1)
  328. except Exception as e:
  329. print(f"\nERROR: {e}")
  330. sys.exit(1)
  331. finally:
  332. if nfc is not None:
  333. nfc.close()
  334. if __name__ == "__main__":
  335. run_diagnostics()