# AO_Rfid

Owns the 125 kHz RFID front end of a Kode Dot: EM4100 reads, T5577 clones and field captures on a worker, one job at a time.

## What it is

**AO_Rfid** owns the 125 kHz low-frequency RFID front end on the antenna board of a Kode Dot. It reads an EM4100 tag, clones an EM4100 ID onto a rewritable T5577 tag, and listens without sending anything to measure the field another device puts on the coil. A capture is the third job, and `test component=rfid` is the fourth.

The front end is discrete: the board has no RFID chip. A carrier from a PWM channel drives the coil, a tag loads the field, an envelope detector and op-amp stages turn the load into a voltage on the demod line, and an ADC samples it. That is why AO_Rfid cannot poll a chip the way AO_Nfc does. Its work blocks, so it gives the slow work to a worker task and keeps every decision. The circuit, the codec and the jobs are on `sdk/kdrv/rfid`.

The carrier is on only while a job asks for it, and it drops when the job ends. So silence on `RFID_TAG_SIG` means that nobody asked for a read. It does not mean that no tag was presented.

## How it is connected

```mermaid
flowchart LR
  subgraph p4["On the P4"]
    ao["AO_Rfid<br/>priority 23"] -- "kqp_worker_submit()" --> wk["rfid_worker<br/>priority 23"]
    wk -- "RFID_WORKER_DONE_SIG" --> ao
  end
  app["kode_rfid_*()<br/>AO_Console"] -- "RFID_*_REQ_SIG" --> ao
  wk -- "kdrv_rfid_lf_run()" --> fe["Carrier, coil, ADC2"]
  ao -- "RFID_TAG_SIG" --> subs["Subscribers"]
  class ao ao
  class fe chip
  class app,subs app
```

*AO_Rfid and its worker*

### What it claims and how it runs

AO_Rfid claims `KBSP_RES_RFID` ("rfid-lf"), whole: the carrier drive, the sense line, the demod input, `RFID_PULL` (GPIO45) and the ADC-continuous handle that a read and a capture use. The LEDC channel and timer of the carrier are not part of the claim; the driver takes them from `khal` while the field is up. `RFID_PULL` is pulled high by R11 and a high level turns on a transistor that shunts the envelope detector, so it rests high and goes low only while the receiver is in use. When a job ends and nothing follows, the owner calls `kdrv_rfid_lf_rest()`.

It runs at priority 23 (`KODE_PRIO_RFID`), the top of the band above the device owners, with a 16-event queue and a 3072-byte stack, because the AO only dispatches. The worker has the same priority: the T5577 downlink is a burst of busy-waits that must not be preempted in the middle of a bit. The worker's 8192-byte stack (`RFID_WORKER_STACK`) is in PSRAM, its queue holds one job (`RFID_WORKER_DEPTH`), and it is a task of the `kqp_worker` mechanism.

`kode_init()` starts AO_Rfid after AO_Nfc. No Kconfig option turns it off, since a device without an owner is not allowed.

### A missing antenna

A board with no antenna PCB is a normal state and AO_Rfid cannot see it, because there is no chip to ask. A read or a clone times out on flat windows, and a capture answers `KODE_OK` over a floating demod pin. Nothing here refuses a request for a missing front end.

### One job at a time

```mermaid
stateDiagram-v2
  [*] --> Idle
  Idle --> Working: request accepted, job submitted, deadline armed
  Working --> Idle: RFID_WORKER_DONE_SIG, answer sent
  Working --> Expired: RFID_DEADLINE_SIG first
  Expired --> Idle: late RFID_WORKER_DONE_SIG, answers nobody
```

*The life of a request*

AO_Rfid has one state, `Rfid_ready`, and a `job` flag. `job` is set when the worker takes a job and cleared only by `RFID_WORKER_DONE_SIG`, which carries the sequence number and the driver's status. While it is set, every request is refused with `KODE_DENIED`: a read, a clone, a capture and a certification carrier. The production row publishes a failing verdict at once. The same holds after a deadline has already answered the request the job was for, so memory an ended request lent is never reused while the worker can still write it.

A late outcome answers nobody: it clears `job` and is dropped. The worker waits for an event-pool block instead of dropping the event, since losing it would refuse every later request. A request is refused and never queued, because the answer travels through the requester's memory and two requesters would read each other's results.

### The deadline is a backstop

The ordinary end of a request is the worker's outcome. For a read, a clone or a capture the one-shot `RFID_DEADLINE_SIG` is `ms` plus a second (`RFID_DEADLINE_SLACK_MS`). For the row it is one backstop for the whole row. When it fires before the worker has reported, AO_Rfid logs that the front end stays busy until the worker reports.

The `field` flag follows the carrier, so the `rfid` snapshot can read `up=1` after the deadline of a read.

### Read and clone

Both submit a job, set `field` and arm the deadline. A read that decodes publishes `RFID_TAG_SIG` (the five ID bytes and the RF/div) and records the identity as the last tag. A read never boosts VO4.

A clone raises VO4 to 3.3 V so the T5577 can harvest enough energy to burn its EEPROM, writes the EM4100 frame, and reads the tag back; `KODE_OK` means the read-back matched. A clone does not wait for a tag: the tag must be on the antenna when the request starts.

A read or clone that AO_Rfid turns away because it is busy reaches its caller as `KODE_TIMEOUT`, as if no tag came.

### Capture

`rfid_capture_request()` reads the demod line for up to 150 ms (`KDRV_RFID_LF_CAPTURE_MS_MAX`) with the carrier off, to see someone else's field. It never sets `field`, so `rfid` reads `up=0` before, during and after. The answer has two channels.

A blocked task names `done` and lends `out` (a `krfid_field_t`), `wave` with room for `cap` points, `n`, `rc`, `gen` and `ack`. An AO names `to` and is posted an `RfidFieldRspEvt` by value: `st`, `min`, `mean`, `peak`, `ac`, `n`, `points`. The waveform is never in the event.

A capture that fails is always `KODE_ERROR` with zero statistics, so a dead ADC and a PSRAM shortage look alike from above. A capture moves none of the `rfid` counters.

### The console never blocks

`rfid_read` and `rfid_write` go in flight: AO_Console names itself as `who`, and AO_Rfid answers with an `RfidOpRspEvt` on `RFID_OP_RSP_SIG` and does not signal `done`. A facade call has `who` NULL and waits on its semaphore, so every call site passes `who` unconditionally. `rfid` is the read-only snapshot (`RFID_INFO_REQ_SIG`, answer `RFID_INFO_RSP_SIG`); it answers in every state and never touches the field.

### The generation contract

A blocking caller gives pointers and a semaphore. AO_Rfid writes the answer, writes the generation into `ack`, and signals `done` last, so a caller that wakes for a stale signal keeps waiting. A requester lends memory kept for these requests alone, and lends it again only after AO_Rfid has answered the earlier request, `rc` and `ack` included.

### The production row

There is no reader chip to ask for an identity first, so the row has one check. It cycles the field itself, window by window, reading windows of 150 ms (`KODE_RFID_TEST_WINDOW_MS`). It passes when the same EM4100 ID decodes in 2 windows in a row (`KODE_RFID_TEST_TRIES`), and fails after 16 windows (`KODE_RFID_TEST_WINDOWS`) without that, inside 3.4 s (`KODE_RFID_TEST_BUDGET_MS`).

One verdict covers four things:

- The carrier reaching the tank and radiating.
- The tag harvesting and load-modulating.
- The detector and op-amp stages recovering the data.
- The ADC and decoder framing it.

The row logs `row: window N flat` for each window that does not decode and cannot tell a flat window from signal with no clean frame, since the driver answers `KODE_TIMEOUT` for both. It owns the front end while it runs, publishes only its verdict and refuses facade requests. It logs the ID it read, so an operator can compare it with the fixture's tag.

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

- `READER_TEST_FIELD` energises, brings back one window and passes at once without judging it.
- `READER_TEST_CARD` runs windows as the row does and passes on 2 identical decodes in a row, or publishes `TEST_SKIP_SIG` when 16 windows end with no stable ID.

A busy front end fails the row in every mode.

### Resting with the board

With `CONFIG_KODE_LOWPOWER`, AO_Rfid joins the rounds of AO_LowPower as `rfid` (`sdk/kaos/lowpower`). With nothing asked the carrier is already down and the receiver's pull released. A round releases that pull again, in case a certification carrier or a bench tool left it low. A request or a job still with the worker answers the round busy with `rfid busy`.

Nothing is restored when the board wakes. Every way into the receiver drives the pull low itself, so AO_Rfid listens to the rounds and not to `LOWPOWER_STATE_SIG`.

## Specifications

### Signals

| Signal | Event | Direction | Meaning |
| --- | --- | --- | --- |
| `RFID_READ_REQ_SIG` | `RfidReqEvt` | in, posted | Energise and report the first EM4100 ID. From `rfid_read_request()` or `rfid_read_console_request()` |
| `RFID_WRITE_REQ_SIG` | `RfidWriteReqEvt` | in, posted | Clone a 5-byte ID onto a T5577 |
| `RFID_FIELD_REQ_SIG` | `RfidFieldEvt` | in, posted | The capture |
| `RFID_TEST_REQ_SIG` | `ReaderTestReqEvt`: `mode` | in, posted | `READER_TEST_ROW`, `_FIELD`, `_CARD` |
| `RFID_INFO_REQ_SIG` | `RfidInfoReqEvt` | in, posted | The console's `rfid` |
| `RFID_CERT_REQ_SIG` | | in, posted | Raise, switch or drop the held carrier; `KODEOS_NFC_CERT` only |
| `RFID_WORKER_DONE_SIG` | `KqpWorkerDoneEvt` | in, posted from the worker | A job finished |
| `LOWPOWER_ROUND_SIG` | `LowPowerRoundEvt` | in, subscribed | With `CONFIG_KODE_LOWPOWER`: the receiver's pull released, or busy `rfid busy` |
| `RFID_DEADLINE_SIG` | none | self, one-shot | The backstop |
| `RFID_TAG_SIG` | `RfidTagEvt`: `id[5]`, `rf_div` | out, published | An EM4100 decoded; `id` most significant nibble first, `rf_div` 16, 32 or 64 |
| `RFID_OP_RSP_SIG` | `RfidOpRspEvt` | out, posted to the console | End of `rfid_read` or `rfid_write` |
| `RFID_INFO_RSP_SIG` | `RfidInfoEvt` | out, posted to the asker | The snapshot |
| `RFID_FIELD_RSP_SIG` | `RfidFieldRspEvt` | out, posted to `to` | End of a capture |
| `TEST_RESULT_SIG`, `TEST_SKIP_SIG` | named `rfid` | out, published | Verdict, or no tag to judge |

### Facade, numbers and verbs

| Thing | Value |
| --- | --- |
| Facade | `kode_rfid_read(id[5], &rf_div, ms)`, `kode_rfid_clone(id, ms)`, `kode_rfid_field_read()`, `kode_rfid_carrier()` (cert build only). All block, never from an AO; not reentrant, a second caller gets `KODE_DENIED` |
| `kode_rfid_read()` | `KODE_OK` with `id` and `rf_div` set; `KODE_TIMEOUT` for no tag or a busy front end; `KODE_DENIED` for a second caller; `KODE_ERROR` for NULL or a zero `ms` |
| Capture window | Up to 150 ms; 12000 points at 80 kS/s (`KRFID_EM4100_MAX_SAMPLES`). The facade keeps a PSRAM buffer of one window after the first waveform call and waits `ms` plus 500 ms |
| Console | `rfid`, `rfid_help`, `rfid_read` (`ms=` 100 to 10000, default 2000), `rfid_write id="0102030405"`, `rfid_field ms=`, `test component=rfid` |
| Measured | A quiet antenna with the antennas fitted: `rfid_field ms=50` answers `n` = 4000, mean 617 to 646, peak 711 to 905, `ac` 23 to 42. With no antenna PCB it answers a flat line near 3520 counts with `ac` 0 to 1 |
| Stored settings, Kconfig | None. `KODEOS_NFC_CERT` adds the carrier |

## Gotchas

> **WARNING**
>
> **Your read returns `KODE_TIMEOUT` and there is a tag on the antenna.** The same code means a busy front end: a request, the row or a job the worker has not yet reported holds it. Retry after the previous call ends. `KODE_DENIED` only means a second facade caller.

> **WARNING**
>
> **A capture answers `KODE_OK` with a flat line near 3520.** There is no antenna PCB and the demod pin floats. Nothing detects a missing front end. A tag decoded once is the only witness; the age of the last decode in `rfid` tells no tag from no antenna.

> **NOTE**
>
> **A clone needs the tag in place before you ask.** The worker writes at once and reads one window back. The `ms` of a clone sets only the facade's wait and AO_Rfid's deadline. `KODE_OK` is a verified round trip; bytes that went out with no matching read-back are `KODE_TIMEOUT`.

> **NOTE**
>
> **The field is not off until the worker says so.** A read's deadline can answer first while the carrier belongs to the worker. The front end then stays busy until the worker reports, and a fill is bounded by 512 DMA reads of up to a second each.

## Example code

This reads one EM4100 tag, waiting up to 5 s, and logs the first and last byte of its ID and its bit rate. Hold an EM4100 tag over the antenna. The log level must be `i`: `config key="log" value=i`.

```c
/*
 * rfid_read.c - 2026-10-28 - from Kode with love
 *
 * Asks AO_Rfid to raise the 125 kHz field of the Kode Dot for up to 5 s and
 * logs the first and last byte of the first EM4100 ID it decodes, with the
 * bit rate. Hold an EM4100 tag over the antenna; with none it logs a status.
 */
#include "kode.h"

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

    while (1) {
        uint8_t id[5];
        int rf_div = 0;
        kode_status_t status = kode_rfid_read(id, &rf_div, 5000);

        if (status == KODE_OK) {
            KLOG_I("rfid_read", "id %02X..%02X, RF/%d",
                   (unsigned)id[0], (unsigned)id[4], rf_div);
        } else {
            KLOG_I("rfid_read", "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/rfid`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/rfid): `rfid_ao.c`, the one-job-at-a-time logic, the capture and the row.
- [`kode.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kode/include/kode.h): the `kode_rfid_*` calls.
- [`components/kdrv/rfid`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/rfid): the front-end driver and the EM4100 and T5577 codecs; see `sdk/kdrv/rfid`.
