ESP32 NFC Development

Embedded NFC with ESP-IDF and Arduino

ESP32 NFC Development: ESP-IDF and Arduino

The ESP32 has no built-in NFC hardware, but its SPI and I2C peripherals make it a natural host for external NFC reader ICs. The PN532 over SPI is the most common choice for prototyping; the PN7150 is preferred for production designs needing NCI compliance. This guide covers both ESP-IDF (C) and Arduino (C++) workflows.

Hardware Options

IC Interface Library Best For
PN532 SPI / I2C / HSU Adafruit PN532, Elechouse Prototyping, NTAG, MIFARE
PN7150 I2C + IRQ + VEN nxp-nci-esp32 Production, NCI stack
ST25R3916 SPI ST25R Arduino ISO 15693, long range
RC663 SPI / I2C NXP CLRC663 SDK High-throughput multi-protocol

For IC-level trade-offs see NFC Reader Modules Compared.

PN532 Wiring (SPI)

ESP32 GPIO    PN532 Pin
-----------   ---------
GPIO 18 (SCLK) → SCK
GPIO 23 (MOSI) → MOSI
GPIO 19 (MISO) → MISO
GPIO 5  (SS)   → NSS/CS
3.3V           → VCC (3.3V version) or 5V with level shifter
GND            → GND

Set the DIP switches on the PN532 breakout to SPI mode: switch 1 OFF, switch 2 ON.

Arduino: Reading NDEF with Adafruit PN532

#include <Wire.h>
#include <Adafruit_PN532.h>

#define PN532_SS   5
#define PN532_SCK  18
#define PN532_MOSI 23
#define PN532_MISO 19

Adafruit_PN532 nfc(PN532_SS);

void setup() {
  Serial.begin(115200);
  nfc.begin();

  uint32_t versiondata = nfc.getFirmwareVersion();
  if (!versiondata) {
    Serial.println("PN532 not found. Check wiring.");
    while (1) delay(10);
  }
  Serial.printf("PN532 v%d.%d firmware\n",
    (versiondata >> 16) & 0xFF, (versiondata >> 8) & 0xFF);

  nfc.SAMConfig();
  Serial.println("Waiting for NFC tag...");
}

void loop() {
  uint8_t uid[7];
  uint8_t uidLength;

  if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength)) {
    Serial.print("UID: ");
    for (uint8_t i = 0; i < uidLength; i++) {
      Serial.printf("%02X ", uid[i]);
    }
    Serial.println();
    delay(500);
  }
}

Arduino: Writing NDEF URL to NTAG213

#include <Adafruit_PN532.h>

// NTAG213 user memory starts at page 4
// Each page is 4 bytes
// NDEF TLV 0x03 + length byte + NDEF record + 0xFE terminator

void writeNdefUrl(Adafruit_PN532 &nfc, const char* url) {
  // Build minimal NDEF URI record: https://
  // URI abbreviation 0x04 = "https://"
  uint8_t urlLen = strlen(url);
  uint8_t payloadLen = 1 + urlLen;  // abbrev byte + url bytes
  // Record: MB=1 ME=1 SR=1 TNF=0x01, Type='U', PayloadLen, 0x04, url
  uint8_t ndef[] = {
    0xD1,             // MB | ME | SR | TNF=WellKnown
    0x01,             // Type length = 1
    (uint8_t)payloadLen,
    0x55,             // Type = 'U'
    0x04              // Abbreviation: https://
  };
  // TLV wrapper: 0x03, length, ...ndef..., 0xFE
  // Write pages starting at page 4
  // (Full implementation: pack into 4-byte pages and call nfc.ntag2xx_WritePage)
  Serial.println("Write complete");
}

For production use, consider the NDEF library by Don Coleman (arduino-libraries/NDEF) which handles TLV packing and page alignment automatically.

ESP-IDF: PN532 via SPI (C)

#include "driver/spi_master.h"
#include "pn532.h"  // community driver

static spi_device_handle_t pn532_spi;

void pn532_spi_init(void) {
    spi_bus_config_t buscfg = {
        .miso_io_num = 19, .mosi_io_num = 23,
        .sclk_io_num = 18, .quadwp_io_num = -1, .quadhd_io_num = -1,
    };
    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = 1 * 1000 * 1000,  // 1 MHz
        .mode = 0, .spics_io_num = 5,
        .queue_size = 7,
    };
    spi_bus_initialize(HSPI_HOST, &buscfg, 1);
    spi_bus_add_device(HSPI_HOST, &devcfg, &pn532_spi);
}

The PN532 SPI protocol wraps commands in a TFI (Transport Frame Indicator) envelope. Most community drivers for ESP-IDF abstract this, but understanding the raw framing is essential for debugging with a logic analyser.

PN7150 with NCI Stack (Production)

The PN7150 implements the NCI (NFC Controller Interface) standard, making it the correct choice for production firmware that must pass nfc-forum certification:

// NCI reset and init sequence
uint8_t core_reset[] = {0x20, 0x00, 0x01, 0x00};
uint8_t core_init[]  = {0x20, 0x01, 0x00};
// Send via I2C to PN7150 (I2C addr 0x28)
i2c_master_write_to_device(I2C_NUM_0, 0x28,
    core_reset, sizeof(core_reset), pdMS_TO_TICKS(100));

NXP provides the NFC Reader Library (NXP NFC LIB) with a full NCI stack for ESP-IDF. Link against libnfc_nci_linux or use the ESP-IDF component registry port.

Power Consumption Tips

NFC reader ICs are the single largest power draw in battery-operated designs:

Mode PN532 Current PN7150 Current
Standby 1 mA 0.5 mA
RF polling 55 mA 40 mA
Tag present 65 mA 50 mA
Hard power-off 0 mA 0 mA

Use a GPIO-controlled load switch to power off the NFC IC between scans. For event-driven designs, wire the PN7150's IRQ pin to an ESP32 GPIO with ext0 wake-up to detect field presence without continuous polling.

FreeRTOS Task Pattern

void nfc_task(void *pvParameters) {
    nfc_init();
    while (1) {
        nfc_tag_t tag;
        if (nfc_wait_for_tag(&tag, pdMS_TO_TICKS(5000)) == ESP_OK) {
            ESP_LOGI("NFC", "Tag UID: %s", tag.uid_hex);
            xQueueSend(nfc_event_queue, &tag, 0);
        }
    }
}

Keep NFC operations in their own FreeRTOS task with a dedicated stack of at least 4 KB to avoid stack overflows during SPI DMA transfers.

See also: Python NFC Programming Guide | NFC Arduino and Raspberry Pi | NFC Reader Modules Compared | NFC Chip Comparison Guide

Terms in This Guide