refactor patches and bump to 1.17.2

This commit is contained in:
liquidraver committed 2026-08-20 17:50:48 +02:00
1 parent 9ab9a9b950
commit 5c4ac21bd4
28 files changed
+1382 -856

No files matched your search

+15
View File
@@ -78,6 +78,21 @@ bool LR1110Radio::hwIsReceiving()
return lr11xx_is_receiving(_dev);
}
void LR1110Radio::hwResetAgc()
{
lr11xx_reset_agc(_dev);
}
void LR1110Radio::hwRecalibrate()
{
lr11xx_recalibrate(_dev);
}
int16_t LR1110Radio::hwGetChipTempC()
{
return lr11xx_get_chip_temp_c(_dev);
}
void LR1110Radio::hwSetRxBoost(bool enable)
{
lr11xx_set_rx_boost(_dev, enable);
+3
View File
@@ -34,6 +34,9 @@ protected:
int16_t hwGetCurrentRSSI() override;
bool hwIsReceiving() override;
void hwSetRxBoost(bool enable) override;
void hwResetAgc() override;
void hwRecalibrate() override;
int16_t hwGetChipTempC() override;
uint32_t hwWakeupTimeUs() override;
int hwCadProbe(int8_t level) override;
void hwCadSetPeakOffset(int8_t offset) override;
+17
View File
@@ -167,6 +167,23 @@ uint8_t LR2021Radio::hwCadPeakMax()
return lr20xx_cad_peak_max();
}
void LR2021Radio::hwResetAgc()
{
lr20xx_reset_agc(_dev);
}
void LR2021Radio::hwRecalibrate()
{
/* Front-end calibration included: this is the temperature-drift path, and
* image/FE cal is the part a temperature swing actually invalidates. */
lr20xx_recalibrate(_dev);
}
int16_t LR2021Radio::hwGetChipTempC()
{
return lr20xx_get_chip_temp_c(_dev);
}
uint32_t LR2021Radio::hwWakeupTimeUs()
{
/* Per-device, because the TCXO term dominates and is board-specific:
+3
View File
@@ -52,6 +52,9 @@ protected:
uint8_t hwCadPeakMin() override;
uint8_t hwCadPeakMax() override;
uint32_t hwWakeupTimeUs() override;
void hwResetAgc() override;
void hwRecalibrate() override;
int16_t hwGetChipTempC() override;
};
} /* namespace mesh */
+98
View File
@@ -60,6 +60,8 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
_rx_duty_cycle_enabled(IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)),
_rx_boost_enabled(true),
_dc_last_rx_us(0), _dc_last_sleep_us(0),
_agc_rx_count_shadow(0), _agc_last_activity_ms(0),
_agc_last_cal_temp_c(INT16_MIN),
_config_cached(false),
_has_radio_override(false),
_override_freq(0), _override_bw(0),
@@ -1125,6 +1127,102 @@ bool LoRaRadioBase::isReceiving()
return isChannelActive();
}
/* ── Receiver hygiene ─────────────────────────────────────────────────
*
* Two independent faults, two independent triggers, neither on the packet path.
*
* 1. STUCK AGC. Semtech's remedy is a warm sleep plus recalibration; it is not
* in the datasheets, so it is not up for removal on datasheet reasoning.
* What IS a design choice is when to fire it, and the honest answer is that
* a periodic reset gets it backwards: a timer fires most often on a busy
* channel, which is exactly where a received packet has just PROVED the AGC
* is working, and it fires no more often on a silent one, where a
* desensitised receiver is invisible and nothing else will reveal it.
*
* So trigger on silence instead. If nothing has been heard for
* AGC_IDLE_RESET_MS the receiver is either genuinely idle — in which case
* the reset costs nothing, there is no traffic to miss — or it is deaf, in
* which case this is the only thing that will recover it. Self-targeting in
* both directions: a busy node essentially never resets, and the resets a
* quiet node does perform are free. Arduino MeshCore's periodic
* `agc_reset_interval` addressed the same fault and shipped defaulted to 0
* (off), which is a fair summary of how well a plain timer serves it.
*
* Deliberately NOT reset here: the noise floor. The previous periodic
* implementation (removed in fe6e585) zeroed it on every fire, forcing a
* fresh seed and a full EMA warmup each time — that, not the AGC work, is
* what made it a net loss. Arduino zeroes it too; we do not.
*
* 2. CALIBRATION DRIFT. Image/front-end and PLL/AAF calibration are valid for
* a temperature range, not forever: "Image calibration is necessary if there
* is a frequency change > 10MHz, or a temperature change > 10 C", and the
* LR2021 additionally advises redoing PLL and AAF beyond +/-20 C. A node
* that boots on a hot afternoon and runs into a cold night crosses both.
* Nothing does this automatically: the LR2021's temperature-compensation
* block (DS 6.12) only corrects crystal drift from TX self-heating, and it
* refuses to run at all when a TCXO is fitted.
*
* AGC_RECAL_TEMP_DELTA_C is deliberately tighter than either datasheet
* figure — cheap insurance, and the reading is a junction temperature that
* lags ambient. */
#define AGC_IDLE_RESET_MS 60000U
#define AGC_RECAL_TEMP_DELTA_C 5
void LoRaRadioBase::agcMaintenance()
{
/* Never mid-transmit: both operations sleep the chip. */
if (atomic_get(&_tx_active)) {
return;
}
uint32_t now = (uint32_t)k_uptime_get_32();
/* Any demodulation activity counts as proof of life, errored frames
* included — a CRC failure still means RF reached the demodulator, which
* is precisely what a stuck AGC would prevent. Counting only good
* packets would fire resets on a node that is merely out of range. */
uint32_t rx_total = (uint32_t)atomic_get(&_packets_recv) +
(uint32_t)atomic_get(&_packets_recv_errors);
if (rx_total != _agc_rx_count_shadow || _agc_last_activity_ms == 0) {
_agc_rx_count_shadow = rx_total;
_agc_last_activity_ms = now ? now : 1;
} else if ((now - _agc_last_activity_ms) >= AGC_IDLE_RESET_MS) {
LOG_INF("agc: %u ms without RX activity — resetting AGC",
(unsigned)(now - _agc_last_activity_ms));
hwResetAgc();
/* hwResetAgc() leaves the chip out of RX by contract, so this is
* a genuine re-entry and re-arms the duty cycle if one is set. */
startReceive();
_agc_last_activity_ms = now ? now : 1;
}
/* Temperature drift. Sampled every pass because the read is one cheap
* command; the expensive recalibration is gated on the delta. */
int16_t temp_c = hwGetChipTempC();
if (temp_c == INT16_MIN) {
return; /* backend cannot measure — drift handling disabled */
}
if (_agc_last_cal_temp_c == INT16_MIN) {
_agc_last_cal_temp_c = temp_c; /* first reading is the baseline */
return;
}
int delta = (int)temp_c - (int)_agc_last_cal_temp_c;
if (delta < 0) {
delta = -delta;
}
if (delta >= AGC_RECAL_TEMP_DELTA_C) {
LOG_INF("agc: chip temp moved %d C (%d -> %d) — recalibrating",
delta, (int)_agc_last_cal_temp_c, (int)temp_c);
hwRecalibrate();
startReceive();
_agc_last_cal_temp_c = temp_c;
}
}
void LoRaRadioBase::recoverRxState()
{
/* Called by the Dispatcher on CAD timeout when isReceiving() has been
+31
View File
@@ -121,6 +121,9 @@ public:
void setCadParams(bool auto_enabled, int8_t offset,
uint16_t probe_interval_s, uint8_t busycap_pct) override;
void cadMaintenance() override;
/** Deaf-aware AGC unstick + temperature-drift recalibration.
* Called from Dispatcher::maintenanceLoop(); never on the packet path. */
void agcMaintenance() override;
uint32_t msUntilNextMaintenance() override;
int8_t getCadOffset() const override { return _cad_offset; }
@@ -175,6 +178,26 @@ protected:
virtual uint8_t hwCadPeakMin() { return 0; }
virtual uint8_t hwCadPeakMax() { return 0; }
/* ── Receiver hygiene — see LoRaRadioBase::agcMaintenance() ────── */
/** Unstick a jammed AGC: warm sleep to drop the analog front end, then
* recalibrate on the way back up. Semtech's stated remedy; the
* datasheets do not describe it, so do not "simplify" it away on
* datasheet grounds alone. Must leave the driver out of RX so the
* caller's startReceive() performs a real re-entry rather than hitting
* an idempotent fast path. Default: unsupported, no-op. */
virtual void hwResetAgc() {}
/** Redo the frequency-dependent calibrations (image / front end, and
* PLL+AAF where the part separates them) at the current operating
* frequency. Called on temperature drift, never on the packet path.
* Same RX-state contract as hwResetAgc(). Default: unsupported. */
virtual void hwRecalibrate() {}
/** Chip junction temperature in whole degrees C, or INT16_MIN when the
* backend cannot measure it (which disables drift recalibration). */
virtual int16_t hwGetChipTempC() { return INT16_MIN; }
/** Radio deaf time per duty-cycle wake transition (context restore +
* PLL lock + TCXO startup where fitted), in microseconds. Counts
* against the duty-cycle preamble-catch budget: per SX126x DS rev 2.2
@@ -298,8 +321,16 @@ protected:
* changes once at INF instead of on every RX restart. 0/0 = never
* computed; UINT32_MAX rx = continuous-RX fallback active. */
uint32_t _dc_last_rx_us;
uint32_t _dc_last_sleep_us;
/* agcMaintenance() bookkeeping. RX activity is inferred by sampling the
* existing packet counters rather than timestamping in the RX callback,
* so nothing is added to the ISR path. */
uint32_t _agc_rx_count_shadow;
uint32_t _agc_last_activity_ms;
int16_t _agc_last_cal_temp_c;
/* Config cache — skip redundant hwConfigure() */
struct lora_modem_config _last_cfg;
bool _config_cached;
+21
View File
@@ -75,6 +75,27 @@ bool SX126xRadio::hwIsReceiving()
return sx126x_is_receiving(_dev);
}
void SX126xRadio::hwResetAgc()
{
/* Leaves the chip in STANDBY and the driver state at IDLE, so the
* caller's startReceive() performs a real re-entry. */
sx126x_reset_agc(_dev);
}
void SX126xRadio::hwRecalibrate()
{
/* Calibrate(ALL) on this part already includes image rejection and
* sx126x_reset_agc() re-issues CalibrateImage for the operating
* frequency, so the drift path needs nothing extra. */
sx126x_reset_agc(_dev);
}
/* No hwGetChipTempC(): the SX126x exposes no junction-temperature readout —
* no command in the datasheet, and RadioLib has no getTemperature() for it
* either. Drift recalibration is therefore inactive on this family, which
* costs little: unlike the LR parts, its datasheet gives no temperature
* threshold for image calibration in the first place. */
void SX126xRadio::hwSetRxBoost(bool enable)
{
sx126x_set_rx_boost(_dev, enable);
+2
View File
@@ -29,6 +29,8 @@ protected:
int16_t hwGetCurrentRSSI() override;
bool hwIsReceiving() override;
void hwSetRxBoost(bool enable) override;
void hwResetAgc() override;
void hwRecalibrate() override;
bool hwIsChipBusy() override;
uint32_t hwWakeupTimeUs() override;
int hwCadProbe(int8_t level) override;