# 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

```mermaid
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

| 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

> **WARNING**
>
> **The battery drains fast whenever NFC is on.** The field costs about
> 109 mA for as long as it is up. Raise it to read a card, then drop it.

> **WARNING**
>
> **Nothing answers on SPI.** MOSI and MISO are swapped on the flex connector's
> silkscreen. From the P4's side, MOSI is `GPIO31` and MISO is `GPIO32`; wire
> by the table, not by the label.

> **NOTE**
>
> **A card with its own keys fails to read.** The validation code
> authenticates with the factory keys. Nothing on this site recovers keys.

## 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.

- https://apps.kode.diy/app/b2c426c5-e405-4d14-b0ec-535eff26d869

## Example code

Ask the reader who it is, with no field raised. The log shows `0x31`.

```c
/*
 * 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

- [`kdrv_nfc`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/kdrv_nfc): the reader in `kode-sdk`.
- [`rfal`](https://github.com/kodediy/kode-sdk/tree/main/components/rfal): ST's RFAL stack, vendored.
