Author SHA1 Message Date
ambroserrg fc8cfb4e63 Update documentation for hardware upgrade
Added notices to README.md and CHANGELOG.md about the hardware upgrade
that allows restarting the Ultra by running each RGB color once,
resolving previous firmware modification issues.
2026-03-10 10:29:12 +08:00
47 changed files with 142 additions and 7252 deletions
-5
View File
@@ -708,8 +708,3 @@ FodyWeavers.xsd
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,c++,c,python,visualstudiocode,macos,windows
software/script/tests/nonces.bin
software/script/nonces.bin
.vscode/settings.json
.vscode/tasks.json
firmware/compile_commands.json
firmware/application/compile_commands.json
software/src/target_arch_detect.c
+1 -4
View File
@@ -3,10 +3,7 @@ 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)
- Hardware upgrade: Restarting the Ultra now only requires running each of the three RGB colors once, resolving previous firmware modification issues
- Added commands to dump and clone Mifare tags
- Fix bad missing tools warning (@suut)
- Fix for FAST_READ command for nfc - mf0 tags
+5 -2
View File
@@ -18,6 +18,10 @@ Guangdong, China: [MTools Tec](https://shop.mtoolstec.com/)
Lazada One, Singapore: [Aliexpress by RRG](https://proxgrind.aliexpress.com/store/1101312023)
# Hardware Upgrade Notice
**Important:** The Chameleon Ultra hardware has been upgraded! Restarting the device now only requires running each of the three RGB colors once (equivalent to a restart). This resolves previous issues where firmware modifications could cause the device to malfunction.
# What is it and how to use ?
Read the [available documentation](https://github.com/RfidResearchGroup/ChameleonUltra/wiki).
@@ -26,7 +30,6 @@ Read the [available documentation](https://github.com/RfidResearchGroup/Chameleo
* [ChameleonUltraGUI](https://github.com/GameTec-live/ChameleonUltraGUI)
* [MTools BLE](https://github.com/RfidResearchGroup/ChameleonUltra/wiki/mtoolsble)
* [Mifare Chameleon Tool (iOS only, Beta)](https://apps.apple.com/it/app/mifare-chameleon-tool/id6761231484)
# Videos
@@ -46,4 +49,4 @@ Where do you find the community?
* Devices/chameleon-ultra for usage discussions
* [GameTec_live discord server](https://discord.gg/DJ2A4wxncK)
###### Searching for the docs repo? Find it [here](https://github.com/RfidResearchGroup/ChameleonUltraDocs)
###### Searching for the docs repo? Find it [here](https://github.com/RfidResearchGroup/ChameleonUltraDocs)
+1 -8
View File
@@ -28,7 +28,6 @@ SRC_FILES += \
$(PROJ_DIR)/rfid/nfctag/tag_persistence.c \
$(PROJ_DIR)/rfid/nfctag/hf/crypto1_helper.c \
$(PROJ_DIR)/rfid/nfctag/hf/nfc_14a.c \
$(PROJ_DIR)/rfid/nfctag/hf/nfc_14a_4.c \
$(PROJ_DIR)/rfid/nfctag/hf/nfc_mf1.c \
$(PROJ_DIR)/rfid/nfctag/hf/nfc_mf0_ntag.c \
$(PROJ_DIR)/rfid/nfctag/lf/lf_tag_em.c \
@@ -37,8 +36,6 @@ SRC_FILES += \
$(PROJ_DIR)/rfid/nfctag/lf/utils/manchester.c \
$(PROJ_DIR)/rfid/nfctag/lf/protocols/em410x.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 \
@@ -343,16 +340,12 @@ 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_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/ \
File diff suppressed because it is too large Load Diff
+1 -6
View File
@@ -479,7 +479,7 @@ static void check_wakeup_src(void) {
light_up_by_slot();
// If no operation follows, wait for the timeout and then deep hibernate
sleep_timer_start(settings_get_sleep_timeout());
sleep_timer_start(SLEEP_DELAY_MS_BUTTON_WAKEUP);
} else if ((m_reset_source & (NRF_POWER_RESETREAS_NFC_MASK | NRF_POWER_RESETREAS_LPCOMP_MASK)) ||
(m_gpregret_val & RESET_ON_LF_FIELD_EXISTS_Msk)) {
NRF_LOG_INFO("WakeUp from rfid field");
@@ -667,11 +667,6 @@ static void btn_fn_copy_lf(uint8_t slot, tag_specific_type_t type) {
size = LF_HIDPROX_TAG_ID_SIZE;
data = id_buffer;
break;
case TAG_TYPE_IOPROX:
status = scan_ioprox(id_buffer, 0);
size = LF_IOPROX_TAG_ID_SIZE;
data = id_buffer;
break;
case TAG_TYPE_EM410X:
case TAG_TYPE_EM410X_ELECTRA: {
status = scan_em410x(id_buffer);
-1
View File
@@ -19,7 +19,6 @@
/////////////////////////////////////////////////////////////////////
#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
+2 -2
View File
@@ -90,7 +90,7 @@ BLE_ADVERTISING_DEF(m_advertising);
uint16_t batt_lvl_in_milli_volts = 0;
uint8_t percentage_batt_lvl = 0;
static nrf_saadc_value_t adc_buf[ADC_BUF_COUNT][ADC_BUF_SIZE];
static nrf_saadc_value_t adc_buf[ADC_BUF_SIZE][ADC_BUF_COUNT];
static uint16_t m_conn_handle = BLE_CONN_HANDLE_INVALID; /**< Handle of the current connection. */
static uint16_t m_ble_nus_max_data_len = BLE_GATT_ATT_MTU_DEFAULT - 3; /**< Maximum length of data (in bytes) that can be transmitted to the peer by the Nordic UART service module. */
lf_adc_callback_t m_lf_adc_callback = NULL;
@@ -806,4 +806,4 @@ void unregister_lf_adc_callback(void) {
nrfx_saadc_uninit();
adc_configure();
m_lf_adc_callback = NULL;
}
}
-28
View File
@@ -46,8 +46,6 @@
#define DATA_CMD_GET_BLE_PAIRING_ENABLE (1036)
#define DATA_CMD_SET_BLE_PAIRING_ENABLE (1037)
#define DATA_CMD_GET_ALL_SLOT_NICKS (1038)
#define DATA_CMD_GET_SLEEP_TIMEOUT (1039)
#define DATA_CMD_SET_SLEEP_TIMEOUT (1040)
//
// ******************************************************************
@@ -69,8 +67,6 @@
#define DATA_CMD_MF1_READ_ONE_BLOCK (2008)
#define DATA_CMD_MF1_WRITE_ONE_BLOCK (2009)
#define DATA_CMD_HF14A_RAW (2010)
#define DATA_CMD_HF14A_SCAN_KEEP (2016) /* scan+RATS, keep field alive for APDU exchange */
#define DATA_CMD_HF14A_AUTH_TRACE (2017) /* full anticoll + Crypto1 auth, every frame returned for inspection */
#define DATA_CMD_MF1_MANIPULATE_VALUE_BLOCK (2011)
#define DATA_CMD_MF1_CHECK_KEYS_OF_SECTORS (2012)
#define DATA_CMD_MF1_HARDNESTED_ACQUIRE (2013)
@@ -82,7 +78,6 @@
#define DATA_CMD_HF14A_GET_CONFIG (2200)
#define DATA_CMD_HF14A_SET_CONFIG (2201)
#define DATA_CMD_HF14A_SNIFF (2020)
//
// ******************************************************************
@@ -98,18 +93,11 @@
#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)
#define DATA_CMD_GENERIC_READ (3007)
#define DATA_CMD_CORR_GENERIC_READ (3008)
#define DATA_CMD_IOPROX_SCAN (3010)
#define DATA_CMD_IOPROX_WRITE_TO_T55XX (3011)
#define DATA_CMD_IOPROX_DECODE_RAW (3012)
#define DATA_CMD_IOPROX_COMPOSE_ID (3013)
#define DATA_CMD_LF_T55XX_WRITE (3016)
//
// ******************************************************************
@@ -172,27 +160,11 @@
//
// ******************************************************************
/* ISO14443-4 T=CL emulation commands */
#define DATA_CMD_HF14A_4_APDU_RECV (6000) /* non-blocking poll: firmware->host APDU */
#define DATA_CMD_HF14A_4_APDU_SEND (6001) /* host->firmware APDU response */
#define DATA_CMD_HF14A_4_SET_ANTI_COLL (6002) /* set UID/ATQA/SAK/ATS */
#define DATA_CMD_HF14A_4_STATIC_RESP (6003) /* add/clear static APDU response pair */
#define DATA_CMD_HF14A_4_READER_APDU (6004) /* select+RATS+send APDU, keep field */
#define DATA_CMD_HF14A_4_EMV_SCAN (6005) /* full EMV scan in one call */
#define DATA_CMD_EM410X_SET_EMU_ID (5000)
#define DATA_CMD_EM410X_GET_EMU_ID (5001)
#define DATA_CMD_HIDPROX_SET_EMU_ID (5002)
#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,36 +59,6 @@ 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;
}
/* TX sniff: captures card→reader frames at TX_FRAMESTART */
static nfc_tag_14a_tx_sniff_cb_t m_tx_sniff_cb = NULL;
void nfc_tag_14a_set_tx_sniff_cb(nfc_tag_14a_tx_sniff_cb_t cb) {
m_tx_sniff_cb = cb;
}
void nfc_tag_14a_clear_tx_sniff_cb(void) {
m_tx_sniff_cb = NULL;
}
/* Passive sniff mode: suppress all tag TX responses so the CU does not
* participate in anticollision and avoids colliding with the real card. */
static bool m_sniff_passive = false;
void nfc_tag_14a_set_sniff_passive(bool passive) {
m_sniff_passive = passive;
}
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 };
@@ -356,11 +326,6 @@ 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) {
@@ -385,11 +350,9 @@ void nfc_tag_14a_data_process(uint8_t *p_data) {
if (auto_coll_res != NULL) {
// The status machine is set to the preparation state, and the next operation is to enter the card selection link
m_tag_state_14a = NFC_TAG_STATE_14A_READY;
if (!m_sniff_passive) {
// After receiving the WUPA or REQA instruction, we need to reply to ATQA
nfc_tag_14a_tx_bytes(auto_coll_res->atqa, 2, false);
// NRF_LOG_INFO("ATQA reply: %02x%02x", auto_coll_res->atqa[0], auto_coll_res->atqa[1]);
}
// After receiving the WUPA or REQA instruction, we need to reply to ATQA
nfc_tag_14a_tx_bytes(auto_coll_res->atqa, 2, false);
// NRF_LOG_INFO("ATQA reply: %02x%02x", auto_coll_res->atqa[0], auto_coll_res->atqa[1]);
} else {
m_tag_state_14a = NFC_TAG_STATE_14A_IDLE;
NRF_LOG_INFO("Auto anti-collision resource no exists.");
@@ -505,9 +468,7 @@ void nfc_tag_14a_data_process(uint8_t *p_data) {
}
// Incoming SELECT ALL for any cascade level
if (szDataBits == 16 && p_data[1] == 0x20) {
if (!m_sniff_passive) {
nfc_tag_14a_tx_bytes(uid, 5, false);
}
nfc_tag_14a_tx_bytes(uid, 5, false);
// NRF_LOG_INFO("[MFEMUL_SELECT] SEL Reply.");
break;
}
@@ -521,14 +482,10 @@ void nfc_tag_14a_data_process(uint8_t *p_data) {
if (cl_finished) {
// NRF_LOG_INFO("[MFEMUL_SELECT] m_tag_state_14a = MFEMUL_WORK");
m_tag_state_14a = NFC_TAG_STATE_14A_ACTIVE;
if (!m_sniff_passive) {
nfc_tag_14a_tx_bytes(auto_coll_res->sak, 1, true);
}
nfc_tag_14a_tx_bytes(auto_coll_res->sak, 1, true);
} else {
// It is necessary to continue the level, so we need to respond to a data that marks the incomplete UID in SAK
if (!m_sniff_passive) {
nfc_tag_14a_tx_bytes(m_uid_incomplete_sak, 3, false);
}
nfc_tag_14a_tx_bytes(m_uid_incomplete_sak, 3, false);
}
} else {
// IDLE, not our UID
@@ -554,10 +511,6 @@ void nfc_tag_14a_data_process(uint8_t *p_data) {
}
// RATS instruction
if (p_data[0] == NFC_TAG_14A_CMD_RATS && nfc_tag_14a_checks_crc(p_data, 4)) {
// Reset T=CL layer state for the new session
if (m_tag_handler.cb_reset != NULL) {
m_tag_handler.cb_reset();
}
// Make sure the sub -packaging opens the support of ATS
if (auto_coll_res->ats->length > 0) {
// Take out FSD and return according to the maximum FSD
@@ -602,10 +555,11 @@ static inline void nrf_nfct_reset(void) {
// Use Window Grid frame delay mode.
nrf_nfct_frame_delay_mode_set(NRF_NFCT_FRAME_DELAY_MODE_WINDOWGRID);
/* Use SDD00001 per ISO14443-3 standard.
* Note: SDD00100 was previously used for Windows Phone compatibility
* but breaks standard readers (including Proxmark3). SDD00001 is correct. */
nrf_nfct_sensres_bit_frame_sdd_set(NRF_NFCT_SENSRES_BIT_FRAME_SDD_00001);
/* Begin: Workaround for anomaly 25 */
/* Workaround for wrong SENSRES values require using SDD00001, but here SDD00100 is used
because it is required to operate with Windows Phone */
nrf_nfct_sensres_bit_frame_sdd_set(NRF_NFCT_SENSRES_BIT_FRAME_SDD_00100);
/* End: Workaround for anomaly 25 */
// Restore interrupts.
nrf_nfct_int_enable(int_enabled);
@@ -678,19 +632,7 @@ void nfc_tag_14a_event_callback(nrfx_nfct_evt_t const *p_event) {
}
case NRFX_NFCT_EVT_TX_FRAMESTART: {
// NRF_LOG_INFO("TX start.\n");
if (m_tx_sniff_cb != NULL) {
uint32_t amt = NRF_NFCT->TXD.AMOUNT;
uint16_t tx_bytes = (amt >> NFCT_TXD_AMOUNT_TXDATABYTES_Pos)
& (NFCT_TXD_AMOUNT_TXDATABYTES_Msk >> NFCT_TXD_AMOUNT_TXDATABYTES_Pos);
uint16_t tx_bits_rem = (amt >> NFCT_TXD_AMOUNT_TXDATABITS_Pos)
& (NFCT_TXD_AMOUNT_TXDATABITS_Msk >> NFCT_TXD_AMOUNT_TXDATABITS_Pos);
uint16_t tx_bits = (tx_bits_rem > 0)
? ((tx_bytes - 1) * 8 + tx_bits_rem)
: (tx_bytes * 8);
if (tx_bits > 0 && tx_bytes <= MAX_NFC_TX_BUFFER_SIZE) {
m_tx_sniff_cb(m_nfc_tx_buffer, tx_bits);
}
}
// NRF_LOG_INFO("TX config is %d.\n", nrf_nfct_tx_frame_config_get(NRF_NFCT));
break;
}
case NRFX_NFCT_EVT_TX_FRAMEEND: {
@@ -4,7 +4,7 @@
#include "tag_emulation.h"
#define MAX_NFC_RX_BUFFER_SIZE 257
#define MAX_NFC_TX_BUFFER_SIZE 512 /* must hold PCB + max APDU response */
#define MAX_NFC_TX_BUFFER_SIZE 64
#define NFC_TAG_14A_CRC_LENGTH 2
@@ -82,27 +82,6 @@ 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);
/* TX sniff callback — fires at TX_FRAMESTART with the frame the tag is about
* to send (cardreader direction). Same signature as the RX sniff callback.
* Install alongside nfc_tag_14a_set_sniff_cb() to capture both directions. */
typedef void (*nfc_tag_14a_tx_sniff_cb_t)(const uint8_t *data, uint16_t szBits);
void nfc_tag_14a_set_tx_sniff_cb(nfc_tag_14a_tx_sniff_cb_t cb);
void nfc_tag_14a_clear_tx_sniff_cb(void);
/* Passive sniff mode: when true, suppresses all CU anticollision responses
* (ATQA, UID, SAK) so the CU does not collide with real cards in the field.
* Enable before starting a sniff session, disable on completion. */
void nfc_tag_14a_set_sniff_passive(bool passive);
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);
@@ -1,446 +0,0 @@
/**
* @file nfc_14a_4.c
* @brief ISO14443-4 T=CL emulation for ChameleonUltra
*
* Implements a full ISO14443-4 tag emulator with a static APDU response
* table. The table is populated by the host before field activation, so
* the firmware can respond to an EMV reader autonomously without any USB
* communication while the RF field is active.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <string.h>
#include "nfc_14a_4.h"
#include "nfc_14a.h"
#include "tag_emulation.h"
#include "tag_persistence.h"
#include "fds_util.h"
#include "nrf_log.h"
/* ------------------------------------------------------------------ */
/* PCB byte constants (ISO14443-4 §7) */
/* ------------------------------------------------------------------ */
#define PCB_IBLOCK_MASK 0xC0
#define PCB_IBLOCK_VAL 0x00
#define PCB_RBLOCK_MASK 0xE0
#define PCB_RBLOCK_VAL 0x80 /* R(ACK) = 0xA2/0xA3, R(NAK) = 0xB2/0xB3 */
#define PCB_SBLOCK_MASK 0xC0
#define PCB_SBLOCK_VAL 0xC0
#define PCB_BLOCK_NUM 0x01
#define PCB_CID_FOLLOWING 0x10 /* bit4: CID follows */
#define PCB_NAD_FOLLOWING 0x08 /* bit3: NAD follows */
#define PCB_CHAIN 0x20 /* bit5: chaining flag per ISO14443-4 Table 3 */
#define PCB_SBLOCK_WTX 0x30
#define PCB_SBLOCK_DESELECT 0xC2
#define WTX_VALUE 0x3B /* WTXM=59 (~3s extra wait) */
static inline bool is_iblock(uint8_t pcb) {
return (pcb & PCB_IBLOCK_MASK) == PCB_IBLOCK_VAL;
}
static inline bool is_rblock(uint8_t pcb) {
/* R-block: bit7=1, bit6=0, bit2=1, bit1=0 (mask 0xC6, value 0x82) */
return (pcb & 0xC6) == 0x82;
}
static inline bool is_sblock(uint8_t pcb) {
return (pcb & PCB_SBLOCK_MASK) == PCB_SBLOCK_VAL;
}
/* ------------------------------------------------------------------ */
/* Module state */
/* ------------------------------------------------------------------ */
static nfc_tag_14a_4_information_t *m_tag_information = NULL;
/* Shadow coll-res references into m_tag_information */
static nfc_tag_14a_coll_res_reference_t m_shadow_coll_res;
/* T=CL session state */
static uint8_t m_block_num = 0;
static bool m_cid_supported = false;
static uint8_t m_cid = 0;
static uint8_t m_apdu_buf[NFC_14A_4_MAX_APDU];
static uint16_t m_apdu_len = 0;
static bool m_apdu_pending = false;
static uint8_t m_resp_buf[NFC_14A_4_MAX_APDU];
static uint16_t m_resp_len = 0;
static bool m_response_ready = false;
/* TX scratch buffer */
static uint8_t m_tx_buf[NFC_14A_4_MAX_APDU + 4];
/* Debug counters — readable via hf 14a debug */
static uint8_t m_dbg_iblocks_rx = 0; /* I-blocks received */
static uint8_t m_dbg_iblocks_tx = 0; /* I-blocks sent */
static uint8_t m_dbg_last_rx_pcb = 0; /* PCB of last received I-block */
static uint8_t m_dbg_last_match = 0; /* last find_static_response result */
/* Static APDU response table (RAM copy, populated from m_tag_information) */
static nfc_tag_14a_4_static_response_t m_static_resp[NFC_14A_4_MAX_STATIC_RESPONSES];
static uint8_t m_static_resp_count = 0;
/* Large response overflow (RAM only, > NFC_14A_4_MAX_STATIC_RESP_LEN bytes).
* NOT persisted to flash. Must reload via emv load after power cycle. */
typedef struct {
uint8_t cmd[NFC_14A_4_MAX_STATIC_CMD_LEN];
uint8_t cmd_len;
uint8_t resp[NFC_14A_4_MAX_LARGE_RESP_LEN];
uint16_t resp_len;
} nfc_tag_14a_4_large_response_t;
static nfc_tag_14a_4_large_response_t m_large_resp[NFC_14A_4_MAX_LARGE_RESPONSES];
static uint8_t m_large_resp_count = 0;
/* ------------------------------------------------------------------ */
/* Static response table */
/* ------------------------------------------------------------------ */
void nfc_tag_14a_4_add_static_response(const uint8_t *cmd, uint8_t cmd_len,
const uint8_t *resp, uint16_t resp_len) {
if (cmd_len > NFC_14A_4_MAX_STATIC_CMD_LEN) cmd_len = NFC_14A_4_MAX_STATIC_CMD_LEN;
if (resp_len > NFC_14A_4_MAX_STATIC_RESP_LEN) {
/* Large response: RAM-only overflow table */
if (m_large_resp_count >= NFC_14A_4_MAX_LARGE_RESPONSES) return;
if (resp_len > NFC_14A_4_MAX_LARGE_RESP_LEN) resp_len = NFC_14A_4_MAX_LARGE_RESP_LEN;
nfc_tag_14a_4_large_response_t *le = &m_large_resp[m_large_resp_count++];
le->cmd_len = cmd_len;
le->resp_len = resp_len;
memcpy(le->cmd, cmd, cmd_len);
memcpy(le->resp, resp, resp_len);
return;
}
/* Normal response: flash-backed table */
if (m_static_resp_count >= NFC_14A_4_MAX_STATIC_RESPONSES) return;
nfc_tag_14a_4_static_response_t *e = &m_static_resp[m_static_resp_count++];
e->cmd_len = cmd_len;
e->resp_len = (uint8_t)resp_len;
memcpy(e->cmd, cmd, cmd_len);
memcpy(e->resp, resp, resp_len);
if (m_tag_information &&
m_tag_information->static_resp_count < NFC_14A_4_MAX_STATIC_RESPONSES) {
memcpy(&m_tag_information->static_resp[m_tag_information->static_resp_count++],
e, sizeof(*e));
}
}
void nfc_tag_14a_4_clear_static_responses(void) {
m_static_resp_count = 0;
m_large_resp_count = 0;
if (m_tag_information) {
m_tag_information->static_resp_count = 0;
}
}
static bool find_static_response(const uint8_t *apdu, uint16_t apdu_len,
uint8_t **resp_out, uint16_t *resp_len_out) {
/* Flash-backed table */
for (uint8_t i = 0; i < m_static_resp_count; i++) {
nfc_tag_14a_4_static_response_t *e = &m_static_resp[i];
if (apdu_len >= e->cmd_len &&
memcmp(apdu, e->cmd, e->cmd_len) == 0) {
*resp_out = e->resp;
*resp_len_out = e->resp_len;
return true;
}
}
/* RAM-only large response table */
for (uint8_t i = 0; i < m_large_resp_count; i++) {
nfc_tag_14a_4_large_response_t *e = &m_large_resp[i];
if (apdu_len >= e->cmd_len &&
memcmp(apdu, e->cmd, e->cmd_len) == 0) {
*resp_out = e->resp;
*resp_len_out = e->resp_len;
return true;
}
}
return false;
}
/* ------------------------------------------------------------------ */
/* TX helpers */
/* ------------------------------------------------------------------ */
static void send_iblock(const uint8_t *data, uint16_t len) {
uint8_t pcb = 0x02 | (m_block_num & 0x01);
if (m_cid_supported) pcb |= PCB_CID_FOLLOWING;
uint8_t off = 0;
m_tx_buf[off++] = pcb;
if (m_cid_supported) m_tx_buf[off++] = m_cid & 0x0F;
if (len > NFC_14A_4_MAX_APDU) len = NFC_14A_4_MAX_APDU;
memcpy(&m_tx_buf[off], data, len);
nfc_tag_14a_tx_bytes(m_tx_buf, off + len, true);
m_block_num ^= 1;
}
static void send_rack(void) {
uint8_t pcb = 0xA2 | (m_block_num & 0x01);
if (m_cid_supported) {
pcb |= PCB_CID_FOLLOWING;
uint8_t buf[2] = { pcb, m_cid & 0x0F };
nfc_tag_14a_tx_bytes(buf, 2, true);
} else {
nfc_tag_14a_tx_bytes(&pcb, 1, true);
}
}
static void send_wtx(void) {
uint8_t buf[3];
uint8_t off = 0;
buf[off++] = PCB_SBLOCK_WTX | (m_cid_supported ? PCB_CID_FOLLOWING : 0);
if (m_cid_supported) buf[off++] = m_cid & 0x0F;
buf[off++] = WTX_VALUE;
nfc_tag_14a_tx_bytes(buf, off, true);
}
/* ------------------------------------------------------------------ */
/* State handler (called from NFCT ISR on each received frame) */
/* ------------------------------------------------------------------ */
static void nfc_tag_14a_4_state_handler(uint8_t *data, uint16_t szBytes) {
if (szBytes == 0) return;
uint8_t pcb = data[0];
/* ---- S-block ---- */
if (is_sblock(pcb)) {
if ((pcb & 0xF7) == PCB_SBLOCK_DESELECT) {
/* Echo DESELECT */
nfc_tag_14a_tx_bytes(data, szBytes, true);
nfc_tag_14a_4_reset_handler();
return;
}
if ((pcb & 0x3F) == (PCB_SBLOCK_WTX & 0x3F)) {
/* Reader sending WTX — echo back with our WTXM */
uint8_t wtxm = (szBytes > 1) ? data[szBytes - 1] & 0x3F : WTX_VALUE;
uint8_t resp[3];
uint8_t off = 0;
resp[off++] = PCB_SBLOCK_WTX | (m_cid_supported ? PCB_CID_FOLLOWING : 0);
if (m_cid_supported) resp[off++] = m_cid & 0x0F;
resp[off++] = wtxm;
nfc_tag_14a_tx_bytes(resp, off, true);
/* If we now have a response ready, send it next I-block */
if (m_response_ready) {
m_response_ready = false;
send_iblock(m_resp_buf, m_resp_len);
}
return;
}
return;
}
/* ---- R-block ---- */
if (is_rblock(pcb)) {
send_rack();
return;
}
/* ---- I-block ---- */
if (is_iblock(pcb)) {
uint8_t reader_blknum = pcb & PCB_BLOCK_NUM;
bool has_cid = (pcb & PCB_CID_FOLLOWING) != 0;
bool has_nad = (pcb & PCB_NAD_FOLLOWING) != 0;
bool more_chain = (pcb & PCB_CHAIN) != 0;
uint8_t offset = 1;
if (has_cid) {
/* CID acknowledged but not used in responses (keeps protocol simpler) */
m_cid_supported = false;
offset++; /* skip CID byte */
}
if (has_nad) offset++;
if (offset >= szBytes) {
send_rack();
return;
}
uint16_t apdu_len = szBytes - offset;
if (apdu_len > NFC_14A_4_MAX_APDU) apdu_len = NFC_14A_4_MAX_APDU;
m_dbg_iblocks_rx++;
m_dbg_last_rx_pcb = pcb;
NRF_LOG_INFO("14A4 I-block #%d: reader_blk=%d m_block_num=%d apdu_len=%d",
m_dbg_iblocks_rx, reader_blknum, m_block_num, apdu_len);
/* Block number check per ISO14443-4 §7.5.3.3:
* If block number matches expected, process new APDU.
* If block number does NOT match, it is a retransmit
* resend the last response without re-processing. */
if (reader_blknum != (m_block_num & 0x01)) {
/* Retransmit: resend last response */
if (m_resp_len > 0) {
/* Restore block num to what we sent last time and resend */
m_block_num ^= 1; /* undo the increment from last send */
send_iblock(m_resp_buf, m_resp_len);
} else {
send_rack();
}
return;
}
memcpy(m_apdu_buf, &data[offset], apdu_len);
m_apdu_len = apdu_len;
m_apdu_pending = true;
m_response_ready = false;
if (more_chain) {
send_rack();
return;
}
/* APDU complete — check static table first, then WTX */
{
uint8_t *static_resp = NULL;
uint16_t static_len = 0;
bool _found = find_static_response(m_apdu_buf, apdu_len,
&static_resp, &static_len);
m_dbg_last_match = _found ? 1 : 0;
NRF_LOG_INFO("14A4 find_static: found=%d static_len=%d resp_count=%d",
_found, static_len, m_static_resp_count);
if (_found) {
m_dbg_iblocks_tx++;
memcpy(m_resp_buf, static_resp, static_len);
m_resp_len = static_len;
send_iblock(m_resp_buf, m_resp_len);
} else if (m_response_ready) {
m_response_ready = false;
send_iblock(m_resp_buf, m_resp_len);
} else {
/* No response ready — keep reader alive with WTX */
send_wtx();
}
}
return;
}
NRF_LOG_INFO("14A-4: unknown PCB 0x%02x", pcb);
}
/* ------------------------------------------------------------------ */
/* APDU relay API (for host-driven responses) */
/* ------------------------------------------------------------------ */
bool nfc_tag_14a_4_get_pending_apdu(uint8_t *buf, uint16_t *length) {
if (!m_apdu_pending) return false;
m_apdu_pending = false;
*length = m_apdu_len;
memcpy(buf, m_apdu_buf, m_apdu_len);
return true;
}
void nfc_tag_14a_4_set_response(const uint8_t *data, uint16_t length) {
if (length > NFC_14A_4_MAX_APDU) length = NFC_14A_4_MAX_APDU;
memcpy(m_resp_buf, data, length);
m_resp_len = length;
m_response_ready = true;
}
/* ------------------------------------------------------------------ */
/* Reset handler */
/* ------------------------------------------------------------------ */
void nfc_tag_14a_4_reset_handler(void) {
m_block_num = 0;
m_cid_supported = false;
m_cid = 0;
m_apdu_pending = false;
m_response_ready = false;
m_apdu_len = 0;
m_resp_len = 0;
}
void nfc_tag_14a_4_get_debug_counters(uint8_t *rx, uint8_t *tx,
uint8_t *last_pcb, uint8_t *last_match) {
*rx = m_dbg_iblocks_rx;
*tx = m_dbg_iblocks_tx;
*last_pcb = m_dbg_last_rx_pcb;
*last_match = m_dbg_last_match;
}
/* ------------------------------------------------------------------ */
/* Anti-collision resource */
/* ------------------------------------------------------------------ */
nfc_tag_14a_coll_res_reference_t *nfc_tag_14a_4_get_coll_res(void) {
if (m_tag_information == NULL) return NULL;
m_shadow_coll_res.sak = m_tag_information->res_coll.sak;
m_shadow_coll_res.atqa = m_tag_information->res_coll.atqa;
m_shadow_coll_res.uid = m_tag_information->res_coll.uid;
m_shadow_coll_res.size = &m_tag_information->res_coll.size;
m_shadow_coll_res.ats = &m_tag_information->res_coll.ats;
return &m_shadow_coll_res;
}
/* ------------------------------------------------------------------ */
/* Data load / save / factory callbacks */
/* ------------------------------------------------------------------ */
int nfc_tag_14a_4_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer) {
int info_size = sizeof(nfc_tag_14a_4_information_t);
if (buffer->length < info_size) {
NRF_LOG_ERROR("14A-4 loadcb: buffer too small (%d < %d)",
buffer->length, info_size);
return info_size;
}
m_tag_information = (nfc_tag_14a_4_information_t *)buffer->buffer;
/* Populate RAM static table from persisted slot data */
m_static_resp_count = m_tag_information->static_resp_count;
if (m_static_resp_count > NFC_14A_4_MAX_STATIC_RESPONSES)
m_static_resp_count = NFC_14A_4_MAX_STATIC_RESPONSES;
memcpy(m_static_resp, m_tag_information->static_resp,
m_static_resp_count * sizeof(nfc_tag_14a_4_static_response_t));
nfc_tag_14a_handler_t handler = {
.get_coll_res = nfc_tag_14a_4_get_coll_res,
.cb_state = nfc_tag_14a_4_state_handler,
.cb_reset = nfc_tag_14a_4_reset_handler,
};
nfc_tag_14a_set_handler(&handler);
NRF_LOG_INFO("14A-4 loadcb OK: SAK=%02x uid_sz=%d static_resp=%d",
m_tag_information->res_coll.sak[0],
m_tag_information->res_coll.size,
m_static_resp_count);
return info_size;
}
int nfc_tag_14a_4_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer) {
return sizeof(nfc_tag_14a_4_information_t);
}
bool nfc_tag_14a_4_data_factory(uint8_t slot, tag_specific_type_t tag_type) {
if (tag_type != TAG_TYPE_HF14A_4) return false;
/* Build factory defaults on stack and write directly to FDS
* (same pattern as nfc_tag_mf1_data_factory). */
nfc_tag_14a_4_information_t info;
memset(&info, 0, sizeof(info));
/* Placeholder 7-byte NXP-style UID */
info.res_coll.size = NFC_TAG_14A_UID_DOUBLE_SIZE;
info.res_coll.atqa[0] = 0x04;
info.res_coll.atqa[1] = 0x00;
info.res_coll.sak[0] = 0x20; /* ISO14443-4 */
info.res_coll.uid[0] = 0x04;
info.res_coll.uid[1] = 0x01;
info.res_coll.uid[2] = 0x02;
info.res_coll.uid[3] = 0x03;
info.res_coll.uid[4] = 0x04;
info.res_coll.uid[5] = 0x05;
info.res_coll.uid[6] = 0x06;
static const uint8_t default_ats[] = {
0x10, 0x78, 0x80, 0x70, 0x02, 0x00,
0x31, 0xC1, 0x64, 0x09, 0x97, 0x61,
0x26, 0x00, 0x90, 0x00
};
info.res_coll.ats.length = sizeof(default_ats);
memcpy(info.res_coll.ats.data, default_ats, sizeof(default_ats));
info.static_resp_count = 0;
fds_slot_record_map_t map_info;
get_fds_map_by_slot_sense_type_for_dump(slot, TAG_SENSE_HF, &map_info);
bool ret = fds_write_sync(map_info.id, map_info.key, sizeof(info), &info);
NRF_LOG_INFO("14A-4 factory slot %d: %s", slot, ret ? "OK" : "FAIL");
return ret;
}
@@ -1,71 +0,0 @@
/**
* @file nfc_14a_4.h
* @brief ISO14443-4 T=CL emulation for ChameleonUltra
*
* Implements a full ISO14443-4 tag emulator:
* - I-blocks (information, chaining, CID)
* - R-blocks (ACK/NAK retransmit)
* - S-blocks (WTX to keep reader alive, DESELECT)
* - Static APDU response table (pre-loaded before field, no USB needed
* during field exchange)
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef NFC_14A_4_H
#define NFC_14A_4_H
#include "nfc_14a.h"
#include "tag_emulation.h"
/* Maximum APDU size (FSCI=8 → FSC=256, minus PCB+CRC = 253) */
#define NFC_14A_4_MAX_APDU 260 /* max APDU in RAM; flash entries capped at 253 */
/* Static APDU response table — up to 12 pre-configured command/response pairs.
* Loaded before field activation; firmware responds autonomously without USB. */
#define NFC_14A_4_MAX_STATIC_RESPONSES 12
#define NFC_14A_4_MAX_LARGE_RESPONSES 4 /* RAM-only, for resp > 253 bytes */
#define NFC_14A_4_MAX_LARGE_RESP_LEN 260 /* max large response size */
#define NFC_14A_4_MAX_STATIC_CMD_LEN 16
#define NFC_14A_4_MAX_STATIC_RESP_LEN 253 /* max bytes in flash-backed slot */
typedef struct __attribute__((packed)) {
uint8_t cmd_len;
uint8_t cmd[NFC_14A_4_MAX_STATIC_CMD_LEN];
uint8_t resp_len;
uint8_t resp[NFC_14A_4_MAX_STATIC_RESP_LEN];
} nfc_tag_14a_4_static_response_t;
/**
* Per-slot persistent data layout stored in FDS flash.
* Anti-collision response (UID/ATQA/SAK/ATS) plus the static response table.
*/
typedef struct __attribute__((packed)) {
nfc_tag_14a_coll_res_entity_t res_coll;
uint8_t static_resp_count;
nfc_tag_14a_4_static_response_t static_resp[NFC_14A_4_MAX_STATIC_RESPONSES];
} nfc_tag_14a_4_information_t;
/* Anti-collision resource — used by get_coll_res_data in app_cmd.c */
nfc_tag_14a_coll_res_reference_t *nfc_tag_14a_4_get_coll_res(void);
/* tag_base_map callbacks */
int nfc_tag_14a_4_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer);
int nfc_tag_14a_4_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer);
bool nfc_tag_14a_4_data_factory(uint8_t slot, tag_specific_type_t tag_type);
/* Static response table management (called before hw mode -e) */
void nfc_tag_14a_4_add_static_response(const uint8_t *cmd, uint8_t cmd_len,
const uint8_t *resp, uint16_t resp_len);
void nfc_tag_14a_4_clear_static_responses(void);
/* APDU relay — host-driven responses */
bool nfc_tag_14a_4_get_pending_apdu(uint8_t *buf, uint16_t *length);
void nfc_tag_14a_4_set_response(const uint8_t *data, uint16_t length);
/* Reset handler */
void nfc_tag_14a_4_reset_handler(void);
#endif /* NFC_14A_4_H */
void nfc_tag_14a_4_get_debug_counters(uint8_t *rx, uint8_t *tx, uint8_t *last_pcb, uint8_t *last_match);
@@ -5,13 +5,10 @@
#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"
@@ -24,6 +21,7 @@
NRF_LOG_MODULE_REGISTER();
#define ANT_NO_MOD() nrf_gpio_pin_clear(LF_MOD)
#define LF_125KHZ_BROADCAST_MAX (10)
// Whether the USB light effect is allowed to enable
extern bool g_usb_led_marquee_enable;
@@ -42,8 +40,7 @@ static void lf_field_lost(void) {
g_is_tag_emulating = false; // Reset the flag in the emulation
m_is_lf_emulating = false;
TAG_FIELD_LED_OFF() // Make sure the indicator light of the LF field status
// Re-arm LPCOMP so the next field appearance triggers lpcomp_event_handler.
NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
NRF_LPCOMP->INTENSET = LPCOMP_INTENCLR_CROSS_Msk | LPCOMP_INTENCLR_UP_Msk | LPCOMP_INTENCLR_DOWN_Msk | LPCOMP_INTENCLR_READY_Msk;
// call sleep_timer_start *after* unsetting g_is_tag_emulating
sleep_timer_start(SLEEP_DELAY_MS_FIELD_125KHZ_LOST); // Start the timer to enter the sleep
NRF_LOG_INFO("LF FIELD LOST");
@@ -68,15 +65,12 @@ bool is_lf_field_exists(void) {
* priority is set to APP_IRQ_PRIORITY_HIGH).
*/
static void lpcomp_event_handler(nrf_lpcomp_event_t event) {
// Only when the lf-frequency emulation is not launched, and the analog card is started
// Only when the lf -frequency emulation is not launched, and the analog card is started
if (m_is_lf_emulating || event != NRF_LPCOMP_EVENT_UP) {
return;
}
sleep_timer_stop(); // turn off dormant delay
// Disable LPCOMP during emulation — LF_RSSI fluctuates during load
// modulation and would trigger spurious DOWN events with DETECT_CROSS.
// Field-loss is checked periodically via EVT_END_SEQ0 in pwm_handler.
nrfx_lpcomp_disable();
// set the emulation status logo bit
@@ -89,9 +83,8 @@ static void lpcomp_event_handler(nrf_lpcomp_event_t event) {
set_slot_light_color(RGB_BLUE);
TAG_FIELD_LED_ON()
// Loop continuously — no stop/restart gaps between sequence plays.
// Field-loss is detected in pwm_handler via EVT_END_SEQ0.
nrfx_pwm_simple_playback(&m_broadcast, m_pwm_seq, 1, NRFX_PWM_FLAG_LOOP);
// use precise hardware timer to broadcast card id
nrfx_pwm_simple_playback(&m_broadcast, m_pwm_seq, LF_125KHZ_BROADCAST_MAX, NRFX_PWM_FLAG_STOP);
NRF_LOG_INFO("LF FIELD DETECTED");
}
@@ -108,23 +101,21 @@ static void lpcomp_init(void) {
}
static void pwm_handler(nrfx_pwm_evt_type_t event_type) {
if (event_type == NRFX_PWM_EVT_END_SEQ0) {
// Fired at end of each loop iteration — check field without stopping PWM.
// Mask UP interrupt while sampling to prevent re-entrancy.
NRF_LPCOMP->INTENCLR = LPCOMP_INTENCLR_UP_Msk;
if (!is_lf_field_exists()) {
// Field gone — stop the loop; pwm_handler will get EVT_STOPPED next.
nrfx_pwm_stop(&m_broadcast, false);
}
// Re-enable will happen either in lf_field_lost (via INTENSET) or stays
// suppressed while PWM keeps looping (we only need it after field_lost).
return;
}
if (event_type != NRFX_PWM_EVT_STOPPED) {
return;
}
// after last broadcast, force NO_MOD on antenna to measure field.
ANT_NO_MOD();
lf_field_lost();
bsp_delay_ms(1);
// We don't need any events, but only need to detect the state of the field
NRF_LPCOMP->INTENCLR = LPCOMP_INTENCLR_CROSS_Msk | LPCOMP_INTENCLR_UP_Msk | LPCOMP_INTENCLR_DOWN_Msk | LPCOMP_INTENCLR_READY_Msk;
if (is_lf_field_exists()) {
nrfx_lpcomp_disable();
nrfx_pwm_simple_playback(&m_broadcast, m_pwm_seq, LF_125KHZ_BROADCAST_MAX, NRFX_PWM_FLAG_STOP);
} else {
lf_field_lost();
}
}
static void pwm_init(void) {
@@ -144,23 +135,6 @@ 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()) {
@@ -173,7 +147,6 @@ static void lf_sense_disable(void) {
nrfx_lpcomp_uninit();
m_pwm_seq = NULL;
m_is_lf_emulating = false;
sd_clock_hfclk_release();
}
static enum {
@@ -232,15 +205,6 @@ int lf_tag_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer) {
return LF_HIDPROX_TAG_ID_SIZE;
}
if (type == TAG_TYPE_IOPROX && buffer->length >= LF_IOPROX_TAG_ID_SIZE) {
m_tag_type = type;
void *codec = ioprox.alloc();
m_pwm_seq = ioprox.modulator(codec, buffer->buffer);
ioprox.free(codec);
NRF_LOG_INFO("load lf ioprox data finish.");
return LF_IOPROX_TAG_ID_SIZE;
}
if (type == TAG_TYPE_VIKING && buffer->length >= LF_VIKING_TAG_ID_SIZE) {
m_tag_type = type;
void *codec = viking.alloc();
@@ -250,15 +214,6 @@ 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;
}
@@ -291,17 +246,6 @@ int lf_tag_hidprox_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buff
return m_tag_type == TAG_TYPE_HID_PROX ? LF_HIDPROX_TAG_ID_SIZE : 0;
}
/** @brief Id card deposit card number before callback
* @param type Refined tag type
* @param buffer Data buffer
* @return The length of the data that needs to be saved is that it does not save when 0
*/
int lf_tag_ioprox_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer) {
// Make sure to load this tag before allowing saving
// Just save the original card package directly
return m_tag_type == TAG_TYPE_IOPROX ? LF_IOPROX_TAG_ID_SIZE : 0;
}
/** @brief Id card deposit card number before callback
* @param type Refined tag type
* @param buffer Data buffer
@@ -359,18 +303,6 @@ bool lf_tag_hidprox_data_factory(uint8_t slot, tag_specific_type_t tag_type) {
return lf_tag_data_factory(slot, tag_type, tag_id, sizeof(tag_id));
}
/** @brief Id card deposit card number before callback
* @param slot Card slot number
* @param tag_type Refined tag type
* @return Whether the format is successful, if the formatting is successful, it will return to True, otherwise False will be returned
*/
bool lf_tag_ioprox_data_factory(uint8_t slot, tag_specific_type_t tag_type) {
uint8_t tag_id[16] = {
0x01,0xAA,0x30,0x39,0x00,0x78,0x6A,0xA0,0x33,0x09,0xCF,0xEF,0x00,0x00,0x00,0x00
};
return lf_tag_data_factory(slot, tag_type, tag_id, sizeof(tag_id));
}
/** @brief Id card deposit card number before callback
* @param slot Card slot number
* @param tag_type Refined tag type
@@ -381,13 +313,3 @@ 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));
}
@@ -7,10 +7,8 @@
#define LF_EM410X_TAG_ID_SIZE 5
#define LF_EM410X_ELECTRA_TAG_ID_SIZE 13
#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);
@@ -18,10 +16,6 @@ int lf_tag_em410x_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffe
bool lf_tag_em410x_data_factory(uint8_t slot, tag_specific_type_t tag_type);
int lf_tag_hidprox_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer);
bool lf_tag_hidprox_data_factory(uint8_t slot, tag_specific_type_t tag_type);
int lf_tag_ioprox_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer);
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);
@@ -16,8 +16,6 @@
#define DEMOD_BUFFER_SIZE (32)
#define HIDPROX_RAW_SIZE (96)
// NOTE: These LF_FSK2a_* defines are intentionally local to this .c file
// to allow per-protocol timing tuning (do not move to a shared header).
#define LF_FSK2a_PWM_LO_FREQ_LOOP (5)
#define LF_FSK2a_PWM_LO_FREQ_TOP_VALUE (10)
#define LF_FSK2a_PWM_HI_FREQ_LOOP (6)
@@ -49,7 +47,7 @@ void hidprox_decoder_start(hidprox_codec *d, uint8_t format_hint) {
hidprox_codec *hidprox_codec_alloc(void) {
hidprox_codec *d = malloc(sizeof(hidprox_codec));
d->card = NULL;
d->modem = fsk_alloc(FSK_BITRATE_HID);
d->modem = fsk_alloc();
return d;
}
@@ -1,413 +0,0 @@
#include "ioprox.h"
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include "fskdemod.h"
#include "t55xx.h"
#include "tag_base_type.h"
#define IOPROX_SOF (0x1d)
#define IOPROX_T55XX_BLOCK_COUNT (3)
#define DEMOD_BUFFER_SIZE (32)
#define IOPROX_RAW_SIZE (96)
// NOTE: These LF_FSK2a_* defines are intentionally local to this .c file
// to allow per-protocol timing tuning (do not move to a shared header).
#define LF_FSK2a_PWM_LO_FREQ_LOOP (6)
#define LF_FSK2a_PWM_LO_FREQ_TOP_VALUE (11)
#define LF_FSK2a_PWM_HI_FREQ_LOOP (8)
#define LF_FSK2a_PWM_HI_FREQ_TOP_VALUE (8)
static nrf_pwm_values_wave_form_t m_ioprox_pwm_seq_vals[IOPROX_RAW_SIZE * 6] = {};
nrf_pwm_sequence_t m_ioprox_pwm_seq = {
.values.p_wave_form = m_ioprox_pwm_seq_vals,
.length = NRF_PWM_VALUES_LENGTH(m_ioprox_pwm_seq_vals),
.repeats = 0,
.end_delay = 0,
};
void ioprox_reset_bits(ioprox_codec_t *d) {
d->bit_len = 0;
}
static inline void push_bit(ioprox_codec_t *d, uint8_t bit)
{
if (d->bit_len < IOPROX_MAX_BITS) {
d->bits[d->bit_len++] = bit;
return;
}
// Buffer full: drop the oldest bit and append the new one
memmove(d->bits, d->bits + 1, IOPROX_MAX_BITS - 1);
d->bits[IOPROX_MAX_BITS - 1] = bit;
}
static inline uint8_t get_bit_inv(const uint8_t *bits, uint16_t pos, bool inv)
{
uint8_t b = bits[pos] & 1u;
return inv ? (uint8_t)(b ^ 1u) : b;
}
// Converts a bit array to a 32-bit big-endian integer.
static inline uint32_t bytebits_to_byte(const uint8_t *bits, uint16_t len)
{
uint32_t val = 0;
for (uint16_t i = 0; i < len; i++) {
val = (val << 1) | (bits[i] & 1u);
}
return val;
}
// Reads 8 bits MSB-first from bits[start_pos], optionally inverting each bit.
static inline uint8_t bytebits_to_u8_msb_inv(const uint8_t *bits, uint16_t start_pos, bool inv)
{
uint8_t v = 0;
for (int i = 0; i < 8; i++) {
v = (uint8_t)((v << 1) | get_bit_inv(bits, (uint16_t)(start_pos + i), inv));
}
return v;
}
// Unpacks a raw 8-byte card frame into the codec bit buffer (MSB-first).
bool ioprox_raw8_to_bits(const uint8_t *raw8, ioprox_codec_t *d) {
d->bit_len = 0;
for (int i = 0; i < 8; i++) {
uint8_t byte = raw8[i];
for (int j = 0; j < 8; j++) {
d->bits[d->bit_len++] = (byte >> (7 - j)) & 0x01;
}
}
return true;
}
// ioProx checksum: 0xFF - (sum(b1..b5) & 0xFF)
static inline uint8_t ioprox_checksum5(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4, uint8_t b5)
{
uint16_t sum = (uint16_t)b1 + b2 + b3 + b4 + b5;
return (uint8_t)(0xFFu - (uint8_t)(sum & 0xFFu));
}
// Returns true if 10 starting bits form a valid ioProx preamble.
// Preamble is 9 zeros followed by 1 one (inverted when inv=true).
static bool preamble_match(const uint8_t *d, uint16_t off, bool inv)
{
for (int k = 0; k < 9; k++) {
if ((d[off + k] & 1u) != (inv ? 1u : 0u)) return false;
}
return ((d[off + 9] & 1u) == (inv ? 0u : 1u));
}
// Decodes a 64-bit ioProx frame starting at bit index idx into d->data.
// Returns true only if the checksum passes.
//
// Frame layout (8+1 bit-framing, 7 groups):
// b0: SOF byte (0x00)
// b1: 0xF0 (fixed header)
// b2: facility code
// b3: version
// b4: card number high byte
// b5: card number low byte
// b6: checksum (0xFF - sum(b1..b5))
//
// Output d->data layout (16 bytes):
// [0] version
// [1] facility code
// [2-3] card number (big-endian)
// [4-11] raw8 frame bytes (for debugging and storage)
// [12-15] reserved (0x00)
static bool decode_and_pack(ioprox_codec_t *d, uint16_t idx, bool inv)
{
uint8_t b1, b2, b3, b4, b5, b6;
uint16_t number;
uint32_t raw_block1;
uint32_t raw_block2;
b1 = bytebits_to_u8_msb_inv(d->bits, (uint16_t)(idx + 9), inv);
b2 = bytebits_to_u8_msb_inv(d->bits, (uint16_t)(idx + 18), inv);
b3 = bytebits_to_u8_msb_inv(d->bits, (uint16_t)(idx + 27), inv);
b4 = bytebits_to_u8_msb_inv(d->bits, (uint16_t)(idx + 36), inv);
b5 = bytebits_to_u8_msb_inv(d->bits, (uint16_t)(idx + 45), inv);
b6 = bytebits_to_u8_msb_inv(d->bits, (uint16_t)(idx + 54), inv);
if (ioprox_checksum5(b1, b2, b3, b4, b5) != b6) {
return false;
}
number = (uint16_t)(((uint16_t)b4 << 8) | b5);
raw_block1 = bytebits_to_byte(d->bits + idx, 32);
raw_block2 = bytebits_to_byte(d->bits + idx + 32, 32);
memset(d->data, 0, sizeof(d->data));
d->data[0] = b3; // version
d->data[1] = b2; // facility code
d->data[2] = (uint8_t)(number >> 8); // card number high byte
d->data[3] = (uint8_t)(number & 0xFF); // card number low byte
// Raw frame bytes (human-readable, stable across re-reads)
d->data[4] = (uint8_t)(raw_block1 >> 24);
d->data[5] = (uint8_t)(raw_block1 >> 16);
d->data[6] = (uint8_t)(raw_block1 >> 8);
d->data[7] = (uint8_t)(raw_block1);
d->data[8] = (uint8_t)(raw_block2 >> 24);
d->data[9] = (uint8_t)(raw_block2 >> 16);
d->data[10] = (uint8_t)(raw_block2 >> 8);
d->data[11] = (uint8_t)(raw_block2);
// d->data[12..15] zeroed by memset above
return true;
}
// Decodes a raw 8-byte ioProx card frame into the 16-byte output buffer.
// Returns true if the frame checksum is valid.
bool ioprox_decode_raw_to_data(const uint8_t *raw8, uint8_t *output) {
ioprox_codec_t codec;
memset(&codec, 0, sizeof(codec));
ioprox_raw8_to_bits(raw8, &codec);
if (decode_and_pack(&codec, 0, false)) {
memcpy(output, codec.data, 16);
return true;
}
return false;
}
// Writes 8 bits of v MSB-first into bits[] starting at position pos.
static void write_bits_msb(uint8_t *bits, uint16_t pos, uint8_t v)
{
for (uint8_t i = 0; i < 8; i++) {
bits[pos + i] = (v >> (7 - i)) & 1;
}
}
// Encodes ioProx card parameters into the 16-byte output buffer.
// The encoded frame uses 8+1 bit framing (8 data bits + 1 separator per group).
// Returns false if output pointer is NULL.
bool ioprox_encode_params_to_data(uint8_t version, uint8_t facility, uint16_t number, uint8_t *output)
{
if (!output) return false;
uint8_t b0 = 0x00;
uint8_t b1 = 0xF0;
uint8_t b2 = facility;
uint8_t b3 = version;
uint8_t b4 = (uint8_t)(number >> 8);
uint8_t b5 = (uint8_t)(number & 0xFF);
uint8_t b6 = ioprox_checksum5(b1, b2, b3, b4, b5);
uint8_t bits[64] = {0};
// Pack 7 data bytes using 8+1 framing (data bits + separator)
write_bits_msb(bits, 0, b0); bits[ 8] = 0;
write_bits_msb(bits, 9, b1); bits[17] = 1;
write_bits_msb(bits, 18, b2); bits[26] = 1;
write_bits_msb(bits, 27, b3); bits[35] = 1;
write_bits_msb(bits, 36, b4); bits[44] = 1;
write_bits_msb(bits, 45, b5); bits[53] = 1;
write_bits_msb(bits, 54, b6);
bits[62] = 1;
bits[63] = 1;
// Decoded fields
output[0] = b3; // version
output[1] = b2; // facility code
output[2] = b4; // card number high byte
output[3] = b5; // card number low byte
// Raw bitstream packed into bytes [4..11]
memset(output + 4, 0, 8);
for (int i = 0; i < 64; i++) {
if (bits[i]) {
output[4 + (i / 8)] |= (uint8_t)(1u << (7 - (i % 8)));
}
}
return true;
}
// Scans the tail of the bit buffer for a valid ioProx frame.
// To keep CPU load low, only the last ~5 frames (320 bits) are scanned.
// Returns true if a valid frame (checksum OK) was decoded into d->data.
static bool scan_tail(ioprox_codec_t *d)
{
// Need at least 128 bits to verify two consecutive 64-bit frames
if (d->bit_len < 128) return false;
// Limit scan to the last 320 bits (~5 frames) for performance
uint16_t start_from = (d->bit_len > 320) ? (uint16_t)(d->bit_len - 320) : 0;
for (uint16_t i = start_from; (uint16_t)(i + 64 + 10) <= d->bit_len; i++) {
for (int inv_i = 0; inv_i <= 1; inv_i++) {
bool inv = (inv_i == 1);
// Require two consecutive preambles 64 bits apart (Proxmark-style sync check)
if (!preamble_match(d->bits, i, inv)) continue;
if (!preamble_match(d->bits, i + 64, inv)) continue;
// Try small phase offsets to tolerate minor bit-alignment jitter
for (int8_t phase = -2; phase <= 2; phase++) {
int32_t idx_i = (int32_t)i + (int32_t)phase;
if (idx_i < 0) continue;
if (idx_i + 64 > d->bit_len) continue;
uint16_t idx = (uint16_t)idx_i;
// Validate separator/stop bits within the frame
int bad = 0;
if (((d->bits[idx + 8] & 1u) ^ inv) != 0) bad++;
if (((d->bits[idx + 17] & 1u) ^ inv) != 1) bad++;
if (((d->bits[idx + 26] & 1u) ^ inv) != 1) bad++;
if (((d->bits[idx + 35] & 1u) ^ inv) != 1) bad++;
if (((d->bits[idx + 44] & 1u) ^ inv) != 1) bad++;
if (((d->bits[idx + 53] & 1u) ^ inv) != 1) bad++;
if (((d->bits[idx + 62] & 1u) ^ inv) != 1) bad++;
if (((d->bits[idx + 63] & 1u) ^ inv) != 1) bad++;
// Tolerate up to 2 bad bits to handle noise
if (bad > 2) continue;
if (decode_and_pack(d, idx, inv)) {
return true;
}
}
}
}
return false;
}
// --- Protocol callbacks ---
static void *ioprox_codec_alloc(void)
{
ioprox_codec_t *d = (ioprox_codec_t *)malloc(sizeof(ioprox_codec_t));
if (!d) return NULL;
memset(d, 0, sizeof(*d));
d->modem = fsk_alloc(FSK_BITRATE_IOPROX);
return d;
}
static void ioprox_codec_free(void *codec)
{
ioprox_codec_t *d = (ioprox_codec_t *)codec;
if (!d) return;
if (d->modem) {
fsk_free(d->modem);
d->modem = NULL;
}
free(d);
}
static uint8_t *ioprox_get_data(void *codec)
{
ioprox_codec_t *d = (ioprox_codec_t *)codec;
return d->data;
}
static void ioprox_decoder_start(void *codec, uint8_t format_hint)
{
(void)format_hint;
ioprox_codec_t *d = (ioprox_codec_t *)codec;
d->bit_len = 0;
memset(d->bits, 0, sizeof(d->bits));
memset(d->data, 0, sizeof(d->data));
}
static bool ioprox_decoder_feed(void *codec, uint16_t val)
{
ioprox_codec_t *d = (ioprox_codec_t *)codec;
if (!d || !d->modem) return false;
bool bit = false;
if (!fsk_feed(d->modem, val, &bit)) {
return false;
}
push_bit(d, (uint8_t)(bit ? 1u : 0u));
if (d->bit_len >= 128) {
if (scan_tail(d)) {
ioprox_reset_bits(d);
return true;
}
// Discard oldest 64 bits when buffer grows too large to prevent stale noise buildup
if (d->bit_len >= 512) {
memmove(d->bits, d->bits + 64, 512 - 64);
d->bit_len -= 64;
}
}
return false;
}
static inline void ioprox_emit_bit(int *k, bool bit)
{
if (!bit) {
for (int j = 0; j < LF_FSK2a_PWM_HI_FREQ_LOOP; j++) {
m_ioprox_pwm_seq_vals[*k].channel_0 = LF_FSK2a_PWM_HI_FREQ_TOP_VALUE / 2;
m_ioprox_pwm_seq_vals[*k].counter_top = LF_FSK2a_PWM_HI_FREQ_TOP_VALUE;
(*k)++;
}
} else {
for (int j = 0; j < LF_FSK2a_PWM_LO_FREQ_LOOP; j++) {
m_ioprox_pwm_seq_vals[*k].channel_0 = LF_FSK2a_PWM_LO_FREQ_TOP_VALUE / 2;
m_ioprox_pwm_seq_vals[*k].counter_top = LF_FSK2a_PWM_LO_FREQ_TOP_VALUE;
(*k)++;
}
}
}
// FSK2a modulator: converts the 8 raw card bytes into a PWM sequence for LF transmission.
const nrf_pwm_sequence_t *ioprox_modulator(ioprox_codec_t *d, uint8_t *buf)
{
(void)d;
// Raw card data starts at buf[4] (bytes 0-3 are decoded fields)
uint8_t *raw = &buf[4];
int k = 0;
// Emit 64 bits MSB-first
for (int bi = 0; bi < 8; bi++) {
uint8_t v = raw[bi];
for (int bit = 7; bit >= 0; bit--) {
ioprox_emit_bit(&k, ((v >> bit) & 1u) != 0);
}
}
m_ioprox_pwm_seq.length = (uint16_t)(k * 4);
return &m_ioprox_pwm_seq;
}
const protocol ioprox = {
.tag_type = TAG_TYPE_IOPROX,
.data_size = IOPROX_DATA_SIZE,
.alloc = (codec_alloc)ioprox_codec_alloc,
.free = (codec_free)ioprox_codec_free,
.get_data = (codec_get_data)ioprox_get_data,
.modulator = (modulator)ioprox_modulator,
.decoder = {
.start = (decoder_start)ioprox_decoder_start,
.feed = (decoder_feed)ioprox_decoder_feed,
}
};
// Packs the raw card bitstream into T5577 blocks for writing.
// Block 0: T5577 config word for ioProx (FSK2a, RF/64, max block 2)
// Block 1: first 4 raw bytes
// Block 2: last 4 raw bytes
uint8_t ioprox_t55xx_writer(uint8_t *buf, uint32_t *blks) {
uint8_t *raw = &buf[4];
blks[0] = T5577_IOPROX_CONFIG;
blks[1] = ((uint32_t)raw[0] << 24) |
((uint32_t)raw[1] << 16) |
((uint32_t)raw[2] << 8) |
((uint32_t)raw[3]);
blks[2] = ((uint32_t)raw[4] << 24) |
((uint32_t)raw[5] << 16) |
((uint32_t)raw[6] << 8) |
((uint32_t)raw[7]);
return IOPROX_T55XX_BLOCK_COUNT;
}
@@ -1,30 +0,0 @@
#pragma once
#include <stdint.h>
#include "protocols.h"
#include "fskdemod.h"
// 16-byte payload (data frame for emulation and CLI):
// 0: Version
// 1: Facility Code
// 2-3: Card Number (Big-endian, uint16)
// 4-11: Raw8 (8 bytes of raw card data)
// 12-15: Reserved (0x00000000)
#define IOPROX_DATA_SIZE 16
#define IOPROX_MAX_BITS 256
typedef struct {
fsk_t *modem;
uint8_t bits[IOPROX_MAX_BITS];
uint16_t bit_len;
uint8_t data[IOPROX_DATA_SIZE];
} ioprox_codec_t;
extern const protocol ioprox;
uint8_t ioprox_t55xx_writer(uint8_t *buf, uint32_t *blks);
bool ioprox_decode_raw_to_data(const uint8_t *raw8, uint8_t *output);
bool ioprox_encode_params_to_data(uint8_t ver, uint8_t fc, uint16_t cn, uint8_t *out);
@@ -1,400 +0,0 @@
#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,
},
};
@@ -1,6 +0,0 @@
#pragma once
#include "protocols.h"
extern const protocol pac;
uint8_t pac_t55xx_writer(uint8_t *data, uint32_t *blks);

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