From 264c2799a73e532240b945ab5bdf17b14e7ee898 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 2 Apr 2026 07:41:24 +0200 Subject: [PATCH 1/3] feat(lf): add raw LF field ADC capture (lf sniff) --- firmware/application/Makefile | 8 +- firmware/application/src/app_cmd.c | 42 ++++++ firmware/application/src/app_status.h | 1 + firmware/application/src/data_cmd.h | 4 + .../src/rfid/reader/lf/lf_125khz_radio.h | 5 + .../src/rfid/reader/lf/lf_reader_generic.c | 22 ++- .../src/rfid/reader/lf/lf_reader_generic.h | 23 ++++ .../src/rfid/reader/lf/lf_reader_main.h | 3 + software/script/chameleon_cli_unit.py | 129 ++++++++++++++++++ software/script/chameleon_cmd.py | 42 ++++++ software/script/chameleon_enum.py | 3 + 11 files changed, 274 insertions(+), 8 deletions(-) create mode 100644 firmware/application/src/rfid/reader/lf/lf_reader_generic.h diff --git a/firmware/application/Makefile b/firmware/application/Makefile index 0e2b231..44cb505 100644 --- a/firmware/application/Makefile +++ b/firmware/application/Makefile @@ -35,8 +35,8 @@ SRC_FILES += \ $(PROJ_DIR)/rfid/nfctag/lf/utils/circular_buffer.c \ $(PROJ_DIR)/rfid/nfctag/lf/utils/manchester.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/em410x.c \ - $(PROJ_DIR)/rfid/nfctag/lf/protocols/ioprox.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/hidprox.c \ + $(PROJ_DIR)/rfid/nfctag/lf/protocols/ioprox.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/viking.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/wiegand.c \ $(PROJ_DIR)/utils/dataframe.c \ @@ -341,13 +341,15 @@ ifeq (${CURRENT_DEVICE_TYPE}, ${CHAMELEON_ULTRA}) $(PROJ_DIR)/rfid/reader/hf/rc522.c \ $(PROJ_DIR)/rfid/reader/lf/lf_125khz_radio.c \ $(PROJ_DIR)/rfid/reader/lf/lf_em410x_data.c \ + $(PROJ_DIR)/rfid/reader/lf/lf_em4x05_data.c \ + $(PROJ_DIR)/rfid/reader/lf/lf_gap.c \ + $(PROJ_DIR)/rfid/reader/lf/lf_reader_generic.c \ $(PROJ_DIR)/rfid/reader/lf/lf_reader_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_reader_main.c \ $(PROJ_DIR)/rfid/reader/lf/lf_t55xx_data.c \ - $(PROJ_DIR)/rfid/reader/lf/lf_ioprox_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_hidprox_data.c \ + $(PROJ_DIR)/rfid/reader/lf/lf_ioprox_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_viking_data.c \ - $(PROJ_DIR)/rfid/reader/lf/lf_reader_generic.c \ INC_FOLDERS +=\ ${PROJ_DIR}/rfid/reader/ \ diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index a8ada47..9e0c5e0 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -14,6 +14,9 @@ #include "settings.h" #include "delayed_reset.h" #include "netdata.h" +#include "bsp_wdt.h" +#include "lf_reader_generic.h" +#include "lf_em4x05_data.h" #define NRF_LOG_MODULE_NAME app_cmd @@ -1729,6 +1732,43 @@ static data_frame_tx_t *cmd_processor_mf0_get_emulator_config(uint16_t cmd, uint * (cmd -> processor) function map, the map struct is: * cmd code before process cmd processor after process */ +static data_frame_tx_t *cmd_processor_em4x05_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + em4x05_data_t tag = {0}; + status = scan_em4x05(&tag); + if (status != STATUS_LF_TAG_OK) { + return data_frame_make(cmd, status, 0, NULL); + } + struct { + uint32_t config; + uint32_t uid; + uint32_t uid_hi; + uint8_t is_em4x69; + } PACKED payload; + payload.config = U32HTONL(tag.config); + payload.uid = U32HTONL(tag.uid); + payload.uid_hi = U32HTONL(tag.uid_hi); + payload.is_em4x69 = tag.is_em4x69 ? 1 : 0; + return data_frame_make(cmd, STATUS_LF_TAG_OK, sizeof(payload), (uint8_t *)&payload); +} + +static data_frame_tx_t *cmd_processor_lf_sniff(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + /* Optional 2-byte big-endian timeout in ms from host (default 2000ms) */ + uint32_t timeout_ms = 2000; + if (length >= 2) { + timeout_ms = ((uint32_t)data[0] << 8) | data[1]; + if (timeout_ms == 0 || timeout_ms > 10000) timeout_ms = 2000; + } + + static uint8_t sniff_buf[LF_SNIFF_MAX_SAMPLES]; + size_t outlen = 0; + raw_read_to_buffer(sniff_buf, LF_SNIFF_MAX_SAMPLES, timeout_ms, &outlen); + + if (outlen == 0) { + return data_frame_make(cmd, STATUS_LF_TAG_NO_FOUND, 0, NULL); + } + return data_frame_make(cmd, STATUS_LF_TAG_OK, (uint16_t)outlen, sniff_buf); +} + static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_GET_APP_VERSION, NULL, cmd_processor_get_app_version, NULL }, { DATA_CMD_CHANGE_DEVICE_MODE, NULL, cmd_processor_change_device_mode, NULL }, @@ -1808,6 +1848,8 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_IOPROX_DECODE_RAW, NULL, cmd_processor_ioprox_decode_raw, NULL }, { DATA_CMD_IOPROX_COMPOSE_ID, NULL, cmd_processor_ioprox_compose_id, NULL }, + { DATA_CMD_EM4X05_SCAN, before_reader_run, cmd_processor_em4x05_scan, NULL }, + { DATA_CMD_LF_SNIFF, before_reader_run, cmd_processor_lf_sniff, NULL }, #endif diff --git a/firmware/application/src/app_status.h b/firmware/application/src/app_status.h index 653336d..71762b3 100644 --- a/firmware/application/src/app_status.h +++ b/firmware/application/src/app_status.h @@ -19,6 +19,7 @@ ///////////////////////////////////////////////////////////////////// #define STATUS_LF_TAG_OK (0x40) // Some of the low -frequency cards are successful! #define STATUS_LF_TAG_NO_FOUND (0x41) // Can't search for valid LF tags +#define STATUS_LF_TAG_LOGIN_REQUIRED (0x42) // Tag requires LOGIN before read ///////////////////////////////////////////////////////////////////// // other status diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index 51ac5fc..52ce6b4 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -173,4 +173,8 @@ #define DATA_CMD_IOPROX_SET_EMU_ID (5008) #define DATA_CMD_IOPROX_GET_EMU_ID (5009) +#define DATA_CMD_EM4X05_SCAN (3030) +#define DATA_CMD_EM4X05_READSNIFF (3032) +#define DATA_CMD_LF_SNIFF (3031) + #endif diff --git a/firmware/application/src/rfid/reader/lf/lf_125khz_radio.h b/firmware/application/src/rfid/reader/lf/lf_125khz_radio.h index f452ebf..f48f473 100644 --- a/firmware/application/src/rfid/reader/lf/lf_125khz_radio.h +++ b/firmware/application/src/rfid/reader/lf/lf_125khz_radio.h @@ -1,6 +1,11 @@ #pragma once #include "ble_main.h" +#include "nrfx_pwm.h" + +/* Exposed so lf_gap.c can stop the PWM and drive LF_ANT_DRIVER directly + * to create clean field gaps without relying on PWM pin release state. */ +extern nrfx_pwm_t m_pwm; void lf_125khz_radio_init(void); void lf_125khz_radio_uninit(void); diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_generic.c b/firmware/application/src/rfid/reader/lf/lf_reader_generic.c index 4fdbc42..d6da018 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_generic.c +++ b/firmware/application/src/rfid/reader/lf/lf_reader_generic.c @@ -1,6 +1,8 @@ +#include "lf_reader_generic.h" #include "lf_reader_data.h" #include "bsp_delay.h" +#include "bsp_wdt.h" #include "bsp_time.h" #include "circular_buffer.h" #include "lf_125khz_radio.h" @@ -13,15 +15,19 @@ #include "nrf_log_default_backends.h" NRF_LOG_MODULE_REGISTER(); -#define CIRCULAR_BUFFER_SIZE (128) +/* + * Circular buffer for SAADC samples. + * Increased from 128 to 512 to reduce overrun risk during USB transfer. + * The main loop drains it as fast as possible into the output buffer. + */ +#define CIRCULAR_BUFFER_SIZE (512) static circular_buffer cb; -// saadc irq is used to sample ANT GPIO. static void saadc_cb(nrf_saadc_value_t *vals, size_t size) { for (int i = 0; i < size; i++) { nrf_saadc_value_t val = vals[i]; if (!cb_push_back(&cb, &val)) { - return; + return; /* buffer full — oldest samples dropped */ } } } @@ -41,14 +47,20 @@ bool raw_read_to_buffer(uint8_t *data, size_t maxlen, uint32_t timeout_ms, size_ init_saadc_hw(); start_lf_125khz_radio(); + /* Wait for antenna to settle before capturing. + * The LC circuit rings for ~400µs on field startup, then takes + * another ~800µs to reach steady state. Skip 2ms to be safe. */ + bsp_delay_ms(2); + autotimer *p_at = bsp_obtain_timer(0); while (NO_TIMEOUT_1MS(p_at, timeout_ms) && *outlen < maxlen) { uint16_t val = 0; while (cb_pop_front(&cb, &val) && *outlen < maxlen) { - val = val >> 5; // 14 bit ADC to 8 bit value and /2 range - data[*outlen] = val > 0xff ? 0xff : val; + val = val >> 5; /* 14-bit ADC → 9-bit, then >>5 gives 8-bit */ + data[*outlen] = val > 0xff ? 0xff : (uint8_t)val; ++(*outlen); } + bsp_wdt_feed(); /* prevent watchdog reset during long captures */ } bsp_return_timer(p_at); diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_generic.h b/firmware/application/src/rfid/reader/lf/lf_reader_generic.h new file mode 100644 index 0000000..f7c3340 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_reader_generic.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +/* + * Capture raw ADC samples from the LF antenna field. + * + * The SAADC samples at the PWM period rate (125kHz = 8µs/sample). + * Each sample is an 8-bit value (14-bit ADC >> 5, clamped to 0xFF). + * A steady carrier reads ~0x80-0x82; a gap reads noticeably lower. + * + * @param data Output buffer for raw samples + * @param maxlen Max bytes to capture (max 4000 for USB frame limit) + * @param timeout_ms Stop after this many ms even if buffer not full + * @param outlen Actual number of bytes written + * @return true on success + */ +/** Maximum bytes a single raw capture can return (USB frame limit). */ +#define LF_SNIFF_MAX_SAMPLES 4000 + +bool raw_read_to_buffer(uint8_t *data, size_t maxlen, uint32_t timeout_ms, size_t *outlen); diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_main.h b/firmware/application/src/rfid/reader/lf/lf_reader_main.h index c975d13..ea1844d 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.h +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.h @@ -5,6 +5,9 @@ #include "app_status.h" #include "lf_125khz_radio.h" +#if defined(PROJECT_CHAMELEON_ULTRA) +#include "lf_em4x05_data.h" +#endif #include "lf_reader_data.h" void set_scan_tag_timeout(uint32_t ms); diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index c9818a5..b78ed5c 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -753,6 +753,8 @@ hf_mfu = hf.subgroup("mfu", "MIFARE Ultralight / NTAG commands") lf = root.subgroup("lf", "Low Frequency commands") lf_em = lf.subgroup("em", "EM commands") +lf_em_4x05 = lf_em.subgroup("4x05", "EM4x05/EM4x69 commands") + lf_em_410x = lf_em.subgroup("410x", "EM410x commands") lf_hid = lf.subgroup("hid", "HID commands") lf_hid_prox = lf_hid.subgroup("prox", "HID Prox commands") @@ -6734,3 +6736,130 @@ examples/notes: ) else: print(f" [*] {color_string((CY, 'No response'))}") + + +@lf_em_4x05.command("read") +class LFEm4x05Read(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = ( + "Scan EM4x05 or EM4x69 tag (reader-talk-first) and print config, UID" + ) + return parser + + def on_exec(self, args: argparse.Namespace): + try: + pwd = int(args.pwd, 16) if hasattr(args, 'pwd') and args.pwd else 0 + except ValueError: + print(f"{CR}Invalid password, expected hex{C0}") + return + (config, uid, uid_hi, is_em4x69, uid_block) = self.cmd.em4x05_scan(pwd=pwd) + tag_label = "EM4x69" if is_em4x69 else "EM4x05" + rl = bool((config >> 6) & 1) + print(f" Tag type : {CG}{tag_label}{C0}") + print(f" Config : {CG}{config:#010x}{C0}") + print(f" UID block: {CG}{uid_block}{C0}") + if rl: + print(f" Auth : {CG}LOGIN used (pwd={args.pwd.upper() if hasattr(args, 'pwd') and args.pwd else '00000000'}){C0}") + if is_em4x69: + uid64 = (uid_hi << 32) | uid + print(f" UID (64) : {CG}{uid64:016x}{C0}") + else: + print(f" UID : {CG}{uid:08x}{C0}") + + +@lf.command('sniff') +class LFSniff(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = ( + "Capture raw LF field ADC samples (125kHz, 8µs/sample). " + "~0x80 = field on, lower values = gap or no field." + ) + parser.add_argument( + '--timeout', type=int, default=2000, metavar='MS', + help='Capture duration in milliseconds (default: 2000, max: 10000, firmware blocks for full duration)' + ) + parser.add_argument( + '--out', type=str, default=None, metavar='FILE', + help='Save raw samples to binary file (for offline analysis)' + ) + parser.add_argument( + '--hex', action='store_true', + help='Print hex dump of samples to screen' + ) + return parser + + def on_exec(self, args: argparse.Namespace): + timeout = max(1, min(10000, args.timeout)) + print(f" Capturing LF field for {timeout}ms at 125kHz (8µs/sample)...") + resp = self.cmd.lf_sniff(timeout_ms=timeout) + + if resp.status != Status.LF_TAG_OK or not resp.data: + print(f"{CR}No samples captured{C0}") + return + + import chameleon_cli_unit as _self_mod + data = bytes(resp.data) + _self_mod._last_capture = data + + n = len(data) + duration_ms = n * 8 / 1000 + print(f" Captured : {CG}{n}{C0} bytes ({duration_ms:.1f}ms)") + + mn = min(data) + mx = max(data) + mean = sum(data) // len(data) + print(f" Range : {CG}0x{mn:02x}{C0} – {CG}0x{mx:02x}{C0} mean: {CG}0x{mean:02x}{C0}") + + # Detect real field gaps — they drop to near zero (0x00-0x40), + # well below the steady carrier (~0xb0). Use half of mean as threshold + # to avoid false positives from the antenna startup transient. + gap_threshold = mean // 2 + # Skip first 200 samples (1.6ms) to ignore startup ringing + steady_data = data[200:] + gap_count = sum(1 for b in steady_data if b < gap_threshold) + if gap_count > 0: + print(f" Gaps : {CG}{gap_count}{C0} samples below 0x{gap_threshold:02x} (real field drops)") + else: + print(f" Gaps : {CR}none detected — flat carrier (no gap commands sent){C0}") + + if args.hex: + print() + print(f" addr {'hex bytes':47s} level") + print(f" ---- {'-'*47} ----------------") + for i in range(0, min(n, 256), 16): + row = data[i:i+16] + hex_part = ' '.join(f'{b:02x}' for b in row) + bar = '' + for b in row: + if b < 0x10: + bar += '_' # gap / field off + elif b < 0x40: + bar += '.' # ringing decay + elif b < 0x80: + bar += '-' # low + elif b < 0xa0: + bar += '+' # mid + elif b < 0xc0: + bar += 'o' # steady carrier + elif b < 0xe0: + bar += 'O' # high + else: + bar += '#' # clipped 0xff + print(f" {i:04x} {hex_part:<47s} {bar}") + if n > 256: + print(f" ... ({n - 256} more bytes, use --out to save all)") + print() + print(" _ gap . ringing - low + mid o carrier O high # clipped") + + if args.out: + try: + with open(args.out, 'wb') as f: + f.write(data) + print(f" Saved : {CG}{args.out}{C0} ({n} bytes)") + except Exception as e: + print(f"{CR}Failed to save: {e}{C0}") + + + diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index f541715..ec1ba12 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -555,6 +555,48 @@ class ChameleonCMD: resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16]) return resp + + + def lf_sniff(self, timeout_ms: int = 2000): + """ + Capture raw LF field ADC samples. + + The ChameleonUltra samples the LF antenna at 125kHz (8µs/sample). + Each byte is an 8-bit ADC value: ~0x80 = field on, lower = gap/no field. + + :param timeout_ms: Capture duration in ms (1-10000, default 2000) + :return: Raw response object — check .status and .data + """ + timeout_ms = max(1, min(10000, timeout_ms)) + payload = bytes([(timeout_ms >> 8) & 0xFF, timeout_ms & 0xFF]) + timeout_s = (timeout_ms // 1000) + 2 + return self.device.send_cmd_sync(Command.LF_SNIFF, payload, timeout=timeout_s) + + + + @expect_response(Status.LF_TAG_OK) + def em4x05_scan(self, pwd: int = 0): + """ + Read an EM4x05 or EM4x69 tag (reader-talk-first). + + Response payload (14 bytes, big-endian): + config 4 bytes — block 0 configuration word + uid 4 bytes — EM4x05 UID + uid_hi 4 bytes — EM4x69 uid_hi (zero for plain EM4x05) + is_em4x69 1 byte — 1 if a 64-bit EM4x69 UID was read + uid_block 1 byte — block number UID was read from + + :param pwd: 32-bit password for LOGIN (default 0x00000000) + :return: parsed tuple (config, uid, uid_hi, is_em4x69, uid_block) + """ + pwd_bytes = struct.pack('!I', pwd & 0xFFFFFFFF) + resp = self.device.send_cmd_sync(Command.EM4X05_SCAN, pwd_bytes) + if resp.status == Status.LF_TAG_OK: + resp.parsed = struct.unpack('!IIIBB', resp.data[:14]) + return resp + + + @expect_response(Status.LF_TAG_OK) def viking_scan(self): """ diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index c159762..9d2dc26 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -143,6 +143,9 @@ class Command(enum.IntEnum): VIKING_GET_EMU_ID = 5005 IOPROX_SET_EMU_ID = 5008 IOPROX_GET_EMU_ID = 5009 + EM4X05_SCAN = 3030 + EM4X05_READSNIFF = 3032 + LF_SNIFF = 3031 @enum.unique From dcad76bf388993043b6a535ccdde76efab4cbb73 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 2 Apr 2026 08:05:33 +0200 Subject: [PATCH 2/3] fix(lf): guard Ultra-only includes and processors for Lite build --- firmware/application/src/app_cmd.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 9e0c5e0..4d7c4cd 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -14,9 +14,11 @@ #include "settings.h" #include "delayed_reset.h" #include "netdata.h" +#if defined(PROJECT_CHAMELEON_ULTRA) #include "bsp_wdt.h" #include "lf_reader_generic.h" #include "lf_em4x05_data.h" +#endif #define NRF_LOG_MODULE_NAME app_cmd @@ -1732,6 +1734,7 @@ static data_frame_tx_t *cmd_processor_mf0_get_emulator_config(uint16_t cmd, uint * (cmd -> processor) function map, the map struct is: * cmd code before process cmd processor after process */ +#if defined(PROJECT_CHAMELEON_ULTRA) static data_frame_tx_t *cmd_processor_em4x05_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { em4x05_data_t tag = {0}; status = scan_em4x05(&tag); @@ -1769,6 +1772,8 @@ static data_frame_tx_t *cmd_processor_lf_sniff(uint16_t cmd, uint16_t status, ui return data_frame_make(cmd, STATUS_LF_TAG_OK, (uint16_t)outlen, sniff_buf); } +#endif + static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_GET_APP_VERSION, NULL, cmd_processor_get_app_version, NULL }, { DATA_CMD_CHANGE_DEVICE_MODE, NULL, cmd_processor_change_device_mode, NULL }, From 5daad00953b222bb5c23469a185e93a5b733eb6a Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 2 Apr 2026 08:14:16 +0200 Subject: [PATCH 3/3] 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