guard noisefloor sampling and agcreset while in rx duty cycle

This commit is contained in:
liquidraver
2026-03-07 15:28:50 +01:00
parent 2584e237f6
commit a71da9ba70
6 changed files with 120 additions and 99 deletions
+91 -97
View File
@@ -505,119 +505,113 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold)
return;
}
/* When duty cycle is active the radio alternates between short RX
* windows and sleep. GetRssiInst sent during the sleep phase hangs
* the SPI bus (BUSY stuck high for the full 3 s timeout). Briefly
* switch to continuous RX for the sample, then restore duty cycle.
* Use do{}while(0) so the restore runs from every exit path. */
if (_rx_duty_cycle_enabled) {
hwSetRxDutyCycle(false);
k_sleep(K_MSEC(2)); /* let chip settle into continuous RX */
/* When duty cycle is active the chip alternates between short RX
* windows and sleep. GetRssiInst during the sleep phase hangs the
* SPI bus (BUSY stuck high for the full 3 s timeout). Check the
* BUSY pin directly (GPIO read, no SPI) — if the chip is sleeping,
* skip this cycle and try again next housekeeping tick. */
if (_rx_duty_cycle_enabled && hwIsChipBusy()) {
return;
}
do {
/* Skip if mid-receive — don't want signal energy in the floor. */
if (isReceiving()) {
break;
/* Skip if mid-receive — don't want signal energy in the floor. */
if (isReceiving()) {
return;
}
/* Random delay 0-500 ms before sampling. Breaks phase-lock with
* periodic interference that might be synchronized with our fixed
* 5-second housekeeping cadence. */
uint32_t jitter;
sys_rand_get(&jitter, sizeof(jitter));
k_sleep(K_MSEC(jitter % 500));
/* Re-check after the delay — a packet may have arrived. */
if (isReceiving()) {
return;
}
/* Median of multiple RSSI reads (~200 us). Rejects up to N/2-1
* outliers in either direction without the downward bias of min
* or the spike sensitivity of average. Insertion sort is fine
* for N=8 (28 comparisons worst case, all in registers). */
int16_t samples[NOISE_FLOOR_SAMPLES_PER_TICK];
for (int i = 0; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
samples[i] = hwGetCurrentRSSI();
}
/* Insertion sort — tiny array, branch-friendly on Cortex-M */
for (int i = 1; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
int16_t key = samples[i];
int j = i - 1;
while (j >= 0 && samples[j] > key) {
samples[j + 1] = samples[j];
j--;
}
samples[j + 1] = key;
}
int16_t rssi = (samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2 - 1] +
samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2]) / 2;
/* Random delay 0-500 ms before sampling. Breaks phase-lock with
* periodic interference that might be synchronized with our fixed
* 5-second housekeeping cadence. Skip when duty cycle is active:
* the chip is already out of its pattern and we want to minimise
* time spent in continuous RX. */
if (!_rx_duty_cycle_enabled) {
uint32_t jitter;
sys_rand_get(&jitter, sizeof(jitter));
k_sleep(K_MSEC(jitter % 500));
/* Re-check after the delay — a packet may have arrived. */
if (isReceiving()) {
break;
}
}
/* Median of multiple RSSI reads (~200 us). Rejects up to N/2-1
* outliers in either direction without the downward bias of min
* or the spike sensitivity of average. Insertion sort is fine
* for N=8 (28 comparisons worst case, all in registers). */
int16_t samples[NOISE_FLOOR_SAMPLES_PER_TICK];
for (int i = 0; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
samples[i] = hwGetCurrentRSSI();
}
/* Insertion sort — tiny array, branch-friendly on Cortex-M */
for (int i = 1; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
int16_t key = samples[i];
int j = i - 1;
while (j >= 0 && samples[j] > key) {
samples[j + 1] = samples[j];
j--;
}
samples[j + 1] = key;
}
int16_t rssi = (samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2 - 1] +
samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2]) / 2;
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly. */
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 = 0;
LOG_DBG("noise_floor_cal: seed=%d", _noise_floor);
break;
}
/* Threshold filter with warmup and periodic bypass.
*
* _ema_unguarded counts up from 0 on every tick.
* Ticks 0..W-1 (warmup): all samples accepted for fast convergence
* after seed/reset — prevents a bad seed from locking out the
* real noise floor via a too-tight threshold.
* Ticks W+: threshold filter active. Every Pth tick one sample
* bypasses the filter so the floor can track sustained upward
* shifts (new interference, antenna change).
* The EMA's 1/8 weight naturally dampens isolated spikes. */
const int W = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 — warmup ticks */
const int P = NOISE_FLOOR_UNGUARDED_INTERVAL; /* 16 — periodic interval */
bool warmup = (_ema_unguarded < W);
bool periodic = (!warmup && (_ema_unguarded & (P - 1)) == 0);
_ema_unguarded++; /* wraps at 255 — harmless */
if (!warmup && !periodic &&
rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
break;
}
/* EMA: floor += round_nearest((sample - floor) / W).
* 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 = W / 2; /* 4 */
int step = (diff + (diff > 0 ? half : -half)) / W;
_noise_floor += step;
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly. */
if (_noise_floor == DEFAULT_NOISE_FLOOR) {
_noise_floor = rssi;
if (_noise_floor < -120) _noise_floor = -120;
if (_noise_floor > -50) _noise_floor = -50;
LOG_DBG("noise_floor_cal: rssi=%d, floor=%d, tick=%u",
rssi, _noise_floor, _ema_unguarded - 1);
} while (0);
if (_rx_duty_cycle_enabled) {
hwSetRxDutyCycle(true);
_ema_unguarded = 0;
LOG_DBG("noise_floor_cal: seed=%d", _noise_floor);
return;
}
/* Threshold filter with warmup and periodic bypass.
*
* _ema_unguarded counts up from 0 on every tick.
* Ticks 0..W-1 (warmup): all samples accepted for fast convergence
* after seed/reset — prevents a bad seed from locking out the
* real noise floor via a too-tight threshold.
* Ticks W+: threshold filter active. Every Pth tick one sample
* bypasses the filter so the floor can track sustained upward
* shifts (new interference, antenna change).
* The EMA's 1/8 weight naturally dampens isolated spikes. */
const int W = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 — warmup ticks */
const int P = NOISE_FLOOR_UNGUARDED_INTERVAL; /* 16 — periodic interval */
bool warmup = (_ema_unguarded < W);
bool periodic = (!warmup && (_ema_unguarded & (P - 1)) == 0);
_ema_unguarded++; /* wraps at 255 — harmless */
if (!warmup && !periodic &&
rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
return;
}
/* EMA: floor += round_nearest((sample - floor) / W).
* 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 = W / 2; /* 4 */
int step = (diff + (diff > 0 ? half : -half)) / W;
_noise_floor += step;
if (_noise_floor < -120) _noise_floor = -120;
if (_noise_floor > -50) _noise_floor = -50;
LOG_DBG("noise_floor_cal: rssi=%d, floor=%d, tick=%u",
rssi, _noise_floor, _ema_unguarded - 1);
}
void LoRaRadioBase::resetAGC()
{
/* Don't reset AGC while transmitting or receiving — warm sleep would
* abort the TX or corrupt the incoming packet. maintenanceLoop()
* will retry next housekeeping cycle. */
* will retry next housekeeping cycle.
* Also skip if the chip is in its duty-cycle sleep phase: hwResetAGC()
* holds the SPI mutex with K_FOREVER and would hang for 3 s. */
if (atomic_get(&_tx_active) || isReceiving()) {
return;
}
if (_rx_duty_cycle_enabled && hwIsChipBusy()) {
return;
}
hwResetAGC();
+4
View File
@@ -104,6 +104,10 @@ protected:
/** Reset AGC (chip-specific, may be no-op) */
virtual void hwResetAGC() = 0;
/** Check if chip BUSY pin is high — no SPI, safe to call any time.
* Returns false on chips without a duty-cycle sleep phase (LR1110). */
virtual bool hwIsChipBusy() { return false; }
/* ── Shared helpers available to subclasses ────────────────── */
void buildModemConfig(struct lora_modem_config &cfg, bool tx);
+5
View File
@@ -95,4 +95,9 @@ void SX126xRadio::hwResetAGC()
sx126x_reset_agc(_dev);
}
bool SX126xRadio::hwIsChipBusy()
{
return sx126x_is_chip_busy(_dev);
}
} /* namespace mesh */
+1
View File
@@ -28,6 +28,7 @@ protected:
void hwSetRxBoost(bool enable) override;
void hwSetRxDutyCycle(bool enable) override;
void hwResetAGC() override;
bool hwIsChipBusy() override;
};
} /* namespace mesh */
@@ -61,6 +61,18 @@ void sx126x_set_rx_duty_cycle(const struct device *dev, bool enable);
*/
void sx126x_set_rx_boost(const struct device *dev, bool enable);
/**
* @brief Check if the radio chip is busy (cannot accept SPI commands)
*
* Reads the BUSY GPIO pin directly — no SPI, no blocking.
* Returns true when the chip is in its duty-cycle sleep phase.
* Safe to call at any time.
*
* @param dev LoRa device
* @return true if BUSY pin is high (chip sleeping / processing)
*/
bool sx126x_is_chip_busy(const struct device *dev);
/**
* @brief Reset AGC by performing warm sleep + full recalibration
*
@@ -400,12 +400,17 @@ index 37807cebe38..7d2f95b277e 100644
/* Start transmission with 10 second timeout */
ret = sx126x_set_tx(dev, 10000);
if (ret < 0) {
@@ -927,6 +1156,116 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
@@ -927,6 +1156,121 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
return 0;
}
+/* ── Extension API (sx126x_ext.h) ──────────────────────────────────── */
+
+bool sx126x_is_chip_busy(const struct device *dev)
+{
+ return sx126x_hal_is_busy(dev);
+}
+
+int16_t sx126x_get_rssi_inst(const struct device *dev)
+{
+ struct sx126x_data *data = dev->data;
@@ -517,7 +522,7 @@ index 37807cebe38..7d2f95b277e 100644
static DEVICE_API(lora, sx126x_lora_api) = {
.config = sx126x_lora_config,
.send = sx126x_lora_send,
@@ -952,6 +1291,14 @@ static int sx126x_init(const struct device *dev)
@@ -952,6 +1296,14 @@ static int sx126x_init(const struct device *dev)
data->dev = dev;
atomic_set(&data->state, SX126X_STATE_IDLE);
data->config_valid = false;