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 readerNFC readerActive device generating RF field to initiate communication with tagsView full → 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 15693ISO 15693Standard for vicinity-range smart cards, 1+ meter read rangeView full →, 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 ControllerNFC ControllerDedicated IC managing NFC protocol stack in readers/smartphonesView full → 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