Merge remote-tracking branch 'origin/main' into pr/nfcimport-v2

This commit is contained in:
Fauzan Mirza
2026-04-07 00:49:42 +02:00
30 changed files with 2724 additions and 34 deletions
+2
View File
@@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file.
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
## [unreleased][unreleased]
- Added PAC/Stanley LF protocol support: read, emulate and T55xx clone (@kevihiiin, @danieltwagner)
- Fix firmware application USB serial number (@taichunmin)
- Added ioProx LF protocol support (read, emulate and T55xx clone)
- Added `hf mfu nfcimport` to import Flipper Zero `.nfc` files into MFU/NTAG emulator slots, with `--amiibo` flag for automatic PWD/PACK derivation (@fmuk)
- Added commands to dump and clone Mifare tags
+7 -3
View File
@@ -35,8 +35,9 @@ 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/pac.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 +342,16 @@ 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_pac_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/ \
+167
View File
@@ -14,6 +14,12 @@
#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
#include "nfc_14a.h"
#define NRF_LOG_MODULE_NAME app_cmd
@@ -802,6 +808,15 @@ static data_frame_tx_t *cmd_processor_viking_scan(uint16_t cmd, uint16_t status,
return data_frame_make(cmd, STATUS_LF_TAG_OK, sizeof(card_buffer), card_buffer);
}
static data_frame_tx_t *cmd_processor_pac_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
uint8_t card_id[8] = {0x00};
status = scan_pac(card_id);
if (status != STATUS_LF_TAG_OK) {
return data_frame_make(cmd, status, 0, NULL);
}
return data_frame_make(cmd, STATUS_LF_TAG_OK, sizeof(card_id), card_id);
}
static data_frame_tx_t *cmd_processor_viking_write_to_t55xx(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
typedef struct {
uint8_t id[4];
@@ -817,6 +832,20 @@ static data_frame_tx_t *cmd_processor_viking_write_to_t55xx(uint16_t cmd, uint16
return data_frame_make(cmd, status, 0, NULL);
}
static data_frame_tx_t *cmd_processor_pac_write_to_t55xx(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
typedef struct {
uint8_t id[LF_PAC_TAG_ID_SIZE];
uint8_t new_key[4];
uint8_t old_keys[4];
} PACKED payload_t;
payload_t *payload = (payload_t *)data;
if (length < sizeof(payload_t) || (length - offsetof(payload_t, old_keys)) % sizeof(payload->old_keys) != 0) {
return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
}
status = write_pac_to_t55xx(payload->id, payload->new_key, payload->old_keys, (length - offsetof(payload_t, old_keys)) / sizeof(payload->old_keys));
return data_frame_make(cmd, status, 0, NULL);
}
#define GENERIC_READ_LEN 800
#define GENERIC_READ_TIMEOUT_MS 500
static data_frame_tx_t *cmd_processor_generic_read(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
@@ -1066,6 +1095,26 @@ static data_frame_tx_t *cmd_processor_viking_get_emu_id(uint16_t cmd, uint16_t s
return data_frame_make(cmd, STATUS_SUCCESS, LF_VIKING_TAG_ID_SIZE, buffer->buffer);
}
static data_frame_tx_t *cmd_processor_pac_set_emu_id(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
if (length != LF_PAC_TAG_ID_SIZE) {
return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
}
tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_PAC);
memcpy(buffer->buffer, data, LF_PAC_TAG_ID_SIZE);
tag_emulation_load_by_buffer(TAG_TYPE_PAC, false);
return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL);
}
static data_frame_tx_t *cmd_processor_pac_get_emu_id(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
tag_slot_specific_type_t tag_types;
tag_emulation_get_specific_types_by_slot(tag_emulation_get_slot(), &tag_types);
if (tag_types.tag_lf != TAG_TYPE_PAC) {
return data_frame_make(cmd, STATUS_PAR_ERR, 0, data);
}
tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_PAC);
return data_frame_make(cmd, STATUS_SUCCESS, LF_PAC_TAG_ID_SIZE, buffer->buffer);
}
static nfc_tag_14a_coll_res_reference_t *get_coll_res_data(bool write) {
nfc_tag_14a_coll_res_reference_t *info;
tag_slot_specific_type_t tag_types;
@@ -1729,6 +1778,117 @@ 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);
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);
}
#define HF_SNIFF_BUF_SIZE 3800 /* leave room for USB framing */
#define HF_SNIFF_MAX_FRAMES 200
static uint8_t m_sniff_buf[HF_SNIFF_BUF_SIZE];
static uint16_t m_sniff_buf_len = 0;
static bool m_sniff_active = false;
static uint16_t m_sniff_cb_count = 0; /* debug: total callback invocations */
static void hf14a_sniff_frame_cb(const uint8_t *data, uint16_t szBits) {
m_sniff_cb_count++; /* count even if buffer full or inactive */
if (!m_sniff_active) return;
uint16_t szBytes = (szBits + 7) / 8;
/* Check space: 2 bytes header + data */
if (m_sniff_buf_len + 2 + szBytes > HF_SNIFF_BUF_SIZE) return;
/* Write bit count big-endian */
m_sniff_buf[m_sniff_buf_len++] = (szBits >> 8) & 0xFF;
m_sniff_buf[m_sniff_buf_len++] = szBits & 0xFF;
/* Write frame bytes */
memcpy(&m_sniff_buf[m_sniff_buf_len], data, szBytes);
m_sniff_buf_len += szBytes;
}
static data_frame_tx_t *cmd_processor_hf14a_sniff(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
/* Optional 2-byte big-endian timeout in ms (default 5000ms) */
uint32_t timeout_ms = 5000;
if (length >= 2) {
timeout_ms = ((uint32_t)data[0] << 8) | data[1];
if (timeout_ms == 0 || timeout_ms > 30000) timeout_ms = 5000;
}
/* Reload active slot data before sniffing.
* The NFCT anti-collision response is built from m_tag_information which
* points into the shared tag data buffer. After a slot switch the buffer
* may still contain the previous slot's UID if the FDS async load has not
* completed. A forced reload here ensures the correct UID is presented
* during the sniff session.
* A short settle delay follows to allow the reload to complete before
* the first field detection can trigger the anti-collision path. */
tag_emulation_load_data();
bsp_delay_ms(100);
/* Install sniff callback into the already-running tag emulation stack.
* Do NOT call tag_mode_enter() or sense_switch() here — those reinit
* NFCT and wipe the anti-collision data, breaking the emulation.
* The device must already be in emulator mode (hw mode --emulator)
* with a slot active before running this command. */
m_sniff_buf_len = 0;
m_sniff_cb_count = 0;
m_sniff_active = true;
nfc_tag_14a_set_sniff_cb(hf14a_sniff_frame_cb);
/* Wait for duration, yielding each ms so USB stack stays alive.
* Feed watchdog every iteration — WDT timeout is 5000ms and the
* main loop cannot feed it while we are blocking here. */
autotimer *p_at = bsp_obtain_timer(0);
while (NO_TIMEOUT_1MS(p_at, timeout_ms)) {
bsp_delay_ms(1);
bsp_wdt_feed();
}
bsp_return_timer(p_at);
/* Remove callback and restore normal sense state */
m_sniff_active = false;
nfc_tag_14a_clear_sniff_cb();
tag_emulation_sense_run(); /* restore slot-based sense state */
if (m_sniff_buf_len == 0) {
return data_frame_make(cmd, STATUS_HF_TAG_NO, 0, NULL);
}
return data_frame_make(cmd, STATUS_SUCCESS, m_sniff_buf_len, m_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 },
@@ -1798,6 +1958,8 @@ static cmd_data_map_t m_data_cmd_map[] = {
{ DATA_CMD_VIKING_WRITE_TO_T55XX, before_reader_run, cmd_processor_viking_write_to_t55xx, NULL },
{ DATA_CMD_IOPROX_SCAN, before_reader_run, cmd_processor_ioprox_scan, NULL },
{ DATA_CMD_IOPROX_WRITE_TO_T55XX, before_reader_run, cmd_processor_ioprox_write_to_t55xx, NULL },
{ DATA_CMD_PAC_SCAN, before_reader_run, cmd_processor_pac_scan, NULL },
{ DATA_CMD_PAC_WRITE_TO_T55XX, before_reader_run, cmd_processor_pac_write_to_t55xx, NULL },
{ DATA_CMD_ADC_GENERIC_READ, before_reader_run, cmd_processor_generic_read, NULL },
{ DATA_CMD_HF14A_SET_FIELD_ON, before_reader_run, cmd_processor_hf14a_set_field_on, NULL },
@@ -1808,6 +1970,9 @@ 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 },
{ DATA_CMD_HF14A_SNIFF, NULL, cmd_processor_hf14a_sniff, NULL },
#endif
@@ -1860,6 +2025,8 @@ static cmd_data_map_t m_data_cmd_map[] = {
{ DATA_CMD_IOPROX_GET_EMU_ID, NULL, cmd_processor_ioprox_get_emu_id, NULL },
{ DATA_CMD_VIKING_SET_EMU_ID, NULL, cmd_processor_viking_set_emu_id, NULL },
{ DATA_CMD_VIKING_GET_EMU_ID, NULL, cmd_processor_viking_get_emu_id, NULL },
{ DATA_CMD_PAC_SET_EMU_ID, NULL, cmd_processor_pac_set_emu_id, NULL },
{ DATA_CMD_PAC_GET_EMU_ID, NULL, cmd_processor_pac_get_emu_id, NULL },
};
data_frame_tx_t *cmd_processor_get_device_capabilities(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
+1
View File
@@ -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
+9
View File
@@ -78,6 +78,7 @@
#define DATA_CMD_HF14A_GET_CONFIG (2200)
#define DATA_CMD_HF14A_SET_CONFIG (2201)
#define DATA_CMD_HF14A_SNIFF (2020)
//
// ******************************************************************
@@ -93,6 +94,8 @@
#define DATA_CMD_EM410X_ELECTRA_WRITE_TO_T55XX (3006)
#define DATA_CMD_HIDPROX_SCAN (3002)
#define DATA_CMD_HIDPROX_WRITE_TO_T55XX (3003)
#define DATA_CMD_PAC_SCAN (3014)
#define DATA_CMD_PAC_WRITE_TO_T55XX (3015)
#define DATA_CMD_VIKING_SCAN (3004)
#define DATA_CMD_VIKING_WRITE_TO_T55XX (3005)
#define DATA_CMD_ADC_GENERIC_READ (3009)
@@ -170,7 +173,13 @@
#define DATA_CMD_HIDPROX_GET_EMU_ID (5003)
#define DATA_CMD_VIKING_SET_EMU_ID (5004)
#define DATA_CMD_VIKING_GET_EMU_ID (5005)
#define DATA_CMD_PAC_SET_EMU_ID (5006)
#define DATA_CMD_PAC_GET_EMU_ID (5007)
#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
@@ -59,6 +59,17 @@ const uint16_t ats_fsdi_table[] = {
static volatile bool m_is_responded = false;
// Receiving buffer
static uint8_t m_nfc_rx_buffer[MAX_NFC_RX_BUFFER_SIZE] = { 0x00 };
/* Optional sniff callback — fires for every received frame */
static nfc_tag_14a_sniff_cb_t m_sniff_cb = NULL;
void nfc_tag_14a_set_sniff_cb(nfc_tag_14a_sniff_cb_t cb) {
m_sniff_cb = cb;
}
void nfc_tag_14a_clear_sniff_cb(void) {
m_sniff_cb = NULL;
}
static uint8_t m_nfc_tx_buffer[MAX_NFC_TX_BUFFER_SIZE] = { 0x00 };
// The N -secondary connection needs to use SAK, when the "third 'bit' in SAK is 1 is 1, the logo UID is incomplete
static uint8_t m_uid_incomplete_sak[] = { 0x04, 0xda, 0x17 };
@@ -326,6 +337,11 @@ void nfc_tag_14a_data_process(uint8_t *p_data) {
// Because of this error receiving event caused by this possible interference
return;
}
/* Sniff hook — fire before any tag response logic */
if (m_sniff_cb != NULL) {
m_sniff_cb(p_data, szDataBits);
}
// Manually draw frame, separate data and strange school inspection
#if !NFC_TAG_14A_RX_PARITY_AUTO_DEL_ENABLE
if (szDataBits >= 9) {
@@ -82,6 +82,14 @@ typedef struct {
// Communication reception function that needs to be implemented
typedef void (*nfc_tag_14a_reset_handler_t)(void);
/* Sniff callback — called for every received frame before the tag handler.
* data : raw frame bytes (after parity strip)
* szBits : number of bits received */
typedef void (*nfc_tag_14a_sniff_cb_t)(const uint8_t *data, uint16_t szBits);
void nfc_tag_14a_set_sniff_cb(nfc_tag_14a_sniff_cb_t cb);
void nfc_tag_14a_clear_sniff_cb(void);
typedef void (*nfc_tag_14a_state_handler_t)(uint8_t *data, uint16_t szBits);
typedef nfc_tag_14a_coll_res_reference_t *(*nfc_tag_14a_coll_handler_t)(void);
@@ -5,11 +5,13 @@
#include "bsp_delay.h"
#include "fds_util.h"
#include "nrf_gpio.h"
#include "nrf_soc.h"
#include "nrfx_lpcomp.h"
#include "nrfx_pwm.h"
#include "protocols/em410x.h"
#include "protocols/hidprox.h"
#include "protocols/ioprox.h"
#include "protocols/pac.h"
#include "protocols/viking.h"
#include "syssleep.h"
#include "tag_emulation.h"
@@ -136,6 +138,23 @@ static void pwm_init(void) {
}
static void lf_sense_enable(void) {
// PWM bit timing divides HFCLK by a fixed ratio. On HFINT (64 MHz RC,
// ±1.5% at 25°C after factory trim, wider over temperature) this gives a
// chip-to-chip spread that NRZ readers — which see cumulative error across
// runs of same-polarity bits with no intra-run resync — reject even when
// Manchester/FSK readers don't. Holding HFXO brings the PWM clock to
// ±40 ppm. We can't lock to the reader's carrier (tag-mode antenna taps
// on this board are envelope-only), so this is as good as it gets.
//
// Paired release in lf_sense_disable(). SD reference-counts HFXO requests,
// so this coexists with BLE. Both functions run from thread context
// (tag_mode_enter/tag_emulation_sense_end) where SVCs are safe.
sd_clock_hfclk_request();
uint32_t hfclk_running = 0;
while (!hfclk_running) {
sd_clock_hfclk_is_running(&hfclk_running);
}
lpcomp_init();
pwm_init(); // use precise hardware pwm to broadcast card id
if (is_lf_field_exists()) {
@@ -148,6 +167,7 @@ static void lf_sense_disable(void) {
nrfx_lpcomp_uninit();
m_pwm_seq = NULL;
m_is_lf_emulating = false;
sd_clock_hfclk_release();
}
static enum {
@@ -224,6 +244,15 @@ int lf_tag_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer) {
return LF_VIKING_TAG_ID_SIZE;
}
if (type == TAG_TYPE_PAC && buffer->length >= LF_PAC_TAG_ID_SIZE) {
m_tag_type = type;
void *codec = pac.alloc();
m_pwm_seq = pac.modulator(codec, buffer->buffer);
pac.free(codec);
NRF_LOG_INFO("load lf pac data finish.");
return LF_PAC_TAG_ID_SIZE;
}
NRF_LOG_ERROR("no valid data exists in buffer for tag type: %d.", type);
return 0;
}
@@ -346,3 +375,13 @@ bool lf_tag_viking_data_factory(uint8_t slot, tag_specific_type_t tag_type) {
uint8_t tag_id[4] = {0xDE, 0xAD, 0xBE, 0xEF};
return lf_tag_data_factory(slot, tag_type, tag_id, sizeof(tag_id));
}
int lf_tag_pac_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer) {
return m_tag_type == TAG_TYPE_PAC ? LF_PAC_TAG_ID_SIZE : 0;
}
bool lf_tag_pac_data_factory(uint8_t slot, tag_specific_type_t tag_type) {
// default id: 8 ASCII bytes
uint8_t tag_id[8] = {'C', 'A', 'R', 'D', '0', '0', '0', '1'};
return lf_tag_data_factory(slot, tag_type, tag_id, sizeof(tag_id));
}
@@ -10,6 +10,7 @@
#define LF_IOPROX_TAG_ID_SIZE 16
#define LF_HIDPROX_TAG_ID_SIZE 13
#define LF_VIKING_TAG_ID_SIZE 4
#define LF_PAC_TAG_ID_SIZE 8
void lf_tag_125khz_sense_switch(bool enable);
int lf_tag_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer);
@@ -21,4 +22,6 @@ int lf_tag_ioprox_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffe
bool lf_tag_ioprox_data_factory(uint8_t slot, tag_specific_type_t tag_type);
int lf_tag_viking_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer);
bool lf_tag_viking_data_factory(uint8_t slot, tag_specific_type_t tag_type);
int lf_tag_pac_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer);
bool lf_tag_pac_data_factory(uint8_t slot, tag_specific_type_t tag_type);
bool is_lf_field_exists(void);
@@ -0,0 +1,400 @@
#include "pac.h"
#include <stdlib.h>
#include <string.h>
#include "nordic_common.h"
#include "nrf_pwm.h"
#include "protocols.h"
#include "t55xx.h"
#include "tag_base_type.h"
#define NRF_LOG_MODULE_NAME pac_protocol
#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"
NRF_LOG_MODULE_REGISTER();
#define PAC_DATA_SIZE 8 // 8-byte ASCII card ID
// NRZ at RF/32: 32 carrier cycles per bit.
// With SAADC sampling at 1 sample per carrier cycle, 32 samples = 1 bit.
#define PAC_RF_PER_BIT 32
#define PAC_HALF_BIT 16 // Half-bit for rounding interval → nbits
#define PAC_MAX_BITS_RUN 20 // Max consecutive same-polarity bits we accept
// PAC frame is exactly 128 bits on T55xx (4 blocks × 32 bits):
// 8-bit sync marker (0xFF) + 12 × 10-bit UART frames = 128 bits
#define PAC_FRAME_BITS 128
#define PAC_PREAMBLE_BITS 19
// Preamble: 1111111100100000010 (19 bits) = 0x7F902
#define PAC_PREAMBLE 0x7F902UL
#define PAC_PREAMBLE_INV 0x006FDUL // Bitwise inverse masked to 19 bits
#define PAC_UART_FRAME_BITS 10
#define PAC_PAYLOAD_BYTES 12 // STX + '2' + '0' + 8 card ID + XOR checksum
#define PAC_STX 0x02
// ADC demodulation: spike-clipping + threshold with dead zone.
//
// Signal has 3 amplitude zones:
// NRZ low (~500-2000 ADC) — tag load modulation "0" state
// NRZ high (~4000-6000) — tag load modulation "1" state
// Spikes (~10k-14k) — LC ringing at NRZ transitions (8-20 cycles wide)
//
// Prescan (128 samples) finds the NRZ floor (raw_min), then spike_cap
// = max(raw_min * SPIKE_MULT, MIN_SPIKE_CAP) clips spikes while preserving
// the NRZ high level. MIN_SPIKE_CAP ensures spike_cap is never below the
// NRZ high level, even when raw_min correctly captures NRZ low.
#define PAC_PRESCAN_SAMPLES 128 // ~1ms: raw min detection
#define PAC_WARMUP_SAMPLES 600 // ~5ms: threshold calibration on clipped signal
#define PAC_SPIKE_MULT 3 // Clip at 3× floor
#define PAC_MIN_SPIKE_CAP 8000 // Floor: preserves NRZ high (~5000) always
#define PAC_THRESH_FUZZ 75 // Dead zone: 25%-75% of clipped range
// Auto-recalibrate if no frame found within this many Phase 3 samples.
// ~5 frame periods = 5 × 128 bits × 32 samples/bit = 20480 samples (~164ms).
// Gives ~3 calibration attempts in a 500ms scan window.
#define PAC_RECAL_SAMPLES 20480
typedef struct {
// NRZ shift register (128 bits)
uint64_t raw_hi; // upper 64 bits
uint64_t raw_lo; // lower 64 bits
bool polarity; // current NRZ level (toggled on each edge)
uint16_t bit_count; // total bits shifted in (capped at PAC_FRAME_BITS)
uint8_t card_id[PAC_DATA_SIZE];
// ADC → NRZ demodulation state (spike-clip + threshold with dead zone)
uint32_t total_samples; // total samples processed
int16_t raw_min; // minimum raw sample seen (for spike cap)
int32_t spike_cap; // clip level
int16_t clip_max; // max of clipped samples during warmup
int16_t clip_min; // min of clipped samples during warmup
int16_t thresh_high; // above this → bit=1
int16_t thresh_low; // below this → bit=0, between → keep previous
bool adc_state; // current demodulated binary level
bool has_signal; // true after first threshold crossing
uint32_t sample_count; // samples since last transition
uint32_t decode_samples; // Phase 3 samples since last calibration
} pac_codec;
// Shift one bit into the 128-bit register.
static void shift_bit(pac_codec *d, bool bit) {
d->raw_hi = (d->raw_hi << 1) | (d->raw_lo >> 63);
d->raw_lo = (d->raw_lo << 1) | (bit ? 1 : 0);
}
// Extract a single bit from the 128-bit register.
// Position 0 = MSB of raw_hi (oldest), position 127 = LSB of raw_lo (newest).
static bool get_bit(pac_codec *d, uint16_t pos) {
if (pos < 64) {
return (d->raw_hi >> (63 - pos)) & 1;
}
return (d->raw_lo >> (127 - pos)) & 1;
}
// Decode a 10-bit UART frame at bit position 'start'.
// Frame: start(0) + 7 data bits LSB-first + odd parity + stop(1).
static int decode_uart_byte(pac_codec *d, uint16_t start, bool inverted) {
#define RD(pos) (inverted ? !get_bit(d, (pos)) : get_bit(d, (pos)))
if (RD(start)) {
return -1;
}
uint8_t byte_val = 0;
uint8_t ones = 0;
for (int i = 0; i < 7; i++) {
if (RD(start + 1 + i)) {
byte_val |= (1 << i);
ones++;
}
}
if (RD(start + 8)) {
ones++;
}
if ((ones & 1) == 0) {
return -1;
}
if (!RD(start + 9)) {
return -1;
}
#undef RD
return byte_val;
}
// Check if the 128-bit register contains a valid PAC frame.
static bool try_decode_frame(pac_codec *d, bool inverted) {
uint32_t preamble = 0;
for (int i = 0; i < PAC_PREAMBLE_BITS; i++) {
preamble = (preamble << 1) | (get_bit(d, i) ? 1 : 0);
}
uint32_t expected = inverted ? PAC_PREAMBLE_INV : PAC_PREAMBLE;
if (preamble != expected) {
return false;
}
uint8_t decoded[PAC_PAYLOAD_BYTES];
for (int i = 0; i < PAC_PAYLOAD_BYTES; i++) {
uint16_t frame_start = 8 + i * PAC_UART_FRAME_BITS;
int val = decode_uart_byte(d, frame_start, inverted);
if (val < 0) {
return false;
}
decoded[i] = (uint8_t)val;
}
if (decoded[0] != PAC_STX) {
return false;
}
uint8_t xor_check = 0;
for (int i = 3; i < 3 + PAC_DATA_SIZE; i++) {
xor_check ^= decoded[i];
}
if (xor_check != decoded[11]) {
return false;
}
memcpy(d->card_id, &decoded[3], PAC_DATA_SIZE);
return true;
}
// Process a demodulated NRZ edge interval (in samples = carrier cycles).
static bool pac_process_interval(pac_codec *d, uint32_t interval) {
uint32_t nbits = (interval + PAC_HALF_BIT) / PAC_RF_PER_BIT;
if (nbits < 1 || nbits > PAC_MAX_BITS_RUN) {
d->raw_hi = 0;
d->raw_lo = 0;
d->polarity = false;
d->bit_count = 0;
return false;
}
for (uint32_t i = 0; i < nbits; i++) {
shift_bit(d, d->polarity);
if (d->bit_count < PAC_FRAME_BITS) {
d->bit_count++;
}
if (d->bit_count >= PAC_FRAME_BITS) {
if (try_decode_frame(d, false) || try_decode_frame(d, true)) {
return true;
}
}
}
d->polarity = !d->polarity;
return false;
}
static pac_codec *pac_alloc(void) {
pac_codec *codec = malloc(sizeof(pac_codec));
return codec;
}
static void pac_free(pac_codec *d) {
free(d);
}
static uint8_t *pac_get_data(pac_codec *d) {
return d->card_id;
}
static void pac_decoder_start(pac_codec *d, uint8_t format) {
memset(d, 0, sizeof(pac_codec));
d->raw_min = 32767; // INT16_MAX: first sample updates it
d->spike_cap = 0x7FFFFFFF; // INT32_MAX: no capping until prescan completes
d->clip_max = -32768; // INT16_MIN: first clipped sample updates it
d->clip_min = 32767; // INT16_MAX: first clipped sample updates it
}
// Feed a raw ADC sample (one per carrier cycle at 125kHz).
// Spike-clipping + threshold with dead zone (PM3 nrzRawDemod style).
static bool pac_decoder_feed(pac_codec *d, uint16_t raw_sample) {
int16_t sample = (int16_t)raw_sample;
d->total_samples++;
// Phase 1: Prescan — track raw minimum to find the NRZ floor.
if (d->total_samples <= PAC_PRESCAN_SAMPLES) {
if (sample < d->raw_min && sample > 0) {
d->raw_min = sample;
}
if (d->total_samples == PAC_PRESCAN_SAMPLES) {
d->spike_cap = (int32_t)d->raw_min * PAC_SPIKE_MULT;
if (d->spike_cap < PAC_MIN_SPIKE_CAP) {
d->spike_cap = PAC_MIN_SPIKE_CAP;
}
}
return false;
}
// Clip spikes: LC ringing transients are replaced with the cap level.
if (sample > d->spike_cap) {
sample = d->spike_cap;
}
uint32_t warmup_samples = d->total_samples - PAC_PRESCAN_SAMPLES;
// Phase 2: Warmup — track min/max of clipped samples to find NRZ levels.
if (warmup_samples <= PAC_WARMUP_SAMPLES) {
if (sample > d->clip_max) d->clip_max = sample;
if (sample < d->clip_min) d->clip_min = sample;
if (warmup_samples == PAC_WARMUP_SAMPLES) {
int16_t range = d->clip_max - d->clip_min;
d->thresh_high = d->clip_min + (range * PAC_THRESH_FUZZ) / 100;
d->thresh_low = d->clip_min + (range * (100 - PAC_THRESH_FUZZ)) / 100;
}
return false;
}
// Phase 3: Per-sample threshold with dead zone.
// Auto-recalibrate if no frame found after enough decode samples —
// the one-shot calibration may have captured an unlucky NRZ segment.
d->decode_samples++;
if (d->decode_samples >= PAC_RECAL_SAMPLES) {
pac_decoder_start(d, 0);
return false;
}
d->sample_count++;
bool new_state = d->adc_state;
if (sample >= d->thresh_high) {
new_state = true;
} else if (sample <= d->thresh_low) {
new_state = false;
} else {
return false;
}
if (!d->has_signal) {
d->has_signal = true;
d->adc_state = new_state;
d->sample_count = 0;
return false;
}
if (new_state == d->adc_state) {
return false;
}
// Transition detected — process the interval
uint32_t interval = d->sample_count;
d->sample_count = 0;
d->adc_state = new_state;
return pac_process_interval(d, interval);
}
// --- Modulator (emulation) ---
static nrf_pwm_values_wave_form_t m_pac_pwm_seq_vals[PAC_FRAME_BITS] = {};
static const nrf_pwm_sequence_t m_pac_pwm_seq = {
.values.p_wave_form = m_pac_pwm_seq_vals,
.length = NRF_PWM_VALUES_LENGTH(m_pac_pwm_seq_vals),
.repeats = 0,
.end_delay = 0,
};
// Build the 128-bit NRZ bitstream from 8-byte card ID.
// Frame: 0xFF sync (8 bits) + 12 × 10-bit UART frames = 128 bits.
// UART frame: start(0) + 7 data bits LSB-first + odd parity + stop(1).
// Payload bytes: STX(0x02), '2', '0', card_id[0..7], XOR checksum.
static void pac_build_bitstream(const uint8_t *card_id, uint8_t *bits_out) {
uint8_t payload[PAC_PAYLOAD_BYTES];
payload[0] = PAC_STX;
payload[1] = '2';
payload[2] = '0';
memcpy(&payload[3], card_id, PAC_DATA_SIZE);
// XOR checksum over card ID bytes (indices 3..10)
uint8_t xor_check = 0;
for (int i = 3; i < 3 + PAC_DATA_SIZE; i++) {
xor_check ^= payload[i];
}
payload[11] = xor_check;
int bit_pos = 0;
// 8-bit sync marker: 0xFF (all ones)
for (int i = 0; i < 8; i++) {
bits_out[bit_pos++] = 1;
}
// 12 UART frames
for (int f = 0; f < PAC_PAYLOAD_BYTES; f++) {
uint8_t byte_val = payload[f];
// Start bit (0)
bits_out[bit_pos++] = 0;
// 7 data bits, LSB first
uint8_t ones = 0;
for (int i = 0; i < 7; i++) {
uint8_t bit = (byte_val >> i) & 1;
bits_out[bit_pos++] = bit;
ones += bit;
}
// Odd parity: set so total ones (data + parity) is odd
uint8_t parity = (ones & 1) ? 0 : 1;
bits_out[bit_pos++] = parity;
// Stop bit (1)
bits_out[bit_pos++] = 1;
}
}
static const nrf_pwm_sequence_t *pac_modulator(pac_codec *d, uint8_t *buf) {
uint8_t bits[PAC_FRAME_BITS];
pac_build_bitstream(buf, bits);
// NRZ: output must be CONSTANT within each bit period (no mid-bit transition).
// Per nRF52840 PS: compare >= counter_top → pin held HIGH; compare = 0 → pin held LOW.
// Use compare = counter_top + 1 (not counter_top) to avoid the compare == counter_top
// boundary where a 1-tick output glitch may occur due to simultaneous compare-match
// and counter-wrap. Real PAC readers with hardware edge detection are sensitive to this;
// PM3's software NRZ demod is not (it averages over the bit period).
for (int i = 0; i < PAC_FRAME_BITS; i++) {
m_pac_pwm_seq_vals[i].channel_0 = bits[i] ? (PAC_RF_PER_BIT + 1) : 0;
m_pac_pwm_seq_vals[i].counter_top = PAC_RF_PER_BIT;
}
return &m_pac_pwm_seq;
}
#define PAC_T55XX_BLOCK_COUNT 5 // 1 config + 4 data blocks
uint8_t pac_t55xx_writer(uint8_t *data, uint32_t *blks) {
uint8_t bits[PAC_FRAME_BITS];
pac_build_bitstream(data, bits);
blks[0] = T5577_PAC_CONFIG;
for (int b = 0; b < 4; b++) {
uint32_t word = 0;
for (int i = 0; i < 32; i++) {
word = (word << 1) | bits[b * 32 + i];
}
blks[b + 1] = word;
}
return PAC_T55XX_BLOCK_COUNT;
}
const protocol pac = {
.tag_type = TAG_TYPE_PAC,
.data_size = PAC_DATA_SIZE,
.alloc = (codec_alloc)pac_alloc,
.free = (codec_free)pac_free,
.get_data = (codec_get_data)pac_get_data,
.modulator = (modulator)pac_modulator,
.decoder =
{
.start = (decoder_start)pac_decoder_start,
.feed = (decoder_feed)pac_decoder_feed,
},
};
@@ -0,0 +1,6 @@
#pragma once
#include "protocols.h"
extern const protocol pac;
uint8_t pac_t55xx_writer(uint8_t *data, uint32_t *blks);
@@ -75,6 +75,12 @@ extern "C" {
T5577_PWD | \
(2 << T5577_MAXBLOCK_SHIFT))
#define T5577_PAC_CONFIG ( \
T5577_MODULATION_DIRECT | \
T5577_BITRATE_RF_32 | \
T5577_PWD | \
(4 << T5577_MAXBLOCK_SHIFT))
void t55xx_write_data(uint32_t passwd, uint32_t *blks, uint8_t blk_count);
void t55xx_reset_passwd(uint32_t old_passwd, uint32_t new_passwd);
@@ -43,6 +43,7 @@ typedef enum {
// securakey
// gallagher
// PAC/Stanley
TAG_TYPE_PAC = 150,
// Presco
// Visa2000
// Viking
@@ -107,7 +108,7 @@ typedef enum {
}
#define TAG_SPECIFIC_TYPE_LF_VALUES \
TAG_TYPE_EM410X, TAG_TYPE_EM410X_ELECTRA, TAG_TYPE_HID_PROX, TAG_TYPE_IOPROX, TAG_TYPE_VIKING
TAG_TYPE_EM410X, TAG_TYPE_EM410X_ELECTRA, TAG_TYPE_PAC, TAG_TYPE_HID_PROX, TAG_TYPE_IOPROX, TAG_TYPE_VIKING
#define TAG_SPECIFIC_TYPE_HF_VALUES \
TAG_TYPE_MIFARE_Mini, TAG_TYPE_MIFARE_1024, TAG_TYPE_MIFARE_2048, \
@@ -94,6 +94,7 @@ static tag_base_handler_map_t tag_base_map[] = {
{TAG_SENSE_LF, TAG_TYPE_HID_PROX, lf_tag_data_loadcb, lf_tag_hidprox_data_savecb, lf_tag_hidprox_data_factory, &m_tag_data_lf},
{TAG_SENSE_LF, TAG_TYPE_IOPROX, lf_tag_data_loadcb, lf_tag_ioprox_data_savecb, lf_tag_ioprox_data_factory, &m_tag_data_lf},
{TAG_SENSE_LF, TAG_TYPE_VIKING, lf_tag_data_loadcb, lf_tag_viking_data_savecb, lf_tag_viking_data_factory, &m_tag_data_lf},
{TAG_SENSE_LF, TAG_TYPE_PAC, lf_tag_data_loadcb, lf_tag_pac_data_savecb, lf_tag_pac_data_factory, &m_tag_data_lf},
// MF1 tag emulation
{TAG_SENSE_HF, TAG_TYPE_MIFARE_Mini, nfc_tag_mf1_data_loadcb, nfc_tag_mf1_data_savecb, nfc_tag_mf1_data_factory, &m_tag_data_hf},
{TAG_SENSE_HF, TAG_TYPE_MIFARE_1024, nfc_tag_mf1_data_loadcb, nfc_tag_mf1_data_savecb, nfc_tag_mf1_data_factory, &m_tag_data_hf},
@@ -80,6 +80,7 @@ void tag_emulation_init(void);
void tag_emulation_save(void);
// Starting and ending of the emulation card
void tag_emulation_load_data(void);
void tag_emulation_sense_run(void);
void tag_emulation_sense_end(void);
@@ -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);
@@ -0,0 +1,346 @@
#include "lf_em4x05_data.h"
#include <stdlib.h>
#include <string.h>
#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;
}
@@ -0,0 +1,52 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#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
@@ -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;
}
@@ -0,0 +1,93 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#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

Some files were not shown because too many files have changed in this diff Show More