mirror of
https://github.com/RfidResearchGroup/ChameleonUltra.git
synced 2026-05-12 11:22:59 -07:00
Improve PAC/Stanley NRZ reader reliability
Three fixes that together bring rapid-fire read reliability from ~20% to 100%: - Add MIN_SPIKE_CAP floor (8000) to prevent spike_cap from clipping NRZ high when prescan correctly captures NRZ low. Without this, spike_cap = raw_min*3 ≈ 2820 collapses the signal range. - Reorder carrier-before-SAADC in pac_read(): start the 125kHz field and wait 10ms before enabling ADC sampling, so prescan calibration sees real NRZ signal levels rather than T55XX power-on-reset noise. - Add auto-recalibration: if no valid frame is found after 20480 Phase 3 samples (~164ms, ~5 frame periods), reset the decoder to Phase 1 and re-calibrate from fresh samples. This gives ~3 calibration attempts per 500ms scan window instead of just one. Tested with Proxmark3 sim (15 consecutive rapid-fire reads, 100%) and T55XX tag (write-read roundtrip + 15x rapid-fire, 100%).
This commit is contained in:
@@ -30,11 +30,26 @@
|
||||
#define PAC_PAYLOAD_BYTES 12 // STX + '2' + '0' + 8 card ID + XOR checksum
|
||||
#define PAC_STX 0x02
|
||||
|
||||
// ADC demodulation parameters
|
||||
#define PAC_PRESCAN_SAMPLES 128 // Samples for raw min/max detection (spike cap)
|
||||
#define PAC_WARMUP_SAMPLES 600 // Samples for threshold calibration (~5ms)
|
||||
#define PAC_SPIKE_MULT 3 // Samples > raw_min * SPIKE_MULT are spike transients
|
||||
#define PAC_THRESH_FUZZ 75 // PM3-style: threshold at 75% of signal range
|
||||
// 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)
|
||||
@@ -44,10 +59,10 @@ typedef struct {
|
||||
uint16_t bit_count; // total bits shifted in (capped at PAC_FRAME_BITS)
|
||||
uint8_t card_id[PAC_DATA_SIZE];
|
||||
|
||||
// ADC → NRZ demodulation state (per-sample threshold with dead zone)
|
||||
// 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 detection)
|
||||
int32_t spike_cap; // clip level: raw values above this are transients
|
||||
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
|
||||
@@ -55,6 +70,7 @@ typedef struct {
|
||||
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.
|
||||
@@ -193,21 +209,21 @@ static void pac_decoder_start(pac_codec *d, uint8_t format) {
|
||||
}
|
||||
|
||||
// Feed a raw ADC sample (one per carrier cycle at 125kHz).
|
||||
// PM3-inspired approach: clip spikes, then per-sample threshold with dead zone.
|
||||
// No moving average — edges are detected directly on clipped samples, giving
|
||||
// much tighter timing than the ~16 sample group delay of a moving average.
|
||||
// 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 characterize data levels.
|
||||
// The NRZ data levels are the lowest values; spikes are 5-15x higher.
|
||||
// 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;
|
||||
}
|
||||
@@ -224,8 +240,6 @@ static bool pac_decoder_feed(pac_codec *d, uint16_t raw_sample) {
|
||||
if (sample > d->clip_max) d->clip_max = sample;
|
||||
if (sample < d->clip_min) d->clip_min = sample;
|
||||
if (warmup_samples == PAC_WARMUP_SAMPLES) {
|
||||
// PM3-style thresholds: 75% fuzz creates a dead zone between levels.
|
||||
// high = 75% of peak, low = bottom + 25% of range.
|
||||
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;
|
||||
@@ -233,9 +247,14 @@ static bool pac_decoder_feed(pac_codec *d, uint16_t raw_sample) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 3: Per-sample threshold with dead zone (PM3 nrzRawDemod style).
|
||||
// Values above thresh_high → 1, below thresh_low → 0, between → keep previous.
|
||||
// No moving average means near-zero edge timing jitter.
|
||||
// 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;
|
||||
@@ -244,7 +263,7 @@ static bool pac_decoder_feed(pac_codec *d, uint16_t raw_sample) {
|
||||
} else if (sample <= d->thresh_low) {
|
||||
new_state = false;
|
||||
} else {
|
||||
return false; // Dead zone: no state change
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!d->has_signal) {
|
||||
|
||||
@@ -43,9 +43,14 @@ bool pac_read(uint8_t *data, uint32_t timeout_ms) {
|
||||
void *codec = pac.alloc();
|
||||
pac.decoder.start(codec, 0);
|
||||
|
||||
// Start carrier first, then wait for T55XX POR (~5ms) before
|
||||
// enabling SAADC. This ensures the prescan calibration phase
|
||||
// sees real NRZ signal levels rather than power-up noise.
|
||||
start_lf_125khz_radio();
|
||||
bsp_delay_ms(10);
|
||||
|
||||
cb_init(&cb, PAC_BUFFER_SIZE, sizeof(uint16_t));
|
||||
init_pac_hw();
|
||||
start_lf_125khz_radio();
|
||||
|
||||
bool ok = false;
|
||||
autotimer *p_at = bsp_obtain_timer(0);
|
||||
|
||||
Reference in New Issue
Block a user