diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index d140b40..611fdb7 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -24,7 +24,7 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board, _in_recv_mode(false), _tx_active(false), _last_rssi(0), _last_snr(0), _rx_head(0), _rx_tail(0), - _noise_floor(DEFAULT_NOISE_FLOOR), _calibration_threshold(0), + _noise_floor(DEFAULT_NOISE_FLOOR), _calibration_threshold(0), _ema_unguarded(0), _rx_duty_cycle_enabled(IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)), _rx_boost_enabled(true), _config_cached(false), @@ -517,22 +517,42 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold) int16_t rssi = hwGetCurrentRSSI(); - /* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly. */ + /* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly + * and start warmup window where all samples are accepted. */ if (_noise_floor == DEFAULT_NOISE_FLOOR) { _noise_floor = rssi; if (_noise_floor < -120) _noise_floor = -120; if (_noise_floor > -50) _noise_floor = -50; + _ema_unguarded = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 ticks */ LOG_DBG("noise_floor_cal: seed=%d", _noise_floor); return; } - /* Reject samples above floor + threshold (interference / signal). */ - if (rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) { + /* Threshold filter with periodic unguarded samples. + * During warmup (after seed/reset), all samples are accepted so the + * EMA converges quickly. After warmup, every Nth tick (N = EMA window) + * one sample bypasses the filter so the floor can track sustained + * upward shifts (new interference source, antenna change, etc.). + * The EMA's 1/8 weight naturally dampens isolated spikes. */ + if (_ema_unguarded > 0) { + _ema_unguarded--; + } else if (rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) { return; } + /* Reload: next unguarded sample in N ticks. */ + if (_ema_unguarded == 0) { + _ema_unguarded = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 */ + } - /* EMA: floor += (sample - floor) >> SHIFT. Integer-only. */ - _noise_floor += (rssi - _noise_floor) >> NOISE_FLOOR_EMA_SHIFT; + /* EMA: floor += round_nearest((sample - floor) / 8). + * Plain >> has downward bias (-1>>3 == -1 but +1>>3 == 0). + * Plain / has a ±7 dead zone (small drifts ignored). + * Round-to-nearest: add half the divisor before dividing, + * with sign-aware bias so both directions are symmetric. */ + int diff = rssi - _noise_floor; + int half = (1 << NOISE_FLOOR_EMA_SHIFT) / 2; /* 4 */ + int step = (diff + (diff > 0 ? half : -half)) / (1 << NOISE_FLOOR_EMA_SHIFT); + _noise_floor += step; if (_noise_floor < -120) _noise_floor = -120; if (_noise_floor > -50) _noise_floor = -50; @@ -549,10 +569,11 @@ void LoRaRadioBase::resetAGC() hwResetAGC(); - /* Reset noise floor sampling so it reconverges from scratch. + /* Reset noise floor so it reconverges from scratch (seed + warmup). * Without this, a stuck _noise_floor of -120 makes the sampling threshold * too low to accept normal samples, self-reinforcing the stuck value. */ _noise_floor = DEFAULT_NOISE_FLOOR; + _ema_unguarded = 0; } bool LoRaRadioBase::isReceiving() diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index feaa247..fc3532a 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -140,6 +140,7 @@ protected: /* Noise floor calibration state */ int _noise_floor; int _calibration_threshold; + uint8_t _ema_unguarded; /* ticks until next unfiltered sample */ /* Power saving */ bool _rx_duty_cycle_enabled; diff --git a/zephcore/tools/formatter/src/main.c b/zephcore/tools/formatter/src/main.c index 978f65e..b9bdcc9 100644 --- a/zephcore/tools/formatter/src/main.c +++ b/zephcore/tools/formatter/src/main.c @@ -4,13 +4,12 @@ * Erases all filesystem partitions and reboots into Adafruit UF2 DFU mode * for clean firmware installation. * - * nRF52 builds use hardcoded flash addresses (identical across all boards) - * so a single UF2 works on every board with the same SoftDevice version. + * All platforms use DTS FIXED_PARTITION_EXISTS guards — no hardcoded addresses. + * nRF52 boards sharing the same SoftDevice version use the same partition dtsi, + * so a single UF2 works on every board with that SoftDevice version. * QSPI is compile-time conditional — included only when building for a * QSPI-capable board target (pin config is board-specific). * - * Non-nRF52 builds use DTS FIXED_PARTITION_EXISTS guards as before. - * * Build: * SD v7: west build -b t1000_e/nrf52840 zephcore/tools/formatter --pristine * SD v6: west build -b rak4631/nrf52840 zephcore/tools/formatter --pristine @@ -36,20 +35,6 @@ /* Adafruit UF2 bootloader magic — enter mass storage DFU mode */ #define BOOTLOADER_DFU_UF2_MAGIC 0x57 -/* - * nRF52840 Arduino MeshCore partition addresses. - * These are IDENTICAL across all nRF52 boards (SD v6 and v7): - * ExtraFS @ 0xD4000 (100KB) — contacts, channels, blobs - * InternalFS @ 0xED000 (28KB) — prefs, identity, BLE settings - * - * Only the SoftDevice/app boundary differs (v6=0x26000, v7=0x27000), - * which doesn't affect the formatter since we don't touch app flash. - */ -#define NRF52_EXTRAFS_OFF 0xD4000 -#define NRF52_EXTRAFS_SIZE 0x19000 /* 100KB */ -#define NRF52_INTERNALFS_OFF 0xED000 -#define NRF52_INTERNALFS_SIZE 0x7000 /* 28KB */ - /* ── LED feedback (optional — may not match actual board) ──── */ #if DT_NODE_EXISTS(DT_ALIAS(led0)) && !IS_NRF52 @@ -68,35 +53,8 @@ static void led_on(void) {} static void led_off(void) {} #endif -/* ── Flash erase helpers ───────────────────────────────────── */ +/* ── Flash erase helper ──────────────────────────────────────── */ -#if IS_NRF52 -/** - * Erase a region of internal flash by absolute address. - * Works on any nRF52840 board regardless of DTS. - */ -static int erase_region(const struct device *dev, off_t offset, size_t size, - const char *name) -{ - printk(" %s: erasing 0x%lx - 0x%lx (%u KB)...", - name, (unsigned long)offset, - (unsigned long)(offset + size), - (unsigned)(size / 1024)); - - int rc = flash_erase(dev, offset, size); - if (rc) { - printk(" FAILED (rc %d)\n", rc); - } else { - printk(" OK\n"); - } - return rc; -} -#endif /* IS_NRF52 */ - -#if !IS_NRF52 -/** - * Erase a DTS partition by flash_area ID (non-nRF52 boards). - */ static int erase_partition(uint8_t id, const char *name) { const struct flash_area *fa; @@ -124,7 +82,6 @@ static int erase_partition(uint8_t id, const char *name) flash_area_close(fa); return rc; } -#endif /* !IS_NRF52 */ /* ── Main ────────────────────────────────────────────────────── */ @@ -142,77 +99,29 @@ int main(void) led_init(); led_on(); -#if IS_NRF52 - /* - * nRF52 universal path: hardcoded addresses, no DTS dependency. - * This binary works on ANY nRF52840 board with the same SoftDevice - * version (determines UF2 load address, not erase targets). - */ - const struct device *flash_dev = DEVICE_DT_GET(DT_NODELABEL(flash_controller)); - - if (!device_is_ready(flash_dev)) { - printk(" ERROR: flash device not ready!\n"); + /* ── LittleFS partition (all platforms) ── */ +#if FIXED_PARTITION_EXISTS(lfs_partition) + if (erase_partition(FIXED_PARTITION_ID(lfs_partition), "LittleFS (/lfs)")) { errors++; - } else { - if (erase_region(flash_dev, NRF52_EXTRAFS_OFF, - NRF52_EXTRAFS_SIZE, "ExtraFS (contacts/channels)")) { - errors++; - } - if (erase_region(flash_dev, NRF52_INTERNALFS_OFF, - NRF52_INTERNALFS_SIZE, "InternalFS (prefs/identity)")) { - errors++; - } } - - /* QSPI: try to erase if present via flash_area (compiled in by DTS). - * On boards without QSPI, FIXED_PARTITION_EXISTS is false at compile - * time so this block is excluded — no runtime probe needed. */ -#if FIXED_PARTITION_EXISTS(qspi_storage_partition) - { - const struct flash_area *fa; - int rc = flash_area_open(FIXED_PARTITION_ID(qspi_storage_partition), &fa); - if (rc == 0) { - printk(" QSPI: erasing 0x%lx (%u KB, may take a while)...", - (unsigned long)fa->fa_off, - (unsigned)(fa->fa_size / 1024)); - rc = flash_area_erase(fa, 0, fa->fa_size); - printk(rc ? " FAILED (rc %d)\n" : " OK\n", rc); - if (rc) errors++; - flash_area_close(fa); - } else { - printk(" QSPI: not accessible (rc %d) — skipped\n", rc); - } - } -#else - printk(" QSPI: not present in build target — skipped\n"); #endif -#else /* !IS_NRF52 */ - /* - * Non-nRF52 path: use DTS partitions (board-specific builds). - */ + /* ── NVS storage partition (ESP32 etc.) ── */ #if FIXED_PARTITION_EXISTS(storage_partition) if (erase_partition(FIXED_PARTITION_ID(storage_partition), "NVS (storage)")) { errors++; } #endif -#if FIXED_PARTITION_EXISTS(lfs_partition) - if (erase_partition(FIXED_PARTITION_ID(lfs_partition), "LittleFS (lfs)")) { - errors++; - } -#endif - + /* ── QSPI external flash (boards with external QSPI) ── */ #if FIXED_PARTITION_EXISTS(qspi_storage_partition) - if (erase_partition(FIXED_PARTITION_ID(qspi_storage_partition), "QSPI external")) { + if (erase_partition(FIXED_PARTITION_ID(qspi_storage_partition), "QSPI external (/ext)")) { errors++; } #else - printk(" QSPI: not present on this board — skipped\n"); + printk(" QSPI: not present in build target — skipped\n"); #endif -#endif /* IS_NRF52 */ - led_off(); /* ── Summary ── */