SDK
125 kHz RFID
The low-frequency front end on the antenna PCB: a discrete 125 kHz reader that decodes EM4100 tags, writes them onto a T5577, and measures a foreign field.
What it is
Access cards, key fobs and pet tags often work at 125 kHz, the low-frequency (LF) band of RFID. A Kode Dot reads and copies the common ones with a circuit on the antenna PCB. There is no reader chip: the circuit is discrete, built from separate transistors, a comparator and op-amps, and the ESP32-P4 does the rest in software.
The driver does three things:
- It reads an EM4100 or EM4102 tag. The P4 drives a 125 kHz carrier onto a printed loop antenna, the passive tag answers by changing the load it puts on the field, and the P4 samples the demodulated signal and decodes the 40-bit ID.
- It clones an EM4100 ID onto a rewritable T5577 tag, by opening timed gaps in the carrier.
- It captures a window of the demodulated line with the carrier off, to see what another 125 kHz field puts on the coil, such as a reader or a cloner.
The decoder works on the raw envelope and reads weakly coupled tags only if they are flat and close to the antenna. Other LF formats are outside this driver.
How it is connected
flowchart LR subgraph p4["ESP32-P4"] ao["AO_Rfid<br/>rfid_worker"] --> drv["kdrv_rfid_lf"] end subgraph pcb["Antenna PCB"] hb["MOSFET<br/>half-bridge"] --> coil["loop antenna<br/>ANT1"] coil --> det["envelope detector<br/>op-amps"] end drv -- "LEDC 125 kHz, GPIO47" --> hb det -- "RFID_RF_IN, GPIO54, ADC2" --> drv drv -- "GPIO45, RFID_PULL" --> det class ao ao class hb,coil,det chip
AO_Rfid (sdk/kaos/rfid) owns the front end and its ADC (KBSP_RES_RFID), and is the only caller. It runs at priority KODE_PRIO_RFID (23), high on purpose, so nothing interrupts a T5577 write in the middle of a bit. Blocking work leaves the AO for its worker, rfid_worker (8192-byte stack, in PSRAM, since no job touches internal flash), which calls kdrv_rfid_lf_run().
The pins
Four pins carry the circuit (KBSP_PIN_RFID_*):
| Pin | Name | What it does |
|---|---|---|
| GPIO47 | RFID_OUT | The carrier. It switches Q8, an inverter with a 510 Ω pull-up to the system rail, which drives the complementary pair Q7 and Q9 into the coil through two 4.7 Ω resistors |
| GPIO46 | RFID_CARRIER | An input only. A TL331 comparator squares the coil's zero crossings. The driver leaves it a plain input |
| GPIO54 | RFID_RF_IN | The demodulated envelope, read by the ADC. It idles at 0.62 V and swings 0 to 1.25 V |
| GPIO45 | RFID_PULL | Gate of Q2, which shunts the envelope detector to ground. High means the receiver hears nothing |
The coil and a 3.3 nF capacitor form a series-resonant tank, and the whole stage runs from the system rail (SYS of the BQ25896, see sdk/kdrv/bq25896), so the swing on the coil follows the system rail. The receiver is a diode envelope detector followed by gain stages (5.7, then two inverting low-pass stages of gain -10 and about 73 kHz). Those values come from the DVT schematic and nobody has measured them.
Rest, ADC, carrier and rail
RFID_PULL rests high. An unconnected pin is high anyway, because R11 on the main PCB pulls it up. The driver drives it low for every read, capture and clone, and kdrv_rfid_lf_rest() raises it again. Held low it costs about 330 µA through R11.
The demod line is read on ADC2. GPIO54 is ADC2_CH5. GPIO23 is the C5's enable line and also ADC1_CH7, so a read on ADC1 would reconfigure the enable line and drop the radio. The analog positions of the expansion header are on ADC1, a unit of their own.
The carrier is a 125 kHz square wave from one LEDC channel, taken through khal's PWM service with a timer of its own: 8-bit resolution, duty 128 of 256, drive strength at maximum. "Off" means no channel is held. If the expansion header holds all four LEDC timers, kdrv_rfid_lf_field_on() returns KODE_NO_RESOURCES and the read or clone ends at once.
VO4, the IO rail. GPIO45 to GPIO47 sit in the VDD_IO_5 bank, fed by LDO channel 4 of the P4. A clone needs the rail at 3.3 V so the T5577 harvests enough energy to program its memory, and a read leaves it at its boot level.
With boost true the driver tries to acquire the channel at 3300 mV. When a mounted microSD card already holds that channel, the acquire fails and the driver shares the rail as it stands, which is already 3.3 V for a standard card.
The decoder
krfid_em4100_decode() thresholds the raw samples halfway between the lowest and highest and computes run lengths. It estimates the Manchester half-bit as the smallest run many others confirm, rebuilds the half-bit stream and decodes it over both pairing phases and both polarities. Then it checks the frame: nine header ones, row parity, column parity and a zero stop bit.
It returns:
- 0 with a 5-byte ID (most-significant nibble first);
- -1 when the window is flat (peak-to-peak under 120 counts);
- -2 when a tag is present but no clean frame came out.
It rejects an all-zero ID.
A diagnostics structure reports the peak-to-peak, the half-bit width, the bit rate (RF/16, 32 or 64), the phase, the polarity and where the frame starts, even on failure. The thresholding stays on the raw envelope on purpose, because smoothing breaks a read with weak coupling.
krfid_em4100_frame() is the inverse: it turns a 5-byte ID into the 64-bit frame a T5577 holds. The decoder's scratch space (krfid_em4100_work_t, 84 KB) is lent by the caller for one decode, so the codec holds no large static and names no allocator.
The read
kdrv_rfid_lf_read_window() fills one window of 12000 samples at 80 kS/s, which is 150 ms and well over two 64-bit frames, and decodes it. The read buffers (the window plus the scratch, 108 KB in all) are taken from PSRAM on first use, and the continuous ADC handle on ADC2 channel 5 is created on first use.
A READ job raises the field without the boost and reads windows until one decodes or the time asked for runs out. It stops at once on any failure but KODE_TIMEOUT, and then drops the field.
The T5577 downlink
kdrv_rfid_lf_clone() writes three blocks of page 0. Block 0 gets the config word 0x00148040 (Manchester, RF/64, two data blocks), block 1 the top 32 bits of the EM4100 frame, and block 2 the low 32.
Each block is sent bit by bit as on-off keying on the carrier, in field clocks of 8 µs: a start gap of 30, a 0 is 24 clocks of carrier and a 1 is 56, a write gap of 18 between bits, and 700 clocks for the EEPROM to program. A gap is the carrier's timer paused, a busy wait, and the timer resumed, so a retune on the header cannot stretch it.
The driver waits one tick between blocks, and the CLONE job then reads a window back to verify the write. The write starts at once and does not wait for a tag, so the tag must already be on the antenna.
The capture and the certification carrier
kdrv_rfid_lf_field_capture() is the read window with no carrier and no decoder. It keeps only valid conversions of the demod line, then describes the samples (krfid_wave_stats()) and squeezes them into a waveform (krfid_wave_decimate()). It runs on the worker only, never under the driver's own carrier.
A build with CONFIG_KODEOS_NFC_CERT also has kdrv_rfid_lf_cert_carrier(), a continuous maximum-drive carrier, plain or on-off modulated, for the FCC emission measurement. The front end cannot sense its own antenna, so success means the carrier was commanded. It does not prove that RF radiated.
The files
The files of the component:
| File | What it does |
|---|---|
include/kdrv_rfid_lf.h, src/kdrv_rfid_lf.c | The front end: pins, carrier, VO4, ADC window, read, capture, T5577 clone, the jobs, the certification carrier |
include/krfid_em4100.h, src/krfid_em4100.c | The EM4100 decoder and frame encoder. Pure C, no ESP-IDF |
include/krfid_wave.h, src/krfid_wave.c | Window statistics and peak-per-bucket decimation. Pure C |
include/krfid_env.h, src/krfid_env.c | Folds RMT comparator pulse widths into one envelope sample per carrier period. Pure C. Only the host tests call it |
Specifications
The front end
| Thing | Value |
|---|---|
| Carrier | 125 kHz (RFID_CARRIER_HZ), 8-bit LEDC, 50 % duty |
| Demod input | GPIO54, ADC2_CH5, continuous mode, 80 kS/s (RFID_SAMPLE_HZ) |
| Window | 12000 samples (KRFID_EM4100_MAX_SAMPLES) = 150 ms. A capture is cut to KDRV_RFID_LF_CAPTURE_MS_MAX (150) |
| ADC read | DMA wait up to 1000 ms; the fill gives up after 512 reads, so a dead ADC cannot hang the worker |
| Buffers | 108 KB in PSRAM, taken on use and returned at field_off() |
| Boost | VO4 at 3300 mV for a clone and for a held certification carrier, never for a read |
| T5577 config word | 0x00148040 |
| T5577 timing | Field clock 8 µs: start gap 30, write gap 18, 0 24, 1 56, program 700 |
| Tags read | EM4100 and EM4102 at RF/16, RF/32 and RF/64 |
The driver
| Call | What it does |
|---|---|
kdrv_rfid_lf_init() | Configures RFID_PULL (rest, high) and RFID_CARRIER (input). Always KODE_OK: a board without the antenna PCB still boots |
kdrv_rfid_lf_field_on(boost), _field_off() | Carrier up (and VO4 with boost); down, with the ADC handle, the buffers and VO4 released |
kdrv_rfid_lf_rest() | Raise RFID_PULL. Refused while a job runs or the carrier is up |
kdrv_rfid_lf_read_window(id, &diag) | Fill one window and decode it. The field must be up. KODE_OK; KODE_TIMEOUT for a flat or undecodable window; KODE_ERROR for a NULL id or an ADC that would not configure; KODE_NO_RESOURCES when PSRAM is out |
kdrv_rfid_lf_clone(id) | Write the three T5577 blocks. KODE_DENIED with the field down, KODE_ERROR for a NULL id |
kdrv_rfid_lf_field_capture(ms, &stats, wave, cap, &n) | The carrier-off window. KODE_DENIED off the worker or under the carrier, KODE_ERROR for ms 0 or an ADC that delivered nothing. Outputs are written only on KODE_OK, and each may be NULL |
kdrv_rfid_lf_run(job, id, &rf_div) | Run a READ, CLONE or CAPTURE job. Worker only |
kdrv_rfid_lf_set_worker_check(fn) | The owner tells the driver how to recognise the worker's task, so a capture from any other task is refused |
kdrv_rfid_lf_cert_carrier(modulated) | Hold a maximum-drive carrier (needs CONFIG_KODEOS_NFC_CERT) |
Gotchas
Example code
Hold an EM4100 key fob against the antenna and this example reads its ID. Each call keeps the field up for up to three seconds. With a fob on the antenna the log reads like ID 1A2B3C4D5E at RF/64.
/*
* rfid_read.c - 2026-10-28 - from Kode with love
*
* Reads a 125 kHz EM4100 tag, three seconds at a time, and logs its 40-bit
* ID. Hold a key fob flat against the antenna of the Kode Dot. The log
* shows a line such as "ID 1A2B3C4D5E at RF/64" when a tag is read.
*/
#include <stdio.h>
#include "kode.h"
#define TAG "rfid_read"
/* Bring the system up, then read tags forever. */
void app_main(void)
{
uint8_t id[5];
int rf_div;
kode_init();
while (1) {
if (kode_rfid_read(id, &rf_div, 3000) == KODE_OK) {
KLOG_I(TAG, "ID %02X%02X%02X%02X%02X at RF/%d", (unsigned)id[0],
(unsigned)id[1], (unsigned)id[2], (unsigned)id[3],
(unsigned)id[4], rf_div);
}
/* A busy front end answers at once, so pause between asks. */
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/rfid: the front end, the decoder and the capture arithmetic.kdrv_rfid_lf.h: the calls, the job type and the limits.components/kaos/rfid: AO_Rfid, the owner (sdk/kaos/rfid).