From 4473b261eeae772e76f44007bb8d311af5c7257f Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:27:54 +0200 Subject: [PATCH] rx path fixes, dc.restart wired in to lr chips, TX timeout magic number is gone --- zephcore/adapters/radio/LR1110Radio.cpp | 10 ++ zephcore/adapters/radio/LR1110Radio.h | 7 + zephcore/adapters/radio/LR2021Radio.cpp | 10 ++ zephcore/adapters/radio/LR2021Radio.h | 7 + zephcore/adapters/radio/LoRaRadioBase.cpp | 8 +- .../drivers/lora/lr11xx/lr11xx_lora.c | 163 ++++++++++++++++-- .../drivers/lora/lr11xx/lr11xx_lora.h | 22 +++ .../drivers/lora/lr20xx/lr20xx_lora.c | 126 ++++++++++++-- .../drivers/lora/lr20xx/lr20xx_lora.h | 22 +++ .../0014-lora-sx126x-tx-timeout-airtime.patch | 70 ++++++++ zephcore/src/main_companion.cpp | 14 ++ 11 files changed, 426 insertions(+), 33 deletions(-) create mode 100644 zephcore/patches/zephyr/0014-lora-sx126x-tx-timeout-airtime.patch diff --git a/zephcore/adapters/radio/LR1110Radio.cpp b/zephcore/adapters/radio/LR1110Radio.cpp index 5761fcf..54fb650 100644 --- a/zephcore/adapters/radio/LR1110Radio.cpp +++ b/zephcore/adapters/radio/LR1110Radio.cpp @@ -31,6 +31,16 @@ void LR1110Radio::begin() LoRaRadioBase::begin(); } +uint32_t LR1110Radio::getDutyCycleTimeoutRestarts() const +{ + return lr11xx_get_dc_timeout_restarts(_dev); +} + +void LR1110Radio::resetDutyCycleTimeoutRestarts() +{ + lr11xx_reset_dc_timeout_restarts(_dev); +} + /* ── Hardware primitives ──────────────────────────────────────────────── */ bool LR1110Radio::hwConfigure(const struct lora_modem_config &cfg) diff --git a/zephcore/adapters/radio/LR1110Radio.h b/zephcore/adapters/radio/LR1110Radio.h index e31ff99..7c43023 100644 --- a/zephcore/adapters/radio/LR1110Radio.h +++ b/zephcore/adapters/radio/LR1110Radio.h @@ -18,6 +18,13 @@ public: void begin() override; + /* Duty-cycle false-preamble re-arm count, behind `get dc.restarts`. + * Without these the base class's stub answers 0 forever, and the one + * instrument for tuning the RX window reads clean on a node that is + * re-arming constantly. */ + uint32_t getDutyCycleTimeoutRestarts() const override; + void resetDutyCycleTimeoutRestarts() override; + protected: /* Hardware primitives */ bool hwConfigure(const struct lora_modem_config &cfg) override; diff --git a/zephcore/adapters/radio/LR2021Radio.cpp b/zephcore/adapters/radio/LR2021Radio.cpp index 47b9f64..4f12c9e 100644 --- a/zephcore/adapters/radio/LR2021Radio.cpp +++ b/zephcore/adapters/radio/LR2021Radio.cpp @@ -51,6 +51,16 @@ void LR2021Radio::resetStats() lr20xx_reset_freq_offset(_dev); } +uint32_t LR2021Radio::getDutyCycleTimeoutRestarts() const +{ + return lr20xx_get_dc_timeout_restarts(_dev); +} + +void LR2021Radio::resetDutyCycleTimeoutRestarts() +{ + lr20xx_reset_dc_timeout_restarts(_dev); +} + int LR2021Radio::formatFreqErrorStatus(char *buf, int cap) { struct lr20xx_freq_offset_stats st; diff --git a/zephcore/adapters/radio/LR2021Radio.h b/zephcore/adapters/radio/LR2021Radio.h index ed2a6d6..181d063 100644 --- a/zephcore/adapters/radio/LR2021Radio.h +++ b/zephcore/adapters/radio/LR2021Radio.h @@ -18,6 +18,13 @@ public: void begin() override; + /* Duty-cycle false-preamble re-arm count, behind `get dc.restarts`. + * Without these the base class's stub answers 0 forever, and the one + * instrument for tuning the RX window reads clean on a node that is + * re-arming constantly. */ + uint32_t getDutyCycleTimeoutRestarts() const override; + void resetDutyCycleTimeoutRestarts() override; + /* LoRa side detectors — LR2021-only multi-SF receive. */ bool configSideDetectors(const uint8_t *sfs, uint8_t num) override; diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index cda8d17..f624d70 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -6,6 +6,7 @@ #include "LoRaRadioBase.h" #include "radio_common.h" #include +#include /* MAX_TRANS_UNIT */ #include #include #include @@ -94,7 +95,12 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board, * it from this thread costs no SPI and cannot race the radio. */ uint32_t LoRaRadioBase::txWaitBudgetMs() const { - uint32_t air = lora_airtime(_dev, _tx_len ? _tx_len : 255); + /* _tx_len is published by startSendRaw() before it releases + * _tx_start_sem, so by the time this thread runs it is always the length + * of the transmit in flight — the fallback is defensive only, and uses + * the protocol maximum rather than a literal so it tracks MAX_TRANS_UNIT + * if the FIFO bound ever moves. */ + uint32_t air = lora_airtime(_dev, _tx_len ? _tx_len : MAX_TRANS_UNIT); /* All four drivers behind this class implement .airtime (native sx126x, * lr11xx, lr20xx, loramac-node sx127x), and lora_airtime() dereferences 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 d779ad2..429f7a9 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -99,19 +99,33 @@ struct lr11xx_data { uint32_t dc_rx_ms; uint32_t dc_sleep_ms; + /* Duty-cycle re-arms triggered by a timeout, i.e. the false-preamble + * case: UM §7.2.6 restarts the window timer with 2*RxPeriod + + * SleepPeriod on preamble detection, and when that expires with no + * packet the chip leaves the loop and the host puts it back. A climbing + * rate means the window is catching noise rather than packets. Exposed + * as `get dc.restarts`, which reported a hardcoded 0 on this radio until + * nothing counted them. */ + atomic_t dc_timeout_restarts; + /* CAD state */ lora_cad_cb cad_cb; void *cad_user_data; struct k_sem cad_sem; int cad_result; /* No cad_active flag: there was one, written at four sites and read at - * none. The SX126x driver's copy IS read (sx126x_handle_irq_timeout() - * returns early on it), so this one was inherited without the read that - * gave it a purpose. Not needed here: the TIMEOUT branch below is only - * reachable during a CAD when the chip took the CAD->TX exit, and that - * path sets tx_active first. Exclusion between CADs comes from both - * entry points (LBT from startSendRaw, probes from cadMaintenance) - * running on the mesh loop thread. */ + * none. The SX126x driver's copy IS read — sx126x_handle_irq_timeout() + * returns early on it — so this was inherited without the read that gave + * it a purpose. + * + * It is not needed here, and the reason is the locking model, not luck: + * sx126x_irq_work_handler() takes no lock at all, so there its timeout + * path genuinely races a CAD being set up on the mesh thread and the + * flag is the only thing preventing a teardown. This handler holds + * spi_mutex across its whole body, and every CAD entry point holds that + * same mutex across SetStandby -> clear_irq_status(ALL) -> SetCad — so + * the two cannot interleave, and any TIMEOUT latched before the CAD is + * discarded by the clear inside lr11xx_do_cad(). */ /* Adaptive-CAD: signed offset applied to the per-SF base detPeak on * every LBT CAD; cad_probe_peak overrides for one calibration probe. */ int8_t cad_peak_offset; @@ -448,7 +462,16 @@ static void lr11xx_start_rx(struct lr11xx_data *data, * Packet params are NOT re-applied here — they persist through SetRx. * RadioLib (Arduino) also skips re-applying packet params on RX restart. * Only TX changes pld_len, and TX→RX goes through full start_rx(). */ -static void lr11xx_restart_rx(struct lr11xx_data *data) +/* Returns 0 if the receiver is believed to be back on air, <0 if a command the + * re-arm depends on was rejected and the caller should escalate. + * + * "Believed" on the duty-cycle path: verifying there would mean a GetStatus + * after SetRxDutyCycle, and that NSS edge terminates the very cycle being + * checked (UM §7.2.6) — the probe would cause the fault it looks for. The check + * sits on the SetStandby instead, which is the command that actually fails on a + * wedged chip and is safe to poll because the standby has just ended any live + * cycle. */ +static int lr11xx_restart_rx(struct lr11xx_data *data) { void *ctx = &data->hal_ctx; @@ -461,17 +484,66 @@ static void lr11xx_restart_rx(struct lr11xx_data *data) * or a false-preamble 2*rx+sleep timeout dropped the chip to * standby). Re-apply boost — same warm-start caution as * start_rx. */ + + /* SetStandby first. The cycle is not necessarily over when we + * get here: UM §7.2.6 terminates the loop on exactly three + * things — a packet detected, a host SetStandby, or an NSS wake + * from sleep — and a LoRa header error is none of them (it + * raises no RX_DONE), so the chip is still cycling on that path. + * Re-arming then means driving an NSS edge into a live cycle, + * and the manual is explicit about that case: the device "is + * woken up from Sleep mode with a falling edge of NSS. In that + * case, the user should send the SetStandby() command to avoid + * race conditions". LR2021 DS §6.3.8 says the same in nearly + * the same words, and there the missing standby was observed on + * hardware as a latched CMD_ERROR with DIO1 stuck high, five + * strikes to a reset, ~88 ms deaf per noise header error. Free + * when the chip is already in standby. */ + lr11xx_system_set_standby(ctx, LR11XX_SYSTEM_STANDBY_CFG_RC); + + /* Safe to poll: the standby above ended any live cycle, so this + * NSS edge has nothing left to disturb. A chip that will not + * even take a SetStandby will not take a duty cycle either. */ + { + lr11xx_system_stat1_t s1 = { 0 }; + + if (lr11xx_system_get_status(ctx, &s1, NULL, NULL) == + LR11XX_STATUS_OK && + s1.command_status != LR11XX_SYSTEM_CMD_STATUS_OK && + s1.command_status != LR11XX_SYSTEM_CMD_STATUS_DATA) { + LOG_WRN("restart_rx: standby rejected (cmd=%d) " + "before duty-cycle re-arm", + (int)s1.command_status); + return -EIO; + } + } + if (data->rx_boost_enabled) { lr11xx_radio_cfg_rx_boosted(ctx, true); } lr11xx_radio_set_rx_duty_cycle(ctx, data->dc_rx_ms, data->dc_sleep_ms, LR11XX_RADIO_RX_DUTY_CYCLE_MODE_RX); - } else { - lr11xx_radio_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF); - /* RX boost persists through SetRx — no re-apply needed. */ + data->in_rx_mode = true; + return 0; } + lr11xx_radio_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF); + /* RX boost persists through SetRx — no re-apply needed. */ data->in_rx_mode = true; + + /* No duty cycle armed, so polling is free of the NSS hazard. */ + { + lr11xx_system_stat2_t s2 = { 0 }; + + if (lr11xx_system_get_status(ctx, NULL, &s2, NULL) == + LR11XX_STATUS_OK && + s2.chip_mode != LR11XX_SYSTEM_CHIP_MODE_RX) { + LOG_WRN("restart_rx: chip is in mode %d, not RX", + (int)s2.chip_mode); + return -EIO; + } + } + return 0; } /* ── DIO1 IRQ handler (work queue, thread context) ──────────────────── */ @@ -652,7 +724,24 @@ static void lr11xx_dio1_work_handler(struct k_work *work) lr11xx_start_rx(data, cfg); rx_restarted = true; } else { - lr11xx_restart_rx(data); + /* Under a duty cycle this is the false-preamble case: + * the 2*RxPeriod + SleepPeriod window the preamble + * detect restarted (UM §7.2.6) expired with no packet, + * so the chip left the loop. Counting it is what makes + * `get dc.restarts` mean something on this radio. */ + if (data->rx_duty_cycle_enabled) { + atomic_inc(&data->dc_timeout_restarts); + } + if (lr11xx_restart_rx(data) < 0) { + /* Escalate to the full path: forces standby, + * reprograms the modem and re-issues SetRx from + * scratch. The LR11xx counterpart of the + * SX126x's retry-from-sleep. If that fails too + * the stuck-DIO1 counter still reaches its + * hardware reset. */ + LOG_WRN("Timeout: light re-arm failed — full RX restart"); + lr11xx_start_rx(data, cfg); + } rx_restarted = true; } } @@ -921,12 +1010,40 @@ static int lr11xx_lora_send_async(const struct device *dev, lr11xx_reset_rx_busy_signals(data); lr11xx_hal_enable_dio1_irq(&data->hal_ctx); - /* Store signal and start TX. 10 s timeout: worst legal preset - * (SF12 / BW62.5, max payload) exceeds 5 s airtime — a shorter - * timeout would abort mid-transmission. Matches the SX126x driver. */ + /* Store signal and start TX. + * + * Chip-side Tx safeguard, scaled from airtime. UM §7.2.3: "If the RTC + * event fires before the end of transmission, it will trigger a TIMEOUT + * IRQ, and stop the device transmission" — so this must exceed real + * airtime. The fixed 10 s it replaces carried the right reasoning with + * the arithmetic never done: the comment said the worst preset "exceeds + * 5 s", but SF12/BW62.5 at 255 bytes CR 4/8 is 28.6 s, so 10 s truncated + * every packet from 76 B up there, and from 31 B at SF12/BW31.25. + * + * Floored at the previous 10000 so nothing that works today tightens. + * Programmed in RTC steps rather than through the millisecond wrapper: + * lr11xx_radio_convert_time_in_ms_to_rtc_step() computes ms * 32768 in + * uint32 and overflows above 131071 ms, which SF12/BW7.81 at 255 B + * (~229 s) reaches. Saturating at the 24-bit field is 512 s. */ data->tx_signal = async; data->tx_active = true; - lr11xx_radio_set_tx(ctx, 10000); + { + uint32_t air_ms = lr11xx_lora_airtime(dev, data_len); + uint32_t tmo_ms = air_ms + (air_ms / 4U) + 500U; + uint64_t steps; + + if (tmo_ms < 10000U) { + tmo_ms = 10000U; + } + steps = ((uint64_t)tmo_ms * 32768U) / 1000U; + if (steps > 0x00FFFFFFU) { + steps = 0x00FFFFFFU; + } + LOG_DBG("SET_TX: airtime=%u ms, timeout=%u ms (%u steps)", + air_ms, tmo_ms, (uint32_t)steps); + lr11xx_radio_set_tx_with_timeout_in_rtc_step(ctx, + (uint32_t)steps); + } k_mutex_unlock(&data->spi_mutex); return 0; @@ -1241,6 +1358,20 @@ bool lr11xx_is_receiving(const struct device *dev) return false; } +uint32_t lr11xx_get_dc_timeout_restarts(const struct device *dev) +{ + struct lr11xx_data *data = dev->data; + + return (uint32_t)atomic_get(&data->dc_timeout_restarts); +} + +void lr11xx_reset_dc_timeout_restarts(const struct device *dev) +{ + struct lr11xx_data *data = dev->data; + + atomic_set(&data->dc_timeout_restarts, 0); +} + uint32_t lr11xx_get_wakeup_time_us(const struct device *dev) { const struct lr11xx_config *cfg = dev->config; 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 ce50097..a22879e 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h @@ -63,6 +63,28 @@ void lr11xx_set_rx_boost(const struct device *dev, bool enable); */ uint32_t lr11xx_get_wakeup_time_us(const struct device *dev); +/** + * @brief Duty-cycle re-arms caused by a timeout since boot or reset + * + * Counts the false-preamble case: UM §7.2.6 restarts the window timer with + * 2*RxPeriod + SleepPeriod on preamble detection, and when that expires with no + * packet the chip leaves the loop and the driver puts it back. A climbing rate + * means the RX window is catching noise rather than packets. Backs + * `get dc.restarts`, which reported a hardcoded 0 on this radio before anything + * counted them. + * + * @param dev LoRa device + * @return re-arm count + */ +uint32_t lr11xx_get_dc_timeout_restarts(const struct device *dev); + +/** + * @brief Clear the duty-cycle timeout re-arm counter + * + * @param dev LoRa device + */ +void lr11xx_reset_dc_timeout_restarts(const struct device *dev); + /** * @brief Get a random number from the radio * 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 82e1c62..ffee9e4 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c @@ -95,6 +95,15 @@ struct lr20xx_data { bool rx_boost_enabled; bool rx_boost_applied; + /* Duty-cycle re-arms triggered by a timeout, i.e. the false-preamble + * case: the preamble-restarted rx_max_time + cycle_time window expired + * with no packet, so the chip left the loop and the host put it back. + * The rate is the instrument for duty-cycle tuning — a climbing count + * means the window is catching noise, not packets. Exposed as + * `get dc.restarts`, which reported a hardcoded 0 on this radio until + * nothing counted them. */ + atomic_t dc_timeout_restarts; + /* Stored duty-cycle timing from recv_duty_cycle() — re-arm paths * must reuse these exact values, never recompute: the window sizing * (detection budget, datasheet completion rule, wake transition) is @@ -108,11 +117,18 @@ struct lr20xx_data { struct k_sem cad_sem; int cad_result; /* 0=free, 1=busy, <0=error */ /* No cad_active flag: there was one, written at five sites and read at - * none. It read like a concurrent-CAD guard and guarded nothing — the - * real exclusion is that every CAD entry point (LBT from startSendRaw, - * probes from cadMaintenance) runs on the mesh loop thread, so two CADs - * cannot overlap by construction. A flag nothing tests is worse than no - * flag: it invites the next reader to assume the invariant is enforced. */ + * none. The SX126x driver's copy IS read — sx126x_handle_irq_timeout() + * returns early on it — so this was inherited without the read that gave + * it a purpose. + * + * It is not needed here, and the reason is the locking model, not luck: + * sx126x_irq_work_handler() takes no lock at all, so there its timeout + * path genuinely races a CAD being set up on the mesh thread and the + * flag is the only thing preventing a teardown. This handler holds + * spi_mutex across its whole body, and every CAD entry point holds that + * same mutex across SetStandby -> clear_irq_status(ALL) -> SetCad — so + * the two cannot interleave, and any TIMEOUT latched before the CAD is + * discarded by the clear inside lr20xx_do_cad(). */ /* Adaptive-CAD: signed offset applied to the per-SF base detPeak on * every LBT CAD; cad_probe_peak overrides for one calibration probe. */ int8_t cad_peak_offset; @@ -1075,7 +1091,17 @@ static void lr20xx_start_rx(struct lr20xx_data *data, /* ── Lightweight RX restart (no modem reconfig) ─────────────────────── */ -static void lr20xx_restart_rx(struct lr20xx_data *data) +/* Returns 0 if the receiver is believed to be back on air, <0 if a command the + * re-arm depends on was rejected and the caller should escalate. + * + * "Believed" is deliberate on the duty-cycle path: verification there would mean + * a GetStatus after SetRxDutyCycle, and an NSS falling edge terminates the cycle + * being checked (DS §6.3.8) — the probe would cause the fault it looks for. So + * the check is placed on the SetStandby that precedes the re-arm, which is the + * command that actually fails when the chip is wedged, and is safe to poll + * because the standby has just ended any live cycle. Continuous RX has no such + * constraint and is checked after SetRx. */ +static int lr20xx_restart_rx(struct lr20xx_data *data) { void *ctx = &data->hal_ctx; @@ -1123,26 +1149,62 @@ static void lr20xx_restart_rx(struct lr20xx_data *data) if (lr20xx_system_get_status(ctx, NULL, &s2, NULL) == LR20XX_STATUS_OK && s2.chip_mode == LR20XX_SYSTEM_CHIP_MODE_RX) { data->in_rx_mode = true; - return; + return 0; } } if (data->rx_duty_cycle_enabled) { - /* SetStandby first: a header/CRC error does not terminate the - * duty-cycle loop (DS 6.3.8 ends it on packet reception), and - * re-arming a live cycle is refused — the refusal latches - * CMD_ERROR and holds DIO1 high. Near-free when already in - * STDBY_RC. See ARCHITECTURE.md 5.5.1. */ + lr20xx_system_stat1_t s1 = { 0 }; + + /* SetStandby first. A header error does not terminate the + * duty-cycle loop — DS §6.3.8 lists exactly three terminators + * (packet reception, a host SetStandby, an NSS wake from sleep) + * and a header error is none of them — so the chip can still be + * cycling here. Re-arming then drives an NSS edge into a live + * cycle, and the datasheet is explicit for that case: on a wake + * "by the device with a falling edge of NSS. A SetStandby + * command should also be sent, to avoid the race conditions". + * Observed on hardware without it: latched CMD_ERROR, DIO1 held + * high, five strikes to a reset, ~88 ms deaf per noise header + * error. Near-free when already in STDBY_RC. + * See ARCHITECTURE.md 5.5.1. */ lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC); + + /* Safe to poll: the standby above ended any live cycle, so this + * NSS edge has nothing left to disturb. If the chip would not + * even take a SetStandby, arming a duty cycle on top of it is + * pointless — say so and let the caller escalate. */ + if (lr20xx_system_get_status(ctx, &s1, NULL, NULL) == + LR20XX_STATUS_OK && + s1.command_status != 2 && s1.command_status != 3) { + LOG_WRN("restart_rx: standby rejected (cmd=%d) before " + "duty-cycle re-arm", s1.command_status); + return -EIO; + } + lr20xx_apply_rx_duty_cycle(data); - } else { - lr20xx_radio_common_set_rx_with_timeout_in_rtc_step( - ctx, 0xFFFFFF); + data->in_rx_mode = true; + return 0; } + lr20xx_radio_common_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF); CHECK_CMD(ctx, "restart_rx set_rx"); data->in_rx_mode = true; + + /* No duty cycle armed, so polling is free of the NSS hazard. */ + { + lr20xx_system_stat2_t s2 = { 0 }; + + if (lr20xx_system_get_status(ctx, NULL, &s2, NULL) == + LR20XX_STATUS_OK && + s2.chip_mode != LR20XX_SYSTEM_CHIP_MODE_RX) { + LOG_WRN("restart_rx: chip is in mode %d, not RX", + s2.chip_mode); + return -EIO; + } + } + return 0; } /* Is the receiver still live? Conservative: true (leave it alone) whenever it @@ -1474,8 +1536,26 @@ static void lr20xx_dio1_work_handler(struct k_work *work) lr20xx_start_rx(data, cfg); rx_restarted = true; } else { + /* Under a duty cycle this is the false-preamble case: + * the preamble-restarted rx_max_time + cycle_time window + * expired with no packet, so the chip left the loop. + * Counting it is what makes `get dc.restarts` mean + * something on this radio. */ + if (data->rx_duty_cycle_enabled) { + atomic_inc(&data->dc_timeout_restarts); + } LOG_DBG("Timeout IRQ — restarting RX"); - lr20xx_restart_rx(data); + if (lr20xx_restart_rx(data) < 0) { + /* The light re-arm could not get the chip back on + * air. Escalate to the full path, which forces + * standby, reprograms the modem and re-issues + * SetRx from scratch — the LR20xx counterpart of + * the SX126x's retry-from-sleep. If that fails + * too the chip is wedged, and the stuck-DIO1 + * counter below still reaches its reset. */ + LOG_WRN("Timeout: light re-arm failed — full RX restart"); + lr20xx_start_rx(data, cfg); + } rx_restarted = true; } } @@ -2294,6 +2374,20 @@ void lr20xx_reset_freq_offset(const struct device *dev) k_mutex_unlock(&data->spi_mutex); } +uint32_t lr20xx_get_dc_timeout_restarts(const struct device *dev) +{ + struct lr20xx_data *data = dev->data; + + return (uint32_t)atomic_get(&data->dc_timeout_restarts); +} + +void lr20xx_reset_dc_timeout_restarts(const struct device *dev) +{ + struct lr20xx_data *data = dev->data; + + atomic_set(&data->dc_timeout_restarts, 0); +} + uint32_t lr20xx_get_wakeup_time_us(const struct device *dev) { const struct lr20xx_config *cfg = dev->config; 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 c08ddcd..6bb6512 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h @@ -65,6 +65,28 @@ uint32_t lr20xx_get_freq_offset(const struct device *dev, */ void lr20xx_reset_freq_offset(const struct device *dev); +/** + * @brief Duty-cycle re-arms caused by a timeout since boot or reset + * + * Counts the false-preamble case: the preamble-restarted rx_max_time + + * cycle_time window expired with no packet, the chip left the duty-cycle loop, + * and the driver put it back. A climbing rate means the RX window is catching + * noise rather than packets, which is the signal for retuning it. Backs + * `get dc.restarts`, which reported a hardcoded 0 on this radio before anything + * counted them. + * + * @param dev LoRa device + * @return re-arm count + */ +uint32_t lr20xx_get_dc_timeout_restarts(const struct device *dev); + +/** + * @brief Clear the duty-cycle timeout re-arm counter + * + * @param dev LoRa device + */ +void lr20xx_reset_dc_timeout_restarts(const struct device *dev); + /** * @brief Radio deaf time for one duty-cycle wake transition, in microseconds * diff --git a/zephcore/patches/zephyr/0014-lora-sx126x-tx-timeout-airtime.patch b/zephcore/patches/zephyr/0014-lora-sx126x-tx-timeout-airtime.patch new file mode 100644 index 0000000..eeeea64 --- /dev/null +++ b/zephcore/patches/zephyr/0014-lora-sx126x-tx-timeout-airtime.patch @@ -0,0 +1,70 @@ +Scale the chip-side Tx timeout from airtime instead of a fixed 10 s. + +sx126x_lora_send_async() programmed SetTx with a constant 10000 ms. DS section +13.1.5 is explicit that when the Tx timer fires the transmission is stopped, so +that constant is not a safeguard on slow presets -- it is a truncation. + +Airtime of a 255-byte packet at CR 4/8 reaches 17.7 s at SF10/BW31.25, 15.9 s at +SF11/BW62.5 and 28.6 s at SF12/BW62.5. Against a 10 s ceiling those presets lost +every packet from 136 B, 154 B and 76 B up respectively, cut mid-transmission, with +nothing in the log connecting the loss to this line: TX_DONE never arrives, the +timeout handler re-arms RX, and the host wait is what eventually reports it. + +Scale as airtime + 25% + 500 ms, floored at the previous 10000 so no preset that +works today gets a tighter deadline. Clamp to 262143 ms: sx126x_set_tx() converts +via SX126X_MS_TO_TIMEOUT (64 steps/ms) and sys_put_be24(), which truncates +silently above that. The ceiling is 262 s against a worst reachable airtime of +~229 s (SF12/BW7.81 at 255 B), so it never binds on a real packet. + +Kept separate from 0003 because that patch cannot be regenerated from the tree, +and the line being changed is upstream context within it rather than an added +line -- same reason 0011, 0012 and 0013 are separate. + +diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c +--- a/drivers/lora/native/sx126x/sx126x.c ++++ b/drivers/lora/native/sx126x/sx126x.c +@@ -1432,6 +1432,7 @@ + + static int sx126x_lora_cad(const struct device *dev, k_timeout_t timeout); + static uint32_t sx126x_cad_timeout_ms(struct sx126x_data *data); ++static uint32_t sx126x_lora_airtime(const struct device *dev, uint32_t data_len); + + static int sx126x_lora_send_async(const struct device *dev, + uint8_t *data_buf, uint32_t data_len, +@@ -1614,8 +1615,34 @@ + } + #endif /* CONFIG_LORA_SX126X_NATIVE_STANDALONE */ + +- /* Start transmission with 10 second timeout */ +- ret = sx126x_set_tx(dev, 10000); ++ /* Chip-side Tx safeguard, scaled from airtime. ++ * ++ * DS §13.1.5: when the Tx timer fires the transmission is stopped, so ++ * this has to exceed real airtime — and a fixed 10 s cannot. A 255-byte ++ * packet at CR 4/8 is 17.7 s at SF10/BW31.25 and 28.6 s at SF12/BW62.5; ++ * on those presets the old constant truncated every packet from 136 B ++ * and 76 B up respectively, mid-transmission and with nothing in the log ++ * tying the loss to this line. ++ * ++ * Floored at the previous 10000 so no preset that works today gets a ++ * tighter deadline, and clamped to the 24-bit field: sx126x_set_tx() ++ * converts with SX126X_MS_TO_TIMEOUT (64 steps/ms at 15.625 us) and then ++ * sys_put_be24(), which would silently truncate above 262143 ms. That ++ * ceiling is 262 s against a worst reachable airtime of ~229 s ++ * (SF12/BW7.81, 255 B), so the clamp never binds on a real packet. */ ++ { ++ uint32_t air_ms = sx126x_lora_airtime(dev, data_len); ++ uint32_t tmo_ms = air_ms + (air_ms / 4U) + 500U; ++ ++ if (tmo_ms < 10000U) { ++ tmo_ms = 10000U; ++ } ++ if (tmo_ms > 262143U) { ++ tmo_ms = 262143U; ++ } ++ LOG_DBG("SET_TX: airtime=%u ms, timeout=%u ms", air_ms, tmo_ms); ++ ret = sx126x_set_tx(dev, tmo_ms); ++ } + if (ret < 0) { + goto out_error; + } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index ae33c53..0d5f48a 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -762,9 +762,23 @@ public: void clearStats() override { lora_radio.resetStats(); + lora_radio.resetDutyCycleTimeoutRestarts(); companion_mesh.resetStats(); } + /* Duty-cycle false-preamble re-arm count. Without these the + * CommonCLICallbacks default answers 0 forever, so `get dc.restarts` + * read clean on every companion regardless of what the radio was doing + * — and the companion is the role the duty cycle actually runs in. + * Mirrors RepeaterMesh::getDutyCycleTimeoutRestarts(). */ + uint32_t getDutyCycleTimeoutRestarts() const override { + return lora_radio.getDutyCycleTimeoutRestarts(); + } + + void resetDutyCycleTimeoutRestarts() override { + lora_radio.resetDutyCycleTimeoutRestarts(); + } + /* Temp radio params — deferred; stub for now. */ void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override {