# AO_Nfc

Owns the NFC reader of a Kode Dot: a field that exists only while you ask, the listen window, card emulation, key sweeps and the production row.

## What it is

**AO_Nfc** owns the 13.56 MHz NFC reader of a Kode Dot, an ST25R3916B on the antenna board. It reads the identity of a tag, reads and writes NDEF messages, tries a dictionary of public keys on a Mifare Classic card, dumps one, listens to a field that another device makes, and presents the Kode Dot as a card.

The radio field is up only while a request asks for it. When nothing is asked, the antenna sends nothing and the chip sleeps with its oscillator off. So silence on `NFC_TAG_SIG` means that nobody asked for a read. It does not mean that no tag was near.

AO_Nfc is a poller: it makes the field and asks the tag, and the tag only answers. It works in small pieces, one discovery step per tick, so it never blocks. When the Kode Dot acts as a card the roles flip and a reader outside decides the timing, so a task of the driver answers and AO_Nfc only watches. The chip, RFAL and the tag layers are on `sdk/kdrv/st25r3916`.

## How it is connected

```mermaid
flowchart LR
  subgraph board["On the Kode Dot"]
    ao["AO_Nfc<br/>priority 19"] -- "kdrv_st25r3916<br/>SPI2" --> chip["ST25R3916B"]
  end
  app["kode_nfc_*()<br/>or an AO"] -- "NFC_*_REQ_SIG" --> ao
  chip -- "13.56 MHz" --> tag["Tag or reader<br/>outside"]
  ao -- "NFC_TAG_SIG,<br/>NFC_EMU_SIG" --> subs["Subscribers"]
  tick["NFC_TICK_SIG,<br/>NFC_DEADLINE_SIG"] -.-> ao
  class ao ao
  class chip chip
  class app,subs app
  class tag ext
```

*AO_Nfc and the reader*

### Resources and states

AO_Nfc claims `KBSP_RES_NFC`: the chip, its interrupt line and the SPI bus (SPI2) that the chip uses alone. It runs at priority 19 (`KODE_PRIO_NFC`), above AO_Ir, with a 16-event queue and a 6144-byte stack for the vendor code (`KQP_AO_STACK_LARGE`). `kode_init()` starts it after AO_Ir when the image links it, which an app does by calling a `kode_nfc_*()` function. An app that calls none leaves AO_Nfc out, and the core claims `KBSP_RES_NFC` as `parked(AO_Nfc)` and puts the chip in power-down, SPI2 left open. `KODEOS_NFC=n` gives the reader to the app, for a driver of its own: nothing starts or parks it, and AO_Nfc, its verbs and the `kode_nfc_*()` calls do not compile, so a call that is left fails to link. `KODEOS_NFC_CERT` adds a maximum-drive carrier for the FCC bench.

It has one state, `Nfc_ready`, and a request kind that says what holds the reader: `REQ_TAG`, `REQ_NDEF`, `REQ_ROW`, `REQ_SWEEP`, `REQ_DUMP`, `REQ_FIELD`, `REQ_EMU` and `REQ_CERT`.

```mermaid
stateDiagram-v2
  [*] --> Idle
  Idle --> FieldUp: a request, chip woken, discovery armed
  FieldUp --> FieldUp: NFC_TICK_SIG, one discovery step
  FieldUp --> Serving: a tag activates, NFC_TAG_SIG
  FieldUp --> Idle: NFC_DEADLINE_SIG, no tag
  Serving --> Idle: answered, field dropped, chip rests
```

*Life of one request*

### One request at a time

**Refused and not queued.** The answer travels through memory the requester keeps for that one request, so two requesters would read each other's result. A request that finds the reader held is refused as follows:

| Request | Refusal |
| --- | --- |
| Read, NDEF exchange, listen window, emulation, carrier | `KODE_DENIED` |
| Production row | A failed verdict |
| Sweep, dump | An empty answer |

A blocking facade call carries a generation number that the answer repeats, so a late answer to a caller that timed out is ignored by the next caller. An AO that asks gets an event instead, since it has no semaphore to sleep on.

### A missing reader

A missing reader is a normal state. AO_Nfc looks for it only at boot. Every request is then answered at once and no field comes up:

| Request | Answer |
| --- | --- |
| Listen window | `KODE_IO_ERROR` |
| Read, NDEF exchange | `KODE_DENIED` |
| The rest | A refusal or a failed verdict |

The console's `nfc` reports it.

### The wake

The chip needs its oscillator stable, which takes 700 µs typically and 10 ms at most, plus an RC calibration on the B variant. Only the first field-on of a request pays it.

### Two timers, both one-shot

- `NFC_TICK_SIG` (10 ms, `NFC_TICK_MS`) is re-armed after every pass. It pumps the discovery worker, takes a burst of a listen window, runs a batch of a sweep or a dump, or samples an emulation.
- `NFC_DEADLINE_SIG` bounds a request's window.

A periodic tick would deliver into an idle AO for nothing. Emulation, a sweep and a dump set no run-length deadline, and the handler ignores an expiry while they run.

### The listen window

`nfc_field_request()` opens a window of up to 200 ms (`KODE_NFC_FIELD_MS_MAX`) with the transmitter held off. Each tick takes one burst of 16 amplitude measurements (`KNFC_LISTEN_BURST`), so a 50 ms window gives five bursts and `n` = 80, and a 200 ms window gives 320. The count is exact because the deadline is handled before the tick that expires with it, and a late burst costs one burst.

A step waits on the chip's conversions in a busy loop, bounded at 10 ms for each. The longest step measured about 5.1 ms, roughly half of every tick while a window runs, and the driver logs it at debug level under `kdrv.nfc`.

The answer is by value: `st`, `min`, `mean`, `peak`, `mod` (peak minus min), `ext` (1 when the chip's detector saw another device's field) and `n`. A blocked task names `done`, an AO names `to`, and both channels are served if both are named.

### Emulation

A reader wants the answer to a Type-2 READ within about 87 µs, which no tick of an AO can promise. `nfc_emulate_request()` arms the driver's listen mode and a worker that serves READ every millisecond, and AO_Nfc holds `REQ_EMU` until `nfc_emulate_stop_request()` and the worker has released the reader.

The card is a `knfc_emu_card_t`: a UID, ATQA and SAK, and optionally an NDEF URI, text or raw message. A coherent identity (UID length matching the ATQA size bits) is presented as it is, for an access reader. Otherwise the driver builds a Type-2 tag with a 7-byte UID that carries the message, which a phone reads as a link or as text.

Once a tick AO_Nfc samples the driver's snapshot and publishes `NFC_EMU_SIG` with a `kind`: `NFC_EMU_KIND_FIELD`, `_SELECTED`, `_READ` or `_STOPPED` (with the total).

Only a served READ shows the card was read. A reader that only enumerates raises `SELECTED`.

### Sweep and dump

`nfc_keys` tries the 2038-key dictionary against one block, eight keys a tick (`NFC_SWEEP_BATCH`); a full sweep of one block takes about 64 s. `nfc_dump` reads one sector a tick with the fast dictionary, streams each block to the log, and reports a sector that no known key opens as locked. It recovers no keys.

### The production row

`test component=nfc` gives two separate verdicts.

**Check 1** compares the `IC_IDENTITY` the driver read at start with 0x31 and turns on no field; it proves the part, the flex and connector J10, and tells an antenna PCB that is not fitted from one whose antenna does not work.

**Check 2** runs only if check 1 passes. It turns the field on and requires a tag. A tag is reusable when it is blank or its NDEF text starts with `KODE`; the row then writes this board's serial and reads it back, which no stray card can fake.

A tag with anything else, or one that refuses the write (every real access card does), is left untouched and read twice for the same UID; that proves the antenna but not that the tag is genuine. A write that goes out and does not read back is a real fault and fails the row.

`nfc_test_request_mode()` adds two modes for the user profile of `sdk/kaos/autotest`, both under the verdict name `nfc`:

- `READER_TEST_FIELD` runs check 1 and then holds the field for `KODE_NFC_TEST_FIELD_MS` (1.5 s) with nothing in front, passing if the part answered and the field stood.
- `READER_TEST_CARD` tries up to `KODE_NFC_TEST_TRIES` (3) windows, passes on any tag and leaves it alone, and publishes `TEST_SKIP_SIG` when none answers.

### Resting with the board

With `CONFIG_KODE_LOWPOWER`, AO_Nfc joins the rounds of AO_LowPower as `nfc` (`sdk/kaos/lowpower`). With nothing asked the field is already down and the reader resting, so a round only makes sure: it closes any open window, drops the field and puts the oscillator to sleep again, which is harmless on a reader that is there already. A request in flight (a read, a window, an emulation, a sweep, a dump, the row or a held carrier) answers the round busy with `nfc busy`.

Nothing is restored when the board wakes. The next request wakes the reader itself, so AO_Nfc listens to the rounds and not to `LOWPOWER_STATE_SIG`.

## Specifications

### Signals

| Signal | Event | Direction | Meaning |
| --- | --- | --- | --- |
| `NFC_READ_REQ_SIG` | `NfcReadEvt` | in, posted | Energise and answer with the first tag's identity |
| `NFC_NDEF_REQ_SIG` | `NfcNdefEvt` | in, posted | Read or write an NDEF message; answer `NFC_NDEF_RSP_SIG` |
| `NFC_FIELD_REQ_SIG` | `NfcFieldEvt`: `ms`, `out`, `rc`, `done`, `gen`, `ack`, `to` | in, posted | The listen window. Answer `NFC_FIELD_RSP_SIG` (`NfcFieldRspEvt`) |
| `NFC_EMU_REQ_SIG` | `NfcEmuReqEvt` | in, posted | Start or stop an emulation. Answer `NFC_EMU_RSP_SIG` (`NfcEmuRspEvt`: `rc`, `reads`) |
| `NFC_KEYS_REQ_SIG`, `NFC_DUMP_REQ_SIG` | with a block, or none | in, posted | The sweep and the dump. Answers `NFC_KEYS_RSP_SIG`, `NFC_DUMP_RSP_SIG` |
| `NFC_INFO_REQ_SIG` | none | in, posted | The console's `nfc`; answer `NFC_INFO_RSP_SIG` |
| `NFC_TEST_REQ_SIG` | `ReaderTestReqEvt`: `mode` | in, posted | `READER_TEST_ROW`, `_FIELD` or `_CARD` |
| `NFC_CERT_REQ_SIG` | | in, posted | Held FCC carrier; only with `KODEOS_NFC_CERT` |
| `NFC_TICK_SIG`, `NFC_DEADLINE_SIG` | none | self, one-shot | The pump and the request window |
| `LOWPOWER_ROUND_SIG` | `LowPowerRoundEvt` | in, subscribed | With `CONFIG_KODE_LOWPOWER`: the field down and the reader resting, or busy `nfc busy` |
| `NFC_TAG_SIG` | `NfcTagEvt`: `uid[10]`, `uid_len`, `atqa`, `sak`, `type` | out, published | A tag activated, only while a request holds the field. An NDEF message never rides the bus |
| `NFC_EMU_SIG` | `NfcEmuEvt`: `kind`, `reads` | out, published | An edge of an emulation |
| `TEST_RESULT_SIG`, `TEST_SKIP_SIG` | named `nfc` | out, published | The verdict, or no tag to judge |

### Facade, numbers and verbs

| Thing | Value |
| --- | --- |
| Identity | `IC_IDENTITY` reads 0x31 on the ST25R3916B |
| Blocking calls | `kode_nfc_read()`, `kode_nfc_ndef_read()`, `kode_nfc_ndef_write_uri()`, `kode_nfc_ndef_write_text()`, `kode_nfc_field_read()` wait out a whole window: never from an AO, not reentrant (a second caller gets `KODE_DENIED`). `kode_nfc_emulate_start()` and `_stop()` present and drop a card. `kode_nfc_carrier()` exists in a cert build only |
| For an AO | `nfc_ndef_request()`, `nfc_field_request()` with `to`, `nfc_emulate_request()`, `nfc_keys_request()`, `nfc_dump_request()`, `nfc_info_request()` |
| Message limit | `KODE_NFC_TEXT_MAX` 96 bytes through the facade |
| Row | Budget 6.5 s (`KODE_NFC_TEST_BUDGET_MS`), keys budget 240 s (`KODE_NFC_KEYS_BUDGET_MS`) |
| Tag types read as NDEF | NTAG (Type 2) and DESFire (Type 4). Mifare Classic is the type `kode_nfc_ndef_read()` denies |
| Console | `nfc`, `nfc_help`, `nfc_read`, `nfc_write`, `nfc_keys`, `nfc_dump`, `nfc_field ms=`, `nfc_emulate`, `nfc_emulate_off`, `test component=nfc`. While a window is open, `nfc` reads `up=0 busy=1`; while emulating, `emulate on=1 reads=N` |
| Stored settings | None |
| Kconfig | `KODEOS_NFC`, `KODEOS_NFC_CERT` |

### Listen window statuses and a measured reference

| Status | Meaning |
| --- | --- |
| `KODE_IO_ERROR` | No reader is fitted. At once. Asking again changes nothing |
| `KODE_DENIED` | Another request holds the reader. At once |
| `KODE_ERROR` | A zero `ms`, or a window that failed to open, failed mid-window or could not restore the registers. Ask again |
| `KODE_NO_RESOURCES` | The event pool is dry; `rc` is written and `ack` and `done` are answered at once |
| `KODE_OK` | Only a window that closed cleanly writes `out` and posts its statistics |

Measured on a Kode Dot with a 50 ms window: a quiet antenna reads `n` = 80 with `min`, `mean` and `peak` all 45, `mod` = 0 and `ext` = 0. A passive Mifare Classic 1K on the antenna reads 46. A phone that scans on the antenna reads 38 between its polling bursts, and a burst that falls inside the window reads `ext` = 1, `peak` 50 to 61, `min` 16 to 30. The phone's bursts fell inside 3 of 12 windows of 50 ms and 1 of 12 windows of 200 ms. So `ext` says a field is present and `mod` says it is active; no fixed level above 45 says either.

## Gotchas

> **WARNING**
>
> **`NFC_TAG_SIG` never arrives although a tag is on the antenna.** The field is off at rest. A subscriber hears a tag only while some request holds the field up. Ask with `kode_nfc_read()` or `nfc_ndef_request()`.

> **WARNING**
>
> **Your read returns `KODE_DENIED`.** Another request holds the reader, a listen window and an emulation included, or no reader is fitted. Nothing was posted and the field never came up. Retry later; a missing reader will not change.

> **NOTE**
>
> **`test component=nfc` writes to the fixture tag.** It writes this board's serial as an NDEF Text record when the tag is blank or already carries a `KODE` text. It never touches another content. On a locked card it falls back to a stable double read, which proves the antenna and not the tag.

> **NOTE**
>
> **A listen window of N ticks holds N bursts, and `n` tells you.** A smaller `n` than 16 per tick means a burst ran past its tick. A window shorter than a tick still measures once.

> **NOTE**
>
> **A dump reads only what a known key opens.** A sector with a custom key is reported as locked and skipped, and the dump goes on.

## Example code

This waits up to 5 s for a tag on the antenna and logs the length of its UID with the first and last byte. Put a card or a phone with NFC on the back of the Kode Dot. The log level must be `i`: `config key="log" value=i`.

```c
/*
 * nfc_uid.c - 2026-10-28 - from Kode with love
 *
 * Asks AO_Nfc to raise the field of the Kode Dot for up to 5 s and logs the
 * identity of the first tag it finds. Hold a card against the antenna and the
 * console prints the UID length and its first and last byte; with no tag it
 * prints the status code.
 */
#include "kode.h"

/* Bring the system up, then read one tag every few seconds. */
void app_main(void)
{
    kode_init();

    while (1) {
        knfc_tag_t tag;
        kode_status_t status = kode_nfc_read(&tag, 5000);

        if (status == KODE_OK) {
            KLOG_I("nfc_uid", "uid_len=%u first=%02X last=%02X",
                   (unsigned)tag.uid_len, (unsigned)tag.uid[0],
                   (unsigned)tag.uid[tag.uid_len - 1]);
        } else {
            KLOG_I("nfc_uid", "no tag, status %d", (int)status);
        }
        kode_delay_ms(2000);
    }
}
```

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

## In the SDK

- [`components/kaos/nfc`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/nfc): `nfc_ao.c`, the request machinery, the listen window, emulation and the row.
- [`kode.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kode/include/kode.h): the `kode_nfc_*` calls.
- [`components/kdrv/nfc`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/nfc): the ST25R3916B driver and the tag layers; see `sdk/kdrv/st25r3916`.
