# ST25R3916B NFC reader

The 13.56 MHz NFC chip: reading tags, emulating a card, measuring a foreign field, and the NDEF, Type 2, Type 4 and Mifare Classic layers above it.

## What it is

The **ST25R3916B** from STMicroelectronics is the NFC front end of a Kode Dot. It does two opposite jobs. As a reader (a poller) it raises a 13.56 MHz radio field and talks to whatever tag or card sits on the antenna. As a card it keeps the field down and answers a reader outside, presenting a UID or a short message that a phone can open.

A third request listens: with the transmitter off, it measures the field another device puts on the antenna, such as a phone or a payment terminal.

The driver is a stack of layers, each doing one thing:

- **`kdrv_st25r3916`** is the chip driver and the platform port of ST's RFAL library. It raises the field, steps discovery, exchanges bytes and runs the emulation.
- **`knfc_tag`** names the tag that answered (Type 2, Type 4, Mifare Classic) and reads or writes its NDEF message.
- **`knfc_ndef`** encodes and parses the records a phone understands: a URI or a Text record.
- **`knfc_mfc`**, **`knfc_mfc_dict`** and **`knfc_crypto1`** authenticate to a Mifare Classic card with a key the system holds.
- **`knfc_emu_img`** builds what an emulated card presents.
- **`knfc_field`** and **`knfc_power`** are the arithmetic of the listen window and the rest-and-wake decision.

A reader that is only a poller reads and writes NDEF on NTAG tags (Type 2), on Type-4 cards, and on Mifare Classic cards with a known key. It does not recover a key it does not have.

## How it is connected

```mermaid
flowchart LR
  subgraph p4["ESP32-P4"]
    ao["AO_Nfc"] --> drv["kdrv_st25r3916"]
    drv --> rfal["RFAL (ST)"]
  end
  subgraph pcb["Antenna PCB"]
    chip["ST25R3916B"] --> ant["antenna"]
  end
  rfal -- "SPI2, 1 MHz, mode 1" --> chip
  chip -- "IRQ, GPIO29" --> drv
  class ao ao
  class chip chip
```

*The NFC reader*

The chip is on the antenna PCB, on the SPI2 bus: CS is GPIO28, CLK GPIO30, MOSI GPIO31, MISO GPIO32, and the interrupt line GPIO29 (`KBSP_PIN_NFC_*`). The driver runs the bus at 1 MHz in mode 1 and drives CS by hand, because RFAL chains several transfers under one CS.

### The owner

**AO_Nfc** (`sdk/kaos/nfc`, priority 19) owns the chip, its interrupt and the SPI2 bus whole (`KBSP_RES_NFC`). It is the only caller. The field exists only while a request is in flight, and AO_Nfc takes it down when the request ends.

The chip has no enable pin, so it is powered whenever the board is. "Off" means the field is down and the oscillator rests. **Absence is a state:** a Kode Dot without the antenna PCB, or with the flex unseated, makes `kdrv_st25r3916_init()` fail. `kdrv_st25r3916_present()` is then false and every later call returns `KODE_DENIED` at once, with no retry.

### The servicer task

RFAL's `st25r3916WaitForInterruptsTimed()` spins on a status word that only the interrupt path writes, and it never yields. A caller inside that spin cannot also end it, so the driver owns one FreeRTOS task, the servicer, at priority `configMAX_PRIORITIES - 2` (4096-byte stack).

That is above every Active Object, because AO_Nfc is the caller, and below the levels ESP-IDF keeps for itself. The GPIO interrupt only wakes the servicer, and the four register reads that collect the interrupt happen on the task.

### Discovery, one step at a time

Nothing blocks on the radio. `kdrv_st25r3916_discover_start()` wakes the chip, raises the field and arms RFAL's NFC-A discovery. `kdrv_st25r3916_worker()` advances it one step and returns, so AO_Nfc pumps it from a timer. `kdrv_st25r3916_activated()` says whether a tag came up, and `kdrv_st25r3916_active_tag()` returns the anticollision result: UID, ATQA and SAK. `knfc_tag` decides which kind of tag the SAK means.

### Rest between uses

Between requests the chip rests in RFAL's power-down mode: oscillator, transmitter, receiver and field detector all off. It wakes inside `discover_start()` and `listen_begin()` and rests again at `field_off()` and at the end of a listen window. The wake waits for the oscillator's stable interrupt, 700 µs typically and 10 ms at most. Registers keep their values through the rest.

`knfc_power_action()` decides which transition needs a wake or a rest, with no radio in it, so the host tests run it directly.

### Listening with the transmitter off

A **listen window** measures a foreign field. `kdrv_st25r3916_listen_begin()` wakes the chip, turns the field off, checks the transmitter really is off, and sets up the receiver once: manual peer detection on the external field detector, both receive channels, the peak detector instead of the mixer, and regulator-shaped AM off.

Each `kdrv_st25r3916_listen_step()` reads the detector and takes 16 amplitude measurements. Each is one direct command, 13.02 mVpp per count. `kdrv_st25r3916_listen_end()` puts every changed bit back, rests the chip and returns the statistics.

A conversion that does not finish within 10 ms means the front end stopped answering. The burst adds nothing, the window ends and the step returns `KODE_ERROR`.

A step costs 5.1 ms measured on a Kode Dot, about half of AO_Nfc's 10 ms tick, and the driver waits in the non-yielding spin for all of it.

The arithmetic is `knfc_field`: minimum, mean, peak and `mod` (peak minus minimum), with a sticky flag for "a field appeared in some burst". The accumulator keeps the sum, so the mean is exact however many bursts there are.

A window never runs under a field, from either side. `listen_begin()` refuses while a field is up, `discover_start()` refuses while a window is open, and `field_off()` closes a window first.

### Emulating a card

Emulation is the poller turned round. `kdrv_st25r3916_emulate_start()` loads an identity into the chip's passive-target memory with `rfalListenStart()`. The chip then answers REQA, anticollision and SELECT in hardware, and the field of the Kode Dot never rises.

The one thing hardware does not answer is the Type-2 READ, which a reader wants within about 87 µs. So a task of its own (priority 5, 4096-byte stack) pumps the listen worker every millisecond and serves READ, FAST_READ and HALT from an image.

`knfc_emu_img` builds that image from a `knfc_emu_card_t`. With a coherent UID of 4, 7 or 10 bytes whose ATQA size bits match, the card is presented verbatim, which is what an access-control reader expects. Otherwise the builder makes a 7-byte NFC Forum Type-2 tag (SENS_RES `0x0044`, SEL_RES `0x00`) carrying your NDEF message, and a phone reads it as a link or as text.

The Type-2 image is 40 pages (160 bytes), with the NDEF area at offset 16. A ready-made message in `raw` (a vCard, a Wi-Fi credential) is wrapped in the Type-2 TLV as it is and wins over `uri` and `ndef`.

`kdrv_st25r3916_emulate_stop()` clears a run flag and returns at once, so an Active Object may call it, and the task closes the window in its own context.

`kdrv_st25r3916_emulate_snapshot()` copies the running state under a spinlock: whether the window is open, whether a reader's field is present, whether it holds the card selected, how many sessions it opened and how many READs were served. Many sessions and no reads mean a link that dies before the first data exchange, which is what metal behind the antenna does.

### The layers above

**`knfc_tag`** reads the SAK in this order:

1. Bit 5 means ISO-DEP (Type 4).
2. Bit 6 means NFC-DEP, which is not supported.
3. Bit 3 with bit 5 clear means Mifare Classic (SAK `0x08` is 1K, `0x18` is 4K).
4. SAK `0x00` means Type 2.

Three tag types hide behind one NDEF read and write:

- **Type 2:** the capability container in page 3 gives the size and writability; the TLV walk starts at page 4 and steps over the lock and memory-control TLVs. A write is refused before the first page if the message does not fit, because a half-written tag is worse than an untouched one.
- **Type 4:** it selects the NDEF application `D2760000850101`, reads the capability container for the file and its limits, and writes the 2-byte length last, so a reader arriving mid-write sees the old length and never half a message. The layer builds its own APDUs, which is why RFAL's T4T composer is off.
- **Mifare Classic:** it reads the Mifare Application Directory and writes where an NDEF card keeps its message. It does not format a card with no directory, because that rewrites the sector keys with no undo.

**`knfc_ndef`** encodes URI and Text records and wraps a message in a TLV (tag `0x03`, terminator `0xFE`). The URI prefix table turns `https://` into one byte. It builds no Smart Poster, because iOS ignores one on a background read. Lengths count bytes, not characters.

**`knfc_mfc`** authenticates in three passes over the raw bit-counting exchange. The reader sends `AUTH(block)` and the card answers a nonce `nt`. The reader seeds the cipher with the key and `uid ^ nt` and replies `{nr}{ar}` encrypted, parity included, and the card replies `at`. Each side has proved the key.

Reads and writes encrypt the same way. A write refuses a sector trailer and block 0 outright. `knfc_mfc_dict` holds 2038 public keys from the Proxmark3 project's dictionary, the common ones first. `knfc_mfc_auth_any()` tries the leading twenty; `knfc_mfc_sweep()` walks the whole list one batch at a time.

**`knfc_crypto1`** is the Crypto1 cipher: a 48-bit shift register kept as two 24-bit halves, with a non-linear filter. It is written from the published description of the algorithm and from no existing implementation. It holds no key recovery and no attack of any kind. A reader clocks the cipher with zero and adds the keystream by XOR. Feeding the ciphertext back is the card's operation.

### The files

The files of the component:

| File | What it does |
| --- | --- |
| `include/kdrv_st25r3916.h`, `src/kdrv_st25r3916.c` | The chip driver and RFAL's platform port: init, discovery, field, listen window, emulation, raw exchanges, the field table, the certification carrier |
| `include/knfc_tag.h`, `src/knfc_tag.c` | Tag identification, the model name, NDEF read and write for the three tag types |
| `include/knfc_ndef.h`, `src/knfc_ndef.c` | URI and Text records, TLV wrap, parse |
| `include/knfc_mfc.h`, `src/knfc_mfc.c` | Mifare Classic authentication, block read and write, sector arithmetic |
| `include/knfc_mfc_dict.h`, `src/knfc_mfc_dict.c` | The 2038-key public dictionary |
| `include/knfc_crypto1.h`, `src/knfc_crypto1.c` | The Crypto1 cipher |
| `include/knfc_emulate.h` | The card and statistics types of emulation (a header only) |
| `include/knfc_emu_img.h`, `src/knfc_emu_img.c` | The identity and the 40-page Type-2 image an emulated card presents |
| `include/knfc_field.h`, `src/knfc_field.c` | Listen-window statistics |
| `include/knfc_power.h`, `src/knfc_power.c` | Wake, rest or nothing |
| `components/rfal` | STMicroelectronics' RFAL 4.0.2 and the ST25R3916 chip driver, vendored |

### RFAL

**RFAL** is vendored: copied without change from X-CUBE-NFC6 and kept under ST's licence SLA0051, with `LICENSE.txt` beside it. It is outside the project's MISRA and Doxygen rules. Two files in it are not ST's: `port/rfal_platform.h` points RFAL's `platformXxx()` macros at the SDK and chooses the features, and `port/rfal_port.h` declares the 13 functions the macros call, which `kdrv_st25r3916.c` defines.

Features on: NFC-A, T2T, ISO-DEP as poller, low-power mode and the listen half. Features off: NFC-B, NFC-F, NFC-V, T1T, ST25TB, NFC-DEP and wake-up mode. That keeps the stack near 29 KB of code against 75 KB with everything on. `ST25R_COM_SINGLETXRX` stays undefined, because the P4's SPI with DMA cannot use one buffer for both directions and `rfalNfcInitialize()` would report a hardware mismatch.

## Specifications

### The chip and the board

| Thing | Value |
| --- | --- |
| Chip | ST25R3916B, `IC_IDENTITY` register `0x3F` reads `0x31` (type 6, revision 1); init refuses anything else |
| Bus | SPI2, 1 MHz, mode 1, CS by hand. CS GPIO28, IRQ GPIO29, CLK GPIO30, MOSI GPIO31, MISO GPIO32 |
| Field | 13.56 MHz, NFC-A poller. Up only during a request |
| SPI buffers | 300 bytes, rounded to 64-byte multiples, taken once from PSRAM at init; the port owns both |
| Servicer | Priority `configMAX_PRIORITIES - 2`, 4096-byte stack |
| Listen detector | Manual peer detection: trips at 105 mVpp, releases at 75 mVpp |
| Listen burst | 16 measurements (`KNFC_LISTEN_BURST`), 10 ms conversion limit, 13.02 mVpp per count |
| Emulation | Task priority 5, 4096-byte stack, 1 ms period. UIDs of 4, 7 or 10 bytes. Messages up to 96 characters (`KNFC_EMU_NDEF_MAX`) or 140 raw bytes (`KNFC_EMU_RAW_MAX`) |
| Exchanges | Type-2 READ returns 4 blocks (16 bytes). APDU up to 512 bytes. A bit-counting transceive for Mifare. Reselect waits 50 ms |

### The driver

| Call | What it does |
| --- | --- |
| `kdrv_st25r3916_init()`, `_present()`, `_identity(&id)` | Start (SPI, interrupt, RFAL), absence test, the identity register as read at init. Init raises nothing |
| `_discover_start()`, `_worker()`, `_activated()`, `_active_tag()`, `_is_iso_dep()`, `_field_off()` | Discovery, one step at a time. `field_off()` drops the tag, closes any window, rests the chip |
| `_listen_begin()`, `_listen_step()`, `_listen_end(&stats)` | The listen window. `KODE_DENIED` with no chip, under a field, during emulation or when a window is open; `KODE_ERROR` if the chip would not wake or a register refused |
| `_emulate_start(&card)`, `_emulate_stop()`, `_emulate_active()`, `_emulate_snapshot(&st)` | Card emulation. Start gives `KODE_DENIED` for a held reader or a bad UID length, `KODE_NO_RESOURCES` when the message does not fit |
| `_t2t_read()`, `_t2t_write()`, `_apdu()`, `_transceive_bits()`, `_reselect()` | Raw exchanges. `reselect()` is a full field cycle: the one recovery that works from every Mifare state |
| `_get(f, &v)`, `_field_info(f)` | The field table below |
| `_cert_carrier(modulated)` | A continuous maximum-drive 13.56 MHz carrier for FCC bench use. Compiled only with `CONFIG_KODEOS_NFC_CERT` |

### The field table

Twenty read-only rows. The interrupt-status registers are left out on purpose: reading one clears it, and that would consume an interrupt RFAL is waiting for. A register read works at rest.

| Rows | What they are |
| --- | --- |
| `identity`, `ic_type`, `ic_rev` | Register `0x3F`, whole and by its bits |
| `op_control`, `en`, `rx_en`, `rx_chn`, `rx_man`, `tx_en`, `wu` | Register `0x02`: oscillator and regulator, receiver, channel, manual channel, the field, wake-up |
| `mode`, `om`, `tr_am` | Register `0x03`: the operation mode, AM rather than OOK |
| `bit_rate`, `txrate`, `rxrate` | Register `0x04`; 0 is 106 kb/s |
| `regulator` | Register `0x2B`, the regulator display in mV |
| `field`, `tags`, `errors` | The driver's own counters: is the field up, tags activated, exchanges that failed |

## Gotchas

> **WARNING**
>
> **A Mifare Classic is recognised, and it cannot be read without its key.** It answers anticollision like any card and nothing else until it authenticates. `knfc_tag_identify()` names it so a read does not fail with no reason. The system tries 2038 public keys and never recovers one that is on no list. Never write a sector trailer or block 0, and never format an unformatted card: `knfc_mfc_write()` refuses the first two and `knfc_tag` the third, because none of them can be undone.

> **WARNING**
>
> **The Mifare authentication needs a freshly selected card, and counts bits.** A plain-text `AUTH` is valid only after a clean selection, so `knfc_mfc_auth()` cycles the field before every attempt. The first frame has 36 bits, four bytes and four parity bits. Rounded up to 40, the card rejects it.

> **NOTE**
>
> **A resting chip is not a missing chip.** At rest the oscillator and the field detector are off, so nothing about the antenna can be seen until the next `discover_start()` or `listen_begin()`. An identity read still works through the rest. Only `present() == false` means the antenna PCB is absent.

> **NOTE**
>
> **A listen window is not card emulation.** Both keep the transmitter off. A window measures and answers nothing. Emulation is a passive target that answers a reader. They are separate requests and never overlap, and neither runs under a field.

## Example code

Hold a tag flat against the antenna and this example identifies it. It asks for a tag, waits up to three seconds with the field up, and logs the model, the UID and the UID length. With a blank NTAG on the antenna the log reads like `NTAG/Ultralight, UID 04A1B2C3D4E5F6 (7 bytes)`.

```c
/*
 * nfc_identify.c - 2026-10-28 - from Kode with love
 *
 * Looks for an NFC tag for three seconds at a time and logs its model and
 * UID. Hold a tag flat against the antenna of the Kode Dot. The log shows
 * a line such as "Mifare Classic 1K, UID 3A9F21C4 (4 bytes)".
 */
#include <stdio.h>

#include "kode.h"

#define TAG "nfc_identify"

/* Bring the system up, then read tags forever. */
void app_main(void)
{
    knfc_tag_t tag;
    char       uid[(2 * KNFC_UID_MAX) + 1];

    kode_init();

    while (1) {
        if (kode_nfc_read(&tag, 3000) == KODE_OK) {
            for (uint8_t i = 0; i < tag.uid_len; i++) {
                snprintf(&uid[2 * i], 3, "%02X", (unsigned)tag.uid[i]);
            }
            KLOG_I(TAG, "%s, UID %s (%u bytes)",
                   knfc_tag_model(tag.atqa, tag.sak), uid,
                   (unsigned)tag.uid_len);
        }

        /* A refusal returns at once, so this keeps the loop gentle. */
        kode_delay_ms(500);
    }
}
```

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. **TBD**: a run on a Kode Dot.

## In the SDK

- [`components/kdrv/nfc`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/nfc): the chip driver and every protocol layer.
- [`kdrv_st25r3916.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/nfc/include/kdrv_st25r3916.h): discovery, listen, emulation, exchanges and the field table.
- [`components/rfal`](https://github.com/kodediy/kode-sdk/tree/main/components/rfal): ST's RFAL, vendored, with its README and licence.
- [`components/kaos/nfc`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/nfc): AO_Nfc, the owner (`sdk/kaos/nfc`).
