SDK

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

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

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 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:

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

SignalEventDirectionMeaning
RFID_READ_REQ_SIGRfidReqEvtin, postedEnergise and report the first EM4100 ID. From rfid_read_request() or rfid_read_console_request()
RFID_WRITE_REQ_SIGRfidWriteReqEvtin, postedClone a 5-byte ID onto a T5577
RFID_FIELD_REQ_SIGRfidFieldEvtin, postedThe capture
RFID_TEST_REQ_SIGReaderTestReqEvt: modein, postedREADER_TEST_ROW, _FIELD, _CARD
RFID_INFO_REQ_SIGRfidInfoReqEvtin, postedThe console's rfid
RFID_CERT_REQ_SIGin, postedRaise, switch or drop the held carrier; KODEOS_NFC_CERT only
RFID_WORKER_DONE_SIGKqpWorkerDoneEvtin, posted from the workerA job finished
LOWPOWER_ROUND_SIGLowPowerRoundEvtin, subscribedWith CONFIG_KODE_LOWPOWER: the receiver's pull released, or busy rfid busy
RFID_DEADLINE_SIGnoneself, one-shotThe backstop
RFID_TAG_SIGRfidTagEvt: id[5], rf_divout, publishedAn EM4100 decoded; id most significant nibble first, rf_div 16, 32 or 64
RFID_OP_RSP_SIGRfidOpRspEvtout, posted to the consoleEnd of rfid_read or rfid_write
RFID_INFO_RSP_SIGRfidInfoEvtout, posted to the askerThe snapshot
RFID_FIELD_RSP_SIGRfidFieldRspEvtout, posted to toEnd of a capture
TEST_RESULT_SIG, TEST_SKIP_SIGnamed rfidout, publishedVerdict, or no tag to judge

Facade, numbers and verbs

ThingValue
Facadekode_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 windowUp 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
Consolerfid, rfid_help, rfid_read (ms= 100 to 10000, default 2000), rfid_write id="0102030405", rfid_field ms=, test component=rfid
MeasuredA 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, KconfigNone. KODEOS_NFC_CERT adds the carrier

Gotchas

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.

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