From 5c4ac21bd46903a1acbef859d7f4ebebf02fc863 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:50:48 +0200 Subject: [PATCH] refactor patches and bump to 1.17.2 --- releasenotes/RELEASE_NOTES_1.17.2-zephcore.md | 56 ++ zephcore/ARCHITECTURE.md | 61 +- zephcore/CMakeLists.txt | 2 +- zephcore/adapters/radio/LR1110Radio.cpp | 15 + zephcore/adapters/radio/LR1110Radio.h | 3 + zephcore/adapters/radio/LR2021Radio.cpp | 17 + zephcore/adapters/radio/LR2021Radio.h | 3 + zephcore/adapters/radio/LoRaRadioBase.cpp | 98 ++ zephcore/adapters/radio/LoRaRadioBase.h | 31 + zephcore/adapters/radio/SX126xRadio.cpp | 21 + zephcore/adapters/radio/SX126xRadio.h | 2 + zephcore/boards/common/zephcore_common.conf | 2 +- zephcore/helpers/CommonCLI.cpp | 4 +- zephcore/include/mesh/Radio.h | 5 + .../drivers/lora/lr11xx/lr11xx_lora.c | 96 ++ .../drivers/lora/lr11xx/lr11xx_lora.h | 26 + .../drivers/lora/lr20xx/lr20xx_lora.c | 174 +++- .../drivers/lora/lr20xx/lr20xx_lora.h | 24 + .../zephyr/0001-lora-lr11xx-build.patch | 35 - .../0001-lora-lr11xx-lr20xx-build.patch | 53 ++ .../zephyr/0002-lora-lr20xx-build.patch | 22 - .../zephyr/0003-lora-sx126x-native.patch | 894 ++++++++++++++---- .../0004-lora-sx127x-62k5-bandwidth.patch | 15 + .../0011-lora-sx126x-band-rssi-cal.patch | 286 ------ .../zephyr/0012-lora-sx126x-dio1-wakeup.patch | 43 - .../0013-lora-sx126x-rx-latch-deadline.patch | 175 ---- .../0014-lora-sx126x-tx-timeout-airtime.patch | 70 -- zephcore/src/Dispatcher.cpp | 5 + 28 files changed, 1382 insertions(+), 856 deletions(-) create mode 100644 releasenotes/RELEASE_NOTES_1.17.2-zephcore.md delete mode 100644 zephcore/patches/zephyr/0001-lora-lr11xx-build.patch create mode 100644 zephcore/patches/zephyr/0001-lora-lr11xx-lr20xx-build.patch delete mode 100644 zephcore/patches/zephyr/0002-lora-lr20xx-build.patch delete mode 100644 zephcore/patches/zephyr/0011-lora-sx126x-band-rssi-cal.patch delete mode 100644 zephcore/patches/zephyr/0012-lora-sx126x-dio1-wakeup.patch delete mode 100644 zephcore/patches/zephyr/0013-lora-sx126x-rx-latch-deadline.patch delete mode 100644 zephcore/patches/zephyr/0014-lora-sx126x-tx-timeout-airtime.patch diff --git a/releasenotes/RELEASE_NOTES_1.17.2-zephcore.md b/releasenotes/RELEASE_NOTES_1.17.2-zephcore.md new file mode 100644 index 0000000..0db9725 --- /dev/null +++ b/releasenotes/RELEASE_NOTES_1.17.2-zephcore.md @@ -0,0 +1,56 @@ +# ZephCore 1.17.2-zephcore + +A **minor release.** Three small changes, two of which only affect specific hardware. If you are running +1.17.1 on anything other than a XIAO nRF52840 or an Ikoka Nano 30 dBm, there is little reason to flash +this. + +> [!NOTE] +> **Most people can skip this.** The one group that should not is **XIAO nRF52840 and Ikoka Nano 30 dBm** +> owners — those boards get twice the charge current and a battery-sense pin that no longer sits at the +> nRF52840's absolute maximum. Everyone else: safe to skip and pick these up with the next real release. +> +> Straight upgrade from 1.17.1 either way — bonds and data survive, no re-pairing. + +--- + +## XIAO nRF52840 / Ikoka Nano 30 dBm: 100 mA charging, and a pin fix + +The BQ25100 on the XIAO module selects its charge current from the HICHG pin, and left floating — the +reset default — it charges at **50 mA**. That pin is now driven, so these boards charge at **100 mA**, +roughly halving the time to fill a pack. Matches Arduino MeshCore's `variants/xiao_nrf52` and +`variants/ikoka_nano_nrf`. + +The battery ADC divider enable (P0.14) is now held low permanently instead of toggled per reading. +Driving it high puts about **3.6 V on the AIN7 node with a 4.2 V cell** — the nRF52840 GPIO absolute +maximum, with no margin, and over it on a warm pack. Holding it low parks the node near 1.4 V and costs +about 2.8 µA, which is nothing beside BLE and LoRa receive. + +> [!WARNING] +> **If your XIAO sits on a carrier board with its own charger** — a TP4056 module, for example — delete +> the `hichg` GPIO hog from the board DTS before flashing. Two chargers must not drive the same pack. + +## Less receiver downtime when the channel is busy — LR2021 only + +On the MeshTracker X1, every transmit that listen-before-talk refused rebuilt the receiver **twice** +before retrying, because the driver restored RX and then the layer above it did too. One of those is now +a no-op, halving the time the node spends deaf per refused attempt. + +No effect on any other board. + +## A muted node now says so + +If listen-before-talk keeps refusing to transmit, that is reported after four seconds of continuous +refusal and flagged in the node's error status. Previously it was logged below debug level with no +counter, so a node that could hear the mesh but never got a word in looked completely idle. + +All roles, all boards. + +--- + +## Known limitations + +- **The two radio changes are not on-air validated** — both are build-verified and reasoned from the + datasheet, not measured on a live mesh. +- **The reported X1 difficulty logging in to repeaters is not fixed here** and remains under + investigation. The listen-before-talk reporting above will make that failure visible in the log if it + is the cause, which is one of several open hypotheses. diff --git a/zephcore/ARCHITECTURE.md b/zephcore/ARCHITECTURE.md index d12dc54..d4e906d 100644 --- a/zephcore/ARCHITECTURE.md +++ b/zephcore/ARCHITECTURE.md @@ -20,6 +20,7 @@ 12. [BLE Protocol Reference](#12-ble-protocol-reference) 13. [Data Storage](#13-data-storage) 14. [Key Call Flows](#14-key-call-flows) +15. [Watchdogs and Recovery Mechanisms](#15-watchdogs-and-recovery-mechanisms) --- @@ -477,6 +478,7 @@ The custom `lr11xx_lora.c` driver handles several LR1110 firmware bugs: - **RX buffer drift**: Buffer base shifts 4 bytes per packet → `clear_rxbuffer()` after every RX - **Header error**: Can shift buffer pointer → standby before RX restart - **DIO1 stuck HIGH**: 5-cycle detection → full hardware reset + recovery +- **BUSY-high wedge**: a command racing the autonomous `SetRxDutyCycle` sleep phase can leave the chip BUSY-high with DIO1 low. No IRQ ever fires, so the event-driven driver never re-arms and the node goes permanently deaf. A wedge-recovery watchdog on its own work queue (`lr11xx_wedge`, `K_PRIO_COOP(7)`) checks every 3 s: after 12 s of DIO1 silence it polls the BUSY GPIO continuously for 250 ms, and a dwell with no low edge means a genuine wedge → hardware reset + RX restart. False-positive free by construction — a healthy chip, continuous or duty-cycled, always drops BUSY low within one cycle. Reads the GPIO only (no SPI), so it cannot itself disturb the chip or race the autonomous DC state machine. - **RX duty cycle**: wired via `SetRxDutyCycle` MODE_RX, sized by the shared adapter math (same as SX126x). The earlier "broken, 23-40% loss" verdict was a window-sizing bug (over-sleep + no header budget), not a chip defect — default-off, HW-verify before production use. ### 5.5 SX127x and LR2021 Paths @@ -926,15 +928,23 @@ Applied automatically at CMake configure time; a failed patch aborts the configu | Patch | Risk | Purpose | |-------|------|---------| -| 0001-lora-lr11xx-build | LOW | Integrates LR11xx driver into Zephyr LoRa build | -| 0002-lora-lr20xx-build | LOW | Integrates LR20xx driver into Zephyr LoRa build | -| 0003-lora-sx126x-native | **HIGH** | DIO1 work queue, duty cycle, RX-busy gating, extension API, errata workarounds | +| 0001-lora-lr11xx-lr20xx-build | LOW | Registers the LR11xx and LR20xx drivers in the Zephyr LoRa build | +| 0003-lora-sx126x-native | **HIGH** | DIO1 work queue, duty cycle, CAD, RX-busy gating, band RSSI/AGC calibration, PA/OCP tuning, extension API | | 0004-lora-sx127x-62k5-bandwidth | LOW | Adds 62.5 kHz bandwidth to the loramac-node backend | -| 0005-gnss-air530z-easy | MEDIUM | EASY ephemeris + removes PM (prevents deadlocks) | +| 0005-gnss-config-and-version-query | MEDIUM | Air530Z nav-rate config + `$PCAS06` version query; NMEA generic dump | | 0006-blobs-py | LOW | Fix `west blobs fetch` KeyError | | 0007-spi-gpio-native-linux | LOW | Wires native-Linux SPI/GPIO drivers into the Zephyr build | | 0008-flash-sim-per-node-file | LOW | Flash simulator defaults to per-node settings file (native Linux) | | 0009-display-ssd16xx-fill-ram-white | LOW | E-paper full-refresh-to-white anti-ghosting helper | +| 0010-uarte-pm-suspend-bounded-rxto-wait | MEDIUM | Bounds the nRF UARTE STOPRX/RXTO spin on PM suspend; unbounded upstream, wedges the mesh thread | + +**One patch per file.** No upstream file is touched by more than one patch, so +apply order is irrelevant and no patch can be anchored inside another's added +lines. Consolidated 2026-08-20 (15 → 9): `0002` folded into `0001`, and +`0011`–`0015` folded into `0003`, each keeping its rationale as a `== section ==` +in that patch's preamble. `0002` is a deliberate numbering gap. Add a new +sx126x fix by regenerating `0003`, never by stacking an `0016` on it — see +`WEST_UPDATE.md`. New drivers in `patches/zephyr-new/` (LR11xx, LR20xx, native-Linux SPI/GPIO, DTS bindings) are copied — not patched — into the Zephyr tree at configure time. @@ -1173,3 +1183,46 @@ main event loop (every 5s) → Dispatcher::maintenanceLoop() → EMA: floor += round((sample - floor) / 8) → clamp [-120, -50] dBm ``` + +--- + +## 15. Watchdogs and Recovery Mechanisms + +**There is no hardware watchdog.** No `CONFIG_WATCHDOG`, no `task_wdt`, no `wdt` +node enabled on any board — the `wdt` nodes visible in board `.dts` files are +inherited SoC definitions, and the `RTCWDT` references in the TTGO board configs +concern the ESP32 ROM bootloader's own watchdog, not something ZephCore arms. +The only consumer of the concept is the boot breadcrumb in `main_companion.cpp`, +which reads `RESET_WATCHDOG` out of `hwinfo_get_reset_cause()` and reports it in +the "Restarted:" v-contact message (see [6.2.1](#621-v-contact-loopback-admin-contact)). + +Everything below is software: bounded stall detection in the layer that owns the +state machine. Each entry names what it recovers, because several are +deliberately diagnostic-only and recover nothing. + +### 15.1 Named watchdogs + +| Watchdog | Location | Period | Trigger → action | +|----------|----------|--------|------------------| +| SX126x parked-RX | `patches/zephyr/0003-lora-sx126x-native.patch` (`sx126x_dc_watchdog_handler`) | `2×(preamble+8)` symbols, floor 250 ms | Duty-cycle only. Two consecutive samples showing the chip parked in full RX after a false preamble detect → re-arm the DC cycle. Two-strike so a sighting can never fall inside one real packet's preamble→header gap and abort a live reception. Counted by `get dc.restarts`. | +| LR11xx wedge-recovery | `lr11xx_lora.c` (`lr11xx_wedge_watchdog_handler`, own `lr11xx_wedge` queue) | 3 s | DIO1 silent >12 s **and** BUSY continuously high for a 250 ms confirm poll → hardware reset + RX restart. See [5.4](#54-lr1110-driver-errata-workarounds). | +| Radio stall | `Dispatcher::maintenanceLoop()` | `RADIO_STALL_THRESHOLD_MS` (8 s) | Radio neither in RX nor mid-TX for the whole window → latch `ERR_EVENT_STARTRX_TIMEOUT`. **Diagnostic only.** The bit is surfaced everywhere: repeater/room-server `stats`, binary telemetry, MQTT uplink, and the companion's BLE device-status response. | +| Contact-dump stall | `main_companion.cpp` housekeeping | housekeeping tick | Dump active and the iterator cursor unmoved across a whole tick → re-post `MESH_EVENT_CONTACT_ITER`. The dump is pumped solely by the BLE/USB tx-idle callback, so one lost kick would strand it silently. | +| BLE advertising | `main_companion.cpp` housekeeping | housekeeping tick | Enabled, not connected, not advertising → `zephcore_ble_set_enabled(true)`. Covers transient `bt_le_adv_start` failure, which would otherwise leave the node undiscoverable until reboot. | +| BLE TX timeout | `ZephyrBLE.cpp` (`BLE_TX_TIMEOUT_MS`) | 2 s | `ble_tx_in_progress` set with no completion callback → clear the flag and proceed to the next TX. Sits 3 s inside the 5 s supervision timeout. | +| USB partial-input | `ZephyrCompanionUSB.cpp` (`USB_FRAME_TIMEOUT_MS`) | byte-driven | Mid-frame or mid-text-line too long → reset parser to `USB_RX_IDLE`. **Not a timer** — it only runs when bytes arrive, so it never wakes a sleeping node. | +| Buzzer safety | `helpers/ui/buzzer.c` (`BUZZER_TONE_MAX_MS`) | 2 s | Note handler stalls → silence PWM + amp off. The PWM block is autonomous and would otherwise drive the pin forever after a crash or work-queue stall. | + +### 15.2 Unnamed, same job + +Timeouts and deadlines that are watchdogs in everything but name: + +| Mechanism | Location | Bounds | +|-----------|----------|--------| +| RX-latch payload deadline | all three custom radio paths: SX126x `patch 0013`, `lr11xx_lora.c`, `lr20xx_lora.c` (`header_seen_at_ms` + `*_max_payload_ms()`) | A `HEADER_VALID` whose packet never completes would pin the TX gate closed and silently mute the node — continuous RX has no symbol timer. Released at 255-byte airtime +25% +100 ms. See [5.2.1](#521-rx-busy-gate-tx-during-rx-prevention). | +| Stuck-DIO1 counter | `lr11xx_lora.c` and `lr20xx_lora.c` | 5 empty DIO1 cycles → hardware reset. Counting rather than timing; on the LR11xx it complements the wedge watchdog rather than replacing it. | +| CAD timeout | `Dispatcher::checkSend()` | 4 s (~20 retry attempts) → `ERR_EVENT_CAD_TIMEOUT` + `recoverRxState()`, rather than falling through to TX. See [5.2.2](#522-cad-timeout-recovery). | +| Chip-side TX timeout | SX126x `SetTx` deadline (`patch 0014`, airtime +25% +500 ms, floored at 10 s, clamped 262143 ms); LR2021 `TIMEOUT` IRQ handler | The chip stops the transmission when this fires, so a fixed value is a truncation, not a safeguard — at SF12/BW62.5 the old flat 10 s cut every packet from 76 bytes up. | +| Serial partial-frame resync | `SerialCompanionTransport.c` (`FRAME_PARTIAL_TIMEOUT_MS`) | 2 s. Parser-level only — deliberately **not** a session or idle timeout; an idle-but-connected companion sits in `RX_IDLE` indefinitely. | +| TCP send timeout | `LinuxTCPTransport.c` | Native sim only. A peer that can't accept a frame in the window is wedged → close it, rather than hang the whole queue. | +| Bounded RXTO wait | `patches/zephyr/0010-uarte-pm-suspend-bounded-rxto-wait.patch` | `uarte_pm_suspend()` busy-waits for RXTO with no timeout upstream. Landing in the STOPRX race with bytes in flight spins forever on the main thread and wedges the entire mesh (observed: RAK3401 1W repeater on 1.16.6, CLI answering only `-> busy`). Backstop for the GPS UART PM path in [7.3](#73-gps-adaptersgps). | diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 0a6192c..7327bb6 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -525,7 +525,7 @@ add_definitions(-DFIRMWARE_BUILD_EPOCH=${ZEPHCORE_BUILD_EPOCH}u) # release is tagged/named, and what the Mesh America catalog uses as its version # key — so the configurator can match a running device against the catalog. Keep # all four identical; the release workflow reads this value directly. -set(ZEPHCORE_FIRMWARE_VERSION "1.17.1-zephcore") +set(ZEPHCORE_FIRMWARE_VERSION "1.17.2-zephcore") add_definitions(-DFIRMWARE_VERSION="${ZEPHCORE_FIRMWARE_VERSION}") add_subdirectory(lib/monocypher) diff --git a/zephcore/adapters/radio/LR1110Radio.cpp b/zephcore/adapters/radio/LR1110Radio.cpp index 54fb650..26e1c6a 100644 --- a/zephcore/adapters/radio/LR1110Radio.cpp +++ b/zephcore/adapters/radio/LR1110Radio.cpp @@ -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); diff --git a/zephcore/adapters/radio/LR1110Radio.h b/zephcore/adapters/radio/LR1110Radio.h index 7c43023..fefac42 100644 --- a/zephcore/adapters/radio/LR1110Radio.h +++ b/zephcore/adapters/radio/LR1110Radio.h @@ -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; diff --git a/zephcore/adapters/radio/LR2021Radio.cpp b/zephcore/adapters/radio/LR2021Radio.cpp index 4f12c9e..26b830d 100644 --- a/zephcore/adapters/radio/LR2021Radio.cpp +++ b/zephcore/adapters/radio/LR2021Radio.cpp @@ -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: diff --git a/zephcore/adapters/radio/LR2021Radio.h b/zephcore/adapters/radio/LR2021Radio.h index 181d063..60c87d7 100644 --- a/zephcore/adapters/radio/LR2021Radio.h +++ b/zephcore/adapters/radio/LR2021Radio.h @@ -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 */ diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index f624d70..ce5f0e9 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -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 diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index 74afceb..e08d685 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -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; diff --git a/zephcore/adapters/radio/SX126xRadio.cpp b/zephcore/adapters/radio/SX126xRadio.cpp index 8931679..ba6b833 100644 --- a/zephcore/adapters/radio/SX126xRadio.cpp +++ b/zephcore/adapters/radio/SX126xRadio.cpp @@ -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); diff --git a/zephcore/adapters/radio/SX126xRadio.h b/zephcore/adapters/radio/SX126xRadio.h index 31754f2..f90b168 100644 --- a/zephcore/adapters/radio/SX126xRadio.h +++ b/zephcore/adapters/radio/SX126xRadio.h @@ -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; diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index c325408..9cace31 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -156,7 +156,7 @@ CONFIG_BT_DIS_FW_REV=y # of truth for the C side, injected as -DFIRMWARE_VERSION). This Kconfig value can't # read a C macro, so it must be bumped here too. Phones that read DIS and phones # that query CMD_DEVICE_QUERY should see the same version. -CONFIG_BT_DIS_FW_REV_STR="1.17.1-zephcore" +CONFIG_BT_DIS_FW_REV_STR="1.17.2-zephcore" CONFIG_BT_DIS_SW_REV=y CONFIG_BT_DIS_SW_REV_STR="Zephyr" CONFIG_BT_DIS_PNP=n diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index a324b81..a84bab6 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -482,7 +482,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch zephcore_buzzer_mode_name(mode)); #endif } else if (memcmp(config, "agc.reset.interval", 18) == 0) { - strcpy(reply, "Removed - use rxduty instead"); + strcpy(reply, "Removed - Automatic AGC reset is on"); } else if (memcmp(config, "multi.acks", 10) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->multi_acks); } else if (memcmp(config, "allow.read.only", 15) == 0) { @@ -742,7 +742,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch * The prefs BYTE is retained (read/written, never acted on) — the * on-disk layout is byte-exact and shifting it would corrupt every * existing node's prefs. */ - strcpy(reply, "Removed - use rxduty instead"); + strcpy(reply, "Removed - Automatic AGC reset is on"); } else if (memcmp(config, "cad.auto ", 9) == 0) { if (memcmp(&config[9], "on", 2) == 0 || memcmp(&config[9], "off", 3) == 0) { _prefs->cad_auto = (config[9] == 'o' && config[10] == 'n') ? 1 : 0; diff --git a/zephcore/include/mesh/Radio.h b/zephcore/include/mesh/Radio.h index 56c0f67..286d962 100644 --- a/zephcore/include/mesh/Radio.h +++ b/zephcore/include/mesh/Radio.h @@ -50,6 +50,11 @@ public: * update stats, maybe step the staircase (auto mode). */ virtual void cadMaintenance() {} + /** Receiver hygiene: deaf-aware AGC unstick and temperature-drift + * recalibration. Both sleep the radio, so this is called only from the + * maintenance tick, never from a packet path. Default: no-op. */ + virtual void agcMaintenance() {} + /* Milliseconds until this radio's periodic work (noise floor sampling, * CAD probing/decay) next needs a call, or MAINTENANCE_IDLE when it has * nothing pending. Radios with no periodic work keep the default. */ 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 42cbaaa..5e1bfdc 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -1427,6 +1427,102 @@ uint32_t lr11xx_get_random(const struct device *dev) return random; } +/* ── Extension API: receiver hygiene ───────────────────────────────── + * + * Warm sleep to drop the analog front end, then recalibrate on the way back up + * — Semtech's stated remedy for a jammed AGC, and the same sequence as Arduino + * MeshCore's lr11x0ResetAGC() (helpers/radiolib/LR11x0Reset.h). Driven from + * LoRaRadioBase::agcMaintenance() on RX silence, never on the packet path. + * + * Unlike the LR2021, calibrate(0x3F) here DOES include image rejection and + * reverts it to the 902-928 MHz default, so the image cal must be re-issued — + * exactly what the Arduino helper does. That makes recal_fe moot on this part: + * both entry points pay it because the calibration forces it. + */ +static void lr11xx_agc_reset_locked(struct lr11xx_data *data) +{ + void *ctx = &data->hal_ctx; + lr11xx_system_sleep_cfg_t sleep_cfg = { + .is_warm_start = true, + .is_rtc_timeout = false, + }; + + lr11xx_system_set_sleep(ctx, sleep_cfg, 0); + k_sleep(K_USEC(500)); + data->hal_ctx.radio_is_sleeping = true; + + lr11xx_system_set_standby(ctx, LR11XX_SYSTEM_STANDBY_CFG_RC); + lr11xx_system_calibrate(ctx, 0x3F); + + if (data->configured) { + uint16_t freq_mhz = data->modem_cfg.frequency / 1000000; + + lr11xx_system_calibrate_image_in_mhz(ctx, freq_mhz - 4, + freq_mhz + 4); + } + + if (data->rx_boost_enabled) { + lr11xx_radio_cfg_rx_boosted(ctx, true); + data->rx_boost_applied = true; + } + + /* Contract with agcMaintenance(): leave the driver out of RX so the + * caller's startReceive() performs a real re-entry. */ + data->in_rx_mode = false; + lr11xx_reset_rx_busy_signals(data); +} + +void lr11xx_reset_agc(const struct device *dev) +{ + struct lr11xx_data *data = dev->data; + + if (!data->configured) { + return; + } + k_mutex_lock(&data->spi_mutex, K_FOREVER); + lr11xx_agc_reset_locked(data); + k_mutex_unlock(&data->spi_mutex); +} + +void lr11xx_recalibrate(const struct device *dev) +{ + /* Same sequence: calibrate(0x3F) already carries the image cal on this + * part, so there is nothing extra for the drift path to do. */ + lr11xx_reset_agc(dev); +} + +/* Junction temperature in whole degrees C, INT16_MIN if unavailable. + * UM: T = (Temp(10:0)/2047 * Vana - Vbe25) * 1000/VbeSlope + 25, with typicals + * Vana 1.35 V, Vbe25 0.7295 V, VbeSlope -1.7 mV/C. Runs once per maintenance + * pass, so the float is free. */ +int16_t lr11xx_get_chip_temp_c(const struct device *dev) +{ + struct lr11xx_data *data = dev->data; + uint16_t raw = 0; + lr11xx_status_t rc; + + if (!data->configured) { + return INT16_MIN; + } + if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { + return INT16_MIN; + } + rc = lr11xx_system_get_temp(&data->hal_ctx, &raw); + k_mutex_unlock(&data->spi_mutex); + + if (rc != LR11XX_STATUS_OK) { + return INT16_MIN; + } + + float v = ((float)(raw & 0x07FF) / 2047.0f) * 1.35f - 0.7295f; + float t = v * (1000.0f / -1.7f) + 25.0f; + + if (t < -128.0f || t > 127.0f) { + return INT16_MIN; /* implausible — treat as unavailable */ + } + return (int16_t)t; +} + /* ── 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 701de81..d12e123 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h @@ -95,6 +95,32 @@ void lr11xx_reset_dc_timeout_restarts(const struct device *dev); */ uint32_t lr11xx_get_random(const struct device *dev); +/** + * @brief Unstick a jammed AGC: warm sleep, then recalibrate. + * + * Leaves the driver out of RX — the caller must startReceive() afterwards. + * + * @param dev LoRa device + */ +void lr11xx_reset_agc(const struct device *dev); + +/** + * @brief Redo the frequency-dependent calibrations (temperature drift path). + * + * On this part calibrate(0x3F) already includes image rejection, so this is + * the same sequence as lr11xx_reset_agc(). + * + * @param dev LoRa device + */ +void lr11xx_recalibrate(const struct device *dev); + +/** + * @brief Junction temperature in whole degrees C, or INT16_MIN if unavailable. + * + * @param dev LoRa device + */ +int16_t lr11xx_get_chip_temp_c(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 f2476e7..79c43cc 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,7 @@ 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. @@ -963,6 +964,134 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data, * lr20xx_lora_recv_duty_cycle(). Returns true on success, false if no * timing has been provided yet (falls back to continuous RX). */ +/* Unstick the AGC, per Semtech's stated remedy: warm sleep to power the analog + * front end down, then recalibrate on the way back up. Same shape as Arduino + * MeshCore's sx126xResetAGC() / lr11x0ResetAGC() (helpers/radiolib/SX126xReset.h, + * LR11x0Reset.h) and + * as our own SX126x duty-cycle re-arm. Caller must hold spi_mutex. + * + * Two LR2021-specific differences from those ports, both saving real time: + * + * 1. NO CalibFE afterwards. On the SX126x and LR11x0, Calibrate(ALL) includes + * image calibration and resets it to the 902-928 MHz default, which is why + * both Arduino helpers re-issue calibrateImage()/calibrateImageRejection(). + * On the LR2021 the front end is a separate command: DS Table 6-28 lists the + * Calibrate blocks as LF_RC, HF_RC, PLL, AAF, MU and PA_OFF — no image, no + * front end. CalibFE (§6.4.2) owns those and its values survive retention + * sleep, so re-running it here would cost ~9 ms to reproduce what is already + * on the chip. The old (dead, never-called) lr20xx_reset_agc() ran it + * unconditionally, which is most of why it looked expensive. + * + * 2. No fixed post-Calibrate delay. DS §6.4.1: "Upon completion of the + * calibration, the chip automatically enters Standby RC mode" — BUSY + * deasserts and the next command's check_device_ready() waits on it, so the + * SX126x's 5 ms busy-wait has no counterpart here. + * + * Budget: 1 ms for the HAL's post-sleep-opcode wait, 1 ms for the warm start + * (DS Table 3-23 TSPDRCW, "retrieve calibration from retained registers"), plus + * the Calibrate itself, which the datasheet does not put a number on. */ +static void lr20xx_agc_reset_locked(struct lr20xx_data *data, bool recal_fe) +{ + void *ctx = &data->hal_ctx; + lr20xx_system_sleep_cfg_t sleep_cfg = { + .is_clk_32k_enabled = false, + /* Retention: keeps the modem config and the PRAM image (DS + * §22.3). A cold sleep here would drop both. */ + .is_ram_retention_enabled = true, + }; + + lr20xx_system_set_sleep_mode(ctx, &sleep_cfg, 0); + lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC); + + /* 0x6F = every block in Table 6-28. Legal only outside Rx/Tx, which the + * standby above guarantees. */ + lr20xx_system_calibrate(ctx, 0x6F); + + /* Re-assert the Rx path after calibrating AAF and MU, both of which are + * receive-chain blocks. Retention preserves the setting across the sleep + * (see the note in lr20xx_apply_rx_duty_cycle), but nothing documents what + * Calibrate leaves behind — and both Arduino helpers re-apply boost here + * for the same reason. One command. */ + 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; + } + + /* Front end only on the drift path. Image/FE calibration is valid for a + * temperature range rather than forever (DS: "necessary if there is a + * frequency change > 10MHz, or a temperature change > 10 C"), so it is + * exactly what a temperature swing invalidates — and exactly what a stuck + * AGC does not, which is why the AGC path skips its ~9 ms. */ + if (recal_fe && data->configured) { + lr20xx_calibrate_front_end(ctx, data->modem_cfg.frequency); + } + + /* Contract with LoRaRadioBase::agcMaintenance(): leave the driver OUT of + * RX. The chip is parked in STDBY_RC here, and the caller's + * startReceive() must perform a real re-entry — without this the + * idempotent fast paths in recv_async / recv_duty_cycle would see + * in_rx_mode still set, refresh the callback and return, leaving the + * receiver switched off. */ + data->in_rx_mode = false; + lr20xx_reset_rx_busy_signals(data); +} + +/* ── Extension API: receiver hygiene ─────────────────────────────────── */ + +void lr20xx_reset_agc(const struct device *dev) +{ + struct lr20xx_data *data = dev->data; + + if (!data->configured) { + return; + } + k_mutex_lock(&data->spi_mutex, K_FOREVER); + lr20xx_agc_reset_locked(data, false); + k_mutex_unlock(&data->spi_mutex); +} + +void lr20xx_recalibrate(const struct device *dev) +{ + struct lr20xx_data *data = dev->data; + + if (!data->configured) { + return; + } + k_mutex_lock(&data->spi_mutex, K_FOREVER); + lr20xx_agc_reset_locked(data, true); + k_mutex_unlock(&data->spi_mutex); +} + +/* Junction temperature in whole degrees C, INT16_MIN if unavailable. + * VALUE_FORMAT_UNIT returns 13.5sb: integer part in the high byte, fraction in + * the low. 13-bit resolution costs nothing here — this runs once per + * maintenance tick, never on the packet path. */ +int16_t lr20xx_get_chip_temp_c(const struct device *dev) +{ + struct lr20xx_data *data = dev->data; + uint16_t raw = 0; + lr20xx_status_t rc; + + if (!data->configured || lr20xx_is_chip_busy(dev)) { + return INT16_MIN; + } + if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { + return INT16_MIN; + } + rc = lr20xx_system_get_temp(&data->hal_ctx, + LR20XX_SYSTEM_VALUE_FORMAT_UNIT, + LR20XX_SYSTEM_MEAS_RES_13_BITS, + LR20XX_SYSTEM_TEMP_SRC_VBE, &raw); + k_mutex_unlock(&data->spi_mutex); + + if (rc != LR20XX_STATUS_OK) { + return INT16_MIN; + } + return (int16_t)((int8_t)(raw >> 8)); +} + static bool lr20xx_apply_rx_duty_cycle(struct lr20xx_data *data) { void *ctx = &data->hal_ctx; @@ -975,31 +1104,28 @@ 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. + /* Deliberately NO RX-boost re-apply here, unlike lr11xx_restart_rx(). + * The LR2021 retains it across the duty-cycle sleep and the datasheet is + * explicit on every step of that: + * §4.4.1 — retention "save[s] all previously programmed chip settings" + * §6.3.8 — "In RxDutyCycleMode, the transceiver context is retained + * during sleep operations", and the inter-window sleep is + * always the with-retention kind + * §7.3.4 — the Rx gain tables, whose entries are G1..G11 plus + * G12_boost0..7 and G13_boost0..7, are "automatically stored + * in the retention memory" + * SetRxPath is an ordinary command, so the boost is part of that retained + * context. SetAdditionalRegToRetain (§6.3.4) is not a counter-argument: + * it takes "the address of an additional peripheral register", for + * directly-poked internals outside the command-programmed context — the + * datasheet's only worked example is the SX1276 freq-hop compatibility + * register 0xF30A24, set by a workaround rather than by a command. * - * 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; - } + * This is where the SX126x and the LR2021 genuinely differ: the SX126x + * does NOT retain Rx gain (DS §9.6 requires pinning it into the warm-start + * retention list, and skipping that costs 3 dB on every wake after the + * first). Do not port that fix here by analogy — it is a real gap on that + * chip and a no-op write on this one. */ /* 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. 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 20a54fc..4596b71 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h @@ -168,6 +168,30 @@ int lr20xx_configure_side_detectors(const struct device *dev, */ uint32_t lr20xx_get_random(const struct device *dev); +/** + * @brief Unstick a jammed AGC: warm sleep, then recalibrate (no front end). + * + * Leaves the driver out of RX — the caller must startReceive() afterwards. + * + * @param dev LoRa device + */ +void lr20xx_reset_agc(const struct device *dev); + +/** + * @brief As lr20xx_reset_agc(), plus a front-end calibration at the operating + * frequency. For temperature drift, not for the packet path. + * + * @param dev LoRa device + */ +void lr20xx_recalibrate(const struct device *dev); + +/** + * @brief Junction temperature in whole degrees C, or INT16_MIN if unavailable. + * + * @param dev LoRa device + */ +int16_t lr20xx_get_chip_temp_c(const struct device *dev); + /** * @brief Set the adaptive-CAD operating detPeak offset * diff --git a/zephcore/patches/zephyr/0001-lora-lr11xx-build.patch b/zephcore/patches/zephyr/0001-lora-lr11xx-build.patch deleted file mode 100644 index 46936f0..0000000 --- a/zephcore/patches/zephyr/0001-lora-lr11xx-build.patch +++ /dev/null @@ -1,35 +0,0 @@ -diff --git a/drivers/lora/CMakeLists.txt b/drivers/lora/CMakeLists.txt -index 9cd035fcf2b..cda2f6dbaf3 100644 ---- a/drivers/lora/CMakeLists.txt -+++ b/drivers/lora/CMakeLists.txt -@@ -1,7 +1,10 @@ - # SPDX-License-Identifier: Apache-2.0 -+# ZephCore patch: adds lr11xx driver subdirectory. - - zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c) - -+add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx) -+ - # zephyr-keep-sorted-start - add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node) - add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORA_BASICS_MODEM lora-basics-modem) -diff --git a/drivers/lora/Kconfig b/drivers/lora/Kconfig -index 657bdb9ce28..0ebee4246f3 100644 ---- a/drivers/lora/Kconfig -+++ b/drivers/lora/Kconfig -@@ -5,6 +5,7 @@ - # - - # Top-level configuration file for LORA drivers. -+# ZephCore patch: adds lr11xx driver Kconfig. - - menuconfig LORA - bool "LoRa drivers" -@@ -60,6 +61,7 @@ config LORA_INIT_PRIORITY - - rsource "Kconfig.sx12xx" - rsource "Kconfig.rylrxxx" -+rsource "lr11xx/Kconfig" - rsource "lora-basics-modem/Kconfig" - rsource "native/Kconfig" - diff --git a/zephcore/patches/zephyr/0001-lora-lr11xx-lr20xx-build.patch b/zephcore/patches/zephyr/0001-lora-lr11xx-lr20xx-build.patch new file mode 100644 index 0000000..af1e216 --- /dev/null +++ b/zephcore/patches/zephyr/0001-lora-lr11xx-lr20xx-build.patch @@ -0,0 +1,53 @@ +Subject: [PATCH] lora: register the lr11xx and lr20xx drivers + +Hooks the two out-of-tree LoRa drivers into the drivers/lora build -- +add_subdirectory_ifdef() in CMakeLists.txt, rsource in Kconfig. The drivers +themselves have no upstream counterpart and are copied into the tree from +patches/zephyr-new/drivers/lora/{lr11xx,lr20xx}/, so there is nothing to patch +on that side. + +Sole owner of drivers/lora/CMakeLists.txt and drivers/lora/Kconfig. Was 0001 +(lr11xx) plus 0002 (lr20xx). 0003 carried both hunks as well, having been +regenerated at some point as a diff of the whole drivers/lora tree -- so on +every configure 0003 failed git apply --check, the stale-file fallback in +zephcore_apply_patches() reset these two files, and 0003 re-supplied the lines +0001 and 0002 had just added. Now that each file has exactly one owner, both +patches apply first time. + +diff --git a/drivers/lora/CMakeLists.txt b/drivers/lora/CMakeLists.txt +index 9cd035fcf2b..18d04ec099a 100644 +--- a/drivers/lora/CMakeLists.txt ++++ b/drivers/lora/CMakeLists.txt +@@ -1,7 +1,11 @@ + # SPDX-License-Identifier: Apache-2.0 ++# ZephCore patch: adds lr11xx driver subdirectory. + + zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c) + ++add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx) ++add_subdirectory_ifdef(CONFIG_LORA_LR20XX lr20xx) ++ + # zephyr-keep-sorted-start + add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node) + add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORA_BASICS_MODEM lora-basics-modem) +diff --git a/drivers/lora/Kconfig b/drivers/lora/Kconfig +index 657bdb9ce28..f8513b37044 100644 +--- a/drivers/lora/Kconfig ++++ b/drivers/lora/Kconfig +@@ -5,6 +5,7 @@ + # + + # Top-level configuration file for LORA drivers. ++# ZephCore patch: adds lr11xx driver Kconfig. + + menuconfig LORA + bool "LoRa drivers" +@@ -60,6 +61,8 @@ config LORA_INIT_PRIORITY + + rsource "Kconfig.sx12xx" + rsource "Kconfig.rylrxxx" ++rsource "lr11xx/Kconfig" ++rsource "lr20xx/Kconfig" + rsource "lora-basics-modem/Kconfig" + rsource "native/Kconfig" + diff --git a/zephcore/patches/zephyr/0002-lora-lr20xx-build.patch b/zephcore/patches/zephyr/0002-lora-lr20xx-build.patch deleted file mode 100644 index c92234d..0000000 --- a/zephcore/patches/zephyr/0002-lora-lr20xx-build.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/drivers/lora/CMakeLists.txt b/drivers/lora/CMakeLists.txt -index cda2f6dbaf3..18d04ec099a 100644 ---- a/drivers/lora/CMakeLists.txt -+++ b/drivers/lora/CMakeLists.txt -@@ -5,5 +5,6 @@ zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c) - - add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx) -+add_subdirectory_ifdef(CONFIG_LORA_LR20XX lr20xx) - - # zephyr-keep-sorted-start - add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node) -diff --git a/drivers/lora/Kconfig b/drivers/lora/Kconfig -index 0ebee4246f3..f8513b37044 100644 ---- a/drivers/lora/Kconfig -+++ b/drivers/lora/Kconfig -@@ -62,5 +62,6 @@ config LORA_INIT_PRIORITY - rsource "Kconfig.sx12xx" - rsource "Kconfig.rylrxxx" - rsource "lr11xx/Kconfig" -+rsource "lr20xx/Kconfig" - rsource "lora-basics-modem/Kconfig" - rsource "native/Kconfig" diff --git a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch index b604956..3d01d70 100644 --- a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch +++ b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch @@ -1,74 +1,227 @@ -diff --git a/drivers/lora/CMakeLists.txt b/drivers/lora/CMakeLists.txt -index 9cd035fcf2b..18d04ec099a 100644 ---- a/drivers/lora/CMakeLists.txt -+++ b/drivers/lora/CMakeLists.txt -@@ -1,7 +1,11 @@ - # SPDX-License-Identifier: Apache-2.0 -+# ZephCore patch: adds lr11xx driver subdirectory. - - zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c) - -+add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx) -+add_subdirectory_ifdef(CONFIG_LORA_LR20XX lr20xx) -+ - # zephyr-keep-sorted-start - add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node) - add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORA_BASICS_MODEM lora-basics-modem) -diff --git a/drivers/lora/Kconfig b/drivers/lora/Kconfig -index 657bdb9ce28..f8513b37044 100644 ---- a/drivers/lora/Kconfig -+++ b/drivers/lora/Kconfig -@@ -5,6 +5,7 @@ - # - - # Top-level configuration file for LORA drivers. -+# ZephCore patch: adds lr11xx driver Kconfig. - - menuconfig LORA - bool "LoRa drivers" -@@ -60,6 +61,8 @@ config LORA_INIT_PRIORITY - - rsource "Kconfig.sx12xx" - rsource "Kconfig.rylrxxx" -+rsource "lr11xx/Kconfig" -+rsource "lr20xx/Kconfig" - rsource "lora-basics-modem/Kconfig" - rsource "native/Kconfig" - -diff --git a/drivers/lora/loramac-node/sx12xx_common.c b/drivers/lora/loramac-node/sx12xx_common.c -index 17689720dd2..09982dc2e70 100644 ---- a/drivers/lora/loramac-node/sx12xx_common.c -+++ b/drivers/lora/loramac-node/sx12xx_common.c -@@ -195,8 +195,15 @@ static void sx12xx_ev_rx_error(void) - /** - * @brief Convert Zephyr bandwidth enum to loramac-node bandwidth index - * -- * The loramac-node library expects bandwidth as an index (0, 1, 2) into its -- * internal Bandwidths[] array, not the actual kHz value. -+ * The loramac-node library expects bandwidth as an index into its internal -+ * Bandwidths[] array (not the actual kHz value). The SX127x hardware -+ * supports sub-125 kHz LoRa bandwidths; index 3 maps to 62.5 kHz (reg BW=6). -+ * -+ * Index mapping (kept in sync with sx1276.c / sx1272.c): -+ * 0 → 125 kHz (reg BW 7) -+ * 1 → 250 kHz (reg BW 8) -+ * 2 → 500 kHz (reg BW 9) -+ * 3 → 62.5 kHz (reg BW 6) — SX127x only - * - * @param bandwidth Zephyr lora_signal_bandwidth enum value - * @param bw_idx Pointer to store the resulting bandwidth index -@@ -215,6 +222,9 @@ static int sx12xx_get_bandwidth_idx(enum lora_signal_bandwidth bandwidth, - case BW_500_KHZ: - *bw_idx = 2; - break; -+ case BW_62_KHZ: -+ *bw_idx = 3; -+ break; - default: - return -EINVAL; - } +Subject: [PATCH] lora: sx126x: ZephCore native driver + +Sole owner of every ZephCore change under drivers/lora/native/sx126x/ -- +sx126x.c, sx126x.h, sx126x_hal.c, sx126x_hal_common.c and sx126x_regs.h. No +other patch in this directory touches those files; see "Why this is one patch" +at the end. The extension-API header sx126x_ext.h is not patched in, it is +copied from patches/zephyr-new/. + +== Base driver == + +Upstream's sx126x is a minimal blocking LoRa driver. ZephCore needs an +event-driven one carrying the primitives the mesh dispatcher is built on. On +top of upstream: + + * DIO1 handling moved off the ISR onto a work queue. + + * RX duty cycle -- sx126x_lora_recv_duty_cycle(), with Rx-gain retention + across warm sleep (sx126x_retain_rx_gain(), DS Sec 9.6), + StopTimerOnPreamble, and a two-strike parked-RX watchdog + (sx126x_dc_watchdog_*) that re-arms the cycle after a false preamble detect + parks the chip in RX. sx126x_get_dc_timeout_restarts() backs `get + dc.restarts`. Window timing is computed by the caller + (LoRaRadioBase::startReceive); the driver accepts explicit periods only. + + * CAD as a first-class operation -- sx126x_lora_cad() / _cad_async(), + SF-scaled detection thresholds (sx126x_cad_detect_peak()), and the adaptive + tuning hooks sx126x_cad_set_peak_offset() / _cad_base_peak() / _cad_probe(). + + * The RX-busy gate the TX path interlocks against: the rx_packet_active latch + promoted on HEADER_VALID, the SF-aware preamble grace + (sx126x_preamble_grace_ms()) covering PREAMBLE_DETECTED, and + sx126x_is_receiving() as the single query -- non-destructive, it never + clears IRQ bits. + + * CAD-busy in-driver RX restore: sx126x_lora_send_async() accepts entry CAS + from both REST_STATE and RX, so the LBT branch tracks was_rx and has + sx126x_restart_rx() put the chip back in RX before -EBUSY propagates to the + C++ caller. sx126x_lora_recv_async() has an idempotent fast path, so the + failure-path startReceive() on that side is a no-op. + + * Extension API for the C++ adapter (sx126x_ext.h): sx126x_is_chip_busy, + sx126x_is_receiving, sx126x_set_rx_boost, sx126x_reset_agc, + sx126x_get_rssi_inst, sx126x_get_wakeup_time_us, the dc-restart counters, + the CAD tuning trio, and sx126x_apply_heltec_reg_patch. + +In sx126x_hal.c, SX1262 Tx params use a measured-optimal PaConfig table indexed +by requested power (RadioLib PR #1662) instead of the datasheet's fixed +dutyCycle=4/hpMax=7 -- same RF output at lower PA supply current below +22 dBm +-- and OCP is raised to 140 mA, without which the high-power PA is +current-limited to ~14-16 dBm at +22 dBm. + +In sx126x_hal_common.c, sx126x_hal_wait_busy() gets a sub-ms busy-wait fast path +ahead of the k_msleep(1) loop (whose scheduler penalty otherwise dominates RX +turnaround), plus a one-shot wake-and-retry recovery before it gives up. + +== Band-aware RSSI/AGC calibration (DS Sec 6.1.6) == + +The SX126x RSSI and AGC gain-tune registers are band-specific. Semtech ships +the chip calibrated for 868-915 MHz on their EVK and publishes a second value +set for 470-490 MHz (DS rev 2.2 Table 6-4, section added Dec 2024). We wrote +none of them, so sub-GHz builds ran the 868-915 calibration. + +That is not cosmetic. Per DS Sec 6.1.6 an incorrect RSSI produces "an +incorrect gain selection in LoRa and GFSK mode", which "can result in a missed +detection (packet loss) or decreased resistance to interference". + +Register 0x08AC is shared: bits 7:2 are AgcSensiAdjust, bits 1:0 select the RX +gain mode. The driver's 0x94/0x96 constants therefore also wrote the 868-915 +AgcSensiAdjust (0x94 >> 2 == 0x25) at every RX-gain re-apply, which would have +clobbered any calibration installed elsewhere. All four write sites now build +the byte from the cached per-band value. + +The low-band column is the datasheet's 470-490 MHz EVK calibration, applied +below 600 MHz. It is NOT a 433 MHz calibration -- Semtech publishes no such +column -- but it is the nearest published low-band data and beats leaving the +868-915 tunes in place. A true 433 figure needs the DS Sec 6.1.6 generator +sweep on real hardware. + +Applied from sx126x_config() after SetRfFrequency so a runtime band change +re-selects it, and re-applied after each Calibrate(ALL) alongside the existing +CalibrateImage re-issue. + +== Mark DIO1 as a system wakeup source == + +Without this, enabling CONFIG_PM on an ESP32 gives a node that light-sleeps +correctly and is deaf: the SX1262 asserts DIO1 on a received packet, but the +SoC is not armed to wake on that pin, so the frame is dropped and the CPU only +resumes on the next RTC timer deadline. The failure is silent and looks like a +success on a current meter, which is the worst possible shape for it. + +The Zephyr ESP32 GPIO driver arms the wake in its CONFIGURE path -- +gpio_esp32_config() reads (flags & GPIO_INT_WAKEUP) at drivers/gpio/gpio_esp32.c +and calls esp_sleep_enable_ext1_wakeup_io() -- not in the interrupt path, which +strips the bit. So the flag has to go on gpio_pin_configure_dt(), not on the +gpio_pin_interrupt_configure_dt() call below it. + +GPIO_INT_WAKEUP is deliberately NOT part of GPIO_INT_MASK (see +include/zephyr/drivers/gpio.h), precisely so it can be passed to +gpio_pin_configure() without tripping its "Interrupt flags are not supported" +assert. Drivers that do not implement wakeup ignore the bit, so this stays a +no-op on nRF/STM32/MG24 rather than needing a per-platform guard. + +Board caveat, not fixable here: on ESP32-S3 esp_sleep_is_valid_wakeup_gpio() +reduces to RTC_GPIO_IS_VALID_GPIO(), i.e. GPIO 0-21 only. Boards wiring DIO1 +above that (xiao_esp32s3 GPIO39, station_g2 GPIO48, thinknode_m9 GPIO42) log +"Pin N is not wakeup capable" and cannot use light sleep with LoRa RX at all. +Heltec V3/V4/V43 and the Wireless Trackers put DIO1 on GPIO14 and are fine. + +== Bound the lifetime of the RX-busy latch (payload-phase deadline) == + +sx126x_is_receiving() reports "busy" for the whole payload phase from the +rx_packet_active latch, which the base driver sets when HEADER_VALID fires and clears +only on a terminal IRQ (RX_DONE / CRC_ERR / RX_TX_TIMEOUT) or an RX (re)start. + +In continuous RX (SX126X_RX_TIMEOUT_CONTINUOUS) there is no symbol timer, so +none of those events is guaranteed to arrive. A HEADER_VALID whose packet never +completes therefore pins the TX gate true forever: the node keeps receiving but +never transmits again until reboot, with nothing in the logs pointing at it. The +duty-cycle parked-RX watchdog does not cover this case -- it is DC-only and +deliberately treats rx_packet_active as a legitimate in-flight packet. + +Add header_seen_at_ms, stamped when the latch is promoted, and release the latch +(clearing the sticky PREAMBLE_DETECTED / SYNC_WORD_VALID / HEADER_VALID / +HEADER_ERR bits) once sx126x_max_payload_ms() has elapsed. That bound is the +airtime of a 255-byte explicit-header packet at the current SF/BW with CR 4/8 +and LDRO pinned on, plus 25% and 100 ms -- deliberately generous, since firing +early would let TX start on top of a packet that is still arriving. + +Arduino MeshCore added the equivalent deadline in 79ef74ea (SX1262) and +0bd871cd (LR11X0); their preamble-phase timeout is already covered here by the +SF-aware grace in the base driver above. + +== 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. + +== Stop recalibrating the SX126x on every duty-cycle re-arm == + +sx126x_restart_rx() ran Calibrate(ALL) on every duty-cycle re-entry -- which is +after every received packet and after every noise header error -- costing ~5 ms +of busy-wait each time, on the receive path, forever. + +Worse, it was self-amplifying: Calibrate(ALL) reverts image calibration to the +902-928 MHz default, clears the AGC band calibration added above, resets the +DIO2 RF-switch role and resets StopTimerOnPreamble. Four of the seven steps in +that branch existed only to repair the damage done by the second one. + +The AGC reset itself is NOT being removed. Semtech give warm sleep plus +recalibration as the remedy for a jammed AGC, and sx126x_reset_agc() still does +exactly that. What was wrong was the trigger. Firing on reception is +anti-correlated with need: it runs most often on a busy channel, where the packet +that triggered it has just proved the AGC is working, and no more often on a +silent one, where a desensitised receiver is invisible and nothing else will +reveal it. LoRaRadioBase::agcMaintenance() now drives it from silence instead -- +after AGC_IDLE_RESET_MS with nothing heard, when by definition there is no +traffic to lose. Arduino MeshCore reached the same conclusion from the other +direction: its periodic agc_reset_interval ships defaulted to 0 (off). + +Per-packet AGC hygiene is unaffected and always came from elsewhere: the RX-gain +write at the end of sx126x_restart_rx() re-applies 0x08AC after SetRx on every +re-entry, which is what actually gives each packet a clean gain state. + +StopTimerOnPreamble and sx126x_retain_rx_gain() are kept. The first because +nobody has verified it survives the duty-cycle sleep unaided; the second because +DS section 9.6 states the Rx gain configuration "is not saved when entering sleep +mode with retention activated", making it mandatory rather than insurance -- this +is the one chip of the three where that is true. + +== Why this is one patch == + +The five sections above shipped as separate files (0011 band-rssi-cal, 0012 +dio1-wakeup, 0013 rx-latch-deadline, 0014 tx-timeout-airtime, 0015 +drop-rearm-recalibration), each carrying a note that it was "kept separate from +0003 because that patch cannot be regenerated from the tree". That was true +while 0003 was hand-written: a git diff of the tree returns the union of 0003 +and every patch stacked on it, so 0003 alone could not be re-emitted. Each fix +therefore became a new file, and sx126x.c ended up five patches deep. + +Stacking is not free. 0011 injects sx126x_apply_agc_band_cal() inside the +if (data->config_valid) body that the base driver introduces, so its context +lines live inside another patch's added lines. An in-place edit of 0003 that +was exactly line-count-neutral still broke 0011 on all three of its files, on a +clean tree with the stamp cleared. Any reflow of a block one patch adds +invalidates every patch anchored in it, and nothing warns you until the next +configure. + +Merging removes the failure mode by construction: one patch owns each file, so +no patch can be anchored inside another's added lines. It also removes a +duplication that had gone unnoticed -- 0003 was last regenerated as a +git diff -- drivers/lora/, which made it a superset of 0001, 0002 and 0004. At +configure time 0003 failed git apply --check, the stale-file fallback in +zephcore_apply_patches() reset all eight of its files (discarding 0001 and +0002), 0003 re-supplied them, then 0004 failed and reset sx12xx_common.c to +re-apply a hunk identical to the one 0003 had just written. The result was +correct only because of that fallback. + +The cost is rebasability: an upstream change to sx126x.c now conflicts with one +large patch rather than one small one. In practice four of the five sections +already touched sx126x.c (only the DIO1 wakeup did not), so a conflict there +always meant rebasing the large patch anyway. Per-change provenance moves from +filenames into the section headings above. + +Regenerate with: + + git -C zephyr diff -- drivers/lora/native/sx126x/ + +appended to this preamble. + diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c -index 30243ba5dc7..f0794661515 100644 +index 30243ba5dc7..d6e53044d77 100644 --- a/drivers/lora/native/sx126x/sx126x.c +++ b/drivers/lora/native/sx126x/sx126x.c @@ -6,14 +6,52 @@ @@ -124,19 +277,20 @@ index 30243ba5dc7..f0794661515 100644 #define SX126X_REST_STATE \ (IS_ENABLED(CONFIG_LORA_SX126X_NATIVE_SLEEP) \ ? SX126X_STATE_SLEEP : SX126X_STATE_IDLE) -@@ -74,6 +81,38 @@ static uint32_t bandwidth_to_hz(enum lora_signal_bandwidth bw) +@@ -74,6 +112,86 @@ static uint32_t bandwidth_to_hz(enum lora_signal_bandwidth bw) } } +/* Reset all software state that indicates "we are currently receiving": -+ * the rx_packet_active latch and the preamble-grace timestamp. Paired -+ * write so the two fields never drift out of sync. Called from every -+ * RX (re)start site, every terminal-event handler (RX_DONE / CRC_ERR / -+ * RX_TX_TIMEOUT), and on TX-state entry. */ ++ * the rx_packet_active latch (and its deadline timestamp) and the ++ * preamble-grace timestamp. Paired write so the fields never drift out of ++ * sync. Called from every RX (re)start site, every terminal-event handler ++ * (RX_DONE / CRC_ERR / RX_TX_TIMEOUT), and on TX-state entry. */ +static inline void sx126x_reset_rx_busy_signals(struct sx126x_data *data) +{ + data->rx_packet_active = false; + atomic_set(&data->preamble_seen_at_ms, 0); ++ atomic_set(&data->header_seen_at_ms, 0); +} + +/* Grace period for the PREAMBLE_DETECTED -> HEADER_VALID gap, SF/BW-aware. @@ -159,11 +313,58 @@ index 30243ba5dc7..f0794661515 100644 + + return (uint32_t)((us + 999U) / 1000U); +} ++ ++/* Upper bound on the payload phase: airtime of a maximum-length (255 byte) ++ * explicit-header packet at the current SF/BW, worst-case coding rate 4/8, ++ * plus margin. Used by sx126x_is_receiving() to bound the lifetime of the ++ * rx_packet_active latch. ++ * ++ * Why the latch needs a deadline at all: it is cleared only by a terminal IRQ ++ * (RX_DONE / CRC_ERR / RX_TX_TIMEOUT) or an RX (re)start. In continuous RX ++ * (SX126X_RX_TIMEOUT_CONTINUOUS) there is no symbol timer, so none of those is ++ * guaranteed to arrive -- a HEADER_VALID that never completes would pin the TX ++ * gate true forever and silently mute the node until reboot. (The parked-RX ++ * watchdog cannot cover this: it is duty-cycle only and deliberately treats ++ * rx_packet_active as a legitimate in-flight packet.) Mirrors the payload ++ * deadline Arduino MeshCore added in 79ef74ea / 0bd871cd. ++ * ++ * Deliberately generous: this is a stuck-state safety net, and clearing the ++ * latch early would let TX start on top of a packet that is still arriving. ++ * The low-data-rate-optimisation term is pinned at 1 (rather than read from ++ * the modem config) because DE=1 yields the larger symbol count of the two, ++ * i.e. the safer bound. */ ++static uint32_t sx126x_max_payload_ms(struct sx126x_data *data) ++{ ++ uint8_t sf = (uint8_t)data->config.datarate; ++ uint32_t bw_hz = bandwidth_to_hz(data->config.bandwidth); ++ ++ if (bw_hz == 0 || sf < 5 || sf > 12) { ++ return 30000; /* safe default if config is uninitialised */ ++ } ++ ++ /* Semtech payload-symbol count with PL=255, CRC on, explicit header, ++ * CR = 4/8 (coded_bits = 8), DE = 1: ++ * n = 8 + ceil((8*PL - 4*SF + 28 + 16) / (4*(SF - 2*DE))) * 8 ++ * DE=1 so the divisor is 4*(SF-2); at SF5 that is 12, never zero. */ ++ uint32_t numer = 8U * 255U + 28U + 16U; ++ uint32_t denom = 4U * (uint32_t)(sf - 2U); ++ ++ if (numer > 4U * (uint32_t)sf) { ++ numer -= 4U * (uint32_t)sf; ++ } ++ uint32_t n_sym = 8U + ((numer + denom - 1U) / denom) * 8U; ++ ++ /* n_sym * 2^sf * 1000000 / bw_hz -> us, then +25% and +100 ms margin. */ ++ uint64_t us = ((uint64_t)n_sym << sf) * 1000000ULL / bw_hz; ++ uint32_t ms = (uint32_t)((us + 999U) / 1000U); ++ ++ return ms + (ms / 4U) + 100U; ++} + static bool should_enable_ldro(enum lora_datarate sf, enum lora_signal_bandwidth bw, const struct sx126x_hal_config *config) { -@@ -222,6 +261,7 @@ static int sx126x_set_packet_params(const struct device *dev, +@@ -222,6 +340,7 @@ static int sx126x_set_packet_params(const struct device *dev, uint8_t invert_iq) { uint8_t buf[6]; @@ -171,7 +372,7 @@ index 30243ba5dc7..f0794661515 100644 sys_put_be16(preamble_len, &buf[0]); buf[2] = header_type; -@@ -229,7 +269,31 @@ static int sx126x_set_packet_params(const struct device *dev, +@@ -229,7 +348,31 @@ static int sx126x_set_packet_params(const struct device *dev, buf[4] = crc_mode; buf[5] = invert_iq; @@ -204,10 +405,124 @@ index 30243ba5dc7..f0794661515 100644 } static int sx126x_set_sync_word(const struct device *dev, bool public_network) -@@ -247,6 +311,14 @@ static int sx126x_set_rx_gain(const struct device *dev, bool boosted) - { - uint8_t val = boosted ? SX126X_RX_GAIN_BOOSTED : SX126X_RX_GAIN_POWER_SAVING; +@@ -243,10 +386,126 @@ static int sx126x_set_sync_word(const struct device *dev, bool public_network) + return sx126x_hal_write_regs(dev, SX126X_REG_LORA_SYNC_WORD_MSB, buf, 2); + } +-static int sx126x_set_rx_gain(const struct device *dev, bool boosted) ++/* RSSI / AGC band calibration (DS Sec 6.1.6, Table 6-4). ++ * ++ * Semtech: "The power seen by the SX1261/2 analog front-end is affected by ++ * external components such as the matching network and RF switches. An ++ * incorrect RSSI results in a sensitivity degradation in (G)FSK mode and an ++ * incorrect gain selection in LoRa and GFSK mode. An incorrect gain can ++ * result in a missed detection (packet loss) or decreased resistance to ++ * interference." So this is an RX-performance register set, not telemetry ++ * cosmetics. ++ * ++ * The chip ships calibrated for 868-915 MHz on the Semtech EVK, which is why ++ * the high-band tunes are all zero -- writing them is a no-op against a fresh ++ * chip and exists only so that a runtime band change (`set freq`) restores ++ * them after a low-band build has installed the other set. ++ * ++ * IMPORTANT: the low-band column is the datasheet's 470-490 MHz EVK ++ * calibration. It is NOT a 433 MHz calibration -- Semtech publishes no such ++ * column. Applying it below 600 MHz is "the nearest published values for a ++ * low band" and is expected to beat leaving the 868-915 calibration in place, ++ * but a true 433 MHz figure needs the DS Sec 6.1.6 generator sweep on real ++ * hardware. Do not present these numbers as calibrated for 433. ++ */ ++struct sx126x_agc_band_cal { ++ uint8_t rssi_meas_cal_h; /* 0x089C, bits 4:0 */ ++ uint8_t rssi_meas_cal_l; /* 0x089D */ ++ uint8_t gforst_powthr; /* 0x08B9 */ ++ uint8_t sensi_adjust; /* 0x08AC, bits 7:2 */ ++ uint8_t gain_tune[SX126X_AGC_GAIN_TUNE_LEN]; /* 0x08F5 .. 0x08FB */ ++}; ++ ++static const struct sx126x_agc_band_cal sx126x_agc_cal_high = { ++ .rssi_meas_cal_h = 0x01, ++ .rssi_meas_cal_l = 0x53, ++ .gforst_powthr = 0x0A, ++ .sensi_adjust = SX126X_AGC_SENSI_ADJUST_HIGH_BAND, ++ .gain_tune = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, ++}; ++ ++static const struct sx126x_agc_band_cal sx126x_agc_cal_low = { ++ .rssi_meas_cal_h = 0x01, ++ .rssi_meas_cal_l = 0x27, ++ .gforst_powthr = 0x04, ++ .sensi_adjust = SX126X_AGC_SENSI_ADJUST_LOW_BAND, ++ /* GainTune_1_2, _3_4, _5_6, _7_8, _9_10, _11_12, _13 */ ++ .gain_tune = { 0xDE, 0xE2, 0x32, 0x44, 0x33, 0x34, 0x04 }, ++}; ++ ++/* Band split. The datasheet only characterises 470-490 and 868-915, so any ++ * threshold between them is a judgement call; 600 MHz sits clear of both the ++ * 433/470 group and the 779/868/915 group that sx126x_calibrate_image() ++ * already distinguishes. */ ++#define SX126X_AGC_LOW_BAND_MAX_HZ 600000000 ++ ++static const struct sx126x_agc_band_cal *sx126x_agc_cal_for(uint32_t freq) ++{ ++ return (freq < SX126X_AGC_LOW_BAND_MAX_HZ) ? &sx126x_agc_cal_low ++ : &sx126x_agc_cal_high; ++} ++ ++/* Apply the band's RSSI/AGC calibration. Caches AgcSensiAdjust for the RX ++ * gain writes, which share register 0x08AC, but does NOT touch 0x08AC itself ++ * -- sx126x_set_rx_gain() owns that register and runs later in ++ * sx126x_config(). */ ++static int sx126x_apply_agc_band_cal(const struct device *dev, uint32_t freq) ++{ ++ struct sx126x_data *data = dev->data; ++ const struct sx126x_agc_band_cal *cal = sx126x_agc_cal_for(freq); ++ uint8_t val; ++ int ret; ++ ++ data->agc_sensi_adjust = cal->sensi_adjust; ++ ++ /* Bits 4:0 only -- preserve the rest of 0x089C. */ ++ ret = sx126x_hal_read_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_H, &val, 1); ++ if (ret < 0) { ++ return ret; ++ } ++ val = (val & ~0x1FU) | (cal->rssi_meas_cal_h & 0x1FU); ++ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_H, &val, 1); ++ if (ret < 0) { ++ return ret; ++ } ++ ++ val = cal->rssi_meas_cal_l; ++ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_L, &val, 1); ++ if (ret < 0) { ++ return ret; ++ } ++ ++ val = cal->gforst_powthr; ++ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_GFORST_POWTHR, &val, 1); ++ if (ret < 0) { ++ return ret; ++ } ++ ++ return sx126x_hal_write_regs(dev, SX126X_REG_AGC_GAIN_TUNE_1_2, ++ cal->gain_tune, ++ SX126X_AGC_GAIN_TUNE_LEN); ++} ++ ++/* Byte to write to 0x08AC: band AgcSensiAdjust in bits 7:2, gain mode in 1:0. */ ++static uint8_t sx126x_rx_gain_byte(const struct device *dev, bool boosted) + { +- uint8_t val = boosted ? SX126X_RX_GAIN_BOOSTED : SX126X_RX_GAIN_POWER_SAVING; ++ struct sx126x_data *data = dev->data; ++ ++ return SX126X_RX_GAIN_VAL(data->agc_sensi_adjust, boosted); ++} + ++static int sx126x_set_rx_gain(const struct device *dev, bool boosted) ++{ ++ uint8_t val = sx126x_rx_gain_byte(dev, boosted); ++ + /* Host-driven RX paths do not rely on retention: the chip resets + * 0x08AC to power-saving (0x94) on every SetRx command, and + * sx126x_restart_rx() / recv_async re-apply the desired gain right @@ -219,7 +534,7 @@ index 30243ba5dc7..f0794661515 100644 return sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &val, 1); } -@@ -296,7 +368,7 @@ static int sx126x_get_packet_status(const struct device *dev, +@@ -296,7 +555,7 @@ static int sx126x_get_packet_status(const struct device *dev, uint8_t buf[3]; int ret; @@ -228,7 +543,7 @@ index 30243ba5dc7..f0794661515 100644 if (ret == 0) { /* RSSI is -value/2 dBm */ *rssi = -((int16_t)buf[0] >> 1); -@@ -367,6 +439,26 @@ static int sx126x_chip_init(const struct device *dev) +@@ -367,6 +626,26 @@ static int sx126x_chip_init(const struct device *dev) return ret; } @@ -255,7 +570,7 @@ index 30243ba5dc7..f0794661515 100644 /* Set packet type to LoRa */ ret = sx126x_set_packet_type(dev, SX126X_PACKET_TYPE_LORA); if (ret < 0) { -@@ -374,10 +466,23 @@ static int sx126x_chip_init(const struct device *dev) +@@ -374,10 +653,23 @@ static int sx126x_chip_init(const struct device *dev) return ret; } @@ -282,7 +597,7 @@ index 30243ba5dc7..f0794661515 100644 if (ret < 0) { LOG_ERR("Set IRQ params failed: %d", ret); return ret; -@@ -398,7 +503,7 @@ static void sx126x_dio1_callback(const struct device *dev) +@@ -398,7 +690,7 @@ static void sx126x_dio1_callback(const struct device *dev) { struct sx126x_data *data = dev->data; @@ -291,7 +606,7 @@ index 30243ba5dc7..f0794661515 100644 } static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx) -@@ -406,7 +511,20 @@ static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx) +@@ -406,7 +698,20 @@ static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx) const struct sx126x_hal_config *config = dev->config; sx126x_hal_set_antenna_enable(dev, enable); @@ -313,7 +628,7 @@ index 30243ba5dc7..f0794661515 100644 sx126x_hal_set_rf_switch(dev, enable, tx); } } -@@ -455,6 +573,229 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev) +@@ -455,6 +760,237 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev) } #endif /* CONFIG_PM_DEVICE */ @@ -372,8 +687,6 @@ index 30243ba5dc7..f0794661515 100644 + * the standby/sleep round-trip that lora_recv_async would do. */ +static int sx126x_restart_rx(const struct device *dev, struct sx126x_data *data) +{ -+ int ret; -+ + /* Clear any stale IRQ bits before we re-enter RX, so a leftover + * PREAMBLE_DETECTED from a partial reception cannot pin + * sx126x_is_receiving() true on the next cycle. Also resets the @@ -382,49 +695,59 @@ index 30243ba5dc7..f0794661515 100644 + sx126x_reset_rx_busy_signals(data); + + if (data->rx_duty_cycle_enabled) { -+ const struct sx126x_hal_config *config = dev->config; -+ -+ /* AGC reset: after RxDone in duty cycle mode the chip is in -+ * STDBY_RC -- the analog frontend (incl. AGC) is already -+ * powered down. Calibrate(ALL) re-initialises ADC/PLL/RC -+ * so the receiver starts clean. Without this, a strong -+ * signal can desensitise the receiver until reboot (the -+ * "near-far" gain-lock problem). -+ * Cost: ~5 ms per received packet -- well within the 32.8 ms -+ * preamble budget (16-sym preamble, need 8 to lock). */ -+ sx126x_calibrate(dev, SX126X_CALIBRATE_ALL); -+ k_busy_wait(5000); -+ ret = sx126x_hal_wait_busy(dev, 50); -+ if (ret < 0) { -+ return ret; -+ } -+ -+ /* Re-issue CalibrateImage for the operating frequency: -+ * Calibrate(ALL) reverts image cal to the chip default -+ * (902-928 MHz band) which kills RX sensitivity on -+ * EU868 / 433 / 779 MHz operation. */ -+ if (data->config_valid) { -+ sx126x_calibrate_image(dev, data->config.frequency); -+ } -+ -+ /* Re-apply DIO2 as RF switch -- Calibrate resets it */ -+ if (config->dio2_tx_enable) { -+ sx126x_set_dio2_as_rf_switch(dev, true); -+ } ++ /* No Calibrate(ALL) here. ++ * ++ * This branch used to run a full recalibration on every ++ * duty-cycle re-arm -- after every received packet AND every ++ * noise header error -- at ~5 ms of busy-wait apiece, plus three ++ * repair steps that existed only to undo it: Calibrate reverts ++ * image calibration to the 902-928 MHz default, clears the AGC ++ * band calibration, resets the DIO2 RF-switch role and resets ++ * StopTimerOnPreamble. Four of the seven steps in this branch ++ * were repairing the second one. ++ * ++ * The AGC work itself is real and is NOT being dropped: Semtech ++ * give warm sleep plus recalibration as the remedy for a jammed ++ * AGC, and sx126x_reset_agc() still performs exactly that. What ++ * was wrong was the trigger. Firing on reception is ++ * anti-correlated with need -- it runs most often on a busy ++ * channel, where the packet that triggered it has just proved ++ * the AGC is working, and no more often on a silent one, where a ++ * desensitised receiver is invisible and nothing else reveals ++ * it. It now runs from LoRaRadioBase::agcMaintenance() on ++ * silence instead: after AGC_IDLE_RESET_MS with nothing heard, ++ * when by definition there is no traffic to miss. Arduino ++ * MeshCore reached the same place from the other direction -- ++ * its periodic `agc_reset_interval` ships defaulted to 0 (off). ++ * ++ * Per-packet AGC state is still handled, and always was: the ++ * RX-gain write at the end of this function re-applies 0x08AC ++ * after SetRx on every single re-entry, which is what actually ++ * gives each packet a clean gain state. ++ * ++ * StopTimerOnPreamble is KEPT below. Its comment blamed ++ * Calibrate for resetting it, but nobody has verified it ++ * survives the duty-cycle sleep unaided, and it is one cheap ++ * command guarding a failure mode (preambles clipped at the ++ * window tail) that is hard to spot. Drop it only on evidence. ++ */ + + /* StopTimerOnPreamble: without it, the duty-cycle RX timer + * keeps running even after a preamble is detected -- a + * preamble arriving at the tail of the RX window can get -+ * cut off when the sleep phase begins. Must be re-issued -+ * every time because Calibrate(ALL) above resets it. */ ++ * cut off when the sleep phase begins. Kept as insurance -- ++ * see the note above. */ + uint8_t stop_on_preamble = 1; + sx126x_hal_write_cmd(dev, SX126X_CMD_STOP_TIMER_ON_PREAMBLE, + &stop_on_preamble, 1); + + /* Keep boosted RX gain alive across the chip-internal -+ * sleep->RX wakes (DS sec 9.6, mandatory for DC mode). -+ * Re-issued here as cheap insurance against Calibrate(ALL) -+ * above disturbing the list. */ ++ * sleep->RX wakes. MANDATORY and unrelated to any calibration: ++ * DS sec 9.6 states the Rx gain configuration "is not saved when ++ * entering sleep mode with retention activated", so without this ++ * every wake after the first loses 3 dB. This chip is the only ++ * one of the three that needs it -- the LR2021 retains it ++ * (DS sec 6.3.8) and the LR11xx documents no caveat either way. */ + sx126x_retain_rx_gain(dev); + + /* Re-apply duty cycle with stored timing */ @@ -459,8 +782,8 @@ index 30243ba5dc7..f0794661515 100644 + * after SetRx) before writing. The gain is therefore applied after the radio + * has entered RX but before any preamble can arrive -- giving a clean AGC + * state for each new packet regardless of the previous packet's signal level. */ -+ uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED -+ : SX126X_RX_GAIN_POWER_SAVING; ++ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled); ++ + sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1); + return 0; +} @@ -543,7 +866,7 @@ index 30243ba5dc7..f0794661515 100644 static int sx126x_set_sleep(const struct device *dev) { struct sx126x_data *data = dev->data; -@@ -533,8 +874,16 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -533,8 +1069,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; @@ -560,7 +883,7 @@ index 30243ba5dc7..f0794661515 100644 /* Get received packet info */ ret = sx126x_get_rx_buffer_status(dev, &payload_len, &offset); if (ret < 0) { -@@ -544,9 +893,26 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -544,9 +1088,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); @@ -590,7 +913,7 @@ index 30243ba5dc7..f0794661515 100644 result.status = -EIO; } else { /* Read payload into shared buffer */ -@@ -564,20 +930,34 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -564,20 +1125,34 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta } } @@ -637,7 +960,7 @@ index 30243ba5dc7..f0794661515 100644 } } else { /* Sync mode */ -@@ -589,8 +969,69 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -589,8 +1164,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; @@ -707,7 +1030,7 @@ index 30243ba5dc7..f0794661515 100644 sx126x_set_sleep(dev); if (data->tx_async_signal != NULL) { -@@ -633,10 +1074,46 @@ static void sx126x_irq_work_handler(struct k_work *work) +@@ -633,10 +1269,54 @@ static void sx126x_irq_work_handler(struct k_work *work) sx126x_handle_irq_rx_done(dev, irq_status); } @@ -749,12 +1072,32 @@ index 30243ba5dc7..f0794661515 100644 + * is stale and we don't want a future is_receiving() to treat + * it as live when the latch eventually clears. */ + atomic_set(&data->preamble_seen_at_ms, 0); ++ /* Start the payload deadline (see sx126x_max_payload_ms). Use 1 ++ * as the "set" sentinel if k_uptime is 0 right after boot. */ ++ { ++ uint32_t now = k_uptime_get_32(); ++ ++ atomic_set(&data->header_seen_at_ms, ++ (atomic_val_t)(now == 0 ? 1U : now)); ++ } + } + /* 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 +1159,7 @@ static int sx126x_lora_config(const struct device *dev, +@@ -679,10 +1359,19 @@ static int sx126x_lora_config(const struct device *dev, + goto out; + } + ++ /* Install the band's RSSI/AGC calibration (DS Sec 6.1.6) before the ++ * RX gain write below, which consumes the AgcSensiAdjust this caches. ++ * Re-run on every config so a runtime frequency change switches bands ++ * instead of leaving the previous band's tunes installed. */ ++ ret = sx126x_apply_agc_band_cal(dev, config->frequency); ++ if (ret < 0) { ++ goto out; ++ } ++ /* Configure PA and TX power based on chip variant and frequency */ ret = sx126x_hal_configure_tx_params(dev, config->tx_power, config->frequency, @@ -763,7 +1106,7 @@ index 30243ba5dc7..f0794661515 100644 if (ret < 0) { goto out; } -@@ -698,6 +1175,29 @@ static int sx126x_lora_config(const struct device *dev, +@@ -698,6 +1387,29 @@ static int sx126x_lora_config(const struct device *dev, goto out; } @@ -793,17 +1136,18 @@ index 30243ba5dc7..f0794661515 100644 /* Set sync word */ ret = sx126x_set_sync_word(dev, config->public_network); if (ret < 0) { -@@ -721,6 +1221,9 @@ out: +@@ -721,6 +1433,10 @@ out: return ret; } +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, struct k_poll_signal *async) -@@ -738,11 +1241,27 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -738,11 +1454,27 @@ static int sx126x_lora_send_async(const struct device *dev, return -EINVAL; } @@ -833,7 +1177,7 @@ index 30243ba5dc7..f0794661515 100644 k_mutex_lock(&data->lock, K_FOREVER); ret = sx126x_ensure_ready(dev); -@@ -752,6 +1271,59 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -752,6 +1484,59 @@ static int sx126x_lora_send_async(const struct device *dev, return ret; } @@ -893,10 +1237,12 @@ index 30243ba5dc7..f0794661515 100644 data->tx_async_signal = async; k_msgq_purge(&data->tx_msgq); -@@ -777,6 +1349,62 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -777,8 +1562,90 @@ static int sx126x_lora_send_async(const struct device *dev, /* Enable antenna and set TX path */ sx126x_set_rf_path(dev, true, true); +- /* Start transmission with 10 second timeout */ +- ret = sx126x_set_tx(dev, 10000); + /* Re-assert PA config, OCP and TX power immediately before SetTx. + * + * sx126x_config() is otherwise the only writer of these three, and the @@ -953,10 +1299,38 @@ index 30243ba5dc7..f0794661515 100644 + } +#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) { -@@ -847,6 +1444,8 @@ static int sx126x_lora_recv(const struct device *dev, uint8_t *data_buf, + goto out_error; + } +@@ -847,6 +1714,8 @@ static int sx126x_lora_recv(const struct device *dev, uint8_t *data_buf, } data->rx_cb = NULL; @@ -965,7 +1339,7 @@ index 30243ba5dc7..f0794661515 100644 k_msgq_purge(&data->rx_msgq); /* Set packet parameters for variable length reception */ -@@ -918,6 +1517,8 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -918,6 +1787,8 @@ static int sx126x_lora_recv_async(const struct device *dev, /* Stop async reception */ data->rx_cb = NULL; data->rx_cb_user_data = NULL; @@ -974,7 +1348,7 @@ index 30243ba5dc7..f0794661515 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 +1533,19 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -932,6 +1803,19 @@ static int sx126x_lora_recv_async(const struct device *dev, return -EINVAL; } @@ -994,7 +1368,7 @@ index 30243ba5dc7..f0794661515 100644 if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_RX)) { LOG_ERR("Busy"); k_mutex_unlock(&data->lock); -@@ -945,8 +1559,18 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -945,8 +1829,18 @@ static int sx126x_lora_recv_async(const struct device *dev, return ret; } @@ -1013,7 +1387,7 @@ index 30243ba5dc7..f0794661515 100644 /* Set packet parameters */ ret = sx126x_set_packet_params(dev, -@@ -959,6 +1583,7 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -959,6 +1853,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; @@ -1021,7 +1395,7 @@ index 30243ba5dc7..f0794661515 100644 sx126x_set_sleep(dev); k_mutex_unlock(&data->lock); return ret; -@@ -971,11 +1596,21 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -971,11 +1866,21 @@ static int sx126x_lora_recv_async(const struct device *dev, ret = sx126x_set_rx(dev, 0); if (ret < 0) { data->rx_cb = NULL; @@ -1036,14 +1410,14 @@ index 30243ba5dc7..f0794661515 100644 + * SetRx without retention, and lora_config sets gain before going to + * sleep, so without this the first packet would be received in + * power-saving gain regardless of DTS rx-boosted. */ -+ uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED -+ : SX126X_RX_GAIN_POWER_SAVING; ++ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled); ++ + sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1); + k_mutex_unlock(&data->lock); return 0; } -@@ -1051,7 +1686,7 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, +@@ -1051,7 +1956,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, @@ -1052,23 +1426,56 @@ index 30243ba5dc7..f0794661515 100644 if (ret < 0) { sx126x_set_sleep(dev); k_mutex_unlock(&data->lock); -@@ -1083,14 +1718,733 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, +@@ -1083,47 +1988,820 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, return 0; } +-static DEVICE_API(lora, sx126x_lora_api) = { +- .config = sx126x_lora_config, +- .send = sx126x_lora_send, +- .send_async = sx126x_lora_send_async, +- .recv = sx126x_lora_recv, +- .recv_async = sx126x_lora_recv_async, +- .airtime = sx126x_lora_airtime, +- .test_cw = sx126x_lora_test_cw, +-}; +/* -- Extension API (sx126x_ext.h) -- */ -+ + +-#ifdef CONFIG_PM_DEVICE +-static int sx126x_pm_action(const struct device *dev, +- enum pm_device_action action) +bool sx126x_is_chip_busy(const struct device *dev) -+{ + { +- switch (action) { +- case PM_DEVICE_ACTION_SUSPEND: +- sx126x_disconnect_rf_gpios(dev); +- return 0; +- case PM_DEVICE_ACTION_RESUME: +- return sx126x_reconnect_rf_gpios(dev); +- default: +- return -ENOTSUP; +- } + return sx126x_hal_is_busy(dev); -+} -+ + } +-#endif + +-static int sx126x_init(const struct device *dev) +int16_t sx126x_get_rssi_inst(const struct device *dev) -+{ -+ struct sx126x_data *data = dev->data; + { + struct sx126x_data *data = dev->data; + uint8_t buf[1]; -+ int ret; -+ + int ret; + +- /* Initialize data structures */ +- k_mutex_init(&data->lock); +- k_msgq_init(&data->tx_msgq, (char *)&data->tx_result, +- sizeof(struct sx126x_tx_result), 1); +- 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); +- data->dev = dev; +- atomic_set(&data->state, SX126X_STATE_IDLE); +- data->config_valid = false; + /* GPIO read -- bail before stalling wait_busy on the duty-cycle + * sleep phase. wait_busy's recovery would force-wake the chip and + * break duty cycle. */ @@ -1102,9 +1509,33 @@ index 30243ba5dc7..f0794661515 100644 + /* Primary source of truth: software latch set by the work handler on + * HEADER_VALID, cleared by terminal events (RX_DONE / CRC_ERR / + * RX_TX_TIMEOUT) and RX (re)start sites. Covers the entire payload -+ * phase without an SPI access. */ ++ * phase without an SPI access. ++ * ++ * Bounded by a payload deadline: in continuous RX no terminal IRQ is ++ * guaranteed, so a HEADER_VALID that never completes would otherwise pin ++ * this gate true forever and mute TX until reboot. */ + if (data->rx_packet_active) { -+ return true; ++ uint32_t seen = (uint32_t)atomic_get(&data->header_seen_at_ms); ++ uint32_t now = k_uptime_get_32(); ++ ++ if (seen == 0 || (now - seen) < sx126x_max_payload_ms(data)) { ++ return true; ++ } ++ ++ if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) { ++ /* Chip is busy elsewhere; re-check on the next poll. */ ++ return true; ++ } ++ LOG_WRN("RX latch stuck %u ms after HEADER_VALID, releasing TX gate", ++ now - seen); ++ /* Drop the sticky reception bits so the next poll starts clean. */ ++ sx126x_clear_irq_status(dev, SX126X_IRQ_PREAMBLE_DETECTED | ++ SX126X_IRQ_SYNC_WORD_VALID | ++ SX126X_IRQ_HEADER_VALID | ++ SX126X_IRQ_HEADER_ERR); ++ sx126x_reset_rx_busy_signals(data); ++ k_mutex_unlock(&data->lock); ++ return false; + } + + if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) { @@ -1704,6 +2135,11 @@ index 30243ba5dc7..f0794661515 100644 + * (902-928 MHz band) which kills RX sensitivity on + * EU868 / 433 / 779 MHz operation. */ + sx126x_calibrate_image(dev, data->config.frequency); ++ /* Same reasoning for the AGC band calibration (DS Sec 6.1.6): ++ * cheap insurance against Calibrate(ALL) disturbing the RSSI / ++ * gain-tune registers. 0x08AC itself is restored by the ++ * RX-gain write on the restart path. */ ++ sx126x_apply_agc_band_cal(dev, data->config.frequency); + + /* Re-apply DIO2 as RF switch -- Calibrate resets it. */ + if (hal_cfg->dio2_tx_enable) { @@ -1752,8 +2188,8 @@ index 30243ba5dc7..f0794661515 100644 + * chip-internal sleep->RX wakes restore it (DS sec 9.6: mandatory for + * SetRxDutyCycle -- without this every window after the first sleep + * listens at power-saving gain, a silent -3 dB sensitivity loss). */ -+ uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED -+ : SX126X_RX_GAIN_POWER_SAVING; ++ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled); ++ + sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1); + sx126x_retain_rx_gain(dev); + @@ -1772,14 +2208,7 @@ index 30243ba5dc7..f0794661515 100644 + return 0; +} + - static DEVICE_API(lora, sx126x_lora_api) = { -- .config = sx126x_lora_config, -- .send = sx126x_lora_send, -- .send_async = sx126x_lora_send_async, -- .recv = sx126x_lora_recv, -- .recv_async = sx126x_lora_recv_async, -- .airtime = sx126x_lora_airtime, -- .test_cw = sx126x_lora_test_cw, ++static DEVICE_API(lora, sx126x_lora_api) = { + .config = sx126x_lora_config, + .send = sx126x_lora_send, + .send_async = sx126x_lora_send_async, @@ -1790,25 +2219,41 @@ index 30243ba5dc7..f0794661515 100644 + .cad_async = sx126x_lora_cad_async, + .recv_duty_cycle = sx126x_lora_recv_duty_cycle, + .test_cw = sx126x_lora_test_cw, - }; - - #ifdef CONFIG_PM_DEVICE -@@ -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; ++}; ++ ++#ifdef CONFIG_PM_DEVICE ++static int sx126x_pm_action(const struct device *dev, ++ enum pm_device_action action) ++{ ++ switch (action) { ++ case PM_DEVICE_ACTION_SUSPEND: ++ sx126x_disconnect_rf_gpios(dev); ++ return 0; ++ case PM_DEVICE_ACTION_RESUME: ++ return sx126x_reconnect_rf_gpios(dev); ++ default: ++ return -ENOTSUP; ++ } ++} ++#endif ++ ++static int sx126x_init(const struct device *dev) ++{ ++ struct sx126x_data *data = dev->data; + const struct sx126x_hal_config *config = dev->config; - int ret; - - /* Initialize data structures */ -@@ -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); ++ int ret; ++ ++ /* Initialize data structures */ ++ k_mutex_init(&data->lock); ++ k_msgq_init(&data->tx_msgq, (char *)&data->tx_result, ++ sizeof(struct sx126x_tx_result), 1); ++ 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); + k_sem_init(&data->cad_sem, 0, 1); - data->dev = dev; - atomic_set(&data->state, SX126X_STATE_IDLE); - data->config_valid = false; ++ data->dev = dev; ++ atomic_set(&data->state, SX126X_STATE_IDLE); ++ data->config_valid = false; + data->rx_cb = NULL; + data->rx_cb_user_data = NULL; + atomic_set(&data->rx_cb_gen, 0); @@ -1818,6 +2263,10 @@ index 30243ba5dc7..f0794661515 100644 + * pick up the boost setting even when the user never calls the + * sx126x_set_rx_boost() extension API. */ + data->rx_boost_enabled = config->rx_boosted; ++ /* Match the calibration the chip ships with, so any 0x08AC write that ++ * happens before the first sx126x_config() keeps the factory value ++ * rather than zeroing AgcSensiAdjust. */ ++ data->agc_sensi_adjust = SX126X_AGC_SENSI_ADJUST_HIGH_BAND; + atomic_set(&data->dc_timeout_restarts, 0); + k_work_init_delayable(&data->dc_watchdog_work, + sx126x_dc_watchdog_handler); @@ -1832,10 +2281,10 @@ index 30243ba5dc7..f0794661515 100644 /* Initialize HAL */ ret = sx126x_hal_init(dev); diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h -index 9dbf3f26586..b18bebceec7 100644 +index 9dbf3f26586..762f8eb78f1 100644 --- a/drivers/lora/native/sx126x/sx126x.h +++ b/drivers/lora/native/sx126x/sx126x.h -@@ -56,13 +56,74 @@ struct sx126x_data { +@@ -56,13 +56,89 @@ struct sx126x_data { /* Async RX callback */ lora_recv_cb rx_cb; void *rx_cb_user_data; @@ -1853,6 +2302,13 @@ index 9dbf3f26586..b18bebceec7 100644 + bool rx_duty_cycle_enabled; + bool rx_boost_enabled; + ++ /* AgcSensiAdjust value for the configured band (DS Table 6-4), cached ++ * because it shares register 0x08AC with the RX gain bits and so has ++ * to be re-applied by every RX-gain write. Defaults to the 868-915 ++ * value the chip ships with; sx126x_config() re-selects it per ++ * frequency. */ ++ uint8_t agc_sensi_adjust; ++ + /* RX-busy latch: set by the work handler when HEADER_VALID fires for + * a real packet, cleared on RX_DONE / CRC_ERR / RX_TX_TIMEOUT / RX + * (re)start / TX-state entry. Read by sx126x_is_receiving() as the @@ -1874,6 +2330,14 @@ index 9dbf3f26586..b18bebceec7 100644 + * terminal-event handler, and on TX-state entry. */ + atomic_t preamble_seen_at_ms; + ++ /* Timestamp (k_uptime_get_32() units, ms) at which rx_packet_active was ++ * promoted by HEADER_VALID. Zero means "no payload phase being timed". ++ * Bounds the latch's lifetime: continuous RX has no symbol timer, so a ++ * HEADER_VALID whose packet never completes produces no terminal IRQ and ++ * would keep the TX gate closed forever. sx126x_is_receiving() releases ++ * the latch once sx126x_max_payload_ms() has elapsed. */ ++ atomic_t header_seen_at_ms; ++ + uint32_t dc_rx_time; /* stored duty cycle rx period (15.625us steps) */ + uint32_t dc_sleep_time; /* stored duty cycle sleep period (15.625us steps) */ + @@ -1911,10 +2375,25 @@ index 9dbf3f26586..b18bebceec7 100644 #endif /* ZEPHYR_DRIVERS_LORA_SX126X_SX126X_INTERNAL_H_ */ diff --git a/drivers/lora/native/sx126x/sx126x_hal.c b/drivers/lora/native/sx126x/sx126x_hal.c -index 34f7089584f..262f5b55aa8 100644 +index 34f7089584f..6f4e2a4434c 100644 --- a/drivers/lora/native/sx126x/sx126x_hal.c +++ b/drivers/lora/native/sx126x/sx126x_hal.c -@@ -220,15 +220,54 @@ int sx126x_hal_configure_tx_params(const struct device *dev, int8_t power, +@@ -150,7 +150,13 @@ int sx126x_hal_init(const struct device *dev) + LOG_ERR("DIO1 GPIO not ready"); + return -ENODEV; + } +- ret = gpio_pin_configure_dt(&config->dio1, GPIO_INPUT); ++ /* GPIO_INT_WAKEUP: let a DIO1 assertion wake the SoC from a system ++ * low-power state, so an inbound packet is not slept through when ++ * CONFIG_PM is enabled. Outside GPIO_INT_MASK by design, so this is ++ * legal here and ignored by drivers without wakeup support. ++ */ ++ ret = gpio_pin_configure_dt(&config->dio1, ++ GPIO_INPUT | GPIO_INT_WAKEUP); + if (ret < 0) { + LOG_ERR("Failed to configure DIO1 GPIO: %d", ret); + return ret; +@@ -220,15 +226,54 @@ int sx126x_hal_configure_tx_params(const struct device *dev, int8_t power, } tx_power = CLAMP(power, SX1261_MIN_POWER, SX1261_MAX_POWER_TX_PARAM); } else { @@ -2011,18 +2490,47 @@ index b2dfc0d75b5..8ab8a0f5738 100644 return 0; diff --git a/drivers/lora/native/sx126x/sx126x_regs.h b/drivers/lora/native/sx126x/sx126x_regs.h -index 7f55c9b96e2..a616f0e6cbc 100644 +index 7f55c9b96e2..3392fcb2934 100644 --- a/drivers/lora/native/sx126x/sx126x_regs.h +++ b/drivers/lora/native/sx126x/sx126x_regs.h -@@ -180,6 +180,11 @@ +@@ -175,10 +175,39 @@ + #define SX126X_LORA_SYNC_WORD_PUBLIC 0x3444 + #define SX126X_LORA_SYNC_WORD_PRIVATE 0x1424 + +-/* RX Gain */ ++/* RX Gain. ++ * ++ * 0x08AC is a shared register: bits 7:2 are AgcSensiAdjust (DS Table 6-4, ++ * band-dependent RSSI/AGC calibration) and bits 1:0 select the RX gain mode. ++ * The legacy 0x94/0x96 constants are the 868-915 MHz band values -- they ++ * happen to encode AgcSensiAdjust 0x25 (0x94 >> 2 == 0x25). Writing them ++ * verbatim in a 470-490 MHz build silently installs the wrong AGC ++ * calibration, so build the byte with SX126X_RX_GAIN_VAL() instead. ++ */ + #define SX126X_REG_RX_GAIN 0x08AC #define SX126X_RX_GAIN_POWER_SAVING 0x94 #define SX126X_RX_GAIN_BOOSTED 0x96 - ++#define SX126X_RX_GAIN_BOOST_BITS 0x02 ++#define SX126X_RX_GAIN_VAL(sensi, boosted) \ ++ (uint8_t)(((sensi) << 2) | ((boosted) ? SX126X_RX_GAIN_BOOST_BITS : 0U)) ++ ++/* RSSI / AGC calibration registers (DS Sec 6.1.6, Table 6-4). ++ * The chip ships calibrated for 868-915 MHz on the Semtech EVK; the ++ * datasheet publishes a second column for 470-490 MHz. */ ++#define SX126X_REG_AGC_RSSI_MEAS_CAL_H 0x089C /* bits 4:0 */ ++#define SX126X_REG_AGC_RSSI_MEAS_CAL_L 0x089D ++#define SX126X_REG_AGC_GFORST_POWTHR 0x08B9 ++#define SX126X_REG_AGC_GAIN_TUNE_1_2 0x08F5 /* .. 0x08FB, 7 bytes */ ++#define SX126X_AGC_GAIN_TUNE_LEN 7 ++ ++/* AgcSensiAdjust (0x08AC bits 7:2) per band */ ++#define SX126X_AGC_SENSI_ADJUST_HIGH_BAND 0x25 /* 868-915 MHz (default) */ ++#define SX126X_AGC_SENSI_ADJUST_LOW_BAND 0x22 /* 470-490 MHz */ ++ +/* RX Gain Retention (DS Sec 9.6) -- tells chip to preserve 0x08AC across + * mode transitions (sleep/standby/TX -> RX). Without retention, the + * chip resets RX gain to power-saving on every SetRx command. */ +#define SX126X_REG_RX_GAIN_RETENTION_0 0x029F -+ + /* TX Clamp Config (workaround for SX1262) */ #define SX126X_REG_TX_CLAMP_CFG 0x08D8 - diff --git a/zephcore/patches/zephyr/0004-lora-sx127x-62k5-bandwidth.patch b/zephcore/patches/zephyr/0004-lora-sx127x-62k5-bandwidth.patch index f2d971e..d968d6f 100644 --- a/zephcore/patches/zephyr/0004-lora-sx127x-62k5-bandwidth.patch +++ b/zephcore/patches/zephyr/0004-lora-sx127x-62k5-bandwidth.patch @@ -1,3 +1,18 @@ +Subject: [PATCH] lora: sx127x: 62.5 kHz bandwidth + +Adds BW_62_KHZ to sx12xx_get_bandwidth_idx(), the Zephyr-enum to +loramac-node-index mapping. The SX127x hardware supports sub-125 kHz LoRa +bandwidths; index 3 is 62.5 kHz (register BW=6). The radio-side half of this -- +the index-3 register mapping and the LDRO rules that come with a >16 ms symbol +time -- lives in the loramac-node module and is added by +patches/modules/loramac-node/0001-sx1276-bw62k5.patch. + +Sole owner of drivers/lora/loramac-node/sx12xx_common.c. A byte-identical copy +of this hunk was also carried in 0003, which meant this patch failed +git apply --check on every configure and only landed because the stale-file +fallback reset the file first. That copy is gone; see 0003's "Why this is one +patch". + diff --git a/drivers/lora/loramac-node/sx12xx_common.c b/drivers/lora/loramac-node/sx12xx_common.c index 17689720dd2..09982dc2e70 100644 --- a/drivers/lora/loramac-node/sx12xx_common.c diff --git a/zephcore/patches/zephyr/0011-lora-sx126x-band-rssi-cal.patch b/zephcore/patches/zephyr/0011-lora-sx126x-band-rssi-cal.patch deleted file mode 100644 index 1ecdc6d..0000000 --- a/zephcore/patches/zephyr/0011-lora-sx126x-band-rssi-cal.patch +++ /dev/null @@ -1,286 +0,0 @@ -Subject: [PATCH] lora: sx126x: band-aware RSSI/AGC calibration (DS Sec 6.1.6) - -The SX126x RSSI and AGC gain-tune registers are band-specific. Semtech ships -the chip calibrated for 868-915 MHz on their EVK and publishes a second value -set for 470-490 MHz (DS rev 2.2 Table 6-4, section added Dec 2024). We wrote -none of them, so sub-GHz builds ran the 868-915 calibration. - -That is not cosmetic. Per DS Sec 6.1.6 an incorrect RSSI produces "an -incorrect gain selection in LoRa and GFSK mode", which "can result in a missed -detection (packet loss) or decreased resistance to interference". - -Register 0x08AC is shared: bits 7:2 are AgcSensiAdjust, bits 1:0 select the RX -gain mode. The driver's 0x94/0x96 constants therefore also wrote the 868-915 -AgcSensiAdjust (0x94 >> 2 == 0x25) at every RX-gain re-apply, which would have -clobbered any calibration installed elsewhere. All four write sites now build -the byte from the cached per-band value. - -The low-band column is the datasheet's 470-490 MHz EVK calibration, applied -below 600 MHz. It is NOT a 433 MHz calibration -- Semtech publishes no such -column -- but it is the nearest published low-band data and beats leaving the -868-915 tunes in place. A true 433 figure needs the DS Sec 6.1.6 generator -sweep on real hardware. - -Applied from sx126x_config() after SetRfFrequency so a runtime band change -re-selects it, and re-applied after each Calibrate(ALL) alongside the existing -CalibrateImage re-issue. - ---- a/drivers/lora/native/sx126x/sx126x_regs.h -+++ b/drivers/lora/native/sx126x/sx126x_regs.h -@@ -175,10 +175,34 @@ - #define SX126X_LORA_SYNC_WORD_PUBLIC 0x3444 - #define SX126X_LORA_SYNC_WORD_PRIVATE 0x1424 - --/* RX Gain */ -+/* RX Gain. -+ * -+ * 0x08AC is a shared register: bits 7:2 are AgcSensiAdjust (DS Table 6-4, -+ * band-dependent RSSI/AGC calibration) and bits 1:0 select the RX gain mode. -+ * The legacy 0x94/0x96 constants are the 868-915 MHz band values -- they -+ * happen to encode AgcSensiAdjust 0x25 (0x94 >> 2 == 0x25). Writing them -+ * verbatim in a 470-490 MHz build silently installs the wrong AGC -+ * calibration, so build the byte with SX126X_RX_GAIN_VAL() instead. -+ */ - #define SX126X_REG_RX_GAIN 0x08AC - #define SX126X_RX_GAIN_POWER_SAVING 0x94 - #define SX126X_RX_GAIN_BOOSTED 0x96 -+#define SX126X_RX_GAIN_BOOST_BITS 0x02 -+#define SX126X_RX_GAIN_VAL(sensi, boosted) \ -+ (uint8_t)(((sensi) << 2) | ((boosted) ? SX126X_RX_GAIN_BOOST_BITS : 0U)) -+ -+/* RSSI / AGC calibration registers (DS Sec 6.1.6, Table 6-4). -+ * The chip ships calibrated for 868-915 MHz on the Semtech EVK; the -+ * datasheet publishes a second column for 470-490 MHz. */ -+#define SX126X_REG_AGC_RSSI_MEAS_CAL_H 0x089C /* bits 4:0 */ -+#define SX126X_REG_AGC_RSSI_MEAS_CAL_L 0x089D -+#define SX126X_REG_AGC_GFORST_POWTHR 0x08B9 -+#define SX126X_REG_AGC_GAIN_TUNE_1_2 0x08F5 /* .. 0x08FB, 7 bytes */ -+#define SX126X_AGC_GAIN_TUNE_LEN 7 -+ -+/* AgcSensiAdjust (0x08AC bits 7:2) per band */ -+#define SX126X_AGC_SENSI_ADJUST_HIGH_BAND 0x25 /* 868-915 MHz (default) */ -+#define SX126X_AGC_SENSI_ADJUST_LOW_BAND 0x22 /* 470-490 MHz */ - - /* RX Gain Retention (DS Sec 9.6) -- tells chip to preserve 0x08AC across - * mode transitions (sleep/standby/TX -> RX). Without retention, the ---- a/drivers/lora/native/sx126x/sx126x.h -+++ b/drivers/lora/native/sx126x/sx126x.h -@@ -70,6 +70,13 @@ - bool rx_duty_cycle_enabled; - bool rx_boost_enabled; - -+ /* AgcSensiAdjust value for the configured band (DS Table 6-4), cached -+ * because it shares register 0x08AC with the RX gain bits and so has -+ * to be re-applied by every RX-gain write. Defaults to the 868-915 -+ * value the chip ships with; sx126x_config() re-selects it per -+ * frequency. */ -+ uint8_t agc_sensi_adjust; -+ - /* RX-busy latch: set by the work handler when HEADER_VALID fires for - * a real packet, cleared on RX_DONE / CRC_ERR / RX_TX_TIMEOUT / RX - * (re)start / TX-state entry. Read by sx126x_is_receiving() as the ---- a/drivers/lora/native/sx126x/sx126x.c -+++ b/drivers/lora/native/sx126x/sx126x.c -@@ -307,9 +307,117 @@ - return sx126x_hal_write_regs(dev, SX126X_REG_LORA_SYNC_WORD_MSB, buf, 2); - } - -+/* RSSI / AGC band calibration (DS Sec 6.1.6, Table 6-4). -+ * -+ * Semtech: "The power seen by the SX1261/2 analog front-end is affected by -+ * external components such as the matching network and RF switches. An -+ * incorrect RSSI results in a sensitivity degradation in (G)FSK mode and an -+ * incorrect gain selection in LoRa and GFSK mode. An incorrect gain can -+ * result in a missed detection (packet loss) or decreased resistance to -+ * interference." So this is an RX-performance register set, not telemetry -+ * cosmetics. -+ * -+ * The chip ships calibrated for 868-915 MHz on the Semtech EVK, which is why -+ * the high-band tunes are all zero -- writing them is a no-op against a fresh -+ * chip and exists only so that a runtime band change (`set freq`) restores -+ * them after a low-band build has installed the other set. -+ * -+ * IMPORTANT: the low-band column is the datasheet's 470-490 MHz EVK -+ * calibration. It is NOT a 433 MHz calibration -- Semtech publishes no such -+ * column. Applying it below 600 MHz is "the nearest published values for a -+ * low band" and is expected to beat leaving the 868-915 calibration in place, -+ * but a true 433 MHz figure needs the DS Sec 6.1.6 generator sweep on real -+ * hardware. Do not present these numbers as calibrated for 433. -+ */ -+struct sx126x_agc_band_cal { -+ uint8_t rssi_meas_cal_h; /* 0x089C, bits 4:0 */ -+ uint8_t rssi_meas_cal_l; /* 0x089D */ -+ uint8_t gforst_powthr; /* 0x08B9 */ -+ uint8_t sensi_adjust; /* 0x08AC, bits 7:2 */ -+ uint8_t gain_tune[SX126X_AGC_GAIN_TUNE_LEN]; /* 0x08F5 .. 0x08FB */ -+}; -+ -+static const struct sx126x_agc_band_cal sx126x_agc_cal_high = { -+ .rssi_meas_cal_h = 0x01, -+ .rssi_meas_cal_l = 0x53, -+ .gforst_powthr = 0x0A, -+ .sensi_adjust = SX126X_AGC_SENSI_ADJUST_HIGH_BAND, -+ .gain_tune = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, -+}; -+ -+static const struct sx126x_agc_band_cal sx126x_agc_cal_low = { -+ .rssi_meas_cal_h = 0x01, -+ .rssi_meas_cal_l = 0x27, -+ .gforst_powthr = 0x04, -+ .sensi_adjust = SX126X_AGC_SENSI_ADJUST_LOW_BAND, -+ /* GainTune_1_2, _3_4, _5_6, _7_8, _9_10, _11_12, _13 */ -+ .gain_tune = { 0xDE, 0xE2, 0x32, 0x44, 0x33, 0x34, 0x04 }, -+}; -+ -+/* Band split. The datasheet only characterises 470-490 and 868-915, so any -+ * threshold between them is a judgement call; 600 MHz sits clear of both the -+ * 433/470 group and the 779/868/915 group that sx126x_calibrate_image() -+ * already distinguishes. */ -+#define SX126X_AGC_LOW_BAND_MAX_HZ 600000000 -+ -+static const struct sx126x_agc_band_cal *sx126x_agc_cal_for(uint32_t freq) -+{ -+ return (freq < SX126X_AGC_LOW_BAND_MAX_HZ) ? &sx126x_agc_cal_low -+ : &sx126x_agc_cal_high; -+} -+ -+/* Apply the band's RSSI/AGC calibration. Caches AgcSensiAdjust for the RX -+ * gain writes, which share register 0x08AC, but does NOT touch 0x08AC itself -+ * -- sx126x_set_rx_gain() owns that register and runs later in -+ * sx126x_config(). */ -+static int sx126x_apply_agc_band_cal(const struct device *dev, uint32_t freq) -+{ -+ struct sx126x_data *data = dev->data; -+ const struct sx126x_agc_band_cal *cal = sx126x_agc_cal_for(freq); -+ uint8_t val; -+ int ret; -+ -+ data->agc_sensi_adjust = cal->sensi_adjust; -+ -+ /* Bits 4:0 only -- preserve the rest of 0x089C. */ -+ ret = sx126x_hal_read_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_H, &val, 1); -+ if (ret < 0) { -+ return ret; -+ } -+ val = (val & ~0x1FU) | (cal->rssi_meas_cal_h & 0x1FU); -+ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_H, &val, 1); -+ if (ret < 0) { -+ return ret; -+ } -+ -+ val = cal->rssi_meas_cal_l; -+ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_L, &val, 1); -+ if (ret < 0) { -+ return ret; -+ } -+ -+ val = cal->gforst_powthr; -+ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_GFORST_POWTHR, &val, 1); -+ if (ret < 0) { -+ return ret; -+ } -+ -+ return sx126x_hal_write_regs(dev, SX126X_REG_AGC_GAIN_TUNE_1_2, -+ cal->gain_tune, -+ SX126X_AGC_GAIN_TUNE_LEN); -+} -+ -+/* Byte to write to 0x08AC: band AgcSensiAdjust in bits 7:2, gain mode in 1:0. */ -+static uint8_t sx126x_rx_gain_byte(const struct device *dev, bool boosted) -+{ -+ struct sx126x_data *data = dev->data; -+ -+ return SX126X_RX_GAIN_VAL(data->agc_sensi_adjust, boosted); -+} -+ - static int sx126x_set_rx_gain(const struct device *dev, bool boosted) - { -- uint8_t val = boosted ? SX126X_RX_GAIN_BOOSTED : SX126X_RX_GAIN_POWER_SAVING; -+ uint8_t val = sx126x_rx_gain_byte(dev, boosted); - - /* Host-driven RX paths do not rely on retention: the chip resets - * 0x08AC to power-saving (0x94) on every SetRx command, and -@@ -661,6 +769,11 @@ - * EU868 / 433 / 779 MHz operation. */ - if (data->config_valid) { - sx126x_calibrate_image(dev, data->config.frequency); -+ /* Same reasoning for the AGC band calibration (DS -+ * Sec 6.1.6): cheap insurance against Calibrate(ALL) -+ * disturbing the RSSI / gain-tune registers. 0x08AC -+ * itself is restored by the RX-gain write below. */ -+ sx126x_apply_agc_band_cal(dev, data->config.frequency); - } - - /* Re-apply DIO2 as RF switch -- Calibrate resets it */ -@@ -715,8 +828,8 @@ - * after SetRx) before writing. The gain is therefore applied after the radio - * has entered RX but before any preamble can arrive -- giving a clean AGC - * state for each new packet regardless of the previous packet's signal level. */ -- uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED -- : SX126X_RX_GAIN_POWER_SAVING; -+ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled); -+ - sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1); - return 0; - } -@@ -1156,6 +1269,15 @@ - goto out; - } - -+ /* Install the band's RSSI/AGC calibration (DS Sec 6.1.6) before the -+ * RX gain write below, which consumes the AgcSensiAdjust this caches. -+ * Re-run on every config so a runtime frequency change switches bands -+ * instead of leaving the previous band's tunes installed. */ -+ ret = sx126x_apply_agc_band_cal(dev, config->frequency); -+ if (ret < 0) { -+ goto out; -+ } -+ - /* Configure PA and TX power based on chip variant and frequency */ - ret = sx126x_hal_configure_tx_params(dev, config->tx_power, - config->frequency, -@@ -1607,8 +1729,8 @@ - * SetRx without retention, and lora_config sets gain before going to - * sleep, so without this the first packet would be received in - * power-saving gain regardless of DTS rx-boosted. */ -- uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED -- : SX126X_RX_GAIN_POWER_SAVING; -+ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled); -+ - sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1); - - k_mutex_unlock(&data->lock); -@@ -2281,6 +2403,11 @@ - * (902-928 MHz band) which kills RX sensitivity on - * EU868 / 433 / 779 MHz operation. */ - sx126x_calibrate_image(dev, data->config.frequency); -+ /* Same reasoning for the AGC band calibration (DS Sec 6.1.6): -+ * cheap insurance against Calibrate(ALL) disturbing the RSSI / -+ * gain-tune registers. 0x08AC itself is restored by the -+ * RX-gain write on the restart path. */ -+ sx126x_apply_agc_band_cal(dev, data->config.frequency); - - /* Re-apply DIO2 as RF switch -- Calibrate resets it. */ - if (hal_cfg->dio2_tx_enable) { -@@ -2352,8 +2479,8 @@ - * chip-internal sleep->RX wakes restore it (DS sec 9.6: mandatory for - * SetRxDutyCycle -- without this every window after the first sleep - * listens at power-saving gain, a silent -3 dB sensitivity loss). */ -- uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED -- : SX126X_RX_GAIN_POWER_SAVING; -+ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled); -+ - sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1); - sx126x_retain_rx_gain(dev); - -@@ -2427,6 +2554,10 @@ - * pick up the boost setting even when the user never calls the - * sx126x_set_rx_boost() extension API. */ - data->rx_boost_enabled = config->rx_boosted; -+ /* Match the calibration the chip ships with, so any 0x08AC write that -+ * happens before the first sx126x_config() keeps the factory value -+ * rather than zeroing AgcSensiAdjust. */ -+ data->agc_sensi_adjust = SX126X_AGC_SENSI_ADJUST_HIGH_BAND; - atomic_set(&data->dc_timeout_restarts, 0); - k_work_init_delayable(&data->dc_watchdog_work, - sx126x_dc_watchdog_handler); diff --git a/zephcore/patches/zephyr/0012-lora-sx126x-dio1-wakeup.patch b/zephcore/patches/zephyr/0012-lora-sx126x-dio1-wakeup.patch deleted file mode 100644 index 295c048..0000000 --- a/zephcore/patches/zephyr/0012-lora-sx126x-dio1-wakeup.patch +++ /dev/null @@ -1,43 +0,0 @@ -Mark DIO1 as a system wakeup source. - -Without this, enabling CONFIG_PM on an ESP32 gives a node that light-sleeps -correctly and is deaf: the SX1262 asserts DIO1 on a received packet, but the -SoC is not armed to wake on that pin, so the frame is dropped and the CPU only -resumes on the next RTC timer deadline. The failure is silent and looks like a -success on a current meter, which is the worst possible shape for it. - -The Zephyr ESP32 GPIO driver arms the wake in its CONFIGURE path -- -gpio_esp32_config() reads (flags & GPIO_INT_WAKEUP) at drivers/gpio/gpio_esp32.c -and calls esp_sleep_enable_ext1_wakeup_io() -- not in the interrupt path, which -strips the bit. So the flag has to go on gpio_pin_configure_dt(), not on the -gpio_pin_interrupt_configure_dt() call below it. - -GPIO_INT_WAKEUP is deliberately NOT part of GPIO_INT_MASK (see -include/zephyr/drivers/gpio.h), precisely so it can be passed to -gpio_pin_configure() without tripping its "Interrupt flags are not supported" -assert. Drivers that do not implement wakeup ignore the bit, so this stays a -no-op on nRF/STM32/MG24 rather than needing a per-platform guard. - -Board caveat, not fixable here: on ESP32-S3 esp_sleep_is_valid_wakeup_gpio() -reduces to RTC_GPIO_IS_VALID_GPIO(), i.e. GPIO 0-21 only. Boards wiring DIO1 -above that (xiao_esp32s3 GPIO39, station_g2 GPIO48, thinknode_m9 GPIO42) log -"Pin N is not wakeup capable" and cannot use light sleep with LoRa RX at all. -Heltec V3/V4/V43 and the Wireless Trackers put DIO1 on GPIO14 and are fine. - ---- a/drivers/lora/native/sx126x/sx126x_hal.c -+++ b/drivers/lora/native/sx126x/sx126x_hal.c -@@ -150,7 +150,13 @@ - LOG_ERR("DIO1 GPIO not ready"); - return -ENODEV; - } -- ret = gpio_pin_configure_dt(&config->dio1, GPIO_INPUT); -+ /* GPIO_INT_WAKEUP: let a DIO1 assertion wake the SoC from a system -+ * low-power state, so an inbound packet is not slept through when -+ * CONFIG_PM is enabled. Outside GPIO_INT_MASK by design, so this is -+ * legal here and ignored by drivers without wakeup support. -+ */ -+ ret = gpio_pin_configure_dt(&config->dio1, -+ GPIO_INPUT | GPIO_INT_WAKEUP); - if (ret < 0) { - LOG_ERR("Failed to configure DIO1 GPIO: %d", ret); - return ret; diff --git a/zephcore/patches/zephyr/0013-lora-sx126x-rx-latch-deadline.patch b/zephcore/patches/zephyr/0013-lora-sx126x-rx-latch-deadline.patch deleted file mode 100644 index 544f8e2..0000000 --- a/zephcore/patches/zephyr/0013-lora-sx126x-rx-latch-deadline.patch +++ /dev/null @@ -1,175 +0,0 @@ -Bound the lifetime of the RX-busy latch (payload-phase deadline). - -sx126x_is_receiving() reports "busy" for the whole payload phase from the -rx_packet_active latch, which patch 0003 sets when HEADER_VALID fires and clears -only on a terminal IRQ (RX_DONE / CRC_ERR / RX_TX_TIMEOUT) or an RX (re)start. - -In continuous RX (SX126X_RX_TIMEOUT_CONTINUOUS) there is no symbol timer, so -none of those events is guaranteed to arrive. A HEADER_VALID whose packet never -completes therefore pins the TX gate true forever: the node keeps receiving but -never transmits again until reboot, with nothing in the logs pointing at it. The -duty-cycle parked-RX watchdog does not cover this case -- it is DC-only and -deliberately treats rx_packet_active as a legitimate in-flight packet. - -Add header_seen_at_ms, stamped when the latch is promoted, and release the latch -(clearing the sticky PREAMBLE_DETECTED / SYNC_WORD_VALID / HEADER_VALID / -HEADER_ERR bits) once sx126x_max_payload_ms() has elapsed. That bound is the -airtime of a 255-byte explicit-header packet at the current SF/BW with CR 4/8 -and LDRO pinned on, plus 25% and 100 ms -- deliberately generous, since firing -early would let TX start on top of a packet that is still arriving. - -Arduino MeshCore added the equivalent deadline in 79ef74ea (SX1262) and -0bd871cd (LR11X0); their preamble-phase timeout is already covered here by the -SF-aware grace in patch 0003. - -Kept separate from 0003 because that patch cannot be regenerated from the tree: -0011 and 0012 also modify this driver and would be folded into it. - -diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c -index 822d0f5..00faca6 100644 ---- a/drivers/lora/native/sx126x/sx126x.c -+++ b/drivers/lora/native/sx126x/sx126x.c -@@ -82,14 +82,15 @@ static uint32_t bandwidth_to_hz(enum lora_signal_bandwidth bw) - } - - /* Reset all software state that indicates "we are currently receiving": -- * the rx_packet_active latch and the preamble-grace timestamp. Paired -- * write so the two fields never drift out of sync. Called from every -- * RX (re)start site, every terminal-event handler (RX_DONE / CRC_ERR / -- * RX_TX_TIMEOUT), and on TX-state entry. */ -+ * the rx_packet_active latch (and its deadline timestamp) and the -+ * preamble-grace timestamp. Paired write so the fields never drift out of -+ * sync. Called from every RX (re)start site, every terminal-event handler -+ * (RX_DONE / CRC_ERR / RX_TX_TIMEOUT), and on TX-state entry. */ - static inline void sx126x_reset_rx_busy_signals(struct sx126x_data *data) - { - data->rx_packet_active = false; - atomic_set(&data->preamble_seen_at_ms, 0); -+ atomic_set(&data->header_seen_at_ms, 0); - } - - /* Grace period for the PREAMBLE_DETECTED -> HEADER_VALID gap, SF/BW-aware. -@@ -113,6 +114,53 @@ static uint32_t sx126x_preamble_grace_ms(struct sx126x_data *data) - return (uint32_t)((us + 999U) / 1000U); - } - -+/* Upper bound on the payload phase: airtime of a maximum-length (255 byte) -+ * explicit-header packet at the current SF/BW, worst-case coding rate 4/8, -+ * plus margin. Used by sx126x_is_receiving() to bound the lifetime of the -+ * rx_packet_active latch. -+ * -+ * Why the latch needs a deadline at all: it is cleared only by a terminal IRQ -+ * (RX_DONE / CRC_ERR / RX_TX_TIMEOUT) or an RX (re)start. In continuous RX -+ * (SX126X_RX_TIMEOUT_CONTINUOUS) there is no symbol timer, so none of those is -+ * guaranteed to arrive -- a HEADER_VALID that never completes would pin the TX -+ * gate true forever and silently mute the node until reboot. (The parked-RX -+ * watchdog cannot cover this: it is duty-cycle only and deliberately treats -+ * rx_packet_active as a legitimate in-flight packet.) Mirrors the payload -+ * deadline Arduino MeshCore added in 79ef74ea / 0bd871cd. -+ * -+ * Deliberately generous: this is a stuck-state safety net, and clearing the -+ * latch early would let TX start on top of a packet that is still arriving. -+ * The low-data-rate-optimisation term is pinned at 1 (rather than read from -+ * the modem config) because DE=1 yields the larger symbol count of the two, -+ * i.e. the safer bound. */ -+static uint32_t sx126x_max_payload_ms(struct sx126x_data *data) -+{ -+ uint8_t sf = (uint8_t)data->config.datarate; -+ uint32_t bw_hz = bandwidth_to_hz(data->config.bandwidth); -+ -+ if (bw_hz == 0 || sf < 5 || sf > 12) { -+ return 30000; /* safe default if config is uninitialised */ -+ } -+ -+ /* Semtech payload-symbol count with PL=255, CRC on, explicit header, -+ * CR = 4/8 (coded_bits = 8), DE = 1: -+ * n = 8 + ceil((8*PL - 4*SF + 28 + 16) / (4*(SF - 2*DE))) * 8 -+ * DE=1 so the divisor is 4*(SF-2); at SF5 that is 12, never zero. */ -+ uint32_t numer = 8U * 255U + 28U + 16U; -+ uint32_t denom = 4U * (uint32_t)(sf - 2U); -+ -+ if (numer > 4U * (uint32_t)sf) { -+ numer -= 4U * (uint32_t)sf; -+ } -+ uint32_t n_sym = 8U + ((numer + denom - 1U) / denom) * 8U; -+ -+ /* n_sym * 2^sf * 1000000 / bw_hz -> us, then +25% and +100 ms margin. */ -+ uint64_t us = ((uint64_t)n_sym << sf) * 1000000ULL / bw_hz; -+ uint32_t ms = (uint32_t)((us + 999U) / 1000U); -+ -+ return ms + (ms / 4U) + 100U; -+} -+ - static bool should_enable_ldro(enum lora_datarate sf, enum lora_signal_bandwidth bw, - const struct sx126x_hal_config *config) - { -@@ -1225,6 +1273,14 @@ static void sx126x_irq_work_handler(struct k_work *work) - * is stale and we don't want a future is_receiving() to treat - * it as live when the latch eventually clears. */ - atomic_set(&data->preamble_seen_at_ms, 0); -+ /* Start the payload deadline (see sx126x_max_payload_ms). Use 1 -+ * as the "set" sentinel if k_uptime is 0 right after boot. */ -+ { -+ uint32_t now = k_uptime_get_32(); -+ -+ atomic_set(&data->header_seen_at_ms, -+ (atomic_val_t)(now == 0 ? 1U : now)); -+ } - } - - /* Re-enable the DIO1 interrupt for the next event (unless sleeping) */ -@@ -1886,9 +1942,33 @@ bool sx126x_is_receiving(const struct device *dev) - /* Primary source of truth: software latch set by the work handler on - * HEADER_VALID, cleared by terminal events (RX_DONE / CRC_ERR / - * RX_TX_TIMEOUT) and RX (re)start sites. Covers the entire payload -- * phase without an SPI access. */ -+ * phase without an SPI access. -+ * -+ * Bounded by a payload deadline: in continuous RX no terminal IRQ is -+ * guaranteed, so a HEADER_VALID that never completes would otherwise pin -+ * this gate true forever and mute TX until reboot. */ - if (data->rx_packet_active) { -- return true; -+ uint32_t seen = (uint32_t)atomic_get(&data->header_seen_at_ms); -+ uint32_t now = k_uptime_get_32(); -+ -+ if (seen == 0 || (now - seen) < sx126x_max_payload_ms(data)) { -+ return true; -+ } -+ -+ if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) { -+ /* Chip is busy elsewhere; re-check on the next poll. */ -+ return true; -+ } -+ LOG_WRN("RX latch stuck %u ms after HEADER_VALID, releasing TX gate", -+ now - seen); -+ /* Drop the sticky reception bits so the next poll starts clean. */ -+ sx126x_clear_irq_status(dev, SX126X_IRQ_PREAMBLE_DETECTED | -+ SX126X_IRQ_SYNC_WORD_VALID | -+ SX126X_IRQ_HEADER_VALID | -+ SX126X_IRQ_HEADER_ERR); -+ sx126x_reset_rx_busy_signals(data); -+ k_mutex_unlock(&data->lock); -+ return false; - } - - if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) { -diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h -index 02bbaac..762f8eb 100644 ---- a/drivers/lora/native/sx126x/sx126x.h -+++ b/drivers/lora/native/sx126x/sx126x.h -@@ -98,6 +98,14 @@ struct sx126x_data { - * terminal-event handler, and on TX-state entry. */ - atomic_t preamble_seen_at_ms; - -+ /* Timestamp (k_uptime_get_32() units, ms) at which rx_packet_active was -+ * promoted by HEADER_VALID. Zero means "no payload phase being timed". -+ * Bounds the latch's lifetime: continuous RX has no symbol timer, so a -+ * HEADER_VALID whose packet never completes produces no terminal IRQ and -+ * would keep the TX gate closed forever. sx126x_is_receiving() releases -+ * the latch once sx126x_max_payload_ms() has elapsed. */ -+ atomic_t header_seen_at_ms; -+ - uint32_t dc_rx_time; /* stored duty cycle rx period (15.625us steps) */ - uint32_t dc_sleep_time; /* stored duty cycle sleep period (15.625us steps) */ - diff --git a/zephcore/patches/zephyr/0014-lora-sx126x-tx-timeout-airtime.patch b/zephcore/patches/zephyr/0014-lora-sx126x-tx-timeout-airtime.patch deleted file mode 100644 index eeeea64..0000000 --- a/zephcore/patches/zephyr/0014-lora-sx126x-tx-timeout-airtime.patch +++ /dev/null @@ -1,70 +0,0 @@ -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/Dispatcher.cpp b/zephcore/src/Dispatcher.cpp index bf41cb5..1f16186 100644 --- a/zephcore/src/Dispatcher.cpp +++ b/zephcore/src/Dispatcher.cpp @@ -181,6 +181,11 @@ void Dispatcher::maintenanceLoop() * we only surface offset changes so the app layer can persist them. */ _radio->cadMaintenance(); + /* Receiver hygiene: deaf-aware AGC unstick + temperature-drift + * recalibration. Both sleep the chip, so they live here rather than on + * any packet path. */ + _radio->agcMaintenance(); + int8_t cad_off = _radio->getCadOffset(); if (!cad_offset_shadow_valid) {