diff --git a/src/main.cpp b/src/main.cpp index 273b420..60facfa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -60,6 +60,19 @@ static uint32_t lastSweep2Ms = 0; static float sweepFreq1 = SWEEP_1_CENTER_MHZ; static float sweepFreq2 = SWEEP_2_CENTER_MHZ; +// Fast Frequency Hopping / VCO Calibration Caching +// By caching the CC1101 PLL calibration registers for each sweep frequency, +// we bypass the 720µs auto-calibration during the sweep, reducing hop dead-time +// from ~750µs down to ~40µs (SPI transaction time). This increases jamming efficiency +// from ~76% to >98% at a 3ms dwell time. +struct SweepStepCache { + float freqMhz; + uint8_t freqRegs[3]; // FREQ2, FREQ1, FREQ0 + uint8_t fscalRegs[3]; // FSCAL3, FSCAL2, FSCAL1 +}; +static SweepStepCache sweepTable1[100]; +static SweepStepCache sweepTable2[100]; + // Runtime-adjustable sweep parameters (loaded from NVS) static uint32_t sweepDwellMs = SWEEP_DWELL_MS; static uint8_t sweep1Steps = SWEEP_1_STEPS; @@ -345,6 +358,76 @@ static String capAnalyze() { return String(buf); } +// ─── Raw SPI Helpers for Fast Sweep ──────────────────────────────────────────── +static void spiStrobe(uint8_t csPin, uint8_t strobe) { + spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); + digitalWrite(csPin, LOW); + spi.transfer(strobe); + digitalWrite(csPin, HIGH); + spi.endTransaction(); +} + +static void spiWriteReg(uint8_t csPin, uint8_t reg, uint8_t val) { + spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); + digitalWrite(csPin, LOW); + spi.transfer(reg); + spi.transfer(val); + digitalWrite(csPin, HIGH); + spi.endTransaction(); +} + +static uint8_t spiReadReg(uint8_t csPin, uint8_t reg) { + spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); + digitalWrite(csPin, LOW); + spi.transfer(reg | 0x80); // Read bit + uint8_t val = spi.transfer(0x00); + digitalWrite(csPin, HIGH); + spi.endTransaction(); + return val; +} + +static uint8_t spiReadStatusReg(uint8_t csPin, uint8_t reg) { + spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); + digitalWrite(csPin, LOW); + spi.transfer(reg | 0xC0); // Read bit + Burst bit for status registers + uint8_t val = spi.transfer(0x00); + digitalWrite(csPin, HIGH); + spi.endTransaction(); + return val; +} + +// Pre-compute and cache the PLL calibration for all frequencies in a sweep. +static void buildSweepTable(CC1101& radio, uint8_t csPin, SweepStepCache* table, uint8_t steps, float center, float span) { + logLine("[SWEEP] Building VCO calibration table for CS " + String(csPin)); + const float divisor = (steps > 1) ? (float)(steps - 1) : 1.0f; + + for (uint8_t i = 0; i < steps; i++) { + float freq = center - (span / 2.0f) + (span / divisor) * (float)i; + table[i].freqMhz = freq; + + radio.standby(); + radio.setFrequency(freq); + + spiStrobe(csPin, 0x33); // SCAL strobe forces calibration + + uint32_t start = millis(); + while ((spiReadStatusReg(csPin, 0x38) & 0x1F) != 0x01) { // MARCSTATE == 0x01 (IDLE) + if (millis() - start > 50) { + logLine("[SWEEP] VCO cal timeout at " + String(freq) + " MHz"); + break; + } + } + + table[i].freqRegs[0] = spiReadReg(csPin, 0x0D); // FREQ2 + table[i].freqRegs[1] = spiReadReg(csPin, 0x0E); // FREQ1 + table[i].freqRegs[2] = spiReadReg(csPin, 0x0F); // FREQ0 + + table[i].fscalRegs[0] = spiReadReg(csPin, 0x23); // FSCAL3 + table[i].fscalRegs[1] = spiReadReg(csPin, 0x24); // FSCAL2 + table[i].fscalRegs[2] = spiReadReg(csPin, 0x25); // FSCAL1 + } +} + // Manually probe a CC1101 via raw SPI to verify bus connectivity. // Reads the VERSION register (0xF1 = burst read of reg 0x31). // Returns the raw byte, or 0xFF if bus appears dead. @@ -402,6 +485,7 @@ static void startJamming() { logLine("[R1] init failed: " + String(st1)); } else { radio1Status = 1; + buildSweepTable(radio1, CC1101_1_CS, sweepTable1, sweep1Steps, SWEEP_1_CENTER_MHZ, sweep1SpanMhz); } // Initialize radio 2 with retries @@ -416,6 +500,7 @@ static void startJamming() { logLine("[R2] init failed: " + String(st2)); } else { radio2Status = 1; + buildSweepTable(radio2, CC1101_2_CS, sweepTable2, sweep2Steps, SWEEP_2_CENTER_MHZ, sweep2SpanMhz); } // Start both radios transmitting simultaneously @@ -1501,6 +1586,15 @@ static void handleSweepSettings() { preferences.putFloat("sweep1Span", sweep1SpanMhz); preferences.putFloat("sweep2Span", sweep2SpanMhz); + if (radio1Status >= 1) { + sweepStep1 = 0; + buildSweepTable(radio1, CC1101_1_CS, sweepTable1, sweep1Steps, SWEEP_1_CENTER_MHZ, sweep1SpanMhz); + } + if (radio2Status >= 1) { + sweepStep2 = 0; + buildSweepTable(radio2, CC1101_2_CS, sweepTable2, sweep2Steps, SWEEP_2_CENTER_MHZ, sweep2SpanMhz); + } + logLine("[SWEEP] dwell=" + String(sweepDwellMs) + "ms steps=" + String(sweep1Steps) + "/" + String(sweep2Steps) + " span=" + String(sweep1SpanMhz,2) + "/" + String(sweep2SpanMhz,2) + "MHz"); @@ -1791,24 +1885,38 @@ void setup() { delay(800); // hold boot result on display briefly before switching to live pages } -// Advance one radio to the next sweep frequency. -static void tickSweep(CC1101& radio, uint8_t& step, uint8_t steps, - float center, float span, uint32_t& lastMs, float& curFreq, - uint32_t& hopCnt) { +// Advance one radio to the next sweep frequency using cached VCO calibration. +// Bypasses the ~720µs auto-calibration dead time on every hop. +static void tickSweepFast(uint8_t csPin, uint8_t& step, uint8_t steps, + SweepStepCache* table, uint32_t& lastMs, float& curFreq, + uint32_t& hopCnt) { const uint32_t now = millis(); if (now - lastMs < sweepDwellMs) return; lastMs = now; - const float divisor = (steps > 1) ? (float)(steps - 1) : 1.0f; - float freq = center - (span / 2.0f) + (span / divisor) * (float)step; - if (fabsf(freq - curFreq) > 0.001f) { - radio.standby(); - if (radio.setFrequency(freq) == RADIOLIB_ERR_NONE) { - radio.transmitDirectAsync(); - curFreq = freq; - hopCnt++; - } - } + // Jump to IDLE to safely change registers + spiStrobe(csPin, 0x36); // SIDLE + + // Write cached FREQ registers (0x0D, 0x0E, 0x0F) + spiWriteReg(csPin, 0x0D, table[step].freqRegs[0]); + spiWriteReg(csPin, 0x0E, table[step].freqRegs[1]); + spiWriteReg(csPin, 0x0F, table[step].freqRegs[2]); + + // Write cached FSCAL registers (0x23, 0x24, 0x25) + spiWriteReg(csPin, 0x23, table[step].fscalRegs[0]); + spiWriteReg(csPin, 0x24, table[step].fscalRegs[1]); + spiWriteReg(csPin, 0x25, table[step].fscalRegs[2]); + + // Disable auto-calibration before transmitting (MCSM0 register 0x18, bits 5:4 = 00) + // RadioLib defaults this to 0x18 (0001 1000) which is 01 (calibrate from IDLE to TX). + // We overwrite it to 0x08 (0000 1000) to never auto-calibrate. + spiWriteReg(csPin, 0x18, 0x08); + + // Jump straight to TX without auto-cal + spiStrobe(csPin, 0x35); // STX + + curFreq = table[step].freqMhz; + hopCnt++; step = (step + 1) % steps; } @@ -1890,9 +1998,9 @@ void loop() { // Frequency sweep — hop both radios across their bands while jamming if (jammingEnabled) { if (radio1Status == 2) - tickSweep(radio1, sweepStep1, sweep1Steps, SWEEP_1_CENTER_MHZ, sweep1SpanMhz, lastSweep1Ms, sweepFreq1, hopCount1); + tickSweepFast(CC1101_1_CS, sweepStep1, sweep1Steps, sweepTable1, lastSweep1Ms, sweepFreq1, hopCount1); if (radio2Status == 2) - tickSweep(radio2, sweepStep2, sweep2Steps, SWEEP_2_CENTER_MHZ, sweep2SpanMhz, lastSweep2Ms, sweepFreq2, hopCount2); + tickSweepFast(CC1101_2_CS, sweepStep2, sweep2Steps, sweepTable2, lastSweep2Ms, sweepFreq2, hopCount2); } // Handle serial input for debugging