From 06a0b03a20fb5242e651ef0e95ccb8f3df2c5313 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:27:25 +0200 Subject: [PATCH] LR1110 duty cycle finally and zephyr PM vs UART wedge fixes --- zephcore/Kconfig | 20 ++ zephcore/adapters/gps/ZephyrGPSManager.cpp | 15 ++ zephcore/adapters/radio/LoRaRadioBase.cpp | 17 +- .../drivers/lora/lr11xx/lr11xx_lora.c | 229 +++++++++++++++--- .../zephyr/0003-lora-sx126x-native.patch | 74 +++--- ...0-uarte-pm-suspend-bounded-rxto-wait.patch | 35 +++ 6 files changed, 325 insertions(+), 65 deletions(-) create mode 100644 zephcore/patches/zephyr/0010-uarte-pm-suspend-bounded-rxto-wait.patch diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 2c83be9..590a930 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -581,6 +581,26 @@ config ZEPHCORE_LORA_DC_MIN_SYMBOLS for extra detection margin on very noisy sites. SF5/6 adds +4 internally. Lowering below 8 risks missed packets at low SNR. +config ZEPHCORE_LORA_DC_MARGIN_PCT + int "RX duty cycle: sleep-budget safety margin (percent)" + range 0 40 + default 15 + help + Percentage the per-cycle deaf-time budget is derated before the + sleep period is programmed. The theoretical catch budget assumes + the sleep clock and wake transition are exact, but the chip's sleep + timer runs on an internal RC oscillator (RC64k on SX126x, RTC on + LR11xx) that drifts several percent over temperature, and the wake- + transition estimate is a datasheet figure the datasheet itself calls + "not accurate and may vary". Either overshoot silently pushes real + deaf time past the budget and drops the fraction of packets whose + preamble phase aligns with the window edge — signal-strength- + independent loss that presents as random DC packet drops. This + margin trades a little radio-off time for that robustness and is + shared by every radio (SX126x / LR11xx / LR20xx). Set to 0 to + restore the old zero-margin behaviour; 15% is a conservative default, + lower it once you have measured your board's clock+transition on air. + endmenu config ZEPHCORE_RTC_AUTODISCOVER diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index 6741537..fe03636 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -935,6 +935,21 @@ static void gps_uart_set_power(bool on) if (!device_is_ready(gps_uart_dev)) { return; } + if (!on) { + /* Let the GNSS line go quiet before suspending. Every caller + * cuts module power (GPS_EN low / reset asserted / regulator + * off) immediately before this, but a byte can still be in + * flight. Settle so it finishes and the driver's RX ISR re-arms, + * leaving the receiver armed-and-idle when the suspend's STOPRX + * fires — that state yields RXTO, whereas a just-stopped, + * un-rearmed receiver can produce none and (pre-0010) hung the + * main thread. ~5 ms comfortably covers one character time at + * GNSS baud plus ISR latency; standby happens at most every few + * minutes, so the cost is negligible. Backstop: patch 0010 + * bounds the driver's RXTO wait so a missed RXTO can never hang + * us even if a byte still lands in the race window. */ + k_msleep(5); + } int ret = pm_device_action_run(gps_uart_dev, on ? PM_DEVICE_ACTION_RESUME : PM_DEVICE_ACTION_SUSPEND); diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index dbbe875..6b82107 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -534,8 +534,23 @@ void LoRaRadioBase::startReceive() const uint32_t sym_us = (uint32_t) (((uint64_t)(1U << sf) * 1000000ULL) / bw_hz); const uint32_t trans_us = hwWakeupTimeUs(); - const uint32_t deaf_us = + + /* Theoretical per-cycle deaf budget, then derate by + * CONFIG_..._MARGIN_PCT. The budget assumes the sleep + * clock and wake transition are exact; in reality the + * chip sleep timer runs on an RC oscillator that drifts + * several % over temperature and the wake transition is a + * "may vary" datasheet figure. Either overshoot pushes + * real deaf time past the budget and drops phase-edge + * packets (strength-independent DC loss). Deraging the + * whole budget is slightly stricter than deraging sleep + * alone, which is the safe direction. */ + const uint32_t deaf_budget_us = (uint32_t)(P - 2 * D - 1) * sym_us; + const uint32_t deaf_us = deaf_budget_us - + (uint32_t)(((uint64_t)deaf_budget_us * + CONFIG_ZEPHCORE_LORA_DC_MARGIN_PCT) / + 100U); if (deaf_us > trans_us + 2000) { const uint32_t sleep_us = deaf_us - trans_us; 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 8472d18..621bc8e 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -32,6 +32,16 @@ LOG_MODULE_REGISTER(lr11xx_lora, CONFIG_LORA_LOG_LEVEL); #define LR11XX_DIO1_WQ_STACK_SIZE 2560 K_THREAD_STACK_DEFINE(lr11xx_dio1_wq_stack, LR11XX_DIO1_WQ_STACK_SIZE); +/* Wedge-recovery watchdog — its own low-priority queue so the confirm poll + * (a BUSY-high dwell of up to LR11XX_WEDGE_CONFIRM_MS) never blocks DIO1 RX + * processing. */ +#define LR11XX_WEDGE_WQ_STACK_SIZE 1024 +K_THREAD_STACK_DEFINE(lr11xx_wedge_wq_stack, LR11XX_WEDGE_WQ_STACK_SIZE); + +#define LR11XX_WEDGE_CHECK_MS 3000 /* base cadence between health checks */ +#define LR11XX_WEDGE_IDLE_MS 12000 /* only probe after this much DIO1 silence */ +#define LR11XX_WEDGE_CONFIRM_MS 250 /* continuous BUSY-high past this = wedged */ + /* ── Driver data structures ─────────────────────────────────────────── */ struct lr11xx_config { @@ -120,6 +130,16 @@ struct lr11xx_data { * bit is cleared and TX released. All accesses under spi_mutex. */ uint32_t preamble_seen_at_ms; + /* Wedge-recovery watchdog: the LR1110 can rarely be left BUSY-high with + * DIO1 low (a command racing the autonomous SetRxDutyCycle sleep phase) — + * no IRQ ever fires, so the event-driven driver never re-arms and the node + * goes permanently deaf. last_dio1_ms records the last proof-of-life (a + * handled DIO1 event); the watchdog re-arms RX via a hardware reset when + * BUSY stays continuously high past any legitimate DC cycle with no DIO1. */ + uint32_t last_dio1_ms; + struct k_work_delayable wedge_work; + struct k_work_q wedge_wq; + /* RX data buffer — filled in DIO1 handler, passed to callback */ uint8_t rx_buf[256]; }; @@ -303,12 +323,27 @@ static void lr11xx_apply_modem_config(struct lr11xx_data *data, 0); } -/* ── RX duty cycle — disabled on LR1110 ─────────────────────────────── */ +/* ── RX duty cycle ──────────────────────────────────────────────────── */ -/* LR1110 SetRxDutyCycle is fundamentally broken: both MODE_RX and - * MODE_CAD fail to detect in-progress preambles, dropping 23-40% of - * packets depending on the sleep fraction. The SX1262 handles this - * correctly. LR1110 always uses continuous RX instead. */ +/* SetRxDutyCycle(MODE_RX) works the same way as the SX126x: on a positive + * over-the-air (preamble) detection the chip auto-recomputes its RX timeout + * to 2*rx_period + sleep_period and stays in RX for the whole packet (SWDR001 + * lr11xx_radio.h step 2). Because that extension is native, the LR1110 needs + * NO StopTimerOnPreamble and NO parked-RX watchdog: a false detection lets the + * bounded 2*rx+sleep timer expire and re-arm via the normal RX-timeout path, + * so there is no infinite-park failure mode to recover from (unlike the + * SX126x, whose StopTimerOnPreamble freezes the timer). Window sizing — + * including the shared clock/transition safety margin — is owned by the + * adapter (LoRaRadioBase::startReceive); the driver only re-arms with the + * stored timing. + * + * The earlier "fundamentally broken, 23-40% loss" verdict was a window-sizing + * bug in the adapter, not a chip defect. One item is still HW-unverified: the + * chip's sleep-with-retention warm wakes should keep the boosted RX gain + * (the SX126x needs an explicit retention-list write for this; the LR11xx SDK + * exposes no such list, implying it is automatic — measure sensitivity across + * cycles on a live board to confirm). We re-apply SetRxBoosted on every host + * restart regardless, as cheap insurance. */ /* ── Start RX (internal) ────────────────────────────────────────────── */ @@ -421,6 +456,9 @@ static void lr11xx_dio1_work_handler(struct k_work *work) void *ctx = &data->hal_ctx; bool rx_restarted = false; + /* Proof of life for the wedge watchdog: the chip generated an IRQ. */ + data->last_dio1_ms = k_uptime_get_32(); + k_mutex_lock(&data->spi_mutex, K_FOREVER); /* Read IRQ status then clear ALL bits. Must use ALL_MASK because @@ -453,18 +491,21 @@ static void lr11xx_dio1_work_handler(struct k_work *work) } /* ── RX done ── - * Gated on no error bits: a CRC-failed packet asserts RX_DONE and - * CRC_ERROR together on this chip family (same as SX126x — see the - * sx126x patch's "they co-fire here"; LBM's radio_planner likewise - * checks errors before RX_DONE), so an ungated done-first read - * would deliver corrupted payloads as valid. A good packet - * coalesced with an earlier HEADER_ERROR in the same handler - * window is dropped too — the header error may have shifted the - * RX buffer pointer (errata handling below), making the read - * suspect. The error branch below owns the window instead. */ + * Deliver on RX_DONE unless it is a GENUINE reception failure: + * - CRC_ERROR: a CRC-failed packet asserts RX_DONE+CRC_ERROR together + * on this chip family, so an ungated read would deliver corruption. + * - HEADER_ERROR with NO valid header (SYNC_WORD_HEADER_VALID clear): + * no good header decoded, the buffer is suspect. + * A HEADER_ERROR that coincides with a VALID header is a foreign/colliding + * signal's error while OUR packet decoded fine — RadioLib's readData keeps + * the packet in exactly this case (CRC_ERR || (HDR_ERR && !HDR_VALID)), and + * the SX126x path never routes HEADER_ERROR at all. Dropping these good + * packets was the LR1110 under-load packet-loss bug (verified 2026-07-24: + * ~7-10% loss vs SX1262, load-dependent, size-skewed). */ if ((irq & LR11XX_SYSTEM_IRQ_RX_DONE) && - !(irq & (LR11XX_SYSTEM_IRQ_CRC_ERROR | - LR11XX_SYSTEM_IRQ_HEADER_ERROR))) { + !(irq & LR11XX_SYSTEM_IRQ_CRC_ERROR) && + !((irq & LR11XX_SYSTEM_IRQ_HEADER_ERROR) && + !(irq & LR11XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID))) { lr11xx_radio_rx_buffer_status_t rx_stat; lr11xx_radio_get_rx_buffer_status(ctx, &rx_stat); @@ -477,6 +518,19 @@ static void lr11xx_dio1_work_handler(struct k_work *work) rx_stat.buffer_start_pointer, rx_stat.pld_len_in_bytes); + /* Buffer-shift errata, header-error variant: if a foreign + * HEADER_ERROR coalesced with this good packet, a standby + * must reset the +4 buffer_start_pointer drift so it cannot + * corrupt the NEXT packet's read. The payload is already + * captured above, so resetting now keeps the packet AND the + * errata protection. (A plain RX_DONE with no header error + * gets the same reset from the STDBY_RC rx/tx fallback; this + * makes the coalesced case explicit, not fallback-reliant.) */ + if (irq & LR11XX_SYSTEM_IRQ_HEADER_ERROR) { + lr11xx_system_set_standby(ctx, + LR11XX_SYSTEM_STANDBY_CFG_RC); + } + /* LR1110 errata: RX buffer base shifts 4 bytes per * received packet. Without clearing, after ~64 * packets the offset wraps the 256-byte buffer and @@ -559,32 +613,38 @@ static void lr11xx_dio1_work_handler(struct k_work *work) } } - /* ── CRC / Header error ── - * Plain `CRC || HDR` — no SYNC_WORD_HEADER_VALID gate (RadioLib - * readData's false-alarm filter). IRQ status is bulk-cleared on - * every handler entry, so a set HEADER_ERROR always belongs to - * this window; a SYNC_VALID bit latched by another packet in the - * same window must not suppress the errata standby below. */ - if (irq & (LR11XX_SYSTEM_IRQ_CRC_ERROR | - LR11XX_SYSTEM_IRQ_HEADER_ERROR)) { + /* ── CRC / header error ── + * A HEADER_ERROR that coincides with a VALID header but no RX_DONE means a + * foreign/colliding signal errored while OUR good packet is still + * mid-reception. Do NOT abort it: the old reflex standby()+restart here + * dropped the in-flight packet, and doing that on every foreign header + * error under load is what cost the LR1110 packets (the SX126x never routes + * HEADER_ERROR for exactly this reason). Leave the chip in RX; the good + * packet's own RX_DONE delivers it above. */ + if ((irq & LR11XX_SYSTEM_IRQ_HEADER_ERROR) && + (irq & LR11XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID) && + !(irq & LR11XX_SYSTEM_IRQ_RX_DONE)) { + LOG_DBG("RX: foreign HDR err during valid header — not aborting"); + rx_restarted = true; /* reception continues; skip safety-net restart */ + } else if (irq & (LR11XX_SYSTEM_IRQ_CRC_ERROR | + LR11XX_SYSTEM_IRQ_HEADER_ERROR)) { LOG_DBG("RX error: CRC=%d HDR=%d RXDONE=%d", (irq & LR11XX_SYSTEM_IRQ_CRC_ERROR) ? 1 : 0, (irq & LR11XX_SYSTEM_IRQ_HEADER_ERROR) ? 1 : 0, (irq & LR11XX_SYSTEM_IRQ_RX_DONE) ? 1 : 0); - /* LR1110 errata: a header error makes the chip's reported - * buffer_start_pointer stale by +4 for every subsequent - * packet (stacking) until a standby resets the shift. - * Arduino MeshCore's CustomLR1110 calls standby() on every - * header error unconditionally — mirror that. */ + /* LR1110 errata: a real header error (no valid header) drifts the + * reported buffer_start_pointer +4 per subsequent packet until a + * standby resets it (Arduino MeshCore's CustomLR1110 standbys on + * header error). Only for a genuine header error — a valid header + * is handled above and must never trigger this abort. */ if (irq & LR11XX_SYSTEM_IRQ_HEADER_ERROR) { lr11xx_system_set_standby(ctx, LR11XX_SYSTEM_STANDBY_CFG_RC); } - /* Drop whatever the failed (or coalesced) packet left in - * the RX buffer — RadioLib clears it on the CRC-error read - * path too. */ + /* Drop whatever the failed packet left in the RX buffer — RadioLib + * clears it on the CRC-error read path too. */ lr11xx_regmem_clear_rxbuffer(ctx); if (!data->tx_active) { @@ -958,8 +1018,28 @@ int16_t lr11xx_get_rssi_inst(const struct device *dev) struct lr11xx_data *data = dev->data; int8_t rssi; - k_mutex_lock(&data->spi_mutex, K_FOREVER); - lr11xx_radio_get_rssi_inst(&data->hal_ctx, &rssi); + /* GPIO BUSY read FIRST — bail before the HAL's wait_on_busy stalls (3 s) + * on the autonomous duty-cycle sleep phase. Issuing GetRssiInst (0x0205) + * into a chip parked in SetRxDutyCycle sleep-with-retention wedges it + * BUSY-high permanently (root cause of the 2026-07-24 "T1000 goes deaf" + * wedge). -128 is the busy/contended sentinel the noise-floor sampler + * expects (LoRaRadioBase::triggerNoiseFloorCalibrate retries next tick). + * Direct parity with sx126x_get_rssi_inst. */ + if (gpio_pin_get_dt(&data->hal_ctx.busy)) { + return -128; + } + if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { + return -128; + } + if (gpio_pin_get_dt(&data->hal_ctx.busy)) { + k_mutex_unlock(&data->spi_mutex); + return -128; + } + + if (lr11xx_radio_get_rssi_inst(&data->hal_ctx, &rssi) != LR11XX_STATUS_OK) { + k_mutex_unlock(&data->spi_mutex); + return -128; + } k_mutex_unlock(&data->spi_mutex); return (int16_t)rssi; @@ -987,6 +1067,16 @@ bool lr11xx_is_receiving(const struct device *dev) { struct lr11xx_data *data = dev->data; + /* GPIO BUSY read FIRST: during the autonomous duty-cycle sleep phase the + * chip is asleep (BUSY high) and by definition not mid-packet, so report + * "not receiving" WITHOUT issuing get_irq_status — which, like GetRssiInst, + * can wedge the chip BUSY-high if issued into DC sleep. Same guard as + * lr11xx_get_rssi_inst; this path runs on every TX gate so it is the other + * async command that could hit the sleep window. */ + if (gpio_pin_get_dt(&data->hal_ctx.busy)) { + return false; + } + /* Non-blocking — skip if SPI busy */ if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { return false; @@ -1416,6 +1506,66 @@ static int lr11xx_hw_init(struct lr11xx_data *data, return 0; } +/* ── Wedge-recovery watchdog ────────────────────────────────────────── */ + +/* Detects the "BUSY stuck high, DIO1 silent" wedge and re-arms RX with a + * hardware reset. False-positive free by construction: a healthy chip — + * continuous RX or autonomous DC cycling — always drops BUSY low within one + * duty-cycle period, so a *continuous* BUSY-high dwell longer than any + * legitimate cycle, with no DIO1 event for >12 s, can only be a genuine wedge. + * Passive: reads the BUSY GPIO only (no SPI), so it cannot itself disturb the + * chip or race the autonomous DC state machine. Runs on its own queue at the + * DIO1 priority; the confirm poll yields every 2 ms so a real packet arriving + * mid-confirm is still processed promptly on the DIO1 queue. */ +static void lr11xx_wedge_watchdog_handler(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct lr11xx_data *data = CONTAINER_OF(dwork, struct lr11xx_data, + wedge_work); + const struct lr11xx_config *cfg = data->dev->config; + + /* Only monitor steady RX. */ + if (!data->in_rx_mode || data->tx_active || !data->configured) { + goto rearm; + } + + /* Recent DIO1 activity = chip provably alive. */ + if ((k_uptime_get_32() - data->last_dio1_ms) < LR11XX_WEDGE_IDLE_MS) { + goto rearm; + } + + /* Idle: either quiet RF (chip still DC-cycling) or a wedge. Poll BUSY + * continuously for one confirm window — a healthy chip shows a low edge + * within a cycle; a wedge stays high the whole window. */ + { + uint32_t start = k_uptime_get_32(); + bool saw_low = false; + + while ((k_uptime_get_32() - start) < LR11XX_WEDGE_CONFIRM_MS) { + if (!gpio_pin_get_dt(&data->hal_ctx.busy)) { + saw_low = true; + break; + } + k_msleep(2); + } + if (saw_low) { + goto rearm; /* alive */ + } + } + + LOG_ERR("LR11xx wedge (BUSY stuck, DIO1 silent >%ums) — hardware reset", + LR11XX_WEDGE_IDLE_MS); + k_mutex_lock(&data->spi_mutex, K_FOREVER); + lr11xx_hardware_reset(data, cfg); + lr11xx_start_rx(data, cfg); + k_mutex_unlock(&data->spi_mutex); + data->last_dio1_ms = k_uptime_get_32(); + +rearm: + k_work_schedule_for_queue(&data->wedge_wq, &data->wedge_work, + K_MSEC(LR11XX_WEDGE_CHECK_MS)); +} + /* ── Driver init (lightweight — runs at POST_KERNEL) ────────────────── */ static int lr11xx_lora_init(const struct device *dev) @@ -1437,6 +1587,17 @@ static int lr11xx_lora_init(const struct device *dev) K_PRIO_COOP(7), NULL); k_thread_name_set(&data->dio1_wq.thread, "lr11xx_dio1"); + /* Wedge-recovery watchdog on its own queue (see handler). Same priority + * as DIO1 so it never preempts RX; its confirm poll yields every 2 ms. */ + k_work_init_delayable(&data->wedge_work, lr11xx_wedge_watchdog_handler); + k_work_queue_start(&data->wedge_wq, lr11xx_wedge_wq_stack, + K_THREAD_STACK_SIZEOF(lr11xx_wedge_wq_stack), + K_PRIO_COOP(7), NULL); + k_thread_name_set(&data->wedge_wq.thread, "lr11xx_wedge"); + data->last_dio1_ms = k_uptime_get_32(); + k_work_schedule_for_queue(&data->wedge_wq, &data->wedge_work, + K_MSEC(LR11XX_WEDGE_CHECK_MS)); + /* Check SPI bus */ if (!spi_is_ready_dt(&cfg->bus)) { LOG_ERR("SPI bus not ready"); diff --git a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch index 6ea8b90..8550495 100644 --- a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch +++ b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch @@ -68,7 +68,7 @@ index 17689720dd2..09982dc2e70 100644 return -EINVAL; } diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c -index 30243ba5dc7..5315e5d80ef 100644 +index 30243ba5dc7..f0794661515 100644 --- a/drivers/lora/native/sx126x/sx126x.c +++ b/drivers/lora/native/sx126x/sx126x.c @@ -6,14 +6,21 @@ @@ -282,17 +282,31 @@ index 30243ba5dc7..5315e5d80ef 100644 sx126x_hal_set_rf_switch(dev, enable, tx); } } -@@ -455,6 +573,215 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev) +@@ -455,6 +573,229 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev) } #endif /* CONFIG_PM_DEVICE */ +/* -- Duty-cycle support helpers -- */ + -+/* Watchdog sampling period. Must exceed the longest legitimate -+ * preamble-detect -> header gap at any supported preset so that two -+ * consecutive parked sightings can never be one real packet (worst -+ * case SF12/BW62.5, 16-sym preamble: ~1.9 s). */ -+#define SX126X_DC_WATCHDOG_MS 3000 ++/* Watchdog sampling period, scaled to the active preset. The two-strike ++ * design needs the period to exceed the longest legitimate preamble-detect -> ++ * header gap so two consecutive parked sightings can never both fall inside ++ * one real packet's gap (which would abort a live reception). A fixed value ++ * has to be sized for the slowest preset (SF12/BW62.5, ~1.9 s) -- ~15x larger ++ * than needed at SF8/BW62.5, so every false-preamble park then pins the radio ++ * in full RX for up to two periods, wasting the duty-cycle power savings. ++ * Scale it instead: 2x the (preamble+8)-symbol grace covers the gap with ++ * margin at every preset (SF8/BW62.5 ~0.3 s, SF12/BW62.5 ~3.1 s), floored so ++ * fast presets don't poll the SPI bus raw. Never costs packets -- a parked ++ * radio is still receiving; the period only bounds how long it stays parked. */ ++#define SX126X_DC_WATCHDOG_MIN_MS 250 ++ ++static uint32_t sx126x_dc_watchdog_ms(struct sx126x_data *data) ++{ ++ uint32_t ms = 2U * sx126x_preamble_grace_ms(data); ++ ++ return (ms < SX126X_DC_WATCHDOG_MIN_MS) ? SX126X_DC_WATCHDOG_MIN_MS : ms; ++} + +/* DS sec 9.6: the RX gain register (0x08AC) is not part of the warm-start + * retention memory. RxDutyCycle sleep->RX wakes are chip-internal warm @@ -317,7 +331,7 @@ index 30243ba5dc7..5315e5d80ef 100644 +{ + data->dc_watchdog_strikes = 0; + k_work_reschedule_for_queue(&data->dio1_wq, &data->dc_watchdog_work, -+ K_MSEC(SX126X_DC_WATCHDOG_MS)); ++ K_MSEC(sx126x_dc_watchdog_ms(data))); +} + +/* -- Lightweight RX restart -- */ @@ -492,13 +506,13 @@ index 30243ba5dc7..5315e5d80ef 100644 + +rearm: + k_work_schedule_for_queue(&data->dio1_wq, &data->dc_watchdog_work, -+ K_MSEC(SX126X_DC_WATCHDOG_MS)); ++ K_MSEC(sx126x_dc_watchdog_ms(data))); +} + static int sx126x_set_sleep(const struct device *dev) { struct sx126x_data *data = dev->data; -@@ -533,8 +860,16 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -533,8 +874,16 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta struct sx126x_data *data = dev->data; struct sx126x_rx_result result = { 0 }; uint8_t payload_len = 0, offset = 0; @@ -515,7 +529,7 @@ index 30243ba5dc7..5315e5d80ef 100644 /* Get received packet info */ ret = sx126x_get_rx_buffer_status(dev, &payload_len, &offset); if (ret < 0) { -@@ -544,9 +879,26 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -544,9 +893,26 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta /* Get signal quality */ sx126x_get_packet_status(dev, &result.rssi, &result.snr); @@ -545,7 +559,7 @@ index 30243ba5dc7..5315e5d80ef 100644 result.status = -EIO; } else { /* Read payload into shared buffer */ -@@ -564,20 +916,34 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -564,20 +930,34 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta } } @@ -592,7 +606,7 @@ index 30243ba5dc7..5315e5d80ef 100644 } } else { /* Sync mode */ -@@ -589,8 +955,69 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -589,8 +969,69 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta static void sx126x_handle_irq_timeout(const struct device *dev) { struct sx126x_data *data = dev->data; @@ -662,7 +676,7 @@ index 30243ba5dc7..5315e5d80ef 100644 sx126x_set_sleep(dev); if (data->tx_async_signal != NULL) { -@@ -633,10 +1060,46 @@ static void sx126x_irq_work_handler(struct k_work *work) +@@ -633,10 +1074,46 @@ static void sx126x_irq_work_handler(struct k_work *work) sx126x_handle_irq_rx_done(dev, irq_status); } @@ -709,7 +723,7 @@ index 30243ba5dc7..5315e5d80ef 100644 /* Re-enable the DIO1 interrupt for the next event (unless sleeping) */ if (atomic_get(&data->state) != SX126X_REST_STATE) { sx126x_hal_dio1_irq_enable(dev); -@@ -682,7 +1145,7 @@ static int sx126x_lora_config(const struct device *dev, +@@ -682,7 +1159,7 @@ static int sx126x_lora_config(const struct device *dev, /* Configure PA and TX power based on chip variant and frequency */ ret = sx126x_hal_configure_tx_params(dev, config->tx_power, config->frequency, @@ -718,7 +732,7 @@ index 30243ba5dc7..5315e5d80ef 100644 if (ret < 0) { goto out; } -@@ -698,6 +1161,29 @@ static int sx126x_lora_config(const struct device *dev, +@@ -698,6 +1175,29 @@ static int sx126x_lora_config(const struct device *dev, goto out; } @@ -748,7 +762,7 @@ index 30243ba5dc7..5315e5d80ef 100644 /* Set sync word */ ret = sx126x_set_sync_word(dev, config->public_network); if (ret < 0) { -@@ -721,6 +1207,9 @@ out: +@@ -721,6 +1221,9 @@ out: return ret; } @@ -758,7 +772,7 @@ index 30243ba5dc7..5315e5d80ef 100644 static int sx126x_lora_send_async(const struct device *dev, uint8_t *data_buf, uint32_t data_len, struct k_poll_signal *async) -@@ -738,11 +1227,27 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -738,11 +1241,27 @@ static int sx126x_lora_send_async(const struct device *dev, return -EINVAL; } @@ -788,7 +802,7 @@ index 30243ba5dc7..5315e5d80ef 100644 k_mutex_lock(&data->lock, K_FOREVER); ret = sx126x_ensure_ready(dev); -@@ -752,6 +1257,59 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -752,6 +1271,59 @@ static int sx126x_lora_send_async(const struct device *dev, return ret; } @@ -848,7 +862,7 @@ index 30243ba5dc7..5315e5d80ef 100644 data->tx_async_signal = async; k_msgq_purge(&data->tx_msgq); -@@ -777,6 +1335,31 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -777,6 +1349,31 @@ static int sx126x_lora_send_async(const struct device *dev, /* Enable antenna and set TX path */ sx126x_set_rf_path(dev, true, true); @@ -880,7 +894,7 @@ index 30243ba5dc7..5315e5d80ef 100644 /* Start transmission with 10 second timeout */ ret = sx126x_set_tx(dev, 10000); if (ret < 0) { -@@ -847,6 +1430,8 @@ static int sx126x_lora_recv(const struct device *dev, uint8_t *data_buf, +@@ -847,6 +1444,8 @@ static int sx126x_lora_recv(const struct device *dev, uint8_t *data_buf, } data->rx_cb = NULL; @@ -889,7 +903,7 @@ index 30243ba5dc7..5315e5d80ef 100644 k_msgq_purge(&data->rx_msgq); /* Set packet parameters for variable length reception */ -@@ -918,6 +1503,8 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -918,6 +1517,8 @@ static int sx126x_lora_recv_async(const struct device *dev, /* Stop async reception */ data->rx_cb = NULL; data->rx_cb_user_data = NULL; @@ -898,7 +912,7 @@ index 30243ba5dc7..5315e5d80ef 100644 if (atomic_cas(&data->state, SX126X_STATE_RX, SX126X_STATE_IDLE)) { sx126x_set_standby(dev, SX126X_STANDBY_RC); sx126x_set_sleep(dev); -@@ -932,6 +1519,19 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -932,6 +1533,19 @@ static int sx126x_lora_recv_async(const struct device *dev, return -EINVAL; } @@ -918,7 +932,7 @@ index 30243ba5dc7..5315e5d80ef 100644 if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_RX)) { LOG_ERR("Busy"); k_mutex_unlock(&data->lock); -@@ -945,8 +1545,18 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -945,8 +1559,18 @@ static int sx126x_lora_recv_async(const struct device *dev, return ret; } @@ -937,7 +951,7 @@ index 30243ba5dc7..5315e5d80ef 100644 /* Set packet parameters */ ret = sx126x_set_packet_params(dev, -@@ -959,6 +1569,7 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -959,6 +1583,7 @@ static int sx126x_lora_recv_async(const struct device *dev, SX126X_LORA_IQ_INVERTED : SX126X_LORA_IQ_STANDARD); if (ret < 0) { data->rx_cb = NULL; @@ -945,7 +959,7 @@ index 30243ba5dc7..5315e5d80ef 100644 sx126x_set_sleep(dev); k_mutex_unlock(&data->lock); return ret; -@@ -971,11 +1582,21 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -971,11 +1596,21 @@ static int sx126x_lora_recv_async(const struct device *dev, ret = sx126x_set_rx(dev, 0); if (ret < 0) { data->rx_cb = NULL; @@ -967,7 +981,7 @@ index 30243ba5dc7..5315e5d80ef 100644 k_mutex_unlock(&data->lock); return 0; } -@@ -1051,7 +1672,7 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, +@@ -1051,7 +1686,7 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, /* Set PA config and TX power */ ret = sx126x_hal_configure_tx_params(dev, tx_power, frequency, @@ -976,7 +990,7 @@ index 30243ba5dc7..5315e5d80ef 100644 if (ret < 0) { sx126x_set_sleep(dev); k_mutex_unlock(&data->lock); -@@ -1083,14 +1704,671 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, +@@ -1083,14 +1718,671 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, return 0; } @@ -1655,7 +1669,7 @@ index 30243ba5dc7..5315e5d80ef 100644 }; #ifdef CONFIG_PM_DEVICE -@@ -1112,6 +2390,7 @@ static int sx126x_pm_action(const struct device *dev, +@@ -1112,6 +2404,7 @@ static int sx126x_pm_action(const struct device *dev, static int sx126x_init(const struct device *dev) { struct sx126x_data *data = dev->data; @@ -1663,7 +1677,7 @@ index 30243ba5dc7..5315e5d80ef 100644 int ret; /* Initialize data structures */ -@@ -1121,9 +2400,29 @@ static int sx126x_init(const struct device *dev) +@@ -1121,9 +2414,29 @@ static int sx126x_init(const struct device *dev) k_msgq_init(&data->rx_msgq, (char *)&data->rx_result, sizeof(struct sx126x_rx_result), 1); k_work_init(&data->irq_work, sx126x_irq_work_handler); diff --git a/zephcore/patches/zephyr/0010-uarte-pm-suspend-bounded-rxto-wait.patch b/zephcore/patches/zephyr/0010-uarte-pm-suspend-bounded-rxto-wait.patch new file mode 100644 index 0000000..d77e1dd --- /dev/null +++ b/zephcore/patches/zephyr/0010-uarte-pm-suspend-bounded-rxto-wait.patch @@ -0,0 +1,35 @@ +diff --git a/drivers/serial/uart_nrfx_uarte.c b/drivers/serial/uart_nrfx_uarte.c +index 60e8c23cd17..e78ba2af1fe 100644 +--- a/drivers/serial/uart_nrfx_uarte.c ++++ b/drivers/serial/uart_nrfx_uarte.c +@@ -3011,11 +3011,26 @@ static int uarte_pm_suspend(const struct device *dev) + } + } + #endif ++ bool rxto; ++ + nrf_uarte_task_trigger(uarte, NRF_UARTE_TASK_STOPRX); +- while (!nrf_uarte_event_check(uarte, NRF_UARTE_EVENT_RXTO)) { +- /* Busy wait for event to register */ +- Z_SPIN_DELAY(2); +- } ++ /* ZephCore: bound the RXTO wait. Upstream spins here ++ * forever. In interrupt-driven mode the RX runs on a ++ * 1-byte buffer with no ENDRX_STARTRX short, so the ++ * receiver stops after each byte until the ISR re-arms ++ * it -- and the ENDRX interrupt is disabled just above. ++ * If a byte finishes in that window the receiver is ++ * already stopped and un-rearmed, STOPRX then produces ++ * no RXTO, and this loop never exits, hanging the caller ++ * (the main mesh thread on GPS standby -> whole node ++ * wedges: no LoRa/USB/BLE service until reboot). 4 ms is ++ * orders of magnitude past the normal sub-microsecond ++ * RXTO latency; on timeout we fall through to the ++ * unconditional nrf_uarte_disable() below, which ++ * force-stops the receiver regardless. */ ++ NRFX_WAIT_FOR(nrf_uarte_event_check(uarte, NRF_UARTE_EVENT_RXTO), ++ 2000, 2, rxto); ++ (void)rxto; + nrf_uarte_event_clear(uarte, NRF_UARTE_EVENT_RXSTARTED); + nrf_uarte_event_clear(uarte, NRF_UARTE_EVENT_RXTO); + nrf_uarte_event_clear(uarte, NRF_UARTE_EVENT_ENDRX);