From 29c407464bd06bc9728e7383a40784630ec7d1e5 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 2 Apr 2026 08:14:18 +0200 Subject: [PATCH] fix: make each PR self-contained with all required source files --- .../src/rfid/reader/lf/lf_em4x05_data.c | 346 ++++++++++++++++++ .../src/rfid/reader/lf/lf_em4x05_data.h | 52 +++ .../application/src/rfid/reader/lf/lf_gap.c | 75 ++++ .../application/src/rfid/reader/lf/lf_gap.h | 93 +++++ 4 files changed, 566 insertions(+) create mode 100644 firmware/application/src/rfid/reader/lf/lf_em4x05_data.c create mode 100644 firmware/application/src/rfid/reader/lf/lf_em4x05_data.h create mode 100644 firmware/application/src/rfid/reader/lf/lf_gap.c create mode 100644 firmware/application/src/rfid/reader/lf/lf_gap.h diff --git a/firmware/application/src/rfid/reader/lf/lf_em4x05_data.c b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.c new file mode 100644 index 0000000..d948f38 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.c @@ -0,0 +1,346 @@ +#include "lf_em4x05_data.h" + +#include +#include + +#include "app_status.h" +#include "bsp_delay.h" +#include "bsp_time.h" +#include "circular_buffer.h" +#include "lf_125khz_radio.h" +#include "lf_gap.h" +#include "lf_reader_data.h" +#include "timeslot.h" + +#include "utils/manchester.h" + +#define NRF_LOG_MODULE_NAME lf_em4x05 +#include "nrf_log.h" +#include "nrf_log_ctrl.h" +#include "nrf_log_default_backends.h" +NRF_LOG_MODULE_REGISTER(); + +#define EM4X05_CMD_BITS 9 +#define EM4X05_RESP_BITS 45 +#define EM4X05_ROWS 8 +#define EM4X05_COLS 4 +#define EM4X05_CB_SIZE 256 + +static inline uint8_t odd_parity4(uint8_t nibble) { + nibble ^= nibble >> 2; + nibble ^= nibble >> 1; + return (~nibble) & 1; +} + +static uint8_t em4x05_cmd_parity(uint8_t opcode, uint8_t addr) { + uint8_t o1 = (opcode >> 1) & 1; + uint8_t o0 = (opcode) & 1; + uint8_t a2 = (addr >> 2) & 1; + uint8_t a1 = (addr >> 1) & 1; + uint8_t a0 = (addr) & 1; + uint8_t p2 = (~(o1 ^ o0 ^ a2)) & 1; + uint8_t p1 = (~(o1 ^ a1 ^ a0)) & 1; + uint8_t p0 = (~(o0 ^ a2 ^ a1)) & 1; + return (p2 << 2) | (p1 << 1) | p0; +} + +static uint16_t em4x05_build_cmd(uint8_t opcode, uint8_t addr) { + uint8_t parity = em4x05_cmd_parity(opcode, addr); + return (1u << 8) | ((opcode & 0x3) << 6) | ((addr & 0x7) << 3) | (parity & 0x7); +} + +static bool em4x05_decode_response(const uint8_t *bits, uint32_t *data) { + if (bits[0] != 0) { + return false; + } + uint32_t result = 0; + uint8_t col_parity[EM4X05_COLS] = {0}; + for (int row = 0; row < EM4X05_ROWS; row++) { + int base = 1 + row * (EM4X05_COLS + 1); + uint8_t nibble = 0; + for (int col = 0; col < EM4X05_COLS; col++) { + uint8_t b = bits[base + col] & 1; + nibble = (nibble << 1) | b; + col_parity[col] ^= b; + } + uint8_t rp = bits[base + EM4X05_COLS] & 1; + if (rp != odd_parity4(nibble)) { + NRF_LOG_DEBUG("em4x05: row %d parity fail", row); + return false; + } + result = (result << EM4X05_COLS) | nibble; + } + int cp_base = 1 + EM4X05_ROWS * (EM4X05_COLS + 1); + for (int col = 0; col < EM4X05_COLS; col++) { + uint8_t received_cp = bits[cp_base + col] & 1; + if (received_cp != ((~col_parity[col]) & 1)) { + NRF_LOG_DEBUG("em4x05: col %d parity fail", col); + return false; + } + } + *data = result; + return true; +} + +#define EM4X05_T1 0x40u +#define EM4X05_T15 0x60u +#define EM4X05_T2 0x80u +#define EM4X05_JIT 0x10u + +static uint8_t em4x05_rf64_period(uint8_t interval) { + if (interval >= (EM4X05_T1 - EM4X05_JIT) && interval <= (EM4X05_T1 + EM4X05_JIT)) return 0; + if (interval >= (EM4X05_T15 - EM4X05_JIT) && interval <= (EM4X05_T15 + EM4X05_JIT)) return 1; + if (interval >= (EM4X05_T2 - EM4X05_JIT) && interval <= (EM4X05_T2 + EM4X05_JIT)) return 2; + return 3; +} + +static circular_buffer g_cb; + +static void em4x05_edge_cb(void) { + uint32_t cnt = get_lf_counter_value(); + uint16_t val = (cnt > 0xff) ? 0xff : (uint16_t)(cnt & 0xff); + cb_push_back(&g_cb, &val); + clear_lf_counter_value(); +} + +static uint8_t g_send_opcode; +static uint8_t g_send_addr; +static uint32_t g_send_password; +static volatile bool g_timeslot_done = false; + +/* + * Send one EM4305 command bit. + * Protocol: field ON for bit duration, then write gap (field OFF). + * The write gap delay is padded to compensate for antenna ringing (~200us). + * Field is left ON after the gap ready for the next bit or response window. + */ +static void send_em4305_bit(bool bit) { + if (bit) { + bsp_delay_us(256); /* bit 1: 32 Tc = 256us */ + } else { + bsp_delay_us(184); /* bit 0: 23 Tc = 184us */ + } + stop_lf_125khz_radio(); + bsp_delay_us(250); /* write gap: 128us target + ~122us ringing compensation */ + start_lf_125khz_radio(); +} + +static void em4x05_send_timeslot_cb(void) { + /* 1. Start gap: wake up tag */ + stop_lf_125khz_radio(); + bsp_delay_us(440); /* 55 Tc = 440us */ + + /* 2. Settle: allow tag clock recovery to lock onto carrier */ + start_lf_125khz_radio(); + bsp_delay_us(104); /* 13 carrier cycles = 104us */ + + /* 3. Send 9-bit command MSB first */ + uint16_t cmd = em4x05_build_cmd(g_send_opcode, g_send_addr); + for (int i = 8; i >= 0; i--) { + send_em4305_bit((cmd >> i) & 1); + } + + /* 4. Field stays ON (left by last start_lf in send_em4305_bit) + * Tag will respond ~3 Tc (~24us) after the last write gap */ + g_timeslot_done = true; +} + +static void em4x05_build_data_word(uint32_t data, uint8_t bits[45]) { + uint8_t col_par[4] = {0}; + int pos = 0; + bits[pos++] = 0; + for (int row = 0; row < 8; row++) { + uint8_t nibble = (data >> (28 - row * 4)) & 0xF; + uint8_t rp = 0; + for (int col = 0; col < 4; col++) { + uint8_t b = (nibble >> (3 - col)) & 1; + bits[pos++] = b; + col_par[col] ^= b; + rp ^= b; + } + bits[pos++] = (~rp) & 1; + } + for (int col = 0; col < 4; col++) { + bits[pos++] = (~col_par[col]) & 1; + } +} + +static void em4x05_login_timeslot_cb(void) { + /* Start gap */ + stop_lf_125khz_radio(); + bsp_delay_us(440); + start_lf_125khz_radio(); + bsp_delay_us(104); + + /* LOGIN command: opcode=0b00 (DSBL), addr=0b000 */ + uint16_t cmd = em4x05_build_cmd(EM4X05_OPCODE_DSBL, 0); + for (int i = 8; i >= 0; i--) { + send_em4305_bit((cmd >> i) & 1); + } + + /* Send 45-bit password word using same bit encoding */ + uint8_t pwd_bits[45]; + em4x05_build_data_word(g_send_password, pwd_bits); + for (int i = 0; i < 45; i++) { + send_em4305_bit(pwd_bits[i]); + } + + g_timeslot_done = true; +} + +static bool em4x05_login(uint32_t password, uint32_t timeout_ms) { + g_send_password = password; + g_timeslot_done = false; + + request_timeslot(15000, em4x05_login_timeslot_cb); + + autotimer *p_wait = bsp_obtain_timer(0); + while (!g_timeslot_done && NO_TIMEOUT_1MS(p_wait, 20)) {} + bsp_return_timer(p_wait); + + cb_init(&g_cb, EM4X05_CB_SIZE, sizeof(uint16_t)); + register_rio_callback(em4x05_edge_cb); + lf_125khz_radio_gpiote_enable(); + clear_lf_counter_value(); + + bool ack = false; + autotimer *p_at = bsp_obtain_timer(0); + while (!ack && NO_TIMEOUT_1MS(p_at, timeout_ms)) { + uint16_t interval = 0; + if (!cb_pop_front(&g_cb, &interval)) { + continue; + } + uint8_t period = em4x05_rf64_period((uint8_t)interval); + if (period <= 2) { + ack = true; + } + } + bsp_return_timer(p_at); + lf_125khz_radio_gpiote_disable(); + unregister_rio_callback(); + cb_free(&g_cb); + return ack; +} + +static bool em4x05_read_block(uint8_t addr, uint32_t *data, uint32_t timeout_ms) { + g_send_opcode = EM4X05_OPCODE_READ; + g_send_addr = addr; + g_timeslot_done = false; + + /* + * Timeslot must cover full command transmission: + * start_gap(440) + settle(104) + 9 bits * (256+250) = 5098us + * Use 6000us for margin. + */ + request_timeslot(6000, em4x05_send_timeslot_cb); + + autotimer *p_wait = bsp_obtain_timer(0); + while (!g_timeslot_done && NO_TIMEOUT_1MS(p_wait, 10)) {} + bsp_return_timer(p_wait); + + cb_init(&g_cb, EM4X05_CB_SIZE, sizeof(uint16_t)); + register_rio_callback(em4x05_edge_cb); + lf_125khz_radio_gpiote_enable(); + clear_lf_counter_value(); + + manchester modem = { + .sync = true, + .rp = em4x05_rf64_period, + }; + uint8_t resp_bits[EM4X05_RESP_BITS] = {0}; + uint8_t bit_count = 0; + bool ok = false; + + autotimer *p_at = bsp_obtain_timer(0); + while (!ok && NO_TIMEOUT_1MS(p_at, timeout_ms)) { + uint16_t interval = 0; + if (!cb_pop_front(&g_cb, &interval)) { + continue; + } + bool mbits[2] = {false, false}; + int8_t mbitlen = 0; + manchester_feed(&modem, (uint8_t)interval, mbits, &mbitlen); + if (mbitlen == -1) { + manchester_reset(&modem); + bit_count = 0; + continue; + } + for (int8_t i = 0; i < mbitlen && bit_count < EM4X05_RESP_BITS; i++) { + resp_bits[bit_count++] = mbits[i] ? 1 : 0; + } + if (bit_count >= EM4X05_RESP_BITS) { + ok = em4x05_decode_response(resp_bits, data); + if (!ok) { + memmove(resp_bits, resp_bits + 1, EM4X05_RESP_BITS - 1); + bit_count = EM4X05_RESP_BITS - 1; + } + } + } + bsp_return_timer(p_at); + lf_125khz_radio_gpiote_disable(); + unregister_rio_callback(); + cb_free(&g_cb); + return ok; +} + +bool em4x05_read(em4x05_data_t *out, uint32_t timeout_ms) { + memset(out, 0, sizeof(*out)); + + uint32_t block_timeout = timeout_ms / 4; + if (block_timeout < 100) block_timeout = 100; + + if (!em4x05_read_block(EM4X05_BLOCK_CONFIG, &out->config, block_timeout)) { + NRF_LOG_DEBUG("em4x05: block 0 read failed"); + return false; + } + + if (out->config == 0x00000000 || out->config == 0xFFFFFFFF) { + NRF_LOG_DEBUG("em4x05: invalid config word 0x%08X", out->config); + return false; + } + + bool rl = (out->config >> 6) & 1; + if (rl) { + NRF_LOG_DEBUG("em4x05: RL set, attempting login pwd=%08X", out->password); + if (!em4x05_login(out->password, block_timeout)) { + NRF_LOG_DEBUG("em4x05: login failed"); + out->login_required = true; + return false; + } + out->login_required = false; + NRF_LOG_DEBUG("em4x05: login OK"); + } + + uint8_t lwr = (out->config >> 16) & 0xF; + uint8_t uid_block = (lwr >= 1 && lwr < 14) ? lwr : EM4X05_BLOCK_UID; + + if (!em4x05_read_block(uid_block, &out->uid, block_timeout)) { + NRF_LOG_DEBUG("em4x05: UID block %d read failed", uid_block); + return false; + } + out->uid_block = uid_block; + + uint32_t uid_lo = 0, uid_hi = 0; + if (em4x05_read_block(EM4X69_BLOCK_UID_LO, &uid_lo, block_timeout) && + em4x05_read_block(EM4X69_BLOCK_UID_HI, &uid_hi, block_timeout)) { + out->uid_hi = uid_hi; + out->uid = uid_lo; + out->is_em4x69 = true; + } + + return true; +} + +uint8_t scan_em4x05(em4x05_data_t *out) { + start_lf_125khz_radio(); + bsp_delay_ms(5); + + bool found = em4x05_read(out, 1000); + + stop_lf_125khz_radio(); + + if (!found && out->login_required) { + return STATUS_LF_TAG_LOGIN_REQUIRED; + } + return found ? STATUS_LF_TAG_OK : STATUS_LF_TAG_NO_FOUND; +} diff --git a/firmware/application/src/rfid/reader/lf/lf_em4x05_data.h b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.h new file mode 100644 index 0000000..230d645 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ----------------------------------------------------------------------- + * Constants + * --------------------------------------------------------------------- */ + +#define EM4X05_OPCODE_READ 0x02 +#define EM4X05_OPCODE_WRITE 0x01 +#define EM4X05_OPCODE_PRCT 0x03 +#define EM4X05_OPCODE_DSBL 0x00 + +#define EM4X05_BLOCK_CONFIG 0 +#define EM4X05_BLOCK_PASSWD 1 +#define EM4X05_BLOCK_UID 15 +#define EM4X69_BLOCK_UID_LO 13 +#define EM4X69_BLOCK_UID_HI 14 + +#define EM4X05_RESPONSE_BITS 45 +#define EM4X05_RF_DIV 64 +#define EM4X05_RESPONSE_TIMEOUT_TC 300 + +/* ----------------------------------------------------------------------- + * Data structures + * --------------------------------------------------------------------- */ + +typedef struct { + uint32_t config; /* block 0: configuration word */ + uint32_t uid; /* UID (block determined by LWR or block 15) */ + uint32_t uid_hi; /* EM4x69 only: high word of 64-bit UID */ + bool is_em4x69; /* true if 64-bit UID was successfully read */ + uint8_t uid_block; /* block number where UID was actually read from */ + uint32_t password; /* password to use for LOGIN (default 0x00000000)*/ + bool login_required;/* true if tag has RL bit set and login failed */ +} em4x05_data_t; + +/* ----------------------------------------------------------------------- + * Public API + * --------------------------------------------------------------------- */ + +bool em4x05_read(em4x05_data_t *out, uint32_t timeout_ms); +uint8_t scan_em4x05(em4x05_data_t *out); + +#ifdef __cplusplus +} +#endif diff --git a/firmware/application/src/rfid/reader/lf/lf_gap.c b/firmware/application/src/rfid/reader/lf/lf_gap.c new file mode 100644 index 0000000..3d15fb4 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_gap.c @@ -0,0 +1,75 @@ +#include "lf_gap.h" + +#include "bsp_delay.h" +#include "hw_connect.h" +#include "lf_125khz_radio.h" +#include "lf_reader_data.h" +#include "nrf_gpio.h" + +#define NRF_LOG_MODULE_NAME lf_gap +#include "nrf_log.h" +#include "nrf_log_ctrl.h" +#include "nrf_log_default_backends.h" +NRF_LOG_MODULE_REGISTER(); + +/* ----------------------------------------------------------------------- + * Transmit side + * + * All functions must be called from within a timeslot callback. + * + * Gap generation: we cannot rely on nrfx_pwm_stop() to cut the field + * because when the PWM stops it releases LF_ANT_DRIVER to GPIO state, + * which may leave the antenna driver enabled. Instead we: + * 1. Stop the PWM (releases pin to GPIO) + * 2. Explicitly drive LF_ANT_DRIVER low (field off) + * 3. Delay for the gap duration + * 4. Drive LF_ANT_DRIVER high then restart PWM (field on) + * --------------------------------------------------------------------- */ + +static inline void field_off(void) { + nrfx_pwm_stop(&m_pwm, true); /* stop PWM, releases pin */ + nrf_gpio_cfg_output(LF_ANT_DRIVER); + nrf_gpio_pin_clear(LF_ANT_DRIVER); /* drive low = field off */ +} + +static inline void field_on(void) { + nrf_gpio_pin_set(LF_ANT_DRIVER); /* drive high briefly */ + start_lf_125khz_radio(); /* restart PWM on pin */ +} + +void lf_gap_send_start(void) { + field_off(); + bsp_delay_us(GAP_START_US); + field_on(); +} + +void lf_gap_send_bit(uint8_t bit) { + if (bit & 1) { + bsp_delay_us(GAP_BIT1_US); + } else { + bsp_delay_us(GAP_BIT0_US); + } + field_off(); + bsp_delay_us(GAP_WRITE_US); + field_on(); +} + +void lf_gap_send_u32(uint32_t word) { + lf_gap_send_bits(word, 32); +} + +void lf_gap_send_bits(uint32_t value, uint8_t nbits) { + for (int8_t i = (int8_t)(nbits - 1); i >= 0; i--) { + lf_gap_send_bit((value >> i) & 1); + } +} + +bool lf_gap_detect(uint32_t last_count, uint32_t *gap_tc) { + uint32_t now = get_lf_counter_value(); + uint32_t elapsed = now - last_count; + if (elapsed >= GAP_DETECT_TIMEOUT_TC) { + *gap_tc = elapsed; + return true; + } + return false; +} diff --git a/firmware/application/src/rfid/reader/lf/lf_gap.h b/firmware/application/src/rfid/reader/lf/lf_gap.h new file mode 100644 index 0000000..919c135 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_gap.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * LF reader-talk-first gap detection and transmission. + * + * Reader-talk-first (RTF) protocols like EM4x05/4x69 and EM4x50/4x70 + * communicate with the tag by briefly cutting the 125kHz carrier field. + * A "gap" — carrier off for a calibrated number of carrier cycles — encodes + * one bit. After the command sequence, the reader restores the field and + * listens for the tag's Manchester- or Biphase-encoded response. + * + * Gap timing (EM4x05 / EM4x69, per datasheet): + * Start gap: ~50 Tc (powers up and resets the tag) + * Write gap: ~10 Tc (separates command bits during transmission) + * Bit '0': ~24 Tc field on between gaps + * Bit '1': ~56 Tc field on between gaps + * + * The existing T5577 writer in lf_t55xx_data.c uses the same physical + * mechanism (stop_lf_125khz_radio / bsp_delay_us / start_lf_125khz_radio) + * inside a timeslot callback. This module follows the same pattern. + * + * Gap detection on the receive side: + * The GPIOTE edge-capture counter fires on each carrier envelope edge. + * During a gap the carrier is absent, so no edges arrive. We detect a + * gap by polling the counter and declaring a gap when no edge has arrived + * within GAP_DETECT_TIMEOUT_TC carrier cycles. The gap duration is then + * the elapsed counter value. + * + * Units: all timing constants are in carrier cycles (Tc = 1/125000 s = 8 µs). + * bsp_delay_us() is used for gap transmission; the counter captures elapsed + * carrier cycles on the receive side. + */ + +/* ----------------------------------------------------------------------- + * Transmit timing constants (in microseconds = Tc × 8) + * --------------------------------------------------------------------- */ + +/** Start gap: resets the tag and signals start of a command sequence. */ +#define GAP_START_TC 55 /* PM3 proven: 55*8=440us for EM4x05/4305 */ +#define GAP_START_US (GAP_START_TC * 8) + +/** Write gap: separates command bits during transmission. */ +#define GAP_WRITE_TC 16 /* PM3 proven: 16*8=128us */ +#define GAP_WRITE_US (GAP_WRITE_TC * 8) + +/** Field-on duration encoding bit '0' between write gaps. */ +#define GAP_BIT0_TC 23 /* PM3 proven: 23*8=184us */ +#define GAP_BIT0_US (GAP_BIT0_TC * 8) + +/** Field-on duration encoding bit '1' between write gaps. */ +#define GAP_BIT1_TC 32 /* PM3 proven: 32*8=256us */ +#define GAP_BIT1_US (GAP_BIT1_TC * 8) + +/** + * Listen window after command: time the tag needs before it begins + * transmitting its response (EM4x05 datasheet: ~3 Tc after last gap). + * We wait a generous 50 Tc to be safe with slow tags. + */ +#define GAP_LISTEN_TC 50 +#define GAP_LISTEN_US (GAP_LISTEN_TC * 8) + +/* ----------------------------------------------------------------------- + * Receive timing constants (in carrier cycles) + * --------------------------------------------------------------------- */ + +/** + * Gap detection timeout: if no edge arrives within this many carrier + * cycles, the current interval is treated as a gap. + * Set conservatively above the longest expected normal interval (≈ 2×RF/64 + * = 128 Tc for EM4x05 Manchester at RF/64) but below any deliberate gap. + */ +#define GAP_DETECT_TIMEOUT_TC 200 + +/* ----------------------------------------------------------------------- + * API + * --------------------------------------------------------------------- */ + +void lf_gap_send_start(void); +void lf_gap_send_bit(uint8_t bit); +void lf_gap_send_u32(uint32_t word); +void lf_gap_send_bits(uint32_t value, uint8_t nbits); +bool lf_gap_detect(uint32_t last_count, uint32_t *gap_tc); + +#ifdef __cplusplus +} +#endif