Kode Dot
NFC
The 13.56 MHz reader on a Kode Dot: an ST25R3916B on the antenna board over SPI, what it reads and writes, what it costs, the one thing it cannot do, and the code to talk to it.
What it is
NFC is the radio in your phone that pays for things and reads a tag on a poster: a short-range link at 13.56 MHz between a reader that makes a field and a card that lives on it. The Kode Dot has a real reader chip, an ST25R3916B on the antenna board, so it does what a phone's reader does: finds a card in its field, reads its identity, and, with the right keys, reads and writes what the card stores.
What it does not do is play the card. Presenting the Kode Dot to somebody else's reader as if it were a tag takes two pins of the chip that the antenna board never routed. Reading and cloning is the whole of NFC here.
How it is connected
flowchart LR p4["ESP32-P4"] -- "SPI, GPIO28..32" --> rdr["ST25R3916B"] rdr -- "13.56 MHz loop" --> ant["Antenna"]
The chip sits on the antenna board and talks SPI across the 20-way flex:
chip select on GPIO28, interrupt on GPIO29, clock on GPIO30, MOSI on
GPIO31 and MISO on GPIO32. It is always powered and has no enable pin.
Out of reset it answers on SPI with no field raised, so the wiring test is
free.
Specifications
| Thing | Value |
|---|---|
| Reader | ST25R3916B-AQET, U11, on the antenna board, 27.12 MHz crystal |
| Bus | SPI, mode 1, MSB first, up to 10 MHz. Kode's examples use 1 MHz on SPI2_HOST |
| Pins | CS GPIO28, IRQ GPIO29, CLK GPIO30, MOSI GPIO31, MISO GPIO32 |
| Identity | Register 0x3F reads 0x31 out of reset, before any field |
| Field | About 109 mA while raised, the most expensive thing on the board — measured on a validation board with no C5 fitted, like every figure in device/power |
| Reads | The UID of any NFC-A card. Block read and write on Mifare Classic 1K with the factory keys |
| Emulation | Not on this board: MCU_CLK and EXT_LM have no copper on the antenna board |
| Kode driver | None yet. Kode's validation wraps ST's RFAL stack in nfca_init, nfca_detect_and_select, nfca_field_off |
| Validated | Kode-Dot-Val/09-NFC: identity read, and the UID of a Classic 1K, on a board |
Gotchas
Apps that use it
On the marketplace, apps that put this part to work. Install one from
start/first-app and see it before you write a line.
Example code
Ask the reader who it is, with no field raised. The log shows 0x31.
/*
* nfc_identity.c - 2026-09-18 - from Kode with love
*
* Asks the Kode Dot's NFC reader who it is. Opens the SPI bus the ST25R3916B
* hangs off, reads its identity register with no field raised, and logs the
* byte. 0x31 back means the chip is wired and alive; nothing else on the
* board changes, and the reader costs nothing until a field is raised.
*/
#include "driver/spi_master.h"
#include "esp_log.h"
#define NFC_SCK 30
#define NFC_MOSI 31
#define NFC_MISO 32
#define NFC_CS 28
#define REG_IC_IDENTITY 0x3F
#define READ_MODE 0x40 /* the chip's "register read" prefix */
/* Bus at 1 MHz, mode 1, then one two-byte transaction: address out, value in. */
void app_main(void)
{
spi_bus_config_t bus = { .sclk_io_num = NFC_SCK, .mosi_io_num = NFC_MOSI,
.miso_io_num = NFC_MISO, .quadwp_io_num = -1, .quadhd_io_num = -1 };
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_DISABLED));
spi_device_interface_config_t dev = { .clock_speed_hz = 1000000, .mode = 1,
.spics_io_num = NFC_CS, .queue_size = 1 };
spi_device_handle_t nfc;
ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &dev, &nfc));
uint8_t tx[2] = { READ_MODE | REG_IC_IDENTITY, 0x00 };
uint8_t rx[2] = { 0 };
spi_transaction_t t = { .length = 16, .tx_buffer = tx, .rx_buffer = rx };
ESP_ERROR_CHECK(spi_device_transmit(nfc, &t));
ESP_LOGI("nfc", "IC_IDENTITY = 0x%02X (expected 0x31)", rx[1]);
}Compiled on the date in the header with ESP-IDF v6.1. Reading a card is
ST's RFAL stack on top of this bus; Kode's 09-NFC/st25r3916_uid in the
validation repository does it and read a Mifare Classic 1K on a board.