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 reader is on the antenna board, on an SPI bus across the flex

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

ThingValue
ReaderST25R3916B-AQET, U11, on the antenna board, 27.12 MHz crystal
BusSPI, mode 1, MSB first, up to 10 MHz. Kode's examples use 1 MHz on SPI2_HOST
PinsCS GPIO28, IRQ GPIO29, CLK GPIO30, MOSI GPIO31, MISO GPIO32
IdentityRegister 0x3F reads 0x31 out of reset, before any field
FieldAbout 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
ReadsThe UID of any NFC-A card. Block read and write on Mifare Classic 1K with the factory keys
EmulationNot on this board: MCU_CLK and EXT_LM have no copper on the antenna board
Kode driverNone yet. Kode's validation wraps ST's RFAL stack in nfca_init, nfca_detect_and_select, nfca_field_off
ValidatedKode-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.

In the SDK