diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c index 429f7a9..42cbaaa 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -1427,44 +1427,6 @@ uint32_t lr11xx_get_random(const struct device *dev) return random; } -void lr11xx_reset_agc(const struct device *dev) -{ - struct lr11xx_data *data = dev->data; - - k_mutex_lock(&data->spi_mutex, K_FOREVER); - - /* Warm sleep — powers down analog frontend (resets AGC gain state) */ - lr11xx_system_sleep_cfg_t sleep_cfg = { - .is_warm_start = true, - .is_rtc_timeout = false, - }; - lr11xx_system_set_sleep(&data->hal_ctx, sleep_cfg, 0); - k_sleep(K_USEC(500)); - - /* Wake to STDBY_RC for calibration */ - lr11xx_system_set_standby(&data->hal_ctx, LR11XX_SYSTEM_STANDBY_CFG_RC); - - /* Recalibrate all blocks */ - lr11xx_system_calibrate(&data->hal_ctx, 0x3F); - - /* Re-calibrate image rejection for the actual operating frequency */ - if (data->configured) { - uint16_t freq_mhz = data->modem_cfg.frequency / 1000000; - lr11xx_system_calibrate_image_in_mhz(&data->hal_ctx, - freq_mhz - 4, freq_mhz + 4); - } - - /* Re-apply RX boost if it was enabled */ - if (data->rx_boost_enabled) { - lr11xx_radio_cfg_rx_boosted(&data->hal_ctx, true); - lr11xx_system_clear_irq_status(&data->hal_ctx, - LR11XX_SYSTEM_IRQ_ALL_MASK); - data->rx_boost_applied = true; - } - - k_mutex_unlock(&data->spi_mutex); -} - /* ── Deferred hardware init (runs on first lora_config call) ────────── */ /* ── Driver API: CAD ────────────────────────────────────────────────── */ diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h index a22879e..701de81 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h @@ -95,19 +95,6 @@ void lr11xx_reset_dc_timeout_restarts(const struct device *dev); */ uint32_t lr11xx_get_random(const struct device *dev); -/** - * @brief Reset AGC by performing warm sleep + full recalibration - * - * Warm sleep powers down the analog frontend (resets AGC gain state), - * then Calibrate(0x3F) refreshes all blocks. Re-applies image - * calibration for the operating frequency and RX boost afterward. - * - * Must be called while NOT actively receiving a packet. - * - * @param dev LoRa device - */ -void lr11xx_reset_agc(const struct device *dev); - /** * @brief Set the adaptive-CAD operating detPeak offset * diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c index ffee9e4..f2476e7 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c @@ -491,10 +491,10 @@ static lr20xx_status_t lr20xx_calibrate_front_end(void *ctx, uint32_t freq_hz); * because the patch supersedes them. * * The image is volatile: lost on reset and on cold start, preserved by sleep - * with retention (§22.3). Every sleep this driver issues sets retention - * (lr20xx_reset_agc), and duty-cycle sleep is retention by definition (§6.3.8), - * so the reset paths are the only places it has to be (re)loaded. Costs 2240 - * bytes of flash and +80 nA of retention sleep current. + * with retention (§22.3). Duty-cycle sleep is retention by definition (§6.3.8) + * and this driver issues no other sleep, so the reset paths are the only places + * it has to be (re)loaded. Costs 2240 bytes of flash and +80 nA of retention + * sleep current. * * Best-effort: a chip without the patch still works, so a failure here is a * warning, not a reason to fail init. */ @@ -609,7 +609,7 @@ static void lr20xx_hardware_reset(struct lr20xx_data *data, * caller of this function is post-config (the DIO1 stuck-recovery path * and three standby-failure paths), and lr20xx_start_rx() follows * immediately, so calibrate for the frequency actually about to be - * received on. Same shape as lr20xx_reset_agc(). */ + * received on. */ if (data->configured) { lr20xx_calibrate_front_end(ctx, data->modem_cfg.frequency); } @@ -975,6 +975,32 @@ static bool lr20xx_apply_rx_duty_cycle(struct lr20xx_data *data) return false; } + /* Re-apply RX boost before re-arming — same warm-start caution as + * lr11xx_restart_rx(), which has always done this on its duty-cycle + * path. A duty cycle sleeps the chip between every window (DS §6.3.8), + * and DS §6.3.4 (SetAdditionalRegToRetain, "specifies the address of an + * additional register to retain in Sleep mode") establishes that not + * every register survives that on its own. This driver holds no + * retention entries at all, and the only place set_rx_path was ever + * issued is apply_modem_config() — which the re-arm path never calls, so + * before this the boost was programmed once at RX entry and never again. + * If it does not survive the sleep, every window after the first ran + * 3 dB down: the same failure the SX126x hit, where the fix was pinning + * RX gain into the warm-start retention list. + * + * One SPI command, correct either way. Whether the LR2021 actually needs + * it — or wants a retention entry instead — is still an open question; + * read the gain back after a window, or A/B it on PDR. + * + * Not conditioned on rx_boost_applied: that flag tracks what we + * programmed, not what survived. */ + if (data->rx_boost_enabled) { + lr20xx_radio_common_set_rx_path( + ctx, LR20XX_RADIO_COMMON_RX_PATH_LF, + LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_7); + data->rx_boost_applied = true; + } + /* Clear the CMD_PERR the HAL's clockless wake frame provokes: it is * self-inflicted by the wake and says nothing about the command below. * Left set it holds DIO1 high and poisons the re-arm. */ @@ -2120,6 +2146,28 @@ static int lr20xx_lora_recv_async(const struct device *dev, k_mutex_lock(&data->spi_mutex, K_FOREVER); + /* Idempotent fast path: the chip is already in continuous RX, so there + * is nothing to program — refresh the callback and leave the receiver on + * air. The usual caller is LoRaRadioBase::startSendRaw()'s failure path: + * the LBT branch of send_async has already run lr20xx_start_rx() before + * returning -EBUSY, so without this every CAD-busy verdict paid a SECOND + * full rebuild (standby -> ClearRxFifo -> apply_modem_config -> SetRx) on + * top of the driver's own. startSendRaw's comment has asserted this was + * "a no-op there" ever since the SX126x gained its Phase 2 fast path; on + * this driver it was not, until now. + * + * rx_duty_cycle_enabled is part of the guard, not decoration. This is + * the CONTINUOUS entry point and it clears that flag below, so a caller + * moving a duty-cycled radio to continuous RX has to take the full path: + * fast-pathing there would leave the cycle running on a chip whose + * bookkeeping claims continuous, and nothing would ever stand it down. */ + if (data->in_rx_mode && !data->rx_duty_cycle_enabled) { + data->async_rx_cb = cb; + data->async_rx_user_data = user_data; + k_mutex_unlock(&data->spi_mutex); + return 0; + } + data->async_rx_cb = cb; data->async_rx_user_data = user_data; data->rx_duty_cycle_enabled = false; @@ -2511,42 +2559,6 @@ uint32_t lr20xx_get_random(const struct device *dev) return random; } -void lr20xx_reset_agc(const struct device *dev) -{ - struct lr20xx_data *data = dev->data; - void *ctx = &data->hal_ctx; - - k_mutex_lock(&data->spi_mutex, K_FOREVER); - - /* Warm sleep — powers down analog frontend (resets AGC gain state). - * is_ram_retention_enabled=true = warm sleep (equivalent to LR11xx warm_start). */ - lr20xx_system_sleep_cfg_t sleep_cfg = { - .is_clk_32k_enabled = false, - .is_ram_retention_enabled = true, - }; - /* No delay after the sleep opcode — lr20xx_hal_write() already does a - * k_busy_wait(1000) when it sees 0x0127, so this was a duplicate guard - * (and k_sleep() with a sub-tick duration rounds up to a full tick). */ - lr20xx_system_set_sleep_mode(ctx, &sleep_cfg, 0); - - lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC); - - lr20xx_system_calibrate(ctx, 0x6F); - - if (data->configured) { - lr20xx_calibrate_front_end(ctx, data->modem_cfg.frequency); - } - - if (data->rx_boost_enabled) { - lr20xx_radio_common_set_rx_path( - ctx, LR20XX_RADIO_COMMON_RX_PATH_LF, - LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_7); - data->rx_boost_applied = true; - } - - k_mutex_unlock(&data->spi_mutex); -} - /* ── Driver API: CAD ────────────────────────────────────────────────── */ /* Datasheet Table 6-19 in full: rows = CAD symbol count 1..4, columns = @@ -2852,8 +2864,31 @@ static int lr20xx_lora_recv_duty_cycle(const struct device *dev, return -EINVAL; } + uint32_t rx_ms = k_ticks_to_ms_ceil32(rx_period.ticks); + uint32_t slp_ms = k_ticks_to_ms_ceil32(sleep_period.ticks); + if (rx_ms < 1) rx_ms = 1; + if (slp_ms < 1) slp_ms = 1; + k_mutex_lock(&data->spi_mutex, K_FOREVER); + /* Idempotent fast path, the duty-cycle counterpart of the one in + * recv_async and the same shape as the SX126x's: a cycle already armed + * on exactly these periods has nothing to re-arm, so refresh the + * callback and stay on air instead of tearing a live cycle down and + * paying a full rebuild for it. Timings are computed before the lock so + * they can be compared here. + * + * Any other combination — continuous RX, or a cycle on different periods + * — falls through to the full path below, which re-arms via + * lr20xx_apply_rx_duty_cycle() and therefore still gets the AGC reset. */ + if (data->in_rx_mode && data->rx_duty_cycle_enabled && + data->dc_rx_ms == rx_ms && data->dc_sleep_ms == slp_ms) { + data->async_rx_cb = cb; + data->async_rx_user_data = user_data; + k_mutex_unlock(&data->spi_mutex); + return 0; + } + data->async_rx_cb = cb; data->async_rx_user_data = user_data; @@ -2864,11 +2899,6 @@ static int lr20xx_lora_recv_duty_cycle(const struct device *dev, lr20xx_radio_fifo_clear_rx(ctx); lr20xx_apply_modem_config(data, cfg, false); - uint32_t rx_ms = k_ticks_to_ms_ceil32(rx_period.ticks); - uint32_t slp_ms = k_ticks_to_ms_ceil32(sleep_period.ticks); - if (rx_ms < 1) rx_ms = 1; - if (slp_ms < 1) slp_ms = 1; - /* Store for the re-arm paths (lr20xx_start_rx / lr20xx_restart_rx) * so every re-entry uses exactly this timing. */ data->dc_rx_ms = rx_ms; diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h index 6bb6512..20a54fc 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h @@ -168,13 +168,6 @@ int lr20xx_configure_side_detectors(const struct device *dev, */ uint32_t lr20xx_get_random(const struct device *dev); -/** - * @brief Reset AGC by performing warm sleep + full recalibration - * - * @param dev LoRa device - */ -void lr20xx_reset_agc(const struct device *dev); - /** * @brief Set the adaptive-CAD operating detPeak offset * diff --git a/zephcore/src/Dispatcher.cpp b/zephcore/src/Dispatcher.cpp index 559cff3..bf41cb5 100644 --- a/zephcore/src/Dispatcher.cpp +++ b/zephcore/src/Dispatcher.cpp @@ -517,6 +517,32 @@ void Dispatcher::checkSend() * continuously on a busy site and buries real * faults. */ LOG_DBG("checkSend: startSendRaw refused, re-queuing delay=%u", retry); + + /* Escalate a refusal that will not end. This branch + * used to leave cad_busy_start alone, so a packet the + * driver's own LBT kept rejecting — the pre-TX gate + * having passed — looped indefinitely with no counter, + * no error flag and nothing above DBG. At default log + * level a node in that state looks completely idle + * while it never transmits, which is exactly how a + * chip-side CAD (LR20xx CAD_LBT) fails. + * + * Deliberately no recoverRxState() here, unlike the + * isReceiving() branch above: that one recovers a chip + * suspected of being stuck in RX, whereas a busy + * channel is a true reading and walking the radio + * through REST would only add deaf time. Report and + * keep retrying. */ + if (cad_busy_start == 0) { + cad_busy_start = now; + } else if (now - cad_busy_start > getCADFailMaxDuration()) { + _err_flags |= ERR_EVENT_CAD_TIMEOUT; + LOG_WRN("checkSend: LBT has refused TX for %ums " + "(len=%d, noise=%d) — channel busy or CAD too sensitive", + (unsigned)(now - cad_busy_start), len, + _radio->getNoiseFloor()); + cad_busy_start = now; + } logTxFail(outbound, outbound->getRawLength()); _mgr->queueOutbound(outbound, outbound_priority, futureMillis((int)retry)); outbound = nullptr; @@ -525,6 +551,12 @@ void Dispatcher::checkSend() } } else { outbound_expiry = futureMillis((int)max_airtime); + /* A transmit got out, so the refusal streak above is + * over. Without this the timer keeps running across + * successful sends and the next isolated refusal + * inherits a stale start, reporting a stall that + * already ended. */ + cad_busy_start = 0; } } } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 0d5f48a..b1a45d5 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -511,9 +511,9 @@ static void mesh_event_loop(void) /* Periodic housekeeping — maintenance + UI refresh */ if (events & MESH_EVENT_HOUSEKEEPING) { - /* Radio maintenance: noise floor calibration, AGC reset, - * RX watchdog. Separated from loop() so these never run - * on packet-driven events. */ + /* Radio maintenance: noise floor calibration, adaptive-CAD + * probe, RX watchdog. Separated from loop() so these + * never run on packet-driven events. */ if (companion_mesh_ptr) { companion_mesh_ptr->maintenanceLoop(); } diff --git a/zephcore/src/main_room_server.cpp b/zephcore/src/main_room_server.cpp index 66607fc..b1be665 100644 --- a/zephcore/src/main_room_server.cpp +++ b/zephcore/src/main_room_server.cpp @@ -443,9 +443,9 @@ static void room_event_loop(void) /* Periodic housekeeping — maintenance + display refresh */ if (events & MESH_EVENT_HOUSEKEEPING) { #ifdef ZEPHCORE_LORA - /* Radio maintenance: noise floor calibration, AGC reset, - * RX watchdog. Separated from loop() so these never run - * on packet-driven events. */ + /* Radio maintenance: noise floor calibration, adaptive-CAD + * probe, RX watchdog. Separated from loop() so these + * never run on packet-driven events. */ if (room_mesh_ptr) { room_mesh_ptr->maintenanceLoop(); /* Also drive loop() so time-based actions (advert