From 10b36094ae8e4ad597af061aca471a7ee0ea8c46 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:51:08 +0200 Subject: [PATCH] radio: one LoRaRadio adapter over per-family ops tables; lr20xx cleanup - LoRaRadioBase -> LoRaRadio (final); SX126x/SX127x/LR1110/LR2021Radio replaced by const LoRaRadioOps tables (radio_ops_.cpp), one linked per build; mains construct the radio in one line - lr20xx: vendored Semtech SDK + HAL moved beside the driver (no app-tree sources), DIO1 handler split (CCN 52 -> 10), zc_lora_timing.h adopted, dead exports removed, header rewritten, comment diet (image-identical) - semtech,lr2021 uses the lr1110 property names (irq-gpios, tcxo-voltage, rfsw-*), compiled config byte-identical - correct the stale "LR2021 is not CAD-probed" claim --- docs/ARCHITECTURE.md | 48 +- zephcore/CMakeLists.txt | 13 +- zephcore/adapters/radio/CadController.h | 2 +- zephcore/adapters/radio/LR1110Radio.cpp | 145 -- zephcore/adapters/radio/LR1110Radio.h | 47 - zephcore/adapters/radio/LR2021Radio.cpp | 221 --- zephcore/adapters/radio/LR2021Radio.h | 59 - .../{LoRaRadioBase.cpp => LoRaRadio.cpp} | 161 +- .../radio/{LoRaRadioBase.h => LoRaRadio.h} | 174 +- zephcore/adapters/radio/LoRaRadioOps.h | 67 + zephcore/adapters/radio/NoiseFloorEstimator.h | 2 +- zephcore/adapters/radio/SX126xRadio.cpp | 181 -- zephcore/adapters/radio/SX126xRadio.h | 51 - zephcore/adapters/radio/SX127xRadio.cpp | 88 - zephcore/adapters/radio/SX127xRadio.h | 53 - zephcore/adapters/radio/radio_common.h | 2 +- zephcore/adapters/radio/radio_ops_lr1110.cpp | 43 + zephcore/adapters/radio/radio_ops_lr2021.cpp | 59 + zephcore/adapters/radio/radio_ops_sx126x.cpp | 60 + zephcore/adapters/radio/radio_ops_sx127x.cpp | 18 + zephcore/app/ObserverMesh.cpp | 14 +- zephcore/app/RepeaterMesh.cpp | 8 +- zephcore/app/RepeaterUplink.cpp | 4 +- zephcore/app/RoomServerMesh.cpp | 8 +- zephcore/app/main_observer.cpp | 10 +- .../meshnology_w12/meshnology_w12_procpu.dts | 16 +- .../meshtracker_x1_nrf52840.dts | 6 +- .../promicro_lr2021/promicro_lr2021.dts | 12 +- .../seeed_lr2021_evk_nrf54l15_cpuapp.dts | 6 +- zephcore/helpers/NodePrefs.h | 2 +- zephcore/helpers/ui/ui_mesh_actions.cpp | 8 +- .../drivers/lora/lr11xx/lr11xx_cad_peak.h | 4 +- .../drivers/lora/lr11xx/lr11xx_lora.c | 6 +- .../drivers/lora/lr20xx/CMakeLists.txt | 33 +- .../lora/lr20xx/lr20xx_driver}/LICENSE.txt | 0 .../lr20xx_driver}/lr20xx_driver_version.c | 0 .../lr20xx_driver}/lr20xx_driver_version.h | 0 .../lora/lr20xx/lr20xx_driver}/lr20xx_hal.h | 0 .../lora/lr20xx/lr20xx_driver}/lr20xx_patch.c | 0 .../lora/lr20xx/lr20xx_driver}/lr20xx_patch.h | 0 .../lr20xx_driver}/lr20xx_patch_types.h | 0 .../lr20xx_driver}/lr20xx_pram_lr2021.h | 0 .../lr20xx_driver}/lr20xx_radio_common.c | 0 .../lr20xx_driver}/lr20xx_radio_common.h | 0 .../lr20xx_radio_common_types.h | 0 .../lr20xx/lr20xx_driver}/lr20xx_radio_fifo.c | 0 .../lr20xx/lr20xx_driver}/lr20xx_radio_fifo.h | 0 .../lr20xx_driver}/lr20xx_radio_fifo_types.h | 0 .../lr20xx/lr20xx_driver}/lr20xx_radio_flrc.h | 0 .../lr20xx_driver}/lr20xx_radio_flrc_types.h | 0 .../lr20xx_radio_fsk_common_types.h | 0 .../lr20xx/lr20xx_driver}/lr20xx_radio_lora.c | 0 .../lr20xx/lr20xx_driver}/lr20xx_radio_lora.h | 0 .../lr20xx_driver}/lr20xx_radio_lora_types.h | 0 .../lr20xx/lr20xx_driver}/lr20xx_radio_ook.h | 0 .../lr20xx_driver}/lr20xx_radio_ook_types.h | 0 .../lr20xx/lr20xx_driver}/lr20xx_regmem.c | 0 .../lr20xx/lr20xx_driver}/lr20xx_regmem.h | 0 .../lr20xx/lr20xx_driver}/lr20xx_status.h | 0 .../lr20xx/lr20xx_driver}/lr20xx_system.c | 0 .../lr20xx/lr20xx_driver}/lr20xx_system.h | 0 .../lr20xx_driver}/lr20xx_system_types.h | 0 .../lr20xx_driver}/lr20xx_workarounds.c | 0 .../lr20xx_driver}/lr20xx_workarounds.h | 0 .../drivers/lora}/lr20xx/lr20xx_hal_zephyr.c | 0 .../drivers/lora}/lr20xx/lr20xx_hal_zephyr.h | 0 .../drivers/lora/lr20xx/lr20xx_lora.c | 1454 ++++++----------- .../dts/bindings/lora/semtech,lr2021.yaml | 44 +- .../include/zephyr/drivers/lora/lr11xx_lora.h | 2 +- .../include/zephyr/drivers/lora/lr20xx_lora.h | 242 +-- zephcore/src/RadioIncludes.h | 10 +- zephcore/src/main_companion.cpp | 17 +- zephcore/src/main_repeater.cpp | 17 +- zephcore/src/main_room_server.cpp | 17 +- zephcore/tools/crypto_bench/src/main.cpp | 4 +- 75 files changed, 1065 insertions(+), 2373 deletions(-) delete mode 100644 zephcore/adapters/radio/LR1110Radio.cpp delete mode 100644 zephcore/adapters/radio/LR1110Radio.h delete mode 100644 zephcore/adapters/radio/LR2021Radio.cpp delete mode 100644 zephcore/adapters/radio/LR2021Radio.h rename zephcore/adapters/radio/{LoRaRadioBase.cpp => LoRaRadio.cpp} (92%) rename zephcore/adapters/radio/{LoRaRadioBase.h => LoRaRadio.h} (70%) create mode 100644 zephcore/adapters/radio/LoRaRadioOps.h delete mode 100644 zephcore/adapters/radio/SX126xRadio.cpp delete mode 100644 zephcore/adapters/radio/SX126xRadio.h delete mode 100644 zephcore/adapters/radio/SX127xRadio.cpp delete mode 100644 zephcore/adapters/radio/SX127xRadio.h create mode 100644 zephcore/adapters/radio/radio_ops_lr1110.cpp create mode 100644 zephcore/adapters/radio/radio_ops_lr2021.cpp create mode 100644 zephcore/adapters/radio/radio_ops_sx126x.cpp create mode 100644 zephcore/adapters/radio/radio_ops_sx127x.cpp rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/LICENSE.txt (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_driver_version.c (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_driver_version.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_hal.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_patch.c (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_patch.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_patch_types.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_pram_lr2021.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_common.c (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_common.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_common_types.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_fifo.c (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_fifo.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_fifo_types.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_flrc.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_flrc_types.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_fsk_common_types.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_lora.c (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_lora.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_lora_types.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_ook.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_radio_ook_types.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_regmem.c (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_regmem.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_status.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_system.c (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_system.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_system_types.h (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_workarounds.c (100%) rename zephcore/{adapters/radio/lr20xx => patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver}/lr20xx_workarounds.h (100%) rename zephcore/{adapters/radio => patches/zephyr-new/drivers/lora}/lr20xx/lr20xx_hal_zephyr.c (100%) rename zephcore/{adapters/radio => patches/zephyr-new/drivers/lora}/lr20xx/lr20xx_hal_zephyr.h (100%) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4cc73fe..308c86d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -68,14 +68,12 @@ zephcore/ │ ├── adapters/ # Zephyr HAL implementations │ ├── radio/ # LoRa radio drivers -│ │ ├── LoRaRadioBase.cpp/h # Shared TX/RX state machine, noise floor, AGC -│ │ ├── SX126xRadio.cpp/h # SX126x adapter (native Zephyr driver, patched) -│ │ ├── SX127xRadio.cpp/h # SX127x adapter (loramac-node backend) -│ │ ├── LR1110Radio.cpp/h # LR1110 adapter (custom Zephyr driver) -│ │ ├── LR2021Radio.cpp/h # LR2021 adapter (custom driver) -│ │ ├── radio_common.h # Shared radio types and constants -│ │ ├── lr11xx/ # LR11xx low-level HAL (SPI, GPIO, Semtech SDK) -│ │ └── lr20xx/ # LR20xx low-level HAL (Semtech SDK) +│ │ ├── LoRaRadio.cpp/h # The one radio adapter: TX/RX state machine, noise floor, CAD, hygiene +│ │ ├── LoRaRadioOps.h # Per-family ops table (driver extension functions) +│ │ ├── radio_ops_.cpp # sx126x / sx127x / lr1110 / lr2021 tables (one linked per build) +│ │ ├── CadController.* # Adaptive CAD controller (host-tested) +│ │ ├── NoiseFloorEstimator.h # Noise-floor EMA (host-tested) +│ │ └── radio_common.h, radio_tuning.h # Shared constants │ ├── ble/ZephyrBLE.cpp/h # BLE NUS service, pairing, TX congestion │ ├── board/ZephyrBoard.cpp/h # Battery ADC, LEDs, reboot, bootloader │ ├── clock/ # Millisecond uptime + software RTC + I2C RTC discovery @@ -167,11 +165,11 @@ zephcore/ │ ├── RX delay (score-based prioritization) │ │ └── Maintenance (noise floor, image cal) │ ├─────────────────────────────────────────────────┤ -│ LoRaRadioBase │ Radio HAL -│ ├── SX126xRadio ──► Zephyr SX126x driver │ -│ ├── SX127xRadio ──► loramac-node backend │ -│ ├── LR1110Radio ──► Custom LR11xx driver │ -│ └── LR2021Radio ──► Custom LR20xx driver │ +│ LoRaRadio + kLoRaRadioOps (one per build) │ Radio HAL +│ ├── sx126x ──► Zephyr SX126x driver (0003) │ +│ ├── sx127x ──► loramac-node backend │ +│ ├── lr1110 ──► our LR11xx driver + LBM SDK │ +│ └── lr2021 ──► our LR20xx driver + SDK │ ├─────────────────────────────────────────────────┤ │ Zephyr RTOS (kernel, drivers, BLE, FS, USB) │ Platform └─────────────────────────────────────────────────┘ @@ -356,16 +354,18 @@ All policy timers (6 h rate limit, 7-day suppression, tenure, sample age) anchor ``` mesh::Radio (abstract interface) - └── LoRaRadioBase (shared state machine, ring buffer, noise floor) - ├── SX126xRadio → Zephyr native SX126x driver + sx126x_ext.h - ├── SX127xRadio → Zephyr loramac-node backend (SX1272/76/78) - ├── LR1110Radio → Custom lr11xx_lora.c driver + Semtech HAL - └── LR2021Radio → Custom lr20xx_lora.c driver + Semtech HAL + └── LoRaRadio (final: state machine, ring buffer, noise floor, CAD, hygiene) + uses kLoRaRadioOps, the family's const table of driver extension + functions (LoRaRadioOps.h), one linked per build: + radio_ops_sx126x.cpp → Zephyr native SX126x driver + sx126x_ext.h + radio_ops_sx127x.cpp → Zephyr loramac-node backend (all entries NULL) + radio_ops_lr1110.cpp → our lr11xx_lora.c driver + lr11xx_lora.h + radio_ops_lr2021.cpp → our lr20xx_lora.c driver + lr20xx_lora.h ``` Compile-time selection via the `CONFIG_ZEPHCORE_RADIO_NATIVE` / `_LR1110` / `_LR2021` / `_SX127X` Kconfig options, resolved in `RadioIncludes.h`. The native SX126x path is the default and covers SX1261/62/68, LLCC68, and the STM32WL integrated sub-GHz radio. -### 5.2 LoRaRadioBase State Machine +### 5.2 LoRaRadio State Machine **TX Flow** (LBT — current default; `cad.mode == LORA_CAD_MODE_LBT` is set unconditionally in `buildModemConfig`): 1. `startSendRaw()` → `isReceiving()` final gate → `_tx_active = 1` → **skip** `hwCancelReceive()` and leave `_in_recv_mode = 1` so the driver sees state == RX → `configureTx()` → async send. @@ -382,7 +382,7 @@ Compile-time selection via the `CONFIG_ZEPHCORE_RADIO_NATIVE` / `_LR1110` / `_LR ### 5.2.1 RX-Busy Gate (TX-during-RX prevention) -`LoRaRadioBase::isReceiving()` is the single software source of truth for "currently receiving" and is consulted at three sites: dispatcher initial gate, dispatcher final gate, and `startSendRaw`'s last-moment gate. Logic: +`LoRaRadio::isReceiving()` is the single software source of truth for "currently receiving" and is consulted at three sites: dispatcher initial gate, dispatcher final gate, and `startSendRaw`'s last-moment gate. Logic: ``` isReceiving() @@ -401,7 +401,7 @@ The poll path is otherwise non-destructive — IRQ bits are cleared only by the ### 5.2.2 CAD-Timeout Recovery -`Dispatcher::checkSend()` tracks `cad_busy_start` while `isReceiving()` keeps the TX gate closed. If 4 s elapse (`getCADFailMaxDuration()`), the dispatcher calls `_radio->recoverRxState()` and returns. `LoRaRadioBase::recoverRxState()` does: +`Dispatcher::checkSend()` tracks `cad_busy_start` while `isReceiving()` keeps the TX gate closed. If 4 s elapse (`getCADFailMaxDuration()`), the dispatcher calls `_radio->recoverRxState()` and returns. `LoRaRadio::recoverRxState()` does: ```cpp hwCancelReceive(); // RX → IDLE → STANDBY → SLEEP (REST_STATE) @@ -426,7 +426,7 @@ Algorithm in `triggerNoiseFloorCalibrate()`: dBm): it gates on signal *strength* ≈ link budget, blind to distance, so raising it means "react to strong signals only, ignore faint/echo". The right LBT sensitivity is site-dependent and cannot be derived from the RSSI floor. -`LoRaRadioBase::cadMaintenance()` (housekeeping tick) runs one calibration CAD +`LoRaRadio::cadMaintenance()` (housekeeping tick) runs one calibration CAD probe per `probe.interval` (default **15 s**) at a signed **level** relative to the family's per-SF base detPeak, restarts RX, and classifies busy verdicts with a ground-truth filter. **Key property:** the probe is *skipped* when RSSI > @@ -705,7 +705,7 @@ Autonomous operation features: - **Region filtering**: `RegionMap` with transport key matching per flood packet - **Rate limiting**: 4 requests per 120s (discovery), 4 per 180s (anonymous), 4 failed logins per 180s - **Neighbor tracking**: RSSI/SNR/name/timestamp table (`CONFIG_ZEPHCORE_MAX_NEIGHBOURS`, default 50 slots) -- **Temporary radio params**: `tempradio` command applies freq/bw/sf/cr via `LoRaRadioBase::setRadioOverride()` (does not mutate `_prefs`); auto-revert timer calls `clearRadioOverride()` to fall back to saved prefs +- **Temporary radio params**: `tempradio` command applies freq/bw/sf/cr via `LoRaRadio::setRadioOverride()` (does not mutate `_prefs`); auto-revert timer calls `clearRadioOverride()` to fall back to saved prefs - **WiFi+MQTT uplink** (ESP32, `CONFIG_ZEPHCORE_REPEATER_UPLINK`): `RepeaterUplink.cpp` reports packets observer-style while still repeating; configured via `set uplink.*` CLI ### 6.4 RoomServerMesh @@ -1167,7 +1167,7 @@ upgrade block migrates them. ``` DIO1 interrupt → Zephyr lora driver → async RX callback - → LoRaRadioBase::rxCallbackStatic() → SPSC ring buffer write → _rx_cb() + → LoRaRadio::rxCallbackStatic() → SPSC ring buffer write → _rx_cb() → k_event_post(MESH_EVENT_LORA_RX) → main thread wakes → Dispatcher::loop() → checkRecv() → drain ring buffer → tryParsePacket() → score + airtime calc diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index a5b3d33..f95c258 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -341,7 +341,7 @@ target_sources(app PRIVATE src/StaticPoolPacketManager.cpp src/Utils.cpp adapters/board/ZephyrBoard.cpp - adapters/radio/LoRaRadioBase.cpp + adapters/radio/LoRaRadio.cpp adapters/radio/CadController.cpp adapters/clock/ZephyrMillisecondClock.cpp adapters/clock/ZephyrRTCClock.cpp @@ -464,26 +464,23 @@ if(CONFIG_ZEPHCORE_RADIO_LR1110) # (patches/zephyr/ auto-applied at configure time — see patch block above) # Only the ZephCore adapter is compiled here. target_sources(app PRIVATE - adapters/radio/LR1110Radio.cpp + adapters/radio/radio_ops_lr1110.cpp ) elseif(CONFIG_ZEPHCORE_RADIO_LR2021) message(STATUS "ZephCore Radio: LR2021 (Zephyr LoRa driver)") target_sources(app PRIVATE - adapters/radio/LR2021Radio.cpp - ) - target_include_directories(app PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/adapters/radio/lr20xx + adapters/radio/radio_ops_lr2021.cpp ) elseif(CONFIG_ZEPHCORE_RADIO_SX127X) message(STATUS "ZephCore Radio: SX127x (Zephyr loramac-node driver)") target_sources(app PRIVATE - adapters/radio/SX127xRadio.cpp + adapters/radio/radio_ops_sx127x.cpp ) else() # Default: SX126x via native Zephyr LoRa driver message(STATUS "ZephCore Radio: SX126x (native Zephyr driver)") target_sources(app PRIVATE - adapters/radio/SX126xRadio.cpp + adapters/radio/radio_ops_sx126x.cpp ) endif() diff --git a/zephcore/adapters/radio/CadController.h b/zephcore/adapters/radio/CadController.h index 6065ee8..64691f2 100644 --- a/zephcore/adapters/radio/CadController.h +++ b/zephcore/adapters/radio/CadController.h @@ -2,7 +2,7 @@ * SPDX-License-Identifier: MIT * Adaptive CAD controller: learns the LBT detPeak offset from probe verdicts. * - * Pure algorithm over a four-call hardware interface. LoRaRadioBase decides + * Pure algorithm over a four-call hardware interface. LoRaRadio decides * when to probe, runs the probe and feeds the verdicts in; this class keeps * the per-level statistics and moves the operating offset. Design notes: * devdocs/lld/03-radio-contract.md §13 and docs/ADAPTIVE_CAD.md. diff --git a/zephcore/adapters/radio/LR1110Radio.cpp b/zephcore/adapters/radio/LR1110Radio.cpp deleted file mode 100644 index 9a09a6b..0000000 --- a/zephcore/adapters/radio/LR1110Radio.cpp +++ /dev/null @@ -1,145 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * LR1110 hardware hooks for LoRaRadioBase. - */ - -#include "LR1110Radio.h" -#include - -/* LR11xx driver extension API */ -extern "C" { -#include -} - -#include -LOG_MODULE_REGISTER(lr1110_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); - -namespace mesh { - -LR1110Radio::LR1110Radio(const struct device *lora_dev, MainBoard &board) - : LoRaRadioBase(lora_dev, board) -{ -} - -uint32_t LR1110Radio::getDutyCycleTimeoutRestarts() const -{ - return lr11xx_get_dc_timeout_restarts(_dev); -} - -void LR1110Radio::resetDutyCycleTimeoutRestarts() -{ - lr11xx_reset_dc_timeout_restarts(_dev); -} - -/* ── Hardware primitives ──────────────────────────────────────────────── */ - -bool LR1110Radio::hwConfigure(const struct lora_modem_config &cfg) -{ - int ret = lora_config(_dev, &cfg); - if (ret < 0) { - LOG_ERR("lora_config failed: %d", ret); - return false; - } - return true; -} - -void LR1110Radio::hwCancelReceive() -{ - lora_recv_async(_dev, NULL, NULL); -} - -int LR1110Radio::hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) -{ - return lora_send_async(_dev, buf, len, sig); -} - -int16_t LR1110Radio::hwGetCurrentRSSI() -{ - return lr11xx_get_rssi_inst(_dev); -} - -int LR1110Radio::hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us) -{ - return lr11xx_get_rssi_burst(_dev, out, n, spacing_us); -} - -bool LR1110Radio::hwIsReceiving() -{ - /* Latch + IRQ read. The driver clears sticky bits only to release an - * expired preamble grace or payload deadline (continuous RX has no - * timeout to release them). */ - return lr11xx_is_receiving(_dev); -} - -/* No hwResetAgc() override, and no hwNeedsAgcReset(): this part has no - * jammed-AGC fault to remedy — that one belongs to the SX126x. The driver - * exposes lr11xx_recalibrate() instead, which is the temperature-drift path - * this family genuinely does need, named for what it actually does. */ - - -void LR1110Radio::hwRecalibrate() -{ - lr11xx_recalibrate(_dev); -} - -/* This family's UM gives an explicit temperature threshold for image - * calibration, so drift recalibration is active here. The temperature comes - * from the board, not the radio (LoRaRadioBase::imageCalMaintenance()). */ -bool LR1110Radio::hwHasDriftRecal() -{ - return true; -} - -void LR1110Radio::hwSetRxBoost(bool enable) -{ - lr11xx_set_rx_boost(_dev, enable); -} - -uint32_t LR1110Radio::hwWakeupTimeUs() -{ - return lr11xx_get_wakeup_time_us(_dev); -} - -int LR1110Radio::hwCadProbe(int8_t level) -{ - return lr11xx_cad_probe(_dev, level); -} - -int LR1110Radio::hwCadRxOutcome() -{ - return lr11xx_cad_rx_outcome(_dev); -} - -uint32_t LR1110Radio::hwCadRxTimeoutMs() -{ - return lr11xx_cad_rx_timeout_ms(_dev); -} - -void LR1110Radio::hwCadSetPeakOffset(int8_t offset) -{ - lr11xx_cad_set_peak_offset(_dev, offset); -} - -uint8_t LR1110Radio::hwCadBasePeak() -{ - return lr11xx_cad_base_peak(_dev); -} - -/* The detPeak range lr11xx_do_cad() will actually program. Must match the - * driver's clamp exactly: if the adapter thinks the range is wider, the - * staircase explores offsets that collapse onto one peak and reads the noise - * between them as curvature. Same reasoning as LR2021Radio::hwCadPeakMin — - * and the same symptom was observed here on T1000-E companions, which sat at - * o:-8 with the driver's old floor of 48 already reached. */ -uint8_t LR1110Radio::hwCadPeakMin() -{ - return lr11xx_cad_peak_min(); -} - -uint8_t LR1110Radio::hwCadPeakMax() -{ - return lr11xx_cad_peak_max(); -} - -} /* namespace mesh */ diff --git a/zephcore/adapters/radio/LR1110Radio.h b/zephcore/adapters/radio/LR1110Radio.h deleted file mode 100644 index 59d7328..0000000 --- a/zephcore/adapters/radio/LR1110Radio.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Radio adapter for LR1110/LR1120/LR1121 using Zephyr LoRa driver - * - * Thin wrapper around LoRaRadioBase — only hardware-specific hooks. - */ - -#pragma once - -#include "LoRaRadioBase.h" - -namespace mesh { - -class LR1110Radio : public LoRaRadioBase { -public: - LR1110Radio(const struct device *lora_dev, MainBoard &board); - - /* Duty-cycle false-preamble re-arm count, behind `get dc.restarts`. - * Without these the base class's stub answers 0 forever, and the one - * instrument for tuning the RX window reads clean on a node that is - * re-arming constantly. */ - uint32_t getDutyCycleTimeoutRestarts() const override; - void resetDutyCycleTimeoutRestarts() override; - -protected: - /* Hardware primitives */ - bool hwConfigure(const struct lora_modem_config &cfg) override; - void hwCancelReceive() override; - int hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) override; - int16_t hwGetCurrentRSSI() override; - int hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us) override; - bool hwIsReceiving() override; - void hwSetRxBoost(bool enable) override; - void hwRecalibrate() override; - bool hwHasDriftRecal() override; - uint32_t hwWakeupTimeUs() override; - int hwCadProbe(int8_t level) override; - int hwCadRxOutcome() override; - uint32_t hwCadRxTimeoutMs() override; - void hwCadSetPeakOffset(int8_t offset) override; - uint8_t hwCadBasePeak() override; - uint8_t hwCadPeakMin() override; - uint8_t hwCadPeakMax() override; -}; - -} /* namespace mesh */ diff --git a/zephcore/adapters/radio/LR2021Radio.cpp b/zephcore/adapters/radio/LR2021Radio.cpp deleted file mode 100644 index 797d791..0000000 --- a/zephcore/adapters/radio/LR2021Radio.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * LR2021 hardware hooks for LoRaRadioBase. - */ - -#include "LR2021Radio.h" -#include - -/* LR20xx driver extension API */ -extern "C" { -#include -} - -#include -LOG_MODULE_REGISTER(lr2021_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); - -namespace mesh { - -LR2021Radio::LR2021Radio(const struct device *lora_dev, MainBoard &board) - : LoRaRadioBase(lora_dev, board) -{ -} - -bool LR2021Radio::configSideDetectors(const uint8_t *sfs, uint8_t num) -{ - /* The driver validates against the configured SF/BW, so this must run - * after the modem is configured — which it is: the CLI path only - * reaches here on a live radio. */ - int ret = lr20xx_configure_side_detectors(_dev, sfs, num); - - if (ret < 0) { - LOG_WRN("side detector config rejected: %d", ret); - return false; - } - return true; -} - -void LR2021Radio::resetStats() -{ - LoRaRadioBase::resetStats(); - lr20xx_reset_freq_offset(_dev); -} - -uint32_t LR2021Radio::getDutyCycleTimeoutRestarts() const -{ - return lr20xx_get_dc_timeout_restarts(_dev); -} - -void LR2021Radio::resetDutyCycleTimeoutRestarts() -{ - lr20xx_reset_dc_timeout_restarts(_dev); -} - -int LR2021Radio::formatFreqErrorStatus(char *buf, int cap) -{ - struct lr20xx_freq_offset_stats st; - - if (lr20xx_get_freq_offset(_dev, &st) == 0) { - return snprintf(buf, cap, "no packets measured yet"); - } - - /* Spread and count matter as much as the mean: the mean only - * approximates THIS node's reference error once it is averaged over - * many different peers, because their individual errors cancel and ours - * does not. A tight spread over a handful of packets is one neighbour, - * not a population. */ - return snprintf(buf, cap, - "mean %d Hz, min %d, max %d, %u pkts", - st.mean_hz, st.min_hz, st.max_hz, - (unsigned)st.count); -} - -/* ── Hardware primitives ──────────────────────────────────────────────── */ - -bool LR2021Radio::hwConfigure(const struct lora_modem_config &cfg) -{ - int ret = lora_config(_dev, &cfg); - if (ret < 0) { - LOG_ERR("lora_config failed: %d", ret); - return false; - } - return true; -} - -void LR2021Radio::hwCancelReceive() -{ - lora_recv_async(_dev, NULL, NULL); -} - -int LR2021Radio::hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) -{ - return lora_send_async(_dev, buf, len, sig); -} - -int16_t LR2021Radio::hwGetCurrentRSSI() -{ - return lr20xx_get_rssi_inst(_dev); -} - -int LR2021Radio::hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us) -{ - return lr20xx_get_rssi_burst(_dev, out, n, spacing_us); -} - -/* Deliberately no hwIsChipBusy() override, matching the LR11xx. - * - * It backs LoRaRadioBase::isRadioReady(), which gates TX in startSendRaw() as - * well as the probes. Wiring BUSY into the transmit path stalled sends until - * the dispatcher's 4 s CAD timeout, because the event-driven loop has nothing - * to re-wake a deferred send. The duty-cycle sleep window is handled where it - * belongs instead: skipped in the two incidental pollers - * (lr20xx_is_receiving, lr20xx_get_rssi_inst) and stood down deliberately for - * TX inside the driver. */ - -bool LR2021Radio::hwIsReceiving() -{ - /* The poll itself is non-destructive: lr20xx_is_receiving() reads the - * IRQ register via get_status(), which clears nothing. - * - * Foreign-preamble release is NOT hardware-driven, whatever the comment - * that stood here (copy-pasted from LR1110Radio.cpp) claimed. DS §5.7: - * IRQ status bits are latched until ClearIrq, and continuous RX has no - * timeout — so the driver releases them in software, on an SF-aware - * grace for the preamble and a max-airtime deadline for the header, - * exactly as the LR11xx and SX126x drivers do. */ - return lr20xx_is_receiving(_dev); -} - -void LR2021Radio::hwSetRxBoost(bool enable) -{ - lr20xx_set_rx_boost(_dev, enable); -} - -int LR2021Radio::hwCadProbe(int8_t level) -{ - return lr20xx_cad_probe(_dev, level); -} - -/* Re-enables adaptive CAD on this family. It was excluded in Batch B - * (plan B-D2) because the chip's CAD_RX exit uses cad_timeout, 24 bits of - * 32 MHz periods = 524 ms, against 1704 ms of max-length airtime -- a - * CAD_RX there would truncate receptions. The driver no longer uses that - * exit: a probe runs CAD_ONLY and the DIO1 handler arms the follow-on Rx - * with a normal SetRx, whose timeout is 24 bits of RTC steps = 512 s. The - * ceiling that justified the exclusion does not exist on that path, and the - * same construction is what fixed the SX126x and LR11xx after both of their - * chip CAD_RX exits were measured misbehaving on hardware. - * - * NOT hardware-verified: no LR2021 was available when this was written. - * Verify on a MeshTracker X1 with the same test the LR11xx fix used -- - * reception count while probing (must stay ~100%) and tp becoming - * non-zero -- before trusting it in the field. */ -int LR2021Radio::hwCadRxOutcome() -{ - return lr20xx_cad_rx_outcome(_dev); -} - -uint32_t LR2021Radio::hwCadRxTimeoutMs() -{ - return lr20xx_cad_rx_timeout_ms(_dev); -} - -void LR2021Radio::hwCadSetPeakOffset(int8_t offset) -{ - lr20xx_cad_set_peak_offset(_dev, offset); -} - -uint8_t LR2021Radio::hwCadBasePeak() -{ - return lr20xx_cad_base_peak(_dev); -} - -/* The detPeak range lr20xx_do_cad() will actually program. Must match the - * driver's clamp exactly: if the adapter thinks the range is wider, the - * staircase explores offsets that collapse onto one peak and reads the noise - * between them as curvature — which is how it random-walked to -8 at SF7, - * where base 51 puts everything from -3 down onto peak 48. */ -uint8_t LR2021Radio::hwCadPeakMin() -{ - return lr20xx_cad_peak_min(); -} - -uint8_t LR2021Radio::hwCadPeakMax() -{ - return lr20xx_cad_peak_max(); -} - -/* No hwResetAgc() override, and no hwNeedsAgcReset(): this part has no - * jammed-AGC fault to remedy — that one belongs to the SX126x. The driver - * exposes lr20xx_recalibrate() instead, which is the temperature-drift path - * this family genuinely does need, named for what it actually does. */ - - -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); -} - -/* DS 6.4.2 gives an image/FE calibration temperature range (and advises redoing - * PLL/AAF beyond +/-20 C), so drift recalibration is active here. The reading - * comes from the board — see LoRaRadioBase::imageCalMaintenance(). */ -bool LR2021Radio::hwHasDriftRecal() -{ - return true; -} - -uint32_t LR2021Radio::hwWakeupTimeUs() -{ - /* Per-device, because the TCXO term dominates and is board-specific: - * meshtracker_x1 declares tcxo-startup-delay-ms = <5>, promicro_lr2021 - * is XTAL-only. Inheriting the base class's flat 1500 us under-counted - * the X1's real deaf time by ~4.6 ms, which oversized the duty-cycle - * sleep and dropped window-edge preambles regardless of signal strength. - * Same shape as the SX126x and LR11xx overrides. */ - return lr20xx_get_wakeup_time_us(_dev); -} - -} /* namespace mesh */ diff --git a/zephcore/adapters/radio/LR2021Radio.h b/zephcore/adapters/radio/LR2021Radio.h deleted file mode 100644 index a7ca83a..0000000 --- a/zephcore/adapters/radio/LR2021Radio.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Radio adapter for LR2021 using Zephyr LoRa driver - * - * Thin wrapper around LoRaRadioBase — only hardware-specific hooks. - */ - -#pragma once - -#include "LoRaRadioBase.h" - -namespace mesh { - -class LR2021Radio : public LoRaRadioBase { -public: - LR2021Radio(const struct device *lora_dev, MainBoard &board); - - /* Duty-cycle false-preamble re-arm count, behind `get dc.restarts`. - * Without these the base class's stub answers 0 forever, and the one - * instrument for tuning the RX window reads clean on a node that is - * re-arming constantly. */ - uint32_t getDutyCycleTimeoutRestarts() const override; - void resetDutyCycleTimeoutRestarts() override; - - /* LoRa side detectors — LR2021-only multi-SF receive. */ - bool configSideDetectors(const uint8_t *sfs, uint8_t num) override; - - /* Carrier frequency error accumulated from received packets. LR2021 - * only: driver v2.0.2 decodes it per packet, no other radio here - * reports it. */ - int formatFreqErrorStatus(char *buf, int cap) override; - - /* Also clears the frequency-error accumulator, so `clear stats` gives a - * clean baseline after changing frequency or swapping a module. */ - void resetStats() override; - -protected: - /* Hardware primitives */ - bool hwConfigure(const struct lora_modem_config &cfg) override; - void hwCancelReceive() override; - int hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) override; - int16_t hwGetCurrentRSSI() override; - int hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us) override; - bool hwIsReceiving() override; - void hwSetRxBoost(bool enable) override; - int hwCadProbe(int8_t level) override; - int hwCadRxOutcome() override; - uint32_t hwCadRxTimeoutMs() override; - void hwCadSetPeakOffset(int8_t offset) override; - uint8_t hwCadBasePeak() override; - uint8_t hwCadPeakMin() override; - uint8_t hwCadPeakMax() override; - uint32_t hwWakeupTimeUs() override; - void hwRecalibrate() override; - bool hwHasDriftRecal() override; -}; - -} /* namespace mesh */ diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadio.cpp similarity index 92% rename from zephcore/adapters/radio/LoRaRadioBase.cpp rename to zephcore/adapters/radio/LoRaRadio.cpp index 0be572c..55d413f 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadio.cpp @@ -3,7 +3,7 @@ * LoRa radio base class — shared algorithms for all radio adapters. */ -#include "LoRaRadioBase.h" +#include "LoRaRadio.h" #include "radio_common.h" #include "pm_sleep_guard.h" #include /* MAX_TRANS_UNIT */ @@ -40,8 +40,8 @@ static uint16_t rxDutyDetectSymbols(uint8_t sf) /* ── Constructor ─────────────────────────────────────────────── */ -LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board) - : _loramac_node(false), +LoRaRadio::LoRaRadio(const struct device *lora_dev, MainBoard &board) + : _ops(kLoRaRadioOps), _dev(lora_dev), _prefs(nullptr), _board(&board), _in_recv_mode(0), _tx_active(0), _tx_complete(0), _last_rssi(0), _last_snr(0), @@ -54,7 +54,7 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board) _rssi_bursts(0), _rssi_spread_sum(0), _rssi_degenerate(0), _cad(*this), _probe_interval_s(0), _cad_last_decay_ms(0), _rx_duty_cycle_enabled(IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)), - _rx_boost_enabled(true), + _rx_boost_enabled(kLoRaRadioOps.set_rx_boost != nullptr), _dc_last_rx_us(0), _dc_last_sleep_us(0), _agc_rx_count_shadow(0), _agc_last_activity_ms(0), _agc_rssi_last(0), _agc_rssi_frozen(0), @@ -84,7 +84,7 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board) /* How long to wait for TX_DONE: 2 x airtime + 1 s, never below TX_TIMEOUT_MS. * A flat 5 s is shorter than a long packet on slow presets (255 B at * SF12/BW62.5 is 28.6 s). lora_airtime() is pure math on the cached config. */ -uint32_t LoRaRadioBase::txWaitBudgetMs() const +uint32_t LoRaRadio::txWaitBudgetMs() const { /* _tx_len is set by startSendRaw() before the handoff; 0 is defensive. */ uint32_t air = lora_airtime(_dev, _tx_len ? _tx_len : MAX_TRANS_UNIT); @@ -101,9 +101,9 @@ uint32_t LoRaRadioBase::txWaitBudgetMs() const return MAX(TX_TIMEOUT_MS, 2U * air + 1000U); } -void LoRaRadioBase::txWaitThreadFn(void *p1, void *p2, void *p3) +void LoRaRadio::txWaitThreadFn(void *p1, void *p2, void *p3) { - LoRaRadioBase *self = static_cast(p1); + LoRaRadio *self = static_cast(p1); ARG_UNUSED(p2); ARG_UNUSED(p3); @@ -217,7 +217,7 @@ void LoRaRadioBase::txWaitThreadFn(void *p1, void *p2, void *p3) /* One radio per build, so one stack. */ K_THREAD_STACK_DEFINE(lora_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE); -void LoRaRadioBase::startTxThread() +void LoRaRadio::startTxThread() { if (_tx_thread_running) { return; @@ -232,11 +232,11 @@ void LoRaRadioBase::startTxThread() /* ── RX callback (static, ISR-safe) ──────────────────────────────────── */ -void LoRaRadioBase::rxCallbackStatic(const struct device *dev, uint8_t *data, +void LoRaRadio::rxCallbackStatic(const struct device *dev, uint8_t *data, uint16_t size, int16_t rssi, int8_t snr, void *user_data) { - LoRaRadioBase *self = static_cast(user_data); + LoRaRadio *self = static_cast(user_data); /* NULL data = RX error (CRC/header error) */ if (data == NULL && size == 0) { @@ -284,7 +284,7 @@ void LoRaRadioBase::rxCallbackStatic(const struct device *dev, uint8_t *data, /* ── Config helpers ───────────────────────────────────────────────────── */ -void LoRaRadioBase::buildModemConfig(struct lora_modem_config &cfg, bool tx) +void LoRaRadio::buildModemConfig(struct lora_modem_config &cfg, bool tx) { memset(&cfg, 0, sizeof(cfg)); /* Override wins for freq/bw/sf/cr (tempradio). Power, preamble, and @@ -327,43 +327,43 @@ void LoRaRadioBase::buildModemConfig(struct lora_modem_config &cfg, bool tx) cfg.cad.symbol_num = LORA_CAD_SYMB_4; } -uint32_t LoRaRadioBase::getActiveFrequencyHz() const +uint32_t LoRaRadio::getActiveFrequencyHz() const { float freq_mhz = _has_radio_override ? _override_freq : _prefs->freq; return (uint32_t)(freq_mhz * 1000000.0f + 0.5f); } -uint16_t LoRaRadioBase::getActiveBandwidthKHzX10() const +uint16_t LoRaRadio::getActiveBandwidthKHzX10() const { float bw_khz = _has_radio_override ? _override_bw : _prefs->bw; return (uint16_t)(bw_khz * 10.0f + 0.5f); } -uint8_t LoRaRadioBase::getActiveSpreadingFactor() const +uint8_t LoRaRadio::getActiveSpreadingFactor() const { return _has_radio_override ? _override_sf : _prefs->sf; } -uint8_t LoRaRadioBase::getActiveCodingRate() const +uint8_t LoRaRadio::getActiveCodingRate() const { return _has_radio_override ? _override_cr : _prefs->cr; } -uint16_t LoRaRadioBase::getActivePreambleLength() const +uint16_t LoRaRadio::getActivePreambleLength() const { return preambleLengthForSF(getActiveSpreadingFactor()); } -uint8_t LoRaRadioBase::getActiveSyncWord() const +uint8_t LoRaRadio::getActiveSyncWord() const { /* buildModemConfig() currently sets public_network=false, which maps * Zephyr's LoRa API to the Semtech private sync word. */ return 0x12; } -int8_t LoRaRadioBase::getConfiguredTxPower() const +int8_t LoRaRadio::getConfiguredTxPower() const { int power = _prefs->tx_power_dbm; @@ -421,7 +421,7 @@ static bool onlyDirectionDiffers(const struct lora_modem_config &a, a.tx != b.tx; } -void LoRaRadioBase::configure(bool tx) +void LoRaRadio::configure(bool tx) { struct lora_modem_config cfg; buildModemConfig(cfg, tx); @@ -444,7 +444,7 @@ void LoRaRadioBase::configure(bool tx) * Not used for loramac-node: Radio.SetTxConfig() and Radio.SetRxConfig() * configure completely disjoint internal state (including TxTimeout). * Skipping either on a direction change leaves that state uninitialized. */ - if (!_loramac_node && _config_cached && onlyDirectionDiffers(cfg, _last_cfg)) { + if (!_ops.loramac_node && _config_cached && onlyDirectionDiffers(cfg, _last_cfg)) { LOG_DBG("%s: direction-only change, skip hwConfigure", who); _last_cfg = cfg; return; @@ -464,12 +464,23 @@ void LoRaRadioBase::configure(bool tx) } } -void LoRaRadioBase::configureRx() { configure(false); } -void LoRaRadioBase::configureTx() { configure(true); } +bool LoRaRadio::hwConfigure(const struct lora_modem_config &cfg) +{ + int ret = lora_config(_dev, &cfg); + + if (ret < 0) { + LOG_ERR("lora_config failed: %d", ret); + return false; + } + return true; +} + +void LoRaRadio::configureRx() { configure(false); } +void LoRaRadio::configureTx() { configure(true); } /* ── Lifecycle ────────────────────────────────────────────────────────── */ -void LoRaRadioBase::begin() +void LoRaRadio::begin() { if (_prefs == nullptr) { LOG_ERR("radio begin() without setPrefs()"); @@ -498,9 +509,13 @@ void LoRaRadioBase::begin() LOG_INF("radio started: freq=%u bw=%u sf=%u cr=%u pwr=%d", freq, bw_khz, sf, cr, tx_pwr); + + if (_ops.post_begin) { + _ops.post_begin(_dev); + } } -void LoRaRadioBase::reconfigure() +void LoRaRadio::reconfigure() { hwCancelReceive(); atomic_set(&_in_recv_mode, 0); @@ -519,7 +534,7 @@ void LoRaRadioBase::reconfigure() freq, bw_khz, sf, cr, tx_pwr); } -void LoRaRadioBase::setRadioOverride(float freq, float bw, uint8_t sf, uint8_t cr, +void LoRaRadio::setRadioOverride(float freq, float bw, uint8_t sf, uint8_t cr, bool visiting_new_preset) { _override_freq = freq; @@ -534,7 +549,7 @@ void LoRaRadioBase::setRadioOverride(float freq, float bw, uint8_t sf, uint8_t c _cad.beginVisit(visiting_new_preset); } -void LoRaRadioBase::clearRadioOverride() +void LoRaRadio::clearRadioOverride() { if (!_has_radio_override) { return; @@ -544,7 +559,7 @@ void LoRaRadioBase::clearRadioOverride() _cad.endVisit(); } -void LoRaRadioBase::startReceive() +void LoRaRadio::startReceive() { configureRx(); @@ -642,7 +657,7 @@ void LoRaRadioBase::startReceive() /* ── RX/TX ────────────────────────────────────────────────────────────── */ -int LoRaRadioBase::recvRaw(uint8_t *bytes, int sz) +int LoRaRadio::recvRaw(uint8_t *bytes, int sz) { uint8_t tail = (uint8_t)atomic_get(&_rx_tail); if (atomic_get(&_rx_head) == tail) { @@ -662,7 +677,7 @@ int LoRaRadioBase::recvRaw(uint8_t *bytes, int sz) return (int)len; } -bool LoRaRadioBase::startSendRaw(const uint8_t *bytes, int len) +bool LoRaRadio::startSendRaw(const uint8_t *bytes, int len) { if (len > (int)sizeof(_tx_buf)) { return false; @@ -731,7 +746,7 @@ bool LoRaRadioBase::startSendRaw(const uint8_t *bytes, int len) return true; } -bool LoRaRadioBase::isSendComplete() +bool LoRaRadio::isSendComplete() { /* One-shot, and it owns _packets_sent, like upstream's * RadioLibWrapper::isSendComplete(): the radio and dispatcher tallies @@ -743,28 +758,31 @@ bool LoRaRadioBase::isSendComplete() return false; } -void LoRaRadioBase::onSendFinished() +void LoRaRadio::onSendFinished() { /* Nothing needed — TX state tracked via _tx_active */ } -bool LoRaRadioBase::isInRecvMode() const +bool LoRaRadio::isInRecvMode() const { return atomic_get(&_in_recv_mode) != 0; } -float LoRaRadioBase::getLastRSSI() const +float LoRaRadio::getLastRSSI() const { return _last_rssi; } -float LoRaRadioBase::getLastSNR() const +float LoRaRadio::getLastSNR() const { return _last_snr; } -int LoRaRadioBase::hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us) +int LoRaRadio::hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us) { + if (_ops.rssi_burst) { + return _ops.rssi_burst(_dev, out, n, spacing_us); + } for (int i = 0; i < n; i++) { if (i) { k_busy_wait(spacing_us); @@ -777,7 +795,7 @@ int LoRaRadioBase::hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us) return n; } -bool LoRaRadioBase::isRadioReady() +bool LoRaRadio::isRadioReady() { /* BUSY high means the radio cannot accept SPI commands now * (e.g. duty-cycle sleep phase on SX126x/LR11xx). */ @@ -786,7 +804,7 @@ bool LoRaRadioBase::isRadioReady() /* ── Airtime + scoring ────────────────────────────────────────────────── */ -uint32_t LoRaRadioBase::getEstAirtimeFor(int len_bytes) +uint32_t LoRaRadio::getEstAirtimeFor(int len_bytes) { /* The params the radio is actually running (a tempradio override * included), resolved through the same BW enum as buildModemConfig(). */ @@ -794,7 +812,7 @@ uint32_t LoRaRadioBase::getEstAirtimeFor(int len_bytes) float bw = _has_radio_override ? _override_bw : _prefs->bw; uint8_t cr_val = getActiveCodingRate(); - uint8_t min_sf = _loramac_node ? 6 : 5; + uint8_t min_sf = _ops.loramac_node ? 6 : 5; if (sf < min_sf) sf = min_sf; if (sf > 12) sf = 12; if (bw < 7.0f) bw = 125.0f; @@ -808,7 +826,7 @@ uint32_t LoRaRadioBase::getEstAirtimeFor(int len_bytes) * format. See Semtech's lr11xx_radio_get_lora_time_on_air_numerator. * Treating SF5 as SF6 charged 397 ms for a 126-byte SF5/BW62.5 CR4/8 * packet whose Semtech airtime is 237 ms (confirmed by TX timing). */ - float fine_sync = (!_loramac_node && sf <= 6) ? 1.0f : 0.0f; + float fine_sync = (!_ops.loramac_node && sf <= 6) ? 1.0f : 0.0f; float t_preamble = (preambleLengthForSF(sf) + 4.25f + 2.0f * fine_sync) * t_sym; /* LDRO threshold must track the SX126x driver's should_enable_ldro() @@ -825,7 +843,7 @@ uint32_t LoRaRadioBase::getEstAirtimeFor(int len_bytes) return (uint32_t)((t_preamble + t_payload) * 1000.0f); } -float LoRaRadioBase::packetScore(float snr, int packet_len) +float LoRaRadio::packetScore(float snr, int packet_len) { int sf = _prefs->sf; if (sf < 7 || sf > 12) return 0.0f; @@ -841,12 +859,12 @@ float LoRaRadioBase::packetScore(float snr, int packet_len) /* ── Advanced radio features ──────────────────────────────────────────── */ -int LoRaRadioBase::getNoiseFloor() const +int LoRaRadio::getNoiseFloor() const { return _floor_est.floor(); } -void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold) +void LoRaRadio::triggerNoiseFloorCalibrate(int threshold) { _calibration_threshold = threshold; @@ -983,7 +1001,7 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold) } } -bool LoRaRadioBase::isReceiving() +bool LoRaRadio::isReceiving() { if (!atomic_get(&_in_recv_mode) || atomic_get(&_tx_active)) { return false; @@ -1030,7 +1048,7 @@ bool LoRaRadioBase::isReceiving() /* Two unrelated jobs that share one precondition (chip idle) and one * cadence; kept in separate functions. */ -void LoRaRadioBase::radioMaintenance() +void LoRaRadio::radioMaintenance() { /* Never mid-TX or mid-RX: both jobs warm-sleep the chip. The RX counters * cannot see a packet landing now; isReceiving() can. */ @@ -1070,7 +1088,7 @@ void LoRaRadioBase::radioMaintenance() } /* Receiver watchdog: a stuck AGC shows up as total silence. */ -void LoRaRadioBase::agcIdleMaintenance(uint32_t now) +void LoRaRadio::agcIdleMaintenance(uint32_t now) { if (!hwNeedsAgcReset()) { return; @@ -1098,7 +1116,7 @@ void LoRaRadioBase::agcIdleMaintenance(uint32_t now) } /* Front-end image calibration against temperature drift. */ -void LoRaRadioBase::imageCalMaintenance(uint32_t now) +void LoRaRadio::imageCalMaintenance(uint32_t now) { if (!hwHasDriftRecal()) { return; @@ -1163,7 +1181,7 @@ void LoRaRadioBase::imageCalMaintenance(uint32_t now) _image_cal_last_temp_c = temp_c; } -void LoRaRadioBase::recoverRxState() +void LoRaRadio::recoverRxState() { /* Dispatcher CAD-timeout recovery (isReceiving() pinned > 4 s). Walk * the chip through REST: a bare startReceive() fails the driver's @@ -1174,7 +1192,7 @@ void LoRaRadioBase::recoverRxState() startReceive(); } -bool LoRaRadioBase::isChannelActive(int threshold) +bool LoRaRadio::isChannelActive(int threshold) { if (threshold == 0) { threshold = _calibration_threshold; @@ -1188,7 +1206,7 @@ bool LoRaRadioBase::isChannelActive(int threshold) /* ── Adaptive CAD (LBT detPeak calibration) ───────────────────────────── */ -void LoRaRadioBase::setCadParams(bool auto_enabled, int8_t offset, +void LoRaRadio::setCadParams(bool auto_enabled, int8_t offset, uint16_t probe_interval_s, uint8_t busycap_pct, uint8_t stored_base) { @@ -1206,12 +1224,12 @@ void LoRaRadioBase::setCadParams(bool auto_enabled, int8_t offset, (unsigned)_measure_interval_ms, (unsigned)busycap_pct); } -uint8_t LoRaRadioBase::cadBasePeak() +uint8_t LoRaRadio::cadBasePeak() { return hwCadBasePeak(); } -void LoRaRadioBase::cadMaintenance() +void LoRaRadio::cadMaintenance() { if (_probe_interval_s == 0) { return; @@ -1257,8 +1275,8 @@ void LoRaRadioBase::cadMaintenance() } /* No CAD_RX ground truth, no probing: an unresolvable busy reads as - * FP 0 everywhere and walks the staircase to the sensitive rail. Today - * that is the LR2021, whose 524 ms cad_timeout would truncate packets. */ + * FP 0 everywhere and walks the staircase to the sensitive rail. Only + * radios without hardware CAD (SX127x) report 0 here. */ if (hwCadRxTimeoutMs() == 0) { return; } @@ -1324,7 +1342,7 @@ static uint32_t clampDeadline(int64_t remaining_ms) * probing is enabled). Both hold absolute uptime deadlines, so this is a pure * read — it must not touch the chip, since the event loop calls it on every * wake to decide how long it may sleep. */ -uint32_t LoRaRadioBase::msUntilNextMaintenance() +uint32_t LoRaRadio::msUntilNextMaintenance() { int64_t now = k_uptime_get(); uint32_t next = mesh::MAINTENANCE_IDLE; @@ -1355,7 +1373,7 @@ uint32_t LoRaRadioBase::msUntilNextMaintenance() next, clampDeadline(_cad_last_decay_ms + (int64_t)CAD_STATS_DECAY_MS - now)); } -int LoRaRadioBase::formatCadStatus(char *buf, int cap) +int LoRaRadio::formatCadStatus(char *buf, int cap) { uint8_t base = hwCadBasePeak(); int n = 0; @@ -1438,7 +1456,7 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap) /* ── Power saving ─────────────────────────────────────────────────────── */ -void LoRaRadioBase::enableRxDutyCycle(bool enable) +void LoRaRadio::enableRxDutyCycle(bool enable) { _rx_duty_cycle_enabled = enable; LOG_INF("RX duty cycle %s", enable ? "enabled" : "disabled"); @@ -1451,8 +1469,11 @@ void LoRaRadioBase::enableRxDutyCycle(bool enable) } } -bool LoRaRadioBase::setRxBoost(bool enable) +bool LoRaRadio::setRxBoost(bool enable) { + if (!_ops.set_rx_boost) { + return false; + } _rx_boost_enabled = enable; LOG_INF("RX boost %s (+3dB sensitivity, +2mA)", enable ? "enabled" : "disabled"); @@ -1462,4 +1483,30 @@ bool LoRaRadioBase::setRxBoost(bool enable) return true; } +bool LoRaRadio::setFemRxEnable(bool enable) +{ + if (!_ops.set_fem_rx || !_ops.set_fem_rx(_dev, enable)) { + return false; + } + LOG_INF("FEM RX gain %s", enable ? "enabled" : "disabled"); + return true; +} + +bool LoRaRadio::configSideDetectors(const uint8_t *sfs, uint8_t num) +{ + if (!_ops.side_detectors) { + return false; + } + + /* The driver validates against the configured SF/BW; the CLI only + * reaches here on a live radio. */ + int ret = _ops.side_detectors(_dev, sfs, num); + + if (ret < 0) { + LOG_WRN("side detector config rejected: %d", ret); + return false; + } + return true; +} + } /* namespace mesh */ diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadio.h similarity index 70% rename from zephcore/adapters/radio/LoRaRadioBase.h rename to zephcore/adapters/radio/LoRaRadio.h index 90a4843..2808395 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadio.h @@ -14,12 +14,13 @@ #include "radio_common.h" #include "CadController.h" #include "NoiseFloorEstimator.h" +#include "LoRaRadioOps.h" namespace mesh { -class LoRaRadioBase : public Radio, private CadHw { +class LoRaRadio final : public Radio, private CadHw { public: - LoRaRadioBase(const struct device *lora_dev, MainBoard &board); + LoRaRadio(const struct device *lora_dev, MainBoard &board); /* Mandatory, before begin(): the radio reads freq/bw/sf/cr/power * through this pointer from then on (LoRaConfig only seeds prefs). */ @@ -62,12 +63,14 @@ public: uint32_t getPacketsRecv() const override { return (uint32_t)atomic_get(&_packets_recv); } uint32_t getPacketsSent() const override { return (uint32_t)atomic_get(&_packets_sent); } uint32_t getPacketsRecvErrors() const override { return (uint32_t)atomic_get(&_packets_recv_errors); } - /* Virtual: reached through LoRaRadioBase& (RepeaterMesh::clearStats()), - * so radios with extra accumulators clear them on the same `clear stats`. */ - virtual void resetStats() { + /* `clear stats`, including a family's own accumulators (LR2021). */ + void resetStats() { atomic_set(&_packets_recv, 0); atomic_set(&_packets_sent, 0); atomic_set(&_packets_recv_errors, 0); + if (_ops.reset_stats) { + _ops.reset_stats(_dev); + } } /* Advanced radio features */ @@ -83,19 +86,19 @@ public: void enableRxDutyCycle(bool enable); bool isRxDutyCycleEnabled() const { return _rx_duty_cycle_enabled; } /* Returns false when the chip has no RX boost feature (SX127x). */ - virtual bool setRxBoost(bool enable); + bool setRxBoost(bool enable); bool isRxBoostEnabled() const { return _rx_boost_enabled; } /* External FEM LNA in RX (lna-bypass-gpios): false routes RX around the * LNA, trading its gain for its current. Returns false where the radio * or the board has no such select line. */ - virtual bool setFemRxEnable(bool enable) { (void)enable; return false; } + bool setFemRxEnable(bool enable); /* Multi-SF receive via side detectors (LR2021 only). num = 0 disables. * False when unsupported or the set violates a chip constraint. */ - virtual bool configSideDetectors(const uint8_t *sfs, uint8_t num) { - (void)sfs; (void)num; - return false; + bool configSideDetectors(const uint8_t *sfs, uint8_t num); + int formatFreqErrorStatus(char *buf, int cap) override { + return _ops.format_freq_error ? _ops.format_freq_error(_dev, buf, cap) : 0; } /* Read-only view of the modem config currently used by buildModemConfig(). @@ -112,8 +115,14 @@ public: /* Duty-cycle false-preamble re-arms, counted by the driver (each one * stretches RX time past the nominal cycle). 0 where unsupported. */ - virtual uint32_t getDutyCycleTimeoutRestarts() const { return 0; } - virtual void resetDutyCycleTimeoutRestarts() {} + uint32_t getDutyCycleTimeoutRestarts() const { + return _ops.dc_restarts ? _ops.dc_restarts(_dev) : 0; + } + void resetDutyCycleTimeoutRestarts() { + if (_ops.reset_dc_restarts) { + _ops.reset_dc_restarts(_dev); + } + } /* Adaptive CAD (LBT detPeak calibration) */ void setCadParams(bool auto_enabled, int8_t offset, @@ -136,91 +145,84 @@ public: bool cadRelaxOnTxStarvation() override { return _cad.relaxOnTxStarvation(); } int formatCadStatus(char *buf, int cap) override; -protected: - /* ── Hardware primitives — subclass MUST implement ─────────── */ +private: + /* ── The radio family (LoRaRadioOps.h): the Zephyr LoRa API plus the + * driver's extension table. A NULL entry is the documented default. ── */ + const LoRaRadioOps &_ops; - virtual bool hwConfigure(const struct lora_modem_config &cfg) = 0; - virtual void hwCancelReceive() = 0; - virtual int hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) = 0; - virtual int16_t hwGetCurrentRSSI() = 0; + bool hwConfigure(const struct lora_modem_config &cfg); + void hwCancelReceive() { lora_recv_async(_dev, NULL, NULL); } + int hwSendAsync(uint8_t *buf, uint32_t len, struct k_poll_signal *sig) + { + return lora_send_async(_dev, buf, len, sig); + } + int16_t hwGetCurrentRSSI() { return _ops.rssi_inst ? _ops.rssi_inst(_dev) : -80; } /* Read `n` RSSI samples `spacing_us` apart, bracketed once (on the LR * families each single read tears the duty cycle down and back up). * Returns samples written (< n: refused partway, abandon), or -EAGAIN * when a preamble/header landed in the window (a busy channel, not a - * failing bus). The default per-sample loop suits SX126x and SX127x. */ - virtual int hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us); + * failing bus). Without a family burst: one read per sample. */ + int hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us); /* The radio's "currently receiving" signal: latch + raw IRQ bits. Clears - * sticky bits only to release an expired grace or payload deadline. - * Backs LoRaRadioBase::isReceiving(). */ - virtual bool hwIsReceiving() = 0; - virtual void hwSetRxBoost(bool enable) = 0; + * sticky bits only to release an expired grace or payload deadline. */ + bool hwIsReceiving() { return _ops.is_receiving && _ops.is_receiving(_dev); } + void hwSetRxBoost(bool enable) + { + if (_ops.set_rx_boost) { + _ops.set_rx_boost(_dev, enable); + } + } + /* GPIO-only BUSY check (no SPI). */ + bool hwIsChipBusy() { return _ops.is_chip_busy && _ops.is_chip_busy(_dev); } - /** GPIO-only BUSY check (no SPI). Default false for chips without duty-cycle sleep. */ - virtual bool hwIsChipBusy() { return false; } - - /* ── Adaptive-CAD primitives — defaults suit chips without hardware - * CAD (SX127x): probing unsupported, offset ignored. ───────────── */ - - /** Blocking calibration CAD at (family base detPeak + level). - * Returns 0 = free (chip in STANDBY, caller restarts RX), - * 1 = busy, chip in STANDBY, caller restarts RX, - * 2 = busy, chip left in RX on the detected signal (CAD_RX - * exit mode) -- caller must NOT restart RX, and reads the - * outcome later via hwCadRxOutcome(), - * <0 = error / unsupported. */ - virtual int hwCadProbe(int8_t level) { (void)level; return -ENOSYS; } - - /** Outcome of the RX a hwCadProbe() == 2 left the chip in. - * 1 = a packet completed, so the detection was real; - * 2 = the chip's own CAD timeout expired with nothing decoded; - * 0 = not armed, or the terminal interrupt has not arrived yet. - * Reads driver state only -- no chip access, no polling loop. */ - virtual int hwCadRxOutcome() { return 0; } - - /** How long the chip may stay in a CAD_RX-entered RX before it raises - * its own timeout. Bounds the wait before hwCadRxOutcome() is read. */ - virtual uint32_t hwCadRxTimeoutMs() { return 0; } + /* Blocking calibration CAD at (family base detPeak + level). + * Returns 0 = free (chip in STANDBY, caller restarts RX), + * 1 = busy, chip in STANDBY, caller restarts RX, + * 2 = busy, chip left in RX on the detected signal -- caller must + * NOT restart RX, and reads the outcome later, + * <0 = error / unsupported. */ + int hwCadProbe(int8_t level) + { + return _ops.cad_probe ? _ops.cad_probe(_dev, level) : -ENOSYS; + } + /* Outcome of the RX a probe == 2 left the chip in: 1 = a packet + * completed, 2 = the Rx timed out with nothing decoded, 0 = not armed or + * not resolved yet. Driver state only, no chip access. */ + int hwCadRxOutcome() { return _ops.cad_rx_outcome ? _ops.cad_rx_outcome(_dev) : 0; } + /* The Rx bound a detecting probe programs; 0 = no probing. */ + uint32_t hwCadRxTimeoutMs() + { + return _ops.cad_rx_timeout_ms ? _ops.cad_rx_timeout_ms(_dev) : 0; + } /* CadHw (see CadController.h): offset, base detPeak (0 = unsupported) - * and the driver's detPeak clamp (0/0 = none). Report the clamp: past it - * several offsets program one peak and the staircase random-walks. */ - void hwCadSetPeakOffset(int8_t offset) override { (void)offset; } - uint8_t hwCadBasePeak() override { return 0; } - uint8_t hwCadPeakMin() override { return 0; } - uint8_t hwCadPeakMax() override { return 0; } + * and the driver's detPeak clamp (0/0 = none). */ + void hwCadSetPeakOffset(int8_t offset) override + { + if (_ops.cad_set_peak_offset) { + _ops.cad_set_peak_offset(_dev, offset); + } + } + uint8_t hwCadBasePeak() override { return _ops.cad_base_peak ? _ops.cad_base_peak(_dev) : 0; } + uint8_t hwCadPeakMin() override { return _ops.cad_peak_min ? _ops.cad_peak_min() : 0; } + uint8_t hwCadPeakMax() override { return _ops.cad_peak_max ? _ops.cad_peak_max() : 0; } - /* ── Receiver hygiene — see LoRaRadioBase::radioMaintenance() ─── */ + /* Receiver hygiene (radioMaintenance()). Both leave the driver out of RX, + * so the caller's startReceive() is a real re-entry. Only the SX126x has + * the jammed-AGC fault; only the LR families specify drift recalibration. */ + bool hwNeedsAgcReset() const { return _ops.reset_agc != nullptr; } + void hwResetAgc() { if (_ops.reset_agc) { _ops.reset_agc(_dev); } } + bool hwHasDriftRecal() const { return _ops.recalibrate != nullptr; } + void hwRecalibrate() { if (_ops.recalibrate) { _ops.recalibrate(_dev); } } - /* Unstick a jammed AGC: warm sleep + recalibrate (Semtech's remedy, not in - * the datasheets). Must leave the driver out of RX so the caller's - * startReceive() is a real re-entry. Default: no-op. */ - virtual void hwResetAgc() {} + /* Deaf time per duty-cycle wake (context restore + PLL + TCXO start), in + * us, counted against the catch budget (DS §13.1.7). 1500 suits XTAL + * parts only. */ + uint32_t hwWakeupTimeUs() + { + return _ops.wakeup_time_us ? _ops.wakeup_time_us(_dev) : 1500; + } - /** 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() {} - - /* Only the SX126x has the jammed-AGC fault. Elsewhere the reset is pure - * cost (7.4% packet miss in the following minute on a T1000-E, LLD 03 §13). */ - virtual bool hwNeedsAgcReset() { return false; } - - /* Does this family specify a temperature threshold for image/front-end - * recalibration? LR11xx and LR2021 do; the SX126x datasheet does not, - * and drift recalibration stays inactive there exactly as before. */ - virtual bool hwHasDriftRecal() { return false; } - - /* Deaf time per duty-cycle wake (context restore + PLL + TCXO start), in us, - * counted against the catch budget (DS §13.1.7). The default suits XTAL - * parts only: a TCXO board must override it or window-edge preambles drop. */ - virtual uint32_t hwWakeupTimeUs() { return 1500; } - - /* loramac-node backend: its TX and RX configs are disjoint state, so the - * direction-only fast path in configure() must stay off. */ - bool _loramac_node; - - /* ── Shared helpers available to subclasses ────────────────── */ + /* ── Shared helpers ─────────────────────────────────────────── */ void buildModemConfig(struct lora_modem_config &cfg, bool tx); /* Shared body for configureRx()/configureTx(): builds the modem config for diff --git a/zephcore/adapters/radio/LoRaRadioOps.h b/zephcore/adapters/radio/LoRaRadioOps.h new file mode 100644 index 0000000..ee06b91 --- /dev/null +++ b/zephcore/adapters/radio/LoRaRadioOps.h @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: MIT + * What LoRaRadio needs from a radio family beyond the Zephyr LoRa API: a const + * table of the driver's extension functions. Every entry is optional; NULL + * selects the documented default. Exactly one table, kLoRaRadioOps, is linked + * per build, from radio_ops_.cpp (chosen by the radio Kconfig). + */ + +#pragma once + +#include +#include + +namespace mesh { + +struct LoRaRadioOps { + /* loramac-node backend (SX127x): its TX and RX configs are disjoint + * state, so configure() must not take the direction-only fast path. */ + bool loramac_node; + + /* Instantaneous RSSI in dBm. NULL: no readout, -80 is reported. */ + int16_t (*rssi_inst)(const struct device *dev); + /* A bracketed burst (see LoRaRadio::hwGetRssiBurst). NULL: one + * rssi_inst() per sample. */ + int (*rssi_burst)(const struct device *dev, int16_t *out, int n, + uint32_t spacing_us); + /* RX-busy gate (latch + IRQ read). NULL: never busy. */ + bool (*is_receiving)(const struct device *dev); + /* NULL: the chip has no RX boost; setRxBoost() reports false. */ + void (*set_rx_boost)(const struct device *dev, bool enable); + /* External FEM LNA select. NULL: not software-selectable. */ + bool (*set_fem_rx)(const struct device *dev, bool enable); + /* GPIO-only BUSY check. NULL: never busy. */ + bool (*is_chip_busy)(const struct device *dev); + /* Deaf time per duty-cycle wake, us. NULL: 1500 (XTAL parts). */ + uint32_t (*wakeup_time_us)(const struct device *dev); + /* Duty-cycle false-preamble re-arms. NULL: 0. */ + uint32_t (*dc_restarts)(const struct device *dev); + void (*reset_dc_restarts)(const struct device *dev); + + /* Receiver hygiene. Non-NULL means the family needs it: reset_agc for + * the jammed-AGC fault (SX126x), recalibrate for temperature drift (LR). + * Both leave the driver out of RX. */ + void (*reset_agc)(const struct device *dev); + void (*recalibrate)(const struct device *dev); + + /* Adaptive CAD (see CadController / LoRaRadio::cadMaintenance). NULL + * probe or rx_timeout_ms: no probing. NULL base_peak: no adaptive CAD. */ + int (*cad_probe)(const struct device *dev, int8_t level); + int (*cad_rx_outcome)(const struct device *dev); + uint32_t (*cad_rx_timeout_ms)(const struct device *dev); + void (*cad_set_peak_offset)(const struct device *dev, int8_t offset); + uint8_t (*cad_base_peak)(const struct device *dev); + uint8_t (*cad_peak_min)(void); + uint8_t (*cad_peak_max)(void); + + /* Family extras. */ + void (*post_begin)(const struct device *dev); + int (*side_detectors)(const struct device *dev, const uint8_t *sfs, + uint8_t num); + void (*reset_stats)(const struct device *dev); + int (*format_freq_error)(const struct device *dev, char *buf, int cap); +}; + +extern const LoRaRadioOps kLoRaRadioOps; + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/NoiseFloorEstimator.h b/zephcore/adapters/radio/NoiseFloorEstimator.h index 1eb73f9..02c832b 100644 --- a/zephcore/adapters/radio/NoiseFloorEstimator.h +++ b/zephcore/adapters/radio/NoiseFloorEstimator.h @@ -2,7 +2,7 @@ * SPDX-License-Identifier: MIT * Noise-floor estimator: an EMA over idle-channel RSSI samples. * - * Pure algorithm. LoRaRadioBase owns the sampler (when to read, the + * Pure algorithm. LoRaRadio owns the sampler (when to read, the * idle-RX guards, the median-of-N burst) and feeds each median in here. */ diff --git a/zephcore/adapters/radio/SX126xRadio.cpp b/zephcore/adapters/radio/SX126xRadio.cpp deleted file mode 100644 index 4ce446e..0000000 --- a/zephcore/adapters/radio/SX126xRadio.cpp +++ /dev/null @@ -1,181 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * SX126x hardware hooks for LoRaRadioBase — native Zephyr driver. - */ - -#include "SX126xRadio.h" -#include - -/* Native SX126x driver extension API */ -extern "C" { -#include -} - -#include -LOG_MODULE_REGISTER(sx126x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); - -namespace mesh { - -SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board) - : LoRaRadioBase(lora_dev, board) -{ -} - -void SX126xRadio::begin() -{ - LoRaRadioBase::begin(); - -#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH) - /* Apply undocumented register 0x8B5 RX improvement (MeshCore PR#1398). - * Must run after lora_config() has been called (via startReceive above). */ - sx126x_apply_heltec_reg_patch(_dev); - LOG_INF("Applied Heltec reg 0x8B5 RX patch"); -#endif -} - -/* ── Hardware primitives ──────────────────────────────────────────────── */ - -bool SX126xRadio::hwConfigure(const struct lora_modem_config &cfg) -{ - int ret = lora_config(_dev, &cfg); - if (ret < 0) { - LOG_ERR("lora_config failed: %d", ret); - return false; - } - return true; -} - -void SX126xRadio::hwCancelReceive() -{ - lora_recv_async(_dev, NULL, NULL); -} - -int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) -{ - return lora_send_async(_dev, buf, len, sig); -} - -int16_t SX126xRadio::hwGetCurrentRSSI() -{ - return sx126x_get_rssi_inst(_dev); -} - -bool SX126xRadio::hwIsReceiving() -{ - /* rx_packet_active latch + IRQ read. The driver clears sticky bits only - * to release an expired preamble grace or payload deadline. */ - return sx126x_is_receiving(_dev); -} - -/* The one family that actually has the jammed-AGC fault — see the patch preamble - * in patches/zephyr/0003-lora-sx126x-native.patch, where this remedy and its - * trigger are argued out. The LR parts deliberately do not declare it. */ -bool SX126xRadio::hwNeedsAgcReset() -{ - return true; -} - -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 hwHasDriftRecal() override: unlike the LR parts, the SX126x datasheet - * gives no temperature threshold for image calibration, so drift-triggered - * recalibration stays inactive on this family — unchanged from before, when the - * same result came about because the part exposes no junction-temperature - * readout for the trigger to read. (The trigger now reads board temperature, - * which every board has, so the family gate has to be explicit.) */ - -void SX126xRadio::hwSetRxBoost(bool enable) -{ - sx126x_set_rx_boost(_dev, enable); -} - -bool SX126xRadio::setFemRxEnable(bool enable) -{ - /* Pure driver-side -- no SPI, no chip state. The driver applies it to - * lna-bypass-gpios straight away when the radio is parked in RX, and - * otherwise at the next RX/TX/sleep transition. Returns false when this - * board wires no receive-path select, so the CLI reports "unsupported" - * rather than acknowledging a setting that cannot do anything -- note the - * gate is that line, not antenna-enable-gpios, so a board with a FEM whose - * only control is the chip enable (heltec_wifi_lora32_v4) lands here too. */ - if (!sx126x_set_fem_rx_enable(_dev, enable)) { - return false; - } - LOG_INF("FEM RX gain %s", enable ? "enabled" : "disabled"); - return true; -} - -bool SX126xRadio::hwIsChipBusy() -{ - return sx126x_is_chip_busy(_dev); -} - -uint32_t SX126xRadio::hwWakeupTimeUs() -{ - return sx126x_get_wakeup_time_us(_dev); -} - -int SX126xRadio::hwCadProbe(int8_t level) -{ - return sx126x_cad_probe(_dev, level); -} - -int SX126xRadio::hwCadRxOutcome() -{ - return sx126x_cad_rx_outcome(_dev); -} - -uint32_t SX126xRadio::hwCadRxTimeoutMs() -{ - return sx126x_cad_rx_timeout_ms(_dev); -} - -void SX126xRadio::hwCadSetPeakOffset(int8_t offset) -{ - sx126x_cad_set_peak_offset(_dev, offset); -} - -uint8_t SX126xRadio::hwCadBasePeak() -{ - return sx126x_cad_base_peak(_dev); -} - -/* The detPeak range sx126x_do_cad() will actually program. Must match the - * driver's clamp exactly: if the adapter thinks the range is wider, the - * staircase explores offsets that collapse onto one peak and reads the noise - * between them as curvature. Same reasoning as LR2021Radio::hwCadPeakMin. */ -uint8_t SX126xRadio::hwCadPeakMin() -{ - return sx126x_cad_peak_min(); -} - -uint8_t SX126xRadio::hwCadPeakMax() -{ - return sx126x_cad_peak_max(); -} - -uint32_t SX126xRadio::getDutyCycleTimeoutRestarts() const -{ - return sx126x_get_dc_timeout_restarts(_dev); -} - -void SX126xRadio::resetDutyCycleTimeoutRestarts() -{ - sx126x_reset_dc_timeout_restarts(_dev); -} - -} /* namespace mesh */ diff --git a/zephcore/adapters/radio/SX126xRadio.h b/zephcore/adapters/radio/SX126xRadio.h deleted file mode 100644 index aa81e02..0000000 --- a/zephcore/adapters/radio/SX126xRadio.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using native Zephyr driver - */ - -#pragma once - -#include "LoRaRadioBase.h" - -namespace mesh { - -class SX126xRadio : public LoRaRadioBase { -public: - SX126xRadio(const struct device *lora_dev, MainBoard &board); - - void begin() override; - - /* Select the external FEM's LNA or its bypass path for RX - * (radio.fem.rxgain). Delegates to the driver, which owns the - * receive-path select pin; returns false on a board that wires no such - * pin, i.e. one whose FEM gain is not software-selectable. */ - bool setFemRxEnable(bool enable) override; - - /* Duty-cycle preamble false-positive stats (SX126x-specific) */ - uint32_t getDutyCycleTimeoutRestarts() const override; - void resetDutyCycleTimeoutRestarts() override; - -protected: - /* Hardware primitives */ - bool hwConfigure(const struct lora_modem_config &cfg) override; - void hwCancelReceive() override; - int hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) override; - int16_t hwGetCurrentRSSI() override; - bool hwIsReceiving() override; - void hwSetRxBoost(bool enable) override; - bool hwNeedsAgcReset() override; - void hwResetAgc() override; - void hwRecalibrate() override; - bool hwIsChipBusy() override; - uint32_t hwWakeupTimeUs() override; - int hwCadProbe(int8_t level) override; - int hwCadRxOutcome() override; - uint32_t hwCadRxTimeoutMs() override; - void hwCadSetPeakOffset(int8_t offset) override; - uint8_t hwCadBasePeak() override; - uint8_t hwCadPeakMin() override; - uint8_t hwCadPeakMax() override; -}; - -} /* namespace mesh */ diff --git a/zephcore/adapters/radio/SX127xRadio.cpp b/zephcore/adapters/radio/SX127xRadio.cpp deleted file mode 100644 index a12a829..0000000 --- a/zephcore/adapters/radio/SX127xRadio.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * SX127x hardware hooks for LoRaRadioBase — Zephyr loramac-node driver. - * - * The SX127x driver (loramac-node/sx127x.c) exposes only the standard - * Zephyr LoRa API. Chip-specific features that the SX126x native driver - * provides via sx126x_ext.h are not available here: - * - * hwGetCurrentRSSI() — returns -80 dBm sentinel (no hardware path) - * hwIsReceiving() — always false (no preamble/header IRQ exposed) - * hwSetRxBoost() — no-op (SX127x has no RX boost register) - * hwIsChipBusy() — inherited false (no BUSY pin on SX127x) - * - * Everything else (configure, send, receive) uses the standard API. - */ - -#include "SX127xRadio.h" -#include -#include - -#include -LOG_MODULE_REGISTER(sx127x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); - -namespace mesh { - -SX127xRadio::SX127xRadio(const struct device *lora_dev, MainBoard &board) - : LoRaRadioBase(lora_dev, board) -{ - /* SX127x has no RX boost feature — start with boost disabled */ - _rx_boost_enabled = false; - /* loramac-node requires full lora_config() on every TX/RX direction - * change — Radio.SetTxConfig() and Radio.SetRxConfig() configure - * completely disjoint internal state in the loramac-node library. */ - _loramac_node = true; -} - -/* ── Hardware primitives ──────────────────────────────────────────────── */ - -bool SX127xRadio::hwConfigure(const struct lora_modem_config &cfg) -{ - int ret = lora_config(_dev, &cfg); - if (ret < 0) { - LOG_ERR("lora_config failed: %d", ret); - return false; - } - return true; -} - -void SX127xRadio::hwCancelReceive() -{ - lora_recv_async(_dev, NULL, NULL); -} - -int SX127xRadio::hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) -{ - return lora_send_async(_dev, buf, len, sig); -} - -int16_t SX127xRadio::hwGetCurrentRSSI() -{ - /* SX127x loramac-node driver does not expose an instantaneous RSSI - * function via the standard Zephyr API. Return a plausible noise-floor - * sentinel so LoRaRadioBase::triggerNoiseFloorCalibrate() converges - * to a reasonable value rather than being seeded with garbage. */ - return -80; -} - -bool SX127xRadio::hwIsReceiving() -{ - /* MUST be non-destructive (trivially: stub returns false). - * No preamble/header IRQ accessible through the standard Zephyr LoRa - * API for the loramac-node driver. Returning false means the - * isReceiving() fallback uses isChannelActive() (RSSI-based) on - * SX127x — acceptable as a degraded path; SX127x is not the focus - * of this fix. */ - return false; -} - -void SX127xRadio::hwSetRxBoost(bool enable) -{ - /* SX127x does not have a dedicated RX boost / high-sensitivity mode - * switch. Sensitivity is controlled via lora_config tx_power and - * the DTS power-amplifier-output property. Nothing to do here. */ - ARG_UNUSED(enable); -} - -} /* namespace mesh */ diff --git a/zephcore/adapters/radio/SX127xRadio.h b/zephcore/adapters/radio/SX127xRadio.h deleted file mode 100644 index 775bfa3..0000000 --- a/zephcore/adapters/radio/SX127xRadio.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * ZephCore Radio adapter for SX127x (SX1272/SX1276/SX1278) using Zephyr loramac-node driver. - * - * The SX127x loramac-node driver supports the standard Zephyr LoRa API - * (lora_config, lora_send_async, lora_recv_async) but has no extension - * API for instantaneous RSSI, preamble detection, or RX boost. - * Those features are stubbed out below. - */ - -#pragma once - -#include "LoRaRadioBase.h" - -namespace mesh { - -class SX127xRadio : public LoRaRadioBase { -public: - SX127xRadio(const struct device *lora_dev, MainBoard &board); - - /* SX127x has no RX boost feature — report unsupported so the - * radio.rxgain CLI can reply "Error: unsupported". */ - bool setRxBoost(bool enable) override { - (void)enable; - return false; - } - -protected: - /* Hardware primitives */ - bool hwConfigure(const struct lora_modem_config &cfg) override; - void hwCancelReceive() override; - int hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) override; - - /* SX127x via loramac-node has no instantaneous RSSI API. - * Returns a fixed sentinel (-80 dBm) — noise floor calibration - * will converge on this value rather than the real noise floor. */ - int16_t hwGetCurrentRSSI() override; - - /* SX127x has no preamble/header IRQ accessible via standard API. - * Always returns false — TX will not gate on the chip-level RX-busy - * signal. isReceiving() falls back to the RSSI-based isChannelActive() - * for SX127x users. */ - bool hwIsReceiving() override; - - /* SX127x has no RX boost / LNA gain switch via standard API. No-op. */ - void hwSetRxBoost(bool enable) override; - - /* SX127x has no BUSY pin. Default (false) from base is correct. */ - /* bool hwIsChipBusy() — inherited, returns false */ -}; - -} /* namespace mesh */ diff --git a/zephcore/adapters/radio/radio_common.h b/zephcore/adapters/radio/radio_common.h index 20d9926..64cbe2d 100644 --- a/zephcore/adapters/radio/radio_common.h +++ b/zephcore/adapters/radio/radio_common.h @@ -2,7 +2,7 @@ * SPDX-License-Identifier: MIT * Shared constants and utilities for all LoRa radio adapters. * - * Anything duplicated between SX126xRadio and LR1110Radio belongs here. + * Radio-family-independent pieces used by LoRaRadio. * Radio-specific constants (e.g. SX126x duty cycle math) stay in * their respective headers. */ diff --git a/zephcore/adapters/radio/radio_ops_lr1110.cpp b/zephcore/adapters/radio/radio_ops_lr1110.cpp new file mode 100644 index 0000000..8013111 --- /dev/null +++ b/zephcore/adapters/radio/radio_ops_lr1110.cpp @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: MIT + * LR11xx family (LR1110) for LoRaRadio. + */ + +#include "LoRaRadioOps.h" + +extern "C" { +#include +} + +namespace mesh { + +/* No AGC reset: this part has no jammed-AGC fault, and running the SX126x + * remedy anyway cost packets (LLD 03 §13). Drift recalibration is specified + * (UM: image cal after > 10 C), with the temperature from the board. */ +const LoRaRadioOps kLoRaRadioOps = { + .loramac_node = false, + .rssi_inst = lr11xx_get_rssi_inst, + .rssi_burst = lr11xx_get_rssi_burst, + .is_receiving = lr11xx_is_receiving, + .set_rx_boost = lr11xx_set_rx_boost, + .set_fem_rx = nullptr, + .is_chip_busy = nullptr, + .wakeup_time_us = lr11xx_get_wakeup_time_us, + .dc_restarts = lr11xx_get_dc_timeout_restarts, + .reset_dc_restarts = lr11xx_reset_dc_timeout_restarts, + .reset_agc = nullptr, + .recalibrate = lr11xx_recalibrate, + .cad_probe = lr11xx_cad_probe, + .cad_rx_outcome = lr11xx_cad_rx_outcome, + .cad_rx_timeout_ms = lr11xx_cad_rx_timeout_ms, + .cad_set_peak_offset = lr11xx_cad_set_peak_offset, + .cad_base_peak = lr11xx_cad_base_peak, + .cad_peak_min = lr11xx_cad_peak_min, + .cad_peak_max = lr11xx_cad_peak_max, + .post_begin = nullptr, + .side_detectors = nullptr, + .reset_stats = nullptr, + .format_freq_error = nullptr, +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/radio_ops_lr2021.cpp b/zephcore/adapters/radio/radio_ops_lr2021.cpp new file mode 100644 index 0000000..9808f40 --- /dev/null +++ b/zephcore/adapters/radio/radio_ops_lr2021.cpp @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: MIT + * LR20xx family (LR2021) for LoRaRadio. + */ + +#include "LoRaRadioOps.h" +#include + +extern "C" { +#include +} + +namespace mesh { + +/* Carrier frequency error over received packets. Spread and count matter as + * much as the mean: only a mean over many different peers approximates this + * node's own reference error. */ +static int lr2021_format_freq_error(const struct device *dev, char *buf, int cap) +{ + struct lr20xx_freq_offset_stats st; + + if (lr20xx_get_freq_offset(dev, &st) == 0) { + return snprintf(buf, cap, "no packets measured yet"); + } + return snprintf(buf, cap, "mean %d Hz, min %d, max %d, %u pkts", + st.mean_hz, st.min_hz, st.max_hz, (unsigned)st.count); +} + +/* No AGC reset (no such fault on this part). Drift recalibration is specified. + * Adaptive CAD probing runs CAD_ONLY plus an explicit SetRx in the driver, + * so the 524 ms cad_timeout never bounds it; not yet hardware-verified on an + * LR2021 (LLD 05). */ +const LoRaRadioOps kLoRaRadioOps = { + .loramac_node = false, + .rssi_inst = lr20xx_get_rssi_inst, + .rssi_burst = lr20xx_get_rssi_burst, + .is_receiving = lr20xx_is_receiving, + .set_rx_boost = lr20xx_set_rx_boost, + .set_fem_rx = nullptr, + .is_chip_busy = nullptr, + .wakeup_time_us = lr20xx_get_wakeup_time_us, + .dc_restarts = lr20xx_get_dc_timeout_restarts, + .reset_dc_restarts = lr20xx_reset_dc_timeout_restarts, + .reset_agc = nullptr, + .recalibrate = lr20xx_recalibrate, + .cad_probe = lr20xx_cad_probe, + .cad_rx_outcome = lr20xx_cad_rx_outcome, + .cad_rx_timeout_ms = lr20xx_cad_rx_timeout_ms, + .cad_set_peak_offset = lr20xx_cad_set_peak_offset, + .cad_base_peak = lr20xx_cad_base_peak, + .cad_peak_min = lr20xx_cad_peak_min, + .cad_peak_max = lr20xx_cad_peak_max, + .post_begin = nullptr, + .side_detectors = lr20xx_configure_side_detectors, + .reset_stats = lr20xx_reset_freq_offset, + .format_freq_error = lr2021_format_freq_error, +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/radio_ops_sx126x.cpp b/zephcore/adapters/radio/radio_ops_sx126x.cpp new file mode 100644 index 0000000..9572cc5 --- /dev/null +++ b/zephcore/adapters/radio/radio_ops_sx126x.cpp @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: MIT + * SX126x family (SX1261/2/8, LLCC68, STM32WL) for LoRaRadio. + */ + +#include "LoRaRadioOps.h" + +extern "C" { +#include +} + +#include +LOG_MODULE_REGISTER(sx126x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); + +namespace mesh { + +#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH) +/* Undocumented register 0x8B5 RX improvement (MeshCore PR#1398), applied once + * the first lora_config() has run. */ +static void sx126x_post_begin(const struct device *dev) +{ + sx126x_apply_heltec_reg_patch(dev); + LOG_INF("Applied Heltec reg 0x8B5 RX patch"); +} +#endif + +/* The one family with the jammed-AGC fault, so reset_agc is set (the remedy + * re-issues CalibrateImage itself); drift recalibration is not specified by + * the datasheet, so recalibrate stays NULL. */ +const LoRaRadioOps kLoRaRadioOps = { + .loramac_node = false, + .rssi_inst = sx126x_get_rssi_inst, + .rssi_burst = nullptr, + .is_receiving = sx126x_is_receiving, + .set_rx_boost = sx126x_set_rx_boost, + .set_fem_rx = sx126x_set_fem_rx_enable, + .is_chip_busy = sx126x_is_chip_busy, + .wakeup_time_us = sx126x_get_wakeup_time_us, + .dc_restarts = sx126x_get_dc_timeout_restarts, + .reset_dc_restarts = sx126x_reset_dc_timeout_restarts, + .reset_agc = sx126x_reset_agc, + .recalibrate = nullptr, + .cad_probe = sx126x_cad_probe, + .cad_rx_outcome = sx126x_cad_rx_outcome, + .cad_rx_timeout_ms = sx126x_cad_rx_timeout_ms, + .cad_set_peak_offset = sx126x_cad_set_peak_offset, + .cad_base_peak = sx126x_cad_base_peak, + .cad_peak_min = sx126x_cad_peak_min, + .cad_peak_max = sx126x_cad_peak_max, +#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH) + .post_begin = sx126x_post_begin, +#else + .post_begin = nullptr, +#endif + .side_detectors = nullptr, + .reset_stats = nullptr, + .format_freq_error = nullptr, +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/radio_ops_sx127x.cpp b/zephcore/adapters/radio/radio_ops_sx127x.cpp new file mode 100644 index 0000000..87c7609 --- /dev/null +++ b/zephcore/adapters/radio/radio_ops_sx127x.cpp @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: MIT + * SX127x family over Zephyr's loramac-node backend, for LoRaRadio. + * + * The backend exposes nothing beyond the Zephyr LoRa API: no RSSI readout + * (the noise floor reads a flat -80), no RX-busy gate, no RX boost (the chip + * has none), no CAD and no duty cycle. Every entry is therefore NULL. + */ + +#include "LoRaRadioOps.h" + +namespace mesh { + +const LoRaRadioOps kLoRaRadioOps = { + .loramac_node = true, +}; + +} /* namespace mesh */ diff --git a/zephcore/app/ObserverMesh.cpp b/zephcore/app/ObserverMesh.cpp index 6daefb6..2113e24 100644 --- a/zephcore/app/ObserverMesh.cpp +++ b/zephcore/app/ObserverMesh.cpp @@ -7,7 +7,7 @@ #include "observer_creds.h" #include -#include +#include #include /* generateFirstBootIdentity (hardened keygen) */ #include @@ -147,10 +147,10 @@ void ObserverMesh::buildStatusJson(const char *status, char *out, size_t out_siz uptime_secs, 0u, /* debug_flags */ 0u, /* queue_len */ - ((LoRaRadioBase *)_radio)->getNoiseFloor(), + ((LoRaRadio *)_radio)->getNoiseFloor(), 0u, /* tx_air_secs */ 0u, /* rx_air_secs */ - ((LoRaRadioBase *)_radio)->getPacketsRecvErrors(), + ((LoRaRadio *)_radio)->getPacketsRecvErrors(), false, /* repeat: observer never forwards */ }; meshcore_build_status_json(out, out_size, &sj); @@ -446,7 +446,7 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) if (f >= 300.0f && f <= 1000.0f) { _prefs.freq = f; _store->savePrefs(_prefs); - ((LoRaRadioBase *)_radio)->reconfigure(); + ((LoRaRadio *)_radio)->reconfigure(); snprintf(reply, reply_size, "freq=%.3f MHz", (double)_prefs.freq); } else { snprintf(reply, reply_size, "ERR freq must be 300-1000 MHz"); @@ -457,7 +457,7 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) if (sf >= 7 && sf <= 12) { _prefs.sf = (uint8_t)sf; _store->savePrefs(_prefs); - ((LoRaRadioBase *)_radio)->reconfigure(); + ((LoRaRadio *)_radio)->reconfigure(); snprintf(reply, reply_size, "sf=%u", _prefs.sf); } else { snprintf(reply, reply_size, "ERR sf must be 7-12"); @@ -481,7 +481,7 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) if (bw >= ZC_RADIO_BW_MIN_KHZ && bw <= ZC_RADIO_BW_MAX_KHZ) { _prefs.bw = bw; _store->savePrefs(_prefs); - ((LoRaRadioBase *)_radio)->reconfigure(); + ((LoRaRadio *)_radio)->reconfigure(); snprintf(reply, reply_size, "bw=%.2f kHz", (double)_prefs.bw); } else { snprintf(reply, reply_size, "ERR bw must be 7-500 kHz (or index 0-5)"); @@ -492,7 +492,7 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size) if (cr >= 5 && cr <= 8) { _prefs.cr = (uint8_t)cr; _store->savePrefs(_prefs); - ((LoRaRadioBase *)_radio)->reconfigure(); + ((LoRaRadio *)_radio)->reconfigure(); snprintf(reply, reply_size, "cr=%u", _prefs.cr); } else { snprintf(reply, reply_size, "ERR cr must be 5-8"); diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 49bf917..81c8074 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include #include @@ -28,9 +28,9 @@ #endif #include -/* Helper to get radio driver for stats — uses LoRaRadioBase (works for SX126x and LR1110) */ -static inline mesh::LoRaRadioBase& getRadioDriver(mesh::Radio* radio) { - return *static_cast(radio); +/* Helper to get radio driver for stats — uses LoRaRadio (works for SX126x and LR1110) */ +static inline mesh::LoRaRadio& getRadioDriver(mesh::Radio* radio) { + return *static_cast(radio); } /* Simple sort helper since is not available in Zephyr minimal C++ */ diff --git a/zephcore/app/RepeaterUplink.cpp b/zephcore/app/RepeaterUplink.cpp index fedb0c1..208e1eb 100644 --- a/zephcore/app/RepeaterUplink.cpp +++ b/zephcore/app/RepeaterUplink.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include "observer_creds.h" #include #include @@ -166,7 +166,7 @@ void RepeaterMesh::publishUplinkStatus(const char *status) if (!isUplinkEnabled() || !mqtt_publisher_is_connected()) return; if (_uplink_status_topic[0] == '\0') return; - auto& radio_driver = *static_cast(_radio); + auto& radio_driver = *static_cast(_radio); uint32_t now_epoch = getRTCClock()->getCurrentTime(); char radio_buf[48]; diff --git a/zephcore/app/RoomServerMesh.cpp b/zephcore/app/RoomServerMesh.cpp index 33dd6df..3048ded 100644 --- a/zephcore/app/RoomServerMesh.cpp +++ b/zephcore/app/RoomServerMesh.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include #include #include @@ -30,9 +30,9 @@ #include #include -/* Helper to get radio driver for stats — uses LoRaRadioBase (works for SX126x and LR1110) */ -static inline mesh::LoRaRadioBase& getRadioDriver(mesh::Radio* radio) { - return *static_cast(radio); +/* Helper to get radio driver for stats — uses LoRaRadio (works for SX126x and LR1110) */ +static inline mesh::LoRaRadio& getRadioDriver(mesh::Radio* radio) { + return *static_cast(radio); } LOG_MODULE_REGISTER(zephcore_room, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); diff --git a/zephcore/app/main_observer.cpp b/zephcore/app/main_observer.cpp index 768b7bc..8922b68 100644 --- a/zephcore/app/main_observer.cpp +++ b/zephcore/app/main_observer.cpp @@ -277,15 +277,7 @@ static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); /* Radio is constructed with no prefs pointer; main() binds it to the mesh's * NodePrefs via setPrefs() before observer_mesh.begin(). */ -#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) -static mesh::LR1110Radio lora_radio(lora_dev, s_board); -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021) -static mesh::LR2021Radio lora_radio(lora_dev, s_board); -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X) -static mesh::SX127xRadio lora_radio(lora_dev, s_board); -#else -static mesh::SX126xRadio lora_radio(lora_dev, s_board); -#endif +static mesh::LoRaRadio lora_radio(lora_dev, s_board); static mesh::ObserverMesh observer_mesh(lora_radio, s_ms_clock, s_rtc_clock); static RepeaterDataStore data_store; diff --git a/zephcore/boards/esp32/meshnology_w12/meshnology_w12_procpu.dts b/zephcore/boards/esp32/meshnology_w12/meshnology_w12_procpu.dts index 7e25fdd..f5c8d35 100644 --- a/zephcore/boards/esp32/meshnology_w12/meshnology_w12_procpu.dts +++ b/zephcore/boards/esp32/meshnology_w12/meshnology_w12_procpu.dts @@ -283,7 +283,7 @@ reset-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; /* GPIO12 */ busy-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>; /* GPIO13 */ - dio1-gpios = <&gpio0 14 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>;/* GPIO14 */ + irq-gpios = <&gpio0 14 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>;/* GPIO14 */ /* Chip DIO8 -> GPIO14 (schematic net IO14_LORA_INT2). DIO7 goes to * GPIO7 as a second IRQ line (IO7_LORA_INT1) that ZephCore does not @@ -292,7 +292,7 @@ * be wrong. */ irq-dio = <8>; - /* No tcxo-voltage-mv: schematic sheet 3 shows a plain crystal on + /* No tcxo-voltage: schematic sheet 3 shows a plain crystal on * XTA/XTB (pins 4/5) and pin 6 wired to the VNTC net, not a TCXO * supply. Setting a TCXO voltage on a board without one wedges the * chip at HF-XOSC startup — that is what happened on the @@ -320,12 +320,12 @@ * RFX2402E instead. ZephCore only ever uses the sub-GHz side; the HF * masks are here so the table is a faithful description of the * hardware rather than a half-truth. */ - rfswitch-enable = <0x73>; /* DIO5,6,9,10,11 */ - rfswitch-standby = <0x00>; /* everything off */ - rfswitch-rx-lf = <0x40>; /* DIO11: GC1109 out of shutdown, LNA */ - rfswitch-rx-hf = <0x02>; /* DIO6: 2.4 GHz LNA, GC1109 shut down */ - rfswitch-tx = <0x70>; /* DIO9|10|11: GC1109 full PA */ - rfswitch-tx-hp = <0x01>; /* DIO5: 2.4 GHz PA */ + rfsw-enable = <0x73>; /* DIO5,6,9,10,11 */ + rfsw-standby = <0x00>; /* everything off */ + rfsw-rx-lf = <0x40>; /* DIO11: GC1109 out of shutdown, LNA */ + rfsw-rx-hf = <0x02>; /* DIO6: 2.4 GHz LNA, GC1109 shut down */ + rfsw-tx = <0x70>; /* DIO9|10|11: GC1109 full PA */ + rfsw-tx-hp = <0x01>; /* DIO5: 2.4 GHz PA */ }; }; diff --git a/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts b/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts index a2960d2..d3d7e07 100644 --- a/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts +++ b/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts @@ -421,11 +421,11 @@ reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; busy-gpios = <&gpio0 7 GPIO_ACTIVE_HIGH>; - dio1-gpios = <&gpio1 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + irq-gpios = <&gpio1 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; irq-dio = <8>; - tcxo-voltage-mv = <1600>; - tcxo-startup-delay-ms = <5>; + tcxo-voltage = <1600>; + tcxo-power-startup-delay-ms = <5>; rx-boosted; /* Single antenna path, no DIO-driven RF switch on this board */ diff --git a/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts b/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts index 07da283..4a7deb2 100644 --- a/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts +++ b/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts @@ -147,7 +147,7 @@ reset-gpios = <&gpio0 9 GPIO_ACTIVE_LOW>; busy-gpios = <&gpio0 29 GPIO_ACTIVE_HIGH>; - dio1-gpios = <&gpio0 10 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + irq-gpios = <&gpio0 10 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; irq-dio = <9>; /* chip DIO9 → module pin 15 → MCU P0.10 */ /* TCXO: tried 1800mV on DIO3 (per Meshtastic diy_tcxo) — it WEDGED @@ -161,11 +161,11 @@ * Config from Semtech reference ral_lr20xx_bsp.c: * DIO5: RX_LF | TX_HF * DIO6: RX_LF | TX_LF | RX_HF | TX_HF (always on) */ - rfswitch-enable = <0x03>; - rfswitch-standby = <0x00>; - rfswitch-rx = <0x03>; - rfswitch-tx = <0x02>; - rfswitch-tx-hp = <0x03>; + rfsw-enable = <0x03>; + rfsw-standby = <0x00>; + rfsw-rx = <0x03>; + rfsw-tx = <0x02>; + rfsw-tx-hp = <0x03>; }; }; diff --git a/zephcore/boards/nrf54l/seeed_lr2021_evk/seeed_lr2021_evk_nrf54l15_cpuapp.dts b/zephcore/boards/nrf54l/seeed_lr2021_evk/seeed_lr2021_evk_nrf54l15_cpuapp.dts index 98609a8..aa5dcaf 100644 --- a/zephcore/boards/nrf54l/seeed_lr2021_evk/seeed_lr2021_evk_nrf54l15_cpuapp.dts +++ b/zephcore/boards/nrf54l/seeed_lr2021_evk/seeed_lr2021_evk_nrf54l15_cpuapp.dts @@ -380,14 +380,14 @@ reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; /* D2 = P1.06 */ busy-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; /* D1 = P1.05 */ - dio1-gpios = <&gpio1 4 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* D0 = P1.04 */ + irq-gpios = <&gpio1 4 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* D0 = P1.04 */ irq-dio = <8>; /* chip DIO8 -> module pin 1 -> D0 */ /* * NO TCXO. The module schematic shows crystal Q1, 32 MHz, across * XTA/XTB, and the chip's dual-function VTCXO/VNTC pin is wired * as the NTC thermistor input (R3 + ERT-JZER104F), not as a TCXO - * supply. So tcxo-voltage-mv stays at its default of 0. + * supply. So tcxo-voltage stays at its default of 0. * * Semtech's own shield overlay declares a 1.8 V TCXO here, which * contradicts their schematic. The schematic wins. This is worth @@ -402,7 +402,7 @@ * Switchless dual-band front end: RFO_LF and RFO_HF each drive * their own fixed matching network straight to their own U.FL. * There is no antenna switch and no DIO driving one, so every - * rfswitch-* mask stays at its default of 0 — same as + * rfsw-* mask stays at its default of 0 — same as * meshtracker_x1. ANT_LF is the sub-GHz SMA; the HF/2.4 GHz path * is simply unused by MeshCore. */ diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 8326bf0..22a0779 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -208,7 +208,7 @@ struct NodePrefs { * involves, but the offset itself is persisted and survives — so after a * table change a converged node quietly starts operating somewhere it * never measured. Recording the base turns that into something the - * firmware can correct at boot (see LoRaRadioBase::setCadParams), instead + * firmware can correct at boot (see LoRaRadio::setCadParams), instead * of a "run set cad.reset after upgrading" line in the release notes that * most users will not read. */ uint8_t cad_base; diff --git a/zephcore/helpers/ui/ui_mesh_actions.cpp b/zephcore/helpers/ui/ui_mesh_actions.cpp index 21acdc7..ccf270a 100644 --- a/zephcore/helpers/ui/ui_mesh_actions.cpp +++ b/zephcore/helpers/ui/ui_mesh_actions.cpp @@ -6,7 +6,7 @@ * Extracted from main_companion.cpp. * * This is a .cpp file because it accesses C++ mesh objects (CompanionMesh, - * ZephyrDataStore, LoRaRadioBase, ZephyrBoard, ZephyrRTCClock). + * ZephyrDataStore, LoRaRadio, ZephyrBoard, ZephyrRTCClock). * The extern "C" wrappers are called from ui_task.c (C code). */ @@ -23,7 +23,7 @@ LOG_MODULE_REGISTER(zephcore_ui_actions, CONFIG_ZEPHCORE_UI_ACTIONS_LOG_LEVEL); #include #include -#include +#include #include #include #include @@ -52,7 +52,7 @@ LOG_MODULE_REGISTER(zephcore_ui_actions, CONFIG_ZEPHCORE_UI_ACTIONS_LOG_LEVEL); /* Module-local pointers, set by init */ static CompanionMesh *s_mesh; static ZephyrDataStore *s_data_store; -static mesh::LoRaRadioBase *s_lora_radio; +static mesh::LoRaRadio *s_lora_radio; static mesh::ZephyrBoard *s_board; static mesh::ZephyrRTCClock *s_rtc_clock; static struct k_event *s_mesh_events; @@ -87,7 +87,7 @@ extern "C" void ui_mesh_actions_init(struct k_event *mesh_events, s_mesh_event_ui_action = mesh_event_ui_action; s_mesh = static_cast(companion_mesh); s_data_store = static_cast(data_store); - s_lora_radio = static_cast(lora_radio); + s_lora_radio = static_cast(lora_radio); s_board = static_cast(zephyr_board); s_rtc_clock = static_cast(rtc_clock); } diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_cad_peak.h b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_cad_peak.h index 259f09d..409e715 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_cad_peak.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_cad_peak.h @@ -88,7 +88,7 @@ static inline uint8_t lr11xx_cad_detect_peak(uint8_t sf, uint16_t bw_khz, uint8_ /* More symbols means more looks at the same correlation, so the same * detection quality is reached at a lower threshold. Semtech applies * this correction after the table lookup; we run 4 symbols everywhere - * (LORA_CAD_SYMB_4 in LoRaRadioBase::buildModemConfig), so it always + * (LORA_CAD_SYMB_4 in LoRaRadio::buildModemConfig), so it always * bites, and omitting it was one further count of the SF7 error. */ if (symb_nb >= 8) { peak -= 2; @@ -104,7 +104,7 @@ static inline uint8_t lr11xx_cad_detect_peak(uint8_t sf, uint16_t bw_khz, uint8_ * offset window to match: where base+offset falls outside this, several offsets * collapse onto one peak and the staircase reads sampling noise between * identical configurations as curvature. That is not hypothetical — it is the - * documented failure mode on the LR2021 (see LR2021Radio::hwCadPeakMin), and + * documented failure mode on the LR2021 (see LLD 03 §13, level window), and * the old 48 floor here reproduced it on the LR1110 at SF7. * * 40 is DELIBERATELY left where it was when the sub-125 row was measured down 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 a2387d6..22f64d5 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -107,7 +107,7 @@ struct lr11xx_data { /* Stored duty-cycle timing from recv_duty_cycle() — the re-arm paths * (start_rx / restart_rx) reuse these exact values, never recompute: - * window sizing is owned by the adapter layer (LoRaRadioBase). */ + * window sizing is owned by the adapter layer (LoRaRadio). */ uint32_t dc_rx_ms; uint32_t dc_sleep_ms; @@ -1140,7 +1140,7 @@ static int lr11xx_lora_recv_duty_cycle(const struct device *dev, return -EINVAL; } - /* Explicit timing only — the adapter (LoRaRadioBase) owns the window + /* Explicit timing only — the adapter (LoRaRadio) owns the window * sizing. No driver-side auto-compute. */ if (K_TIMEOUT_EQ(rx_period, K_FOREVER) || K_TIMEOUT_EQ(sleep_period, K_FOREVER)) { @@ -1294,7 +1294,7 @@ int16_t lr11xx_get_rssi_inst(const struct device *dev) int16_t out = -128; /* Non-blocking: a contended bus means the sampler simply retries. -128 - * is the sentinel LoRaRadioBase::triggerNoiseFloorCalibrate expects. */ + * is the sentinel LoRaRadio::triggerNoiseFloorCalibrate expects. */ if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { return -128; } diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/CMakeLists.txt b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/CMakeLists.txt index 962649f..a7ef40e 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/CMakeLists.txt +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/CMakeLists.txt @@ -9,23 +9,22 @@ zephyr_library_compile_definitions(LR20XX_DISABLE_WARNINGS) # set_modulation_params. That workaround now lives in the Patch RAM, which this # driver loads on every reset. Nothing references the defines any more. -zephyr_library_sources(lr20xx_lora.c) - -set(LR20XX_SDK_DIR ${APPLICATION_SOURCE_DIR}/adapters/radio/lr20xx) -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_hal_zephyr.c) -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_radio_common.c) -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_radio_lora.c) -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_radio_fifo.c) -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_system.c) -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_regmem.c) -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_workarounds.c) -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_driver_version.c) -# Firmware Patch RAM loader (DS rev 2.1 section 22.3). The image itself lives in -# lr20xx_pram_lr2021.h, included by lr20xx_lora.c. lr20xx_pram_load.c is -# deliberately not vendored: it only dispatches between the LR2021 and -# LR2012/LR2022 images, and pulling it in would drag 2 KB of LR20x2 patch data -# into a driver that binds to semtech,lr2021 only. -zephyr_library_sources(${LR20XX_SDK_DIR}/lr20xx_patch.c) +zephyr_library_sources(lr20xx_lora.c lr20xx_hal_zephyr.c) +# Semtech lr20xx_driver, vendored unmodified in lr20xx_driver/ (with its +# LICENSE). Not taken from LoRa Basics Modem: that snapshot is older and has no +# PRAM loader (lr20xx_patch.c), which the LR2021 needs. +set(LR20XX_SDK_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lr20xx_driver) +zephyr_library_sources( + ${LR20XX_SDK_DIR}/lr20xx_radio_common.c + ${LR20XX_SDK_DIR}/lr20xx_radio_lora.c + ${LR20XX_SDK_DIR}/lr20xx_radio_fifo.c + ${LR20XX_SDK_DIR}/lr20xx_system.c + ${LR20XX_SDK_DIR}/lr20xx_regmem.c + ${LR20XX_SDK_DIR}/lr20xx_workarounds.c + ${LR20XX_SDK_DIR}/lr20xx_driver_version.c + ${LR20XX_SDK_DIR}/lr20xx_patch.c +) zephyr_library_include_directories(${LR20XX_SDK_DIR}) + zephyr_library_add_dependencies(offsets_h) diff --git a/zephcore/adapters/radio/lr20xx/LICENSE.txt b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/LICENSE.txt similarity index 100% rename from zephcore/adapters/radio/lr20xx/LICENSE.txt rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/LICENSE.txt diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_driver_version.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_driver_version.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_driver_version.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_driver_version.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_driver_version.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_driver_version.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_driver_version.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_driver_version.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_hal.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_hal.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_hal.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_hal.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_patch.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_patch.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_patch.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_patch.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_patch.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_patch.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_patch.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_patch.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_patch_types.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_patch_types.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_patch_types.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_patch_types.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_pram_lr2021.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_pram_lr2021.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_pram_lr2021.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_pram_lr2021.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_common.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_common.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_common.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_common.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_common.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_common_types.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_common_types.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_common_types.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_common_types.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_fifo.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_fifo.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_fifo.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_fifo.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_fifo.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_fifo.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_fifo.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_fifo.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_fifo_types.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_fifo_types.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_fifo_types.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_fifo_types.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_flrc.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_flrc.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_flrc.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_flrc.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_flrc_types.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_flrc_types.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_flrc_types.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_flrc_types.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_fsk_common_types.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_fsk_common_types.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_fsk_common_types.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_fsk_common_types.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_lora.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_lora.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_lora.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_lora.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_lora.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_lora.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_lora.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_lora_types.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_lora_types.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_lora_types.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_lora_types.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_ook.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_ook.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_ook.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_ook.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_ook_types.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_ook_types.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_radio_ook_types.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_radio_ook_types.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_regmem.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_regmem.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_regmem.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_regmem.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_regmem.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_regmem.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_regmem.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_regmem.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_status.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_status.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_status.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_status.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_system.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_system.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_system.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_system.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_system.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_system.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_system.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_system.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_system_types.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_system_types.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_system_types.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_system_types.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_workarounds.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_workarounds.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_workarounds.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_workarounds.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_workarounds.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_workarounds.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_workarounds.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_driver/lr20xx_workarounds.h diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_hal_zephyr.c similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.c rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_hal_zephyr.c diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_hal_zephyr.h similarity index 100% rename from zephcore/adapters/radio/lr20xx/lr20xx_hal_zephyr.h rename to zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_hal_zephyr.h 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 eb726b2..c1a7f28 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c @@ -29,6 +29,7 @@ #include "lr20xx_workarounds.h" #include "lr20xx_regmem.h" #include "lr20xx_patch.h" +#include /* Defines the PRAM image itself (pram_lr2021 / pram_lr2021_size). In C these * are const objects at file scope and therefore have external linkage, so this * header must be included from exactly one translation unit — this one. */ @@ -101,13 +102,8 @@ struct lr20xx_data { bool rx_boost_applied; - /* Duty-cycle re-arms triggered by a timeout, i.e. the false-preamble - * case: the preamble-restarted rx_max_time + cycle_time window expired - * with no packet, so the chip left the loop and the host put it back. - * The rate is the instrument for duty-cycle tuning — a climbing count - * means the window is catching noise, not packets. Exposed as - * `get dc.restarts`, which reported a hardcoded 0 on this radio until - * nothing counted them. */ + /* Duty-cycle false-preamble re-arms (the preamble-extended window expired + * with no packet). `get dc.restarts`. */ atomic_t dc_timeout_restarts; /* Stored duty-cycle timing from recv_duty_cycle() — re-arm paths @@ -122,31 +118,16 @@ struct lr20xx_data { void *cad_user_data; struct k_sem cad_sem; int cad_result; /* 0=free, 1=busy, <0=error */ - /* No cad_active flag: there was one, written at five sites and read at - * none. The SX126x driver's copy IS read — sx126x_handle_irq_timeout() - * returns early on it — so this was inherited without the read that gave - * it a purpose. - * - * It is not needed here, and the reason is the locking model, not luck: - * sx126x_irq_work_handler() takes no lock at all, so there its timeout - * path genuinely races a CAD being set up on the mesh thread and the - * flag is the only thing preventing a teardown. This handler holds - * spi_mutex across its whole body, and every CAD entry point holds that - * same mutex across SetStandby -> clear_irq_status(ALL) -> SetCad — so - * the two cannot interleave, and any TIMEOUT latched before the CAD is - * discarded by the clear inside lr20xx_do_cad(). */ + /* No cad_active flag (the SX126x needs one; this driver does not): the DIO1 + * handler and every CAD entry point hold spi_mutex, so they cannot + * interleave, and lr20xx_do_cad() clears any TIMEOUT latched before it. */ /* Adaptive-CAD: signed offset applied to the per-SF base detPeak on * every LBT CAD; cad_probe_peak overrides for one calibration probe. */ int8_t cad_peak_offset; uint8_t cad_probe_peak; - /* Adaptive-CAD probe ground truth. The chip's own CAD_RX exit is NOT - * used (its cad_timeout is 24 bits of 32 MHz periods = 524 ms, well - * under a 1704 ms max-length packet). Instead a probe runs CAD_ONLY - * and this driver arms the follow-on Rx itself with a normal SetRx, - * whose timeout is 24 bits of RTC steps = 512 s -- so the ceiling that - * once excluded this family from probing does not apply. Same remedy - * as the SX126x and LR11xx, both of which had their chip CAD_RX exits - * fail differently on hardware. */ + /* Adaptive-CAD probe: CAD_ONLY, then this driver arms the follow-on Rx + * with a plain SetRx (RTC steps, 512 s), never the chip's CAD_RX exit + * (bounded by the 524 ms cad_timeout). */ bool cad_probe_rx; atomic_t cad_rx_state; @@ -171,17 +152,11 @@ struct lr20xx_data { int dio1_stuck_count; bool tcxo_disabled; /* set when the TCXO fallback has already fired */ - /* RX-busy tracking for lr20xx_is_receiving(). Both bits are latched - * (DS 5.7) and read non-destructively — continuous RX has no timeout, so - * each needs a software release or a noise preamble pins the TX gate - * forever. All writes under spi_mutex. - * - * PREAMBLE_DETECTED is kept off DIO1 (it fires on noise) and is - * poll-only. SYNC_WORD_HEADER_VALID is routed to DIO1 and stamps - * header_seen_at_ms from the work handler: with a duty cycle armed - * is_receiving() may not touch the bus, so that timestamp is the ONLY - * thing it can answer from, and while nothing wrote it the RX-busy gate - * was dead on every duty-cycled node. */ + /* RX-busy tracking for lr20xx_is_receiving(). Both bits latch (DS 5.7) and + * continuous RX never releases them, so each has a software release. + * PREAMBLE_DETECTED is poll-only; HEADER_VALID is on DIO1 and stamps + * header_seen_at_ms, the only answer with a duty cycle armed. Under + * spi_mutex. */ uint32_t preamble_seen_at_ms; uint32_t header_seen_at_ms; @@ -224,13 +199,8 @@ static void dump_chip_state(struct lr20xx_data *data, const char *label) lr20xx_system_irq_mask_t irq = 0; lr20xx_system_errors_t err = 0; - /* Sample the pins BEFORE issuing any SPI. DS §5: "The BUSY pin is - * automatically asserted on the falling edge of the NSS" — read after - * the two commands below and this dump reports its own footprint, not - * the chip's state. Every BUSY=1 in the X1 bring-up logs came from - * that, and it cost a whole (wrong) conclusion about this chip holding - * BUSY high during Rx. It does not: "In Rx mode, BUSY goes low as soon - * as the chip is ready to receive data." */ + /* Sample the pins before any SPI: NSS asserts BUSY (DS §5), so a later + * read reports its own footprint. In Rx, BUSY goes low once ready. */ int busy = gpio_pin_get_dt(&hal->busy); int dio = gpio_pin_get_dt(&hal->dio1); @@ -358,7 +328,7 @@ static inline bool lr20xx_ldro_enabled(uint8_t sf, uint32_t bw_hz) return (((1U << sf) * 1000000U) / bw_hz) > 16380U; } -/* The CAD symbol count actually programmed. LoRaRadioBase asks for +/* The CAD symbol count actually programmed. LoRaRadio asks for * LORA_CAD_SYMB_4 via mc->cad.symbol_num; the 4 here is the fallback when it is * left zero. Single source so the CAD window, its detPeak (DS Table 6-19 is * 2-D over both) and the blocking-wait budget can never disagree. */ @@ -392,20 +362,9 @@ lr20xx_irq_dio_pull(const struct lr20xx_config *cfg) static void lr20xx_configure_rfswitch(void *ctx, const struct lr20xx_config *cfg) { - /* LR20xx RF switch uses per-DIO configuration. - * DIO5..DIO11 map to enable bitmask bits 0..6 (the chip has no RF - * switch DIOs outside 5..11 — see lr20xx_system_dio_t). - * For each enabled DIO, compute which operational modes - * should drive it HIGH by looking at the per-mode bitmasks. - * - * This used to stop at bit 3 (DIO5..DIO8), which silently dropped the - * top three DIOs from any board that used them: the enable bit was set - * in devicetree, the loop never reached it, so the DIO kept its default - * function and the front end it drives never switched. The Meshnology - * W12 puts its entire sub-GHz front end there (DIO9 = GC1109 CTX, - * DIO10 = CPS, DIO11 = CSD), and with those three left unconfigured the - * FEM stays in the shutdown its 10k pull-downs assert at reset — the - * node is both deaf and mute while every register readback looks fine. */ + /* RF switch: DIO5..DIO11 = enable bits 0..6; each enabled DIO is driven + * high in the modes whose mask has its bit. All seven bits matter (the W12 + * drives its whole sub-GHz FEM from DIO9-11). */ for (int i = 0; i < LR20XX_RFSWITCH_DIO_COUNT; i++) { if (!(cfg->rfswitch_enable & BIT(i))) { continue; @@ -569,13 +528,8 @@ BUILD_ASSERT(ARRAY_SIZE(pa_hf_table) == #define LR20XX_PA_LF_DUTY_CYCLE_UNUSED 6 #define LR20XX_PA_LF_SLICES_UNUSED 7 -/* Which of the chip's two RF paths a frequency belongs to. - * - * 1.5 GHz is Semtech's own split, taken from ral_lr20xx_bsp.c, which uses the - * identical test in get_tx_cfg and get_rx_cfg. It sits in the dead zone - * between the LF path (150-960 MHz) and the HF path (1.9-2.5 GHz), so the exact - * value never has to be argued about — anything a board can legally tune to - * lands unambiguously on one side. */ +/* LF or HF path. 1.5 GHz is Semtech's split (ral_lr20xx_bsp.c), in the dead + * zone between the LF (150-960 MHz) and HF (1.9-2.5 GHz) paths. */ #define LR20XX_HF_BAND_THRESHOLD_HZ 1500000000U static inline bool lr20xx_freq_is_hf(uint32_t freq_hz) @@ -589,14 +543,8 @@ static inline lr20xx_radio_common_rx_path_t lr20xx_rx_path_for(uint32_t freq_hz) : LR20XX_RADIO_COMMON_RX_PATH_LF; } -/* Build the PA configuration for a power level on whichever path the frequency - * selects. Mirrors ral_lr20xx_bsp_get_tx_cfg(): band first, then clamp to that - * band's own limits, then index that band's own table. - * - * The clamp matters as much as the table. The two paths have different - * ceilings — +22 dBm on LF, +12 dBm on HF — so a node carrying the sub-GHz - * default of 22 into the 2.4 GHz band must be pulled down to 12 rather than - * indexed off the end of the HF table. */ +/* PA configuration for a power level: band first, then clamp to that band + * (+22 dBm LF, +12 dBm HF), then its table (ral_lr20xx_bsp_get_tx_cfg()). */ static void lr20xx_get_pa_cfg_for_power(int8_t power_dbm, uint32_t freq_hz, lr20xx_radio_common_pa_cfg_t *pa, int8_t *half_power_out) @@ -652,23 +600,9 @@ static void lr20xx_dc_resume(struct lr20xx_data *data, bool was_armed); * lr20xx_patch.c uses internally (it does not export it). */ #define LR20XX_PRAM_BASE_ADDRESS 0x801000 -/* Load and activate the firmware Patch RAM. - * - * DS §22.3: "using the chip without the PRAM can create performance issues and - * unexpected bugs. The use of the PRAM is therefore highly recommended", and - * §22: "Most of the workarounds are implemented in the Firmware Patch RAM". - * Semtech's own driver confirms the relationship — v2.0.2 added PRAM support and - * deleted its BLE, RTToF and DC-DC workaround functions in the same release, - * because the patch supersedes them. - * - * The image is volatile: lost on reset and on cold start, preserved by sleep - * with retention (§22.3). Duty-cycle sleep is retention by definition (§6.3.8) - * and this driver issues no other sleep, so the reset paths are the only places - * it has to be (re)loaded. Costs 2240 bytes of flash and +80 nA of retention - * sleep current. - * - * Best-effort: a chip without the patch still works, so a failure here is a - * warning, not a reason to fail init. */ +/* Load and activate the firmware Patch RAM (DS §22: most workarounds live + * there; use is "highly recommended"). Volatile across reset and cold start, + * kept by retention sleep. Best-effort: a failure is a warning. */ static void lr20xx_load_pram(struct lr20xx_data *data) { void *ctx = &data->hal_ctx; @@ -676,13 +610,8 @@ static void lr20xx_load_pram(struct lr20xx_data *data) lr20xx_patch_version_t pram = { 0 }; lr20xx_status_t rc; - /* DS §22.3.1: "There is one dedicated to the LR2021 and another one for - * the LR2012/LR2022. The GetVersion(...) API indicates which PRAM is - * needed." Per DS Table 6-40 the LR2021 answers 0x01/0x18; the other two - * answer 0x02/0x00 and want lr20xx_pram_lr20x2.h, which is not vendored - * because this driver only binds to semtech,lr2021. Read the version - * here rather than taking it from the caller so both reset paths get the - * same check. */ + /* The PRAM is chip-specific (DS §22.3.1): only the LR2021 image (0x01/0x18) + * is vendored, since the driver binds only to semtech,lr2021. */ if (lr20xx_system_get_version(ctx, &chip) != LR20XX_STATUS_OK) { LOG_WRN("PRAM: get_version failed — skipping patch load"); return; @@ -748,13 +677,8 @@ static void lr20xx_hardware_reset(struct lr20xx_data *data, tcxo_start_time_periods(cfg->tcxo_startup_delay_ms)); } - /* LDO mode — no cfg_lfclk, no set_reg_mode, no DCDC workarounds. - * - * cfg_lfclk(LF_RC) was tried here on the theory that LF_XOSC_START_ERR - * meant the LF domain needed an explicit enable-and-wait (DS §6.11.1). - * It changed nothing on hardware — the error still appears — so it is - * not the cause and the command is not carried. RC is the reset - * default and the X1 routes no DIO11, so RC is what we get either way. */ + /* LDO mode; no cfg_lfclk (tried against LF_XOSC_START_ERR: no effect), no + * reg mode, no DC-DC workarounds. LF clock stays on the RC default. */ lr20xx_configure_rfswitch(ctx, cfg); @@ -794,22 +718,14 @@ static void lr20xx_hardware_reset(struct lr20xx_data *data, /* ── Front-end calibration ──────────────────────────────────────────── */ -/* Front-end calibration can legitimately fail on RSSI saturation from a nearby - * interferer, and repeating it is the documented cure — RadioLib retries up to - * 10 times for exactly this, and gives up immediately on any other error since - * repeating those cannot help. Getting this wrong is expensive: an uncalibrated - * front end makes the chip reject every set_rx with RXFREQ_NO_FRONT_END_CALIB, - * which then looks like a broken RX path rather than a failed calibration. - */ +/* CalibFE can fail on RSSI saturation from a nearby interferer; retrying is + * the cure (RadioLib retries 10x). Otherwise every set_rx is rejected with + * RXFREQ_NO_FRONT_END_CALIB, which looks like a broken RX path. */ #define LR20XX_MAX_CAL_ATTEMPTS 10 -/* Calibrate the front end at the operating frequency. - * - * CalibFE takes point frequencies in 4 MHz steps (bit 15 = LF/HF path), not a - * band like the SX126x/LR11xx CalibrateImage. The SDK rounds the argument UP - * where the chip's own default truncates DOWN, so both neighbours are - * calibrated, nearest first. Never called on the Tx/Rx path — DS 6.4.2 keeps - * the values on chip across retention sleep. See ARCHITECTURE.md 5.5.1. */ +/* Front-end calibration at the operating frequency: point-based (4 MHz + * steps, bit 15 = HF), both neighbours, nearest first. Never on the Tx/Rx + * path: the values survive retention sleep (DS §6.4.2). */ static lr20xx_status_t lr20xx_calibrate_front_end(void *ctx, uint32_t freq_hz) { /* DS §6.4.2: "Frequencies are given in 4MHz steps (Ex: 900MHz -> @@ -832,13 +748,8 @@ static lr20xx_status_t lr20xx_calibrate_front_end(void *ctx, uint32_t freq_hz) } } - /* All three frequency slots are always sent, unused ones as 0, so - * CalibFE gets its full 8-byte form. The datasheet allows both "not - * providing" the extra slots and "setting other calibration frequencies - * to 0", but a short 4-byte command is the one thing that lines up with - * the CMD_PERR observed across exactly this call, with a clean error - * word and the chip in STBY_RC (so not a mode violation). Zeroed slots - * are documented as no-ops (DS §6.4.2). */ + /* Always the full 8-byte CalibFE (unused slots 0, documented no-ops): the + * short form lined up with the CMD_PERR seen across this call. */ const lr20xx_radio_common_rx_path_t cal_path = lr20xx_rx_path_for(freq_hz); lr20xx_radio_common_front_end_calibration_value_t fe_cal[3] = { @@ -857,14 +768,9 @@ static lr20xx_status_t lr20xx_calibrate_front_end(void *ctx, uint32_t freq_hz) rc = lr20xx_radio_common_calibrate_front_end_helper(ctx, fe_cal, 3); - /* The SDK return code reflects the SPI write, not whether the chip - * executed the command. A CalibFE issued from Rx or Tx answers - * CMD_FAIL (DS §6.4.2) while rc stays OK and no error bit is set, - * so without this check the caller is told the front end was - * calibrated when it was not — and the first symptom is degraded - * RX with a RxFreqNoCalErr nobody connects to this call. Must be - * read BEFORE get_errors(): Stat reports the previous command. - * DS Table 6-38: 0x2 CMD_OK, 0x3 CMD_DAT, anything else rejected. */ + /* rc covers the SPI write only; a CalibFE from Rx/Tx answers CMD_FAIL in the + * status (DS §6.4.2). Read it before get_errors() (Stat is the previous + * command): 0x2 CMD_OK, 0x3 CMD_DAT, anything else rejected. */ if (lr20xx_system_get_status(ctx, &s1, NULL, NULL) == LR20XX_STATUS_OK) { rejected = (s1.command_status != 2 && @@ -939,20 +845,9 @@ static void lr20xx_check_cmd(void *ctx, const char *what) #define CHECK_CMD(ctx, what) do { } while (0) #endif -/* Program the stored side-detector set into the chip (or clear it). - * - * The chip drops every side detector on SetModulationParams, so this has to - * run after each apply_modem_config and after anything else that reprograms - * the modem — the caller owns spi_mutex and must have the chip out of Rx/Tx. - * - * `enable == false` writes the n=0 form, which is how the datasheet says to - * turn the feature off. That is needed around CAD: the constraints are - * directional and mutually exclusive with ours — normal Rx requires the main - * SF to be *lower* than every side SF, CAD requires it to be *higher*. Since - * this driver runs LBT CAD before every TX, side detectors are switched off - * for the CAD and restored when RX is re-armed. (Upstream MeshCore hit the - * same wall from the other side: RadioLib returned -706 on the startReceive - * after a hardware CAD with side detectors enabled.) */ +/* Program the side-detector set (or clear it). SetModulationParams drops + * them, so re-run after every modem reconfigure; caller holds spi_mutex. + * Off for CAD: CAD needs the main SF higher than every side SF, Rx lower. */ static void lr20xx_apply_side_detectors(struct lr20xx_data *data, bool enable) { void *ctx = &data->hal_ctx; @@ -1008,15 +903,8 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data, struct lora_modem_config *mc = &data->modem_cfg; lr20xx_status_t rc; - /* Half of what follows is mode-restricted, and this runs on the TX path - * while the chip may still be in RX: - * SetPacketType — "only works when the chip is in Standby RC, Standby - * Xosc, or Fs mode", and must come first in a setup - * CalibFE — "does not work if device is in Rx or Tx mode" - * Both answer CMD_FAIL from the wrong mode, which surfaces only as a - * CmdError IRQ several commands later. Force standby so the sequence is - * always issued from a legal mode, as RadioLib does before its own - * mode-restricted calls. */ + /* Standby first: SetPacketType and CalibFE are refused from Rx/Tx (CMD_FAIL, + * surfacing commands later), and this runs on the TX path while in RX. */ lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC); CHECK_CMD(ctx, "set_standby"); @@ -1105,27 +993,10 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data, (half_power & 1) ? 5 : 0, rc); } - /* The events the DIO1 handler acts on, plus SYNC_WORD_HEADER_VALID. - * - * PREAMBLE_DETECTED is deliberately NOT here: it fires on noise, and an - * unhandled DIO1 assertion drives the safety path, which restarts RX and - * destroys the very packet that was arriving. It stays readable in the - * IRQ register for lr20xx_is_receiving()'s grace window. - * - * SYNC_WORD_HEADER_VALID used to sit alongside it, which was wrong on - * both counts. A header only latches after a sync-word match AND a - * header CRC, so it fires at most once per real packet, not on noise; - * and the safety-path objection is answered by handling it rather than - * by hiding it (the handler now has a branch that marks the reception as - * continuing). Leaving it off DIO1 meant nothing ever stamped - * header_seen_at_ms, and that timestamp is the entire RX-busy answer - * once a duty cycle is armed -- the poll path is forbidden to touch the - * bus there -- so lr20xx_is_receiving() returned false unconditionally on - * every duty-cycled node, blinding the TX gate, the noise-floor sampler - * and the CAD probe alike. - * - * This IS the split the SX126x driver uses, which the old comment - * claimed while doing something else: preamble off DIO1, header on. */ + /* DIO1 events: the handled ones plus HEADER_VALID (the only RX-busy answer + * with a duty cycle armed; fires once per real packet). PREAMBLE_DETECTED + * stays off: it fires on noise, and the safety path would restart RX over + * the arriving packet. Same split as the SX126x. */ rc = lr20xx_system_set_dio_irq_cfg(ctx, lr20xx_irq_dio(cfg), LR20XX_SYSTEM_IRQ_RX_DONE | LR20XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID | @@ -1155,32 +1026,10 @@ 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. An earlier draft ran CalibFE on every invocation, which is - * most of why this sequence 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. */ +/* Warm sleep + Calibrate (Semtech's AGC remedy shape). No CalibFE after it: + * Calibrate here excludes image/front end (DS Table 6-28) and CalibFE + * survives retention. No post-Calibrate delay: BUSY covers it. Caller + * holds spi_mutex. */ static void lr20xx_recalibrate_locked(struct lr20xx_data *data) { void *ctx = &data->hal_ctx; @@ -1232,17 +1081,9 @@ static void lr20xx_recalibrate_locked(struct lr20xx_data *data) /* ── Extension API: receiver hygiene ─────────────────────────────────── */ -/* Redo the frequency-dependent calibrations after temperature drift: warm - * sleep, Calibrate (LF_RC/HF_RC/PLL/AAF/MU/PA_OFF), rx-boost re-apply, then - * CalibFE at the operating frequency. - * - * Deliberately NOT an "AGC reset". That is an SX126x remedy for an SX126x - * fault; nothing in the LR2021 datasheet describes it on this part, and firing - * the sequence speculatively costs packets — measured on the LR1110 sibling as - * a 7.4%% miss rate in the 60 s afterwards against 0.6%% elsewhere. The driver - * therefore exposes only the operation the datasheet does call for. - * - * Leaves the driver out of RX — the caller must startReceive() afterwards. */ +/* Redo the frequency-dependent calibrations after temperature drift (warm + * sleep, Calibrate, RX-boost re-apply, CalibFE). Not an AGC reset. Leaves + * the driver out of RX; the caller must startReceive(). */ void lr20xx_recalibrate(const struct device *dev) { struct lr20xx_data *data = dev->data; @@ -1273,46 +1114,6 @@ void lr20xx_recalibrate(const struct device *dev) 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) { - return INT16_MIN; - } - if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { - return INT16_MIN; - } - - if (lr20xx_dc_rx_in_flight(data)) { - k_mutex_unlock(&data->spi_mutex); - return INT16_MIN; - } - - /* Bracketed rather than skipped — GetTemp into the sleep phase both - * risks the chip and, per DS §6.3.8, silently ends the cycle. */ - bool armed = lr20xx_dc_suspend(data); - - 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); - - lr20xx_dc_resume(data, armed); - 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; @@ -1325,28 +1126,9 @@ static bool lr20xx_apply_rx_duty_cycle(struct lr20xx_data *data) return false; } - /* 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. - * - * 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. */ + /* No RX-boost re-apply: the LR2021 retains it across duty-cycle sleep + * (DS §4.4.1, §6.3.8, §7.3.4). Only the SX126x needs a retention list; do not + * port that fix here by analogy. */ /* 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. @@ -1354,19 +1136,9 @@ static bool lr20xx_apply_rx_duty_cycle(struct lr20xx_data *data) lr20xx_system_clear_errors(ctx); lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_CMD_ERROR); - /* The second SetRxDutyCycle field is cycle_time, NOT sleep time — - * DS §6.3.8 Table 6-14: "cycle_time specifies the duration of the period - * between the start of consecutive Rx windows", and the chip sleeps for - * cycle_time - rx_max_time. The vendor SDK calls the argument - * `sleep_period_in_ms` and marshals it straight into those bytes, which - * is correct for the LR11xx (whose field really is SleepPeriod) and - * wrong here — the LR2021 redefined it. Passing the sleep time raw - * makes cycle_time < rx_max_time for every preset we use, and the - * datasheet is explicit: "If cycle_time is lower than rx_max_time a - * CMD_ERR is returned in the status of the next command." That is the - * CMD_ERROR IRQ and the rejected commands seen on the X1 the moment a - * duty cycle armed. Convert here rather than at the call sites so - * dc_sleep_ms keeps its plain meaning everywhere else. */ + /* The second SetRxDutyCycle field is cycle_time (rx + sleep), not sleep time + * (DS Table 6-14); the SDK names it sleep_period. cycle_time < rx_max_time is + * a CMD_ERR, which is what the X1 showed the moment a cycle armed. */ lr20xx_radio_common_set_rx_duty_cycle(ctx, data->dc_rx_ms, data->dc_rx_ms + data->dc_sleep_ms, LR20XX_RADIO_COMMON_RX_DUTY_CYCLE_MODE_RX); @@ -1379,32 +1151,10 @@ static bool lr20xx_apply_rx_duty_cycle(struct lr20xx_data *data) /* ── Duty-cycle ownership ───────────────────────────────────────────── * - * Mirror of the LR11xx helpers, for the same reason and from the same text. - * DS §6.3.8 ends the loop on exactly three events — a packet is received (the - * chip "goes back to the configured fallback mode"), the host issues SetStandby - * during the Rx window, or the chip is woken "with a falling edge of NSS", for - * which the datasheet adds "A SetStandby command should also be sent, to avoid - * the race conditions". Word for word the LR11xx rule. - * - * So the same conclusion follows: the host issues every NSS edge and can always - * know whether the cycle is alive, but only by owning it rather than trying to - * dodge the sleep phase. lr20xx_is_chip_busy() is a check-then-act guard — the - * chip can enter sleep between the GPIO read and the NSS assert — and with a - * cycle armed its answer is ambiguous anyway, because BUSY is high both in the - * sleep phase and through ordinary Rx on this family (the reason that helper is - * gated on rx_duty_cycle_enabled in the first place). - * - * The stand-down is deliberately SetStandby and never an NSS poke: waking this - * part by NSS while it is mid-command is what produced the CMD_PERR rejection - * storm on the MeshTracker X1 (see check_device_ready()). - * - * NOT yet verified on hardware — the X1 was not available when this was - * written. The LR11xx equivalent was measured: packet parity with the duty - * cycle armed restored from 10.7% to 100%, and the RSSI sampler from 1 of 407 - * bursts completing to all of them. - * - * Both require data->spi_mutex held by the caller. - */ + * As on the LR11xx (DS §6.3.8 is word for word UM §7.2.6): any NSS edge in + * the sleep phase ends the loop, and BUSY is high in sleep and in Rx alike. + * So suspend = SetStandby (never an NSS poke: that caused the X1's CMD_PERR + * storm), resume = re-arm. Caller holds spi_mutex. LLD 05 §13. */ static bool lr20xx_dc_suspend(struct lr20xx_data *data) { if (!data->rx_duty_cycle_enabled || !data->in_rx_mode) { @@ -1437,25 +1187,9 @@ static void lr20xx_dc_resume(struct lr20xx_data *data, bool was_armed) static uint32_t lr20xx_preamble_grace_ms(struct lr20xx_data *data); -/* Is a duty-cycled reception already under way? Ask before lr20xx_dc_suspend(). - * - * Port of lr11xx_dc_rx_in_flight() -- same fault, same fix. dc_suspend() - * stands the chip down unconditionally, which ends a reception in progress, - * and with the cycle armed lr20xx_is_receiving() answers from the header latch - * only, so the preamble phase is invisible to every discretionary stand-down - * (noise-floor sampler, RSSI read, recalibration, temperature read). On the - * LR1110 that lost ~1 packet in 40 at an 800 ms cadence (T1000-E bench, - * 2026-09-19); each miss raised no IRQ at all and coincided with a sampler - * burst that read the packet itself. - * - * Non-destructive status read (get_status, not get_and_clear). In the sleep - * phase the access wakes the chip through the HAL exactly as the caller's - * SetStandby is about to, and the caller then terminates and re-arms the cycle - * as before. With a preamble latched the chip is in Rx, where bus access is - * routine. A preamble older than the SF-aware grace with no header is stale - * and does not block the caller; dc_resume() clears it. - * - * Caller holds data->spi_mutex. */ +/* Is a duty-cycled reception under way? Ask before dc_suspend(), which would + * end it (the LR11xx lost ~1 packet in 40 without this). Non-destructive + * status read; a preamble past its grace is stale. Caller holds spi_mutex. */ static bool lr20xx_dc_rx_in_flight(struct lr20xx_data *data) { lr20xx_system_irq_mask_t irq = 0; @@ -1482,21 +1216,6 @@ static bool lr20xx_dc_rx_in_flight(struct lr20xx_data *data) return false; } -/* GPIO-only "can the host talk to this chip right now" check, no SPI. - * - * Gated on an armed duty cycle: unlike the SX126x, the LR2021 holds BUSY high - * during ordinary continuous RX, so reporting raw BUSY would tell the caller - * the radio is permanently unavailable — which gates TX as well as the probes - * and mutes the node. With a cycle armed, BUSY high does mean the chip is in - * its own sleep window and must not be disturbed. */ -bool lr20xx_is_chip_busy(const struct device *dev) -{ - struct lr20xx_data *data = dev->data; - - return data->rx_duty_cycle_enabled && - gpio_pin_get_dt(&data->hal_ctx.busy); -} - /* Stand an armed duty cycle down for TX. Polling BUSY for a gap does not work * here: the loop is event-driven, so a deferred send has nothing to re-wake it. * DS 6.3.8 — an NSS edge ends the cycle and "a SetStandby command should also be @@ -1569,16 +1288,9 @@ static void lr20xx_start_rx(struct lr20xx_data *data, /* ── Lightweight RX restart (no modem reconfig) ─────────────────────── */ -/* Returns 0 if the receiver is believed to be back on air, <0 if a command the - * re-arm depends on was rejected and the caller should escalate. - * - * "Believed" is deliberate on the duty-cycle path: verification there would mean - * a GetStatus after SetRxDutyCycle, and an NSS falling edge terminates the cycle - * being checked (DS §6.3.8) — the probe would cause the fault it looks for. So - * the check is placed on the SetStandby that precedes the re-arm, which is the - * command that actually fails when the chip is wedged, and is safe to poll - * because the standby has just ended any live cycle. Continuous RX has no such - * constraint and is checked after SetRx. */ +/* Returns 0 if the receiver is believed back on air, <0 to escalate. With a + * duty cycle the check sits on the SetStandby before the re-arm (a GetStatus + * after SetRxDutyCycle would end the cycle it checks). */ static int lr20xx_restart_rx(struct lr20xx_data *data) { void *ctx = &data->hal_ctx; @@ -1612,15 +1324,8 @@ static int lr20xx_restart_rx(struct lr20xx_data *data) CHECK_CMD(ctx, "restart_rx set_pkt_params"); } - /* "If the device is already in Rx mode, the command fails as timeout - * cannot be updated" (datasheet, SetRx) — and in continuous mode the - * chip never leaves Rx, it just keeps searching. So after a packet there - * is nothing to re-arm: issuing SetRx only earns a CMD_FAIL, whose - * CmdError comes back through DIO1 as another unhandled IRQ. - * - * Skipping it also keeps the receiver on air. Forcing a mode change - * would cost the HF oscillator restart (702 us) plus PLL lock (32 us) - * on the path taken after every single packet. */ + /* Continuous RX never leaves Rx, so there is nothing to re-arm: a SetRx would + * only earn a CMD_FAIL, and a mode change costs ~0.7 ms per packet. */ if (!data->rx_duty_cycle_enabled) { lr20xx_system_stat2_t s2 = {0}; @@ -1634,18 +1339,9 @@ static int lr20xx_restart_rx(struct lr20xx_data *data) if (data->rx_duty_cycle_enabled) { lr20xx_system_stat1_t s1 = { 0 }; - /* SetStandby first. A header error does not terminate the - * duty-cycle loop — DS §6.3.8 lists exactly three terminators - * (packet reception, a host SetStandby, an NSS wake from sleep) - * and a header error is none of them — so the chip can still be - * cycling here. Re-arming then drives an NSS edge into a live - * cycle, and the datasheet is explicit for that case: on a wake - * "by the device with a falling edge of NSS. A SetStandby - * command should also be sent, to avoid the race conditions". - * Observed on hardware without it: latched CMD_ERROR, DIO1 held - * high, five strikes to a reset, ~88 ms deaf per noise header - * error. Near-free when already in STDBY_RC. - * See ARCHITECTURE.md 5.5.1. */ + /* SetStandby first: a header error does not end the duty-cycle loop, and an + * NSS edge into a live cycle is the race DS §6.3.8 warns about (observed: + * CMD_ERROR, DIO1 stuck high, ~88 ms deaf per noise header error). */ lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC); @@ -1756,15 +1452,8 @@ static void lr20xx_log_chip_errors(void *ctx, const char *what) } } -/* Fold one packet's frequency error into the running stats. - * - * Sanity bound first: the SDK sign-extends a 24-bit field, so a garbage read - * would land anywhere in +/-8 MHz. Anything past +/-200 kHz is not a receivable - * LoRa packet at the bandwidths this driver uses (the widest is 500 kHz, and - * the demodulator's own capture range is a fraction of that), so a value out - * there means the three extra GetLoraPacketStatus bytes are not what v2.0.2 - * thinks they are on this firmware. Drop it rather than poison the mean, and - * say so once. */ +/* Fold one packet's frequency error into the stats. Values past +/-200 kHz + * mean the three extra status bytes are junk on this firmware: dropped. */ #define LR20XX_FREQ_OFFSET_SANE_HZ 200000 static void lr20xx_track_freq_offset(struct lr20xx_data *data, int32_t off_hz) @@ -1793,13 +1482,7 @@ static void lr20xx_track_freq_offset(struct lr20xx_data *data, int32_t off_hz) data->freq_off_max_hz = off_hz; } - /* Saturate rather than wrap. The sum is nowhere near trouble — bounded - * to +/-200 kHz per packet by the gate above, int64_t needs ~4.6e13 - * packets — but the uint32_t count wraps at 4.29e9 (~14 years at - * 10 pkt/s), and it wraps to *zero*, which the summary below would then - * divide by. Unreachable in practice; undefined if reached. Freezing - * leaves the mean valid and stale, which is a defensible answer; the - * per-packet last/min/max above keep updating either way. */ + /* Freeze the count rather than wrap it to zero (a divisor below). */ if (data->freq_off_count == UINT32_MAX) { return; } @@ -1808,14 +1491,8 @@ static void lr20xx_track_freq_offset(struct lr20xx_data *data, int32_t off_hz) LOG_DBG("freq offset: %d Hz", off_hz); - /* Periodic summary at INFO. The per-packet value above is only visible - * with CONFIG_LORA_LOG_LEVEL_DBG=y, which makes this whole driver a - * firehose (six lines per apply_modem_config alone) and starts dropping - * messages on a soak long enough to matter. The mean is the number - * worth watching anyway — one packet's offset is our reference error - * plus the transmitter's, so only the average over many peers says - * anything about ours. Every 16 packets keeps it readable in an - * ordinary debug.conf build. */ + /* Periodic INFO summary every 16 packets: the mean over many peers is the + * number that says anything about this node's own reference. */ if ((data->freq_off_count & 0x0F) == 0) { LOG_INF("freq offset: mean %d Hz (min %d, max %d, %u pkts)", (int)(data->freq_off_sum_hz / @@ -1846,24 +1523,17 @@ static inline bool lr20xx_cad_rx_resolve(struct lr20xx_data *data, int outcome) return atomic_cas(&data->cad_rx_state, LR20XX_CAD_RX_ARMED, outcome); } -static void lr20xx_dio1_work_handler(struct k_work *work) +/* The DIO1 handler is one pass over the IRQ word, case by case, in this order. + * Each case helper runs with spi_mutex held. A helper returns true when it has + * released the mutex to invoke a callback, and the handler then returns at + * once; otherwise it reports through *rx_restarted whether it left the receiver + * running, which the safety net at the end relies on. */ + +/* Chip errors, supply faults and the stuck-DIO1 counter: bookkeeping that + * changes no radio state. */ +static void lr20xx_irq_status_notes(struct lr20xx_data *data, uint32_t irq) { - struct lr20xx_data *data = CONTAINER_OF(work, struct lr20xx_data, - dio1_work); - const struct lr20xx_config *cfg = data->dev->config; void *ctx = &data->hal_ctx; - bool rx_restarted = false; - - k_mutex_lock(&data->spi_mutex, K_FOREVER); - - /* Combined get + clear IRQ status */ - lr20xx_system_irq_mask_t irq = 0; - lr20xx_status_t rc = lr20xx_system_get_and_clear_irq_status(ctx, &irq); - - if (rc != LR20XX_STATUS_OK) { - LOG_ERR("Failed to read IRQ status (rc=%d)", rc); - goto safety_check; - } if (irq & (LR20XX_SYSTEM_IRQ_ERROR | LR20XX_SYSTEM_IRQ_CMD_ERROR)) { lr20xx_log_chip_errors(ctx, (irq & LR20XX_SYSTEM_IRQ_CMD_ERROR) @@ -1894,265 +1564,255 @@ static void lr20xx_dio1_work_handler(struct k_work *work) if (irq & ~(LR20XX_SYSTEM_IRQ_ERROR | LR20XX_SYSTEM_IRQ_CMD_ERROR)) { data->dio1_stuck_count = 0; } +} - /* ── RX done ── - * Gated on no error bits: a CRC-failed packet asserts RX_DONE and - * CRC_ERROR together on this chip family (same as SX126x/LR11xx), - * so an ungated done-first read would deliver corrupted payloads - * as valid. A good packet coalesced with an earlier header error - * in the same handler window is dropped too — the aborted packet - * may have left bytes in the RX FIFO, misaligning the read. The - * error branch below owns the window instead. */ - /* Ground truth for a probe Rx: anything proving a transmitter was - * there. A CRC or header error counts as much as a clean packet. */ - if (irq & (LR20XX_SYSTEM_IRQ_RX_DONE | LR20XX_SYSTEM_IRQ_CRC_ERROR | - LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR)) { - lr20xx_cad_rx_resolve(data, LR20XX_CAD_RX_PACKET); +/* ── RX done ── + * Gated on no error bits: RX_DONE and CRC_ERROR co-fire on a failed packet, + * and a good packet coalesced with a header error may be misaligned in the + * FIFO. The error branch owns that window. */ +static bool lr20xx_irq_rx_done(struct lr20xx_data *data, uint32_t irq, + bool *rx_restarted) +{ + void *ctx = &data->hal_ctx; + + if (!(irq & LR20XX_SYSTEM_IRQ_RX_DONE) || + (irq & (LR20XX_SYSTEM_IRQ_CRC_ERROR | + LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR))) { + return false; } - if ((irq & LR20XX_SYSTEM_IRQ_RX_DONE) && - !(irq & (LR20XX_SYSTEM_IRQ_CRC_ERROR | - LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR))) { - uint16_t pkt_len = 0; - lr20xx_radio_common_get_rx_packet_length(ctx, &pkt_len); + uint16_t pkt_len = 0; + lr20xx_radio_common_get_rx_packet_length(ctx, &pkt_len); - if (pkt_len > 0 && pkt_len <= 255) { - lr20xx_radio_lora_packet_status_t pkt_stat; - lr20xx_radio_lora_get_packet_status(ctx, &pkt_stat); + if (pkt_len > 0 && pkt_len <= 255) { + lr20xx_radio_lora_packet_status_t pkt_stat; + lr20xx_radio_lora_get_packet_status(ctx, &pkt_stat); - lr20xx_radio_fifo_read_rx(ctx, data->rx_buf, - (uint16_t)pkt_len); + lr20xx_radio_fifo_read_rx(ctx, data->rx_buf, + (uint16_t)pkt_len); - /* Restart RX before firing callback */ - lr20xx_restart_rx(data); - rx_restarted = true; - - /* When SNR < 0, use signal RSSI for a more - * accurate reading on weak links. - * - * This branch is in fact DEAD, and deliberately left - * that way. Per the vendor struct doc - * (lr20xx_radio_lora_types.h), a negative snr_pkt_raw - * gives rssi_signal = rssi_pkt + snr/4, i.e. strictly - * lower — so the `>` can never hold. The LR11xx driver - * carries the identical dead branch. Removing it here - * alone would shift reported RSSI on this radio relative - * to the others (nothing would change, but the next - * reader would assume it had); fix both or neither. */ - int16_t rssi = pkt_stat.rssi_pkt_in_dbm; - int8_t snr = ((int8_t)pkt_stat.snr_pkt_raw + 2) >> 2; - - if (snr < 0 && - pkt_stat.rssi_signal_pkt_in_dbm > rssi) { - rssi = pkt_stat.rssi_signal_pkt_in_dbm; - } - - /* Carrier frequency error for this packet. Driver - * v2.0.2 reads 9 bytes from GetLoraPacketStatus where DS - * rev 2.1 Table 9-13 documents only 6 — this field comes - * from the three extra bytes, so the driver is ahead of - * the datasheet here. RSSI/SNR/length decode from - * rbuffer[0..5] and are unaffected either way; treat a - * wildly implausible offset as evidence the extra bytes - * are junk on this FW rather than as a real reading. */ - lr20xx_track_freq_offset(data, - pkt_stat.freq_offset_hz); - - k_mutex_unlock(&data->spi_mutex); - - if (data->async_rx_cb) { - data->async_rx_cb(data->dev, data->rx_buf, - (uint8_t)pkt_len, - rssi, snr, - data->async_rx_user_data); - } - return; - } - - LOG_WRN("RX: invalid len %d", pkt_len); + /* Restart RX before firing callback */ lr20xx_restart_rx(data); - rx_restarted = true; - } + *rx_restarted = true; - /* ── CAD done ── */ - if (irq & LR20XX_SYSTEM_IRQ_CAD_DONE) { - bool detected = (irq & LR20XX_SYSTEM_IRQ_CAD_DETECTED) != 0; + /* SNR < 0: prefer the signal RSSI. Dead in practice (the signal RSSI is + * always lower then), and kept in step with the LR11xx: fix both or neither. */ + int16_t rssi = pkt_stat.rssi_pkt_in_dbm; + int8_t snr = ((int8_t)pkt_stat.snr_pkt_raw + 2) >> 2; - /* Stamp as early as possible in the - * handler — everything after this point is the host round-trip - * being measured. Only a free channel leads to a transmit, so - * only that case is worth stamping. */ - data->cad_done_cycles = detected ? 0U : k_cycle_get_32(); - if (!detected && data->cad_done_cycles == 0U) { - data->cad_done_cycles = 1U; /* 0 is the "none" sentinel */ + if (snr < 0 && + pkt_stat.rssi_signal_pkt_in_dbm > rssi) { + rssi = pkt_stat.rssi_signal_pkt_in_dbm; } - LOG_DBG("CAD done: %s", detected ? "activity" : "free"); - - if (data->cad_probe_rx && detected) { - /* CAD_ONLY has returned the chip to STDBY_RC. Arm the - * follow-on Rx with the same call the normal receive path - * uses, bounded by max-length-packet airtime, so whichever - * terminal IRQ follows (RX_DONE / CRC_ERROR / HEADER_ERROR / - * TIMEOUT) is the probe's ground truth. RTC steps, not the - * 524 ms cad_timeout field. */ - uint32_t steps = (uint32_t)(((uint64_t)lr20xx_max_payload_ms(data) - * LR20XX_RTC_FREQ_HZ) / 1000U); - - if (steps > 0x00FFFFFFU) { - steps = 0x00FFFFFFU; - } - atomic_set(&data->cad_rx_state, LR20XX_CAD_RX_ARMED); - lr20xx_radio_common_set_rx_with_timeout_in_rtc_step(ctx, steps); - data->in_rx_mode = true; - rx_restarted = true; - } - - if (data->cad_cb) { - lora_cad_cb cb = data->cad_cb; - void *ud = data->cad_user_data; - - data->cad_cb = NULL; - data->cad_user_data = NULL; - k_mutex_unlock(&data->spi_mutex); - cb(data->dev, detected, ud); - return; - } - - /* Blocking CAD: signal the semaphore */ - data->cad_result = detected ? 1 : 0; - k_sem_give(&data->cad_sem); - } - - /* ── TX done ── */ - if (irq & LR20XX_SYSTEM_IRQ_TX_DONE) { - LOG_DBG("TX done"); - data->tx_active = false; - - lr20xx_start_rx(data, cfg); - rx_restarted = true; - - if (data->tx_signal) { - k_poll_signal_raise(data->tx_signal, 0); - } - } - - /* ── Timeout ── */ - if (irq & LR20XX_SYSTEM_IRQ_TIMEOUT) { - /* The probe's own Rx window expiring with nothing decoded: the - * detection had no packet behind it. */ - lr20xx_cad_rx_resolve(data, LR20XX_CAD_RX_TMOUT); - if (data->tx_active) { - /* The chip's Tx safeguard fired. DS §6.3.6: "the - * transmission is stopped prematurely" — the packet is - * gone and TX_DONE will never arrive. Leaving tx_active - * set here (as this branch used to) made the whole event - * invisible: no branch ran, nothing was logged above DBG, - * and the radio sat in the post-TX state until the host's - * own wait expired. Say it and put the receiver back on - * air, same as the TX_DONE path. - * - * tx_signal is deliberately NOT raised: the C++ wait - * thread counts a raised signal as a completed send and - * would increment packets_sent for a packet that never - * left. Its own timeout owns the accounting. */ - LOG_ERR("TX timeout — chip stopped the transmission, " - "packet lost"); - data->tx_active = false; - lr20xx_start_rx(data, cfg); - rx_restarted = true; - } else { - /* Under a duty cycle this is the false-preamble case: - * the preamble-restarted rx_max_time + cycle_time window - * expired with no packet, so the chip left the loop. - * Counting it is what makes `get dc.restarts` mean - * something on this radio. */ - if (data->rx_duty_cycle_enabled) { - atomic_inc(&data->dc_timeout_restarts); - } - LOG_DBG("Timeout IRQ — restarting RX"); - if (lr20xx_restart_rx(data) < 0) { - /* The light re-arm could not get the chip back on - * air. Escalate to the full path, which forces - * standby, reprograms the modem and re-issues - * SetRx from scratch — the LR20xx counterpart of - * the SX126x's retry-from-sleep. If that fails - * too the chip is wedged, and the stuck-DIO1 - * counter below still reaches its reset. */ - LOG_WRN("Timeout: light re-arm failed — full RX restart"); - lr20xx_start_rx(data, cfg); - } - rx_restarted = true; - } - } - - /* ── CRC / Header error ── - * Plain `CRC || HDR` — no SYNC_WORD_HEADER_VALID gate. IRQ status - * is bulk-cleared on every handler entry, so a set header error - * always belongs to this window; a SYNC_VALID bit latched by - * another packet in the same window must not suppress it. */ - if (irq & (LR20XX_SYSTEM_IRQ_CRC_ERROR | - LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR)) { - LOG_WRN("RX error: CRC=%d HDR=%d RXDONE=%d", - (irq & LR20XX_SYSTEM_IRQ_CRC_ERROR) ? 1 : 0, - (irq & LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR) ? 1 : 0, - (irq & LR20XX_SYSTEM_IRQ_RX_DONE) ? 1 : 0); - - /* Drop whatever the failed (or coalesced) packet left in - * the RX FIFO so the next packet's read starts aligned — - * lr20xx_restart_rx does not clear it (only full start_rx - * does). */ - lr20xx_radio_fifo_clear_rx(ctx); - - if (!data->tx_active) { - lr20xx_restart_rx(data); - rx_restarted = true; - } + /* Carrier frequency error: SDK v2.0.2 reads 3 status bytes the DS does not + * document; implausible values are dropped as junk. */ + lr20xx_track_freq_offset(data, + pkt_stat.freq_offset_hz); k_mutex_unlock(&data->spi_mutex); if (data->async_rx_cb) { - data->async_rx_cb(data->dev, NULL, 0, 0, 0, + data->async_rx_cb(data->dev, data->rx_buf, + (uint8_t)pkt_len, + rssi, snr, data->async_rx_user_data); } + return true; + } + + LOG_WRN("RX: invalid len %d", pkt_len); + lr20xx_restart_rx(data); + *rx_restarted = true; + return false; +} + +/* ── CAD done ── */ +static bool lr20xx_irq_cad_done(struct lr20xx_data *data, uint32_t irq, + bool *rx_restarted) +{ + if (!(irq & LR20XX_SYSTEM_IRQ_CAD_DONE)) { + return false; + } + + bool detected = (irq & LR20XX_SYSTEM_IRQ_CAD_DETECTED) != 0; + + /* Stamp as early as possible in the + * handler — everything after this point is the host round-trip + * being measured. Only a free channel leads to a transmit, so + * only that case is worth stamping. */ + data->cad_done_cycles = detected ? 0U : k_cycle_get_32(); + if (!detected && data->cad_done_cycles == 0U) { + data->cad_done_cycles = 1U; /* 0 is the "none" sentinel */ + } + + LOG_DBG("CAD done: %s", detected ? "activity" : "free"); + + if (data->cad_probe_rx && detected) { + /* CAD_ONLY has returned the chip to STDBY_RC. Arm the + * follow-on Rx with the same call the normal receive path + * uses, bounded by max-length-packet airtime, so whichever + * terminal IRQ follows (RX_DONE / CRC_ERROR / HEADER_ERROR / + * TIMEOUT) is the probe's ground truth. RTC steps, not the + * 524 ms cad_timeout field. */ + uint32_t steps = zc_lora_ms_to_steps24(lr20xx_max_payload_ms(data), + LR20XX_RTC_FREQ_HZ); + + atomic_set(&data->cad_rx_state, LR20XX_CAD_RX_ARMED); + lr20xx_radio_common_set_rx_with_timeout_in_rtc_step(&data->hal_ctx, + steps); + data->in_rx_mode = true; + *rx_restarted = true; + } + + if (data->cad_cb) { + lora_cad_cb cb = data->cad_cb; + void *ud = data->cad_user_data; + + data->cad_cb = NULL; + data->cad_user_data = NULL; + k_mutex_unlock(&data->spi_mutex); + cb(data->dev, detected, ud); + return true; + } + + /* Blocking CAD: signal the semaphore */ + data->cad_result = detected ? 1 : 0; + k_sem_give(&data->cad_sem); + return false; +} + +/* ── TX done ── */ +static void lr20xx_irq_tx_done(struct lr20xx_data *data, + const struct lr20xx_config *cfg, uint32_t irq, + bool *rx_restarted) +{ + if (!(irq & LR20XX_SYSTEM_IRQ_TX_DONE)) { return; } - /* ── Header valid ── - * A packet is past sync word and header CRC and is now in its payload - * phase. Stamp the latch lr20xx_is_receiving() answers from; bounded - * there by lr20xx_max_payload_ms(), and cleared by the terminal event's - * lr20xx_reset_rx_busy_signals(). - * - * Gated on no terminal bit in this same pass -- those branches above own - * the outcome and have already reset the signals, so stamping after them - * would re-arm a gate for a packet that is finished. (The header-error - * branch above returns, so it cannot reach here at all.) - * - * rx_restarted, for the same reason the CAD_RX branch sets it: nothing - * needs restarting, the chip is mid-packet, and letting the safety net - * below fire here would destroy the very packet this branch exists to - * protect -- the failure that kept this IRQ off DIO1 in the first - * place. */ - if ((irq & LR20XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID) && - !(irq & (LR20XX_SYSTEM_IRQ_RX_DONE | LR20XX_SYSTEM_IRQ_CRC_ERROR | - LR20XX_SYSTEM_IRQ_TIMEOUT))) { - uint32_t now = k_uptime_get_32(); + LOG_DBG("TX done"); + data->tx_active = false; - /* 1 as the "set" sentinel if k_uptime is 0 right after boot. */ - data->header_seen_at_ms = (now == 0) ? 1U : now; - /* The latch is the truth source now; a preamble timestamp left - * behind would outlive it and be read as a live grace window. */ - data->preamble_seen_at_ms = 0; - rx_restarted = true; + lr20xx_start_rx(data, cfg); + *rx_restarted = true; + + if (data->tx_signal) { + k_poll_signal_raise(data->tx_signal, 0); + } +} + +/* ── Timeout ── */ +static void lr20xx_irq_timeout(struct lr20xx_data *data, + const struct lr20xx_config *cfg, uint32_t irq, + bool *rx_restarted) +{ + if (!(irq & LR20XX_SYSTEM_IRQ_TIMEOUT)) { + return; } -safety_check: + /* The probe's own Rx window expiring with nothing decoded: the + * detection had no packet behind it. */ + lr20xx_cad_rx_resolve(data, LR20XX_CAD_RX_TMOUT); + if (data->tx_active) { + /* Chip TX timeout (DS §6.3.6): TX_DONE will never come. Re-arm RX but do NOT + * raise tx_signal (it means "sent"); the adapter wait thread owns the loss. */ + LOG_ERR("TX timeout — chip stopped the transmission, " + "packet lost"); + data->tx_active = false; + lr20xx_start_rx(data, cfg); + *rx_restarted = true; + return; + } + + /* Under a duty cycle this is the false-preamble case: + * the preamble-restarted rx_max_time + cycle_time window + * expired with no packet, so the chip left the loop. + * Counting it is what makes `get dc.restarts` mean + * something on this radio. */ + if (data->rx_duty_cycle_enabled) { + atomic_inc(&data->dc_timeout_restarts); + } + LOG_DBG("Timeout IRQ — restarting RX"); + if (lr20xx_restart_rx(data) < 0) { + /* Escalate: full restart (standby, modem reprogram, SetRx). If that fails + * too, the stuck-DIO1 counter still reaches its hardware reset. */ + LOG_WRN("Timeout: light re-arm failed — full RX restart"); + lr20xx_start_rx(data, cfg); + } + *rx_restarted = true; +} + +/* ── CRC / Header error ── + * Plain `CRC || HDR` — no SYNC_WORD_HEADER_VALID gate. IRQ status + * is bulk-cleared on every handler entry, so a set header error + * always belongs to this window; a SYNC_VALID bit latched by + * another packet in the same window must not suppress it. */ +static bool lr20xx_irq_rx_error(struct lr20xx_data *data, uint32_t irq, + bool *rx_restarted) +{ + if (!(irq & (LR20XX_SYSTEM_IRQ_CRC_ERROR | + LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR))) { + return false; + } + + LOG_WRN("RX error: CRC=%d HDR=%d RXDONE=%d", + (irq & LR20XX_SYSTEM_IRQ_CRC_ERROR) ? 1 : 0, + (irq & LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR) ? 1 : 0, + (irq & LR20XX_SYSTEM_IRQ_RX_DONE) ? 1 : 0); + + /* Drop whatever the failed (or coalesced) packet left in + * the RX FIFO so the next packet's read starts aligned — + * lr20xx_restart_rx does not clear it (only full start_rx + * does). */ + lr20xx_radio_fifo_clear_rx(&data->hal_ctx); + + if (!data->tx_active) { + lr20xx_restart_rx(data); + *rx_restarted = true; + } + + k_mutex_unlock(&data->spi_mutex); + + if (data->async_rx_cb) { + data->async_rx_cb(data->dev, NULL, 0, 0, 0, + data->async_rx_user_data); + } + return true; +} + +/* ── Header valid ── + * Stamp the payload-phase latch, unless a terminal bit for our packet came + * in the same pass. rx_restarted: the chip is mid-packet, and the safety + * net must not restart RX over it. */ +static void lr20xx_irq_header_valid(struct lr20xx_data *data, uint32_t irq, + bool *rx_restarted) +{ + if (!(irq & LR20XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID) || + (irq & (LR20XX_SYSTEM_IRQ_RX_DONE | LR20XX_SYSTEM_IRQ_CRC_ERROR | + LR20XX_SYSTEM_IRQ_TIMEOUT))) { + return; + } + + uint32_t now = k_uptime_get_32(); + + /* 1 as the "set" sentinel if k_uptime is 0 right after boot. */ + data->header_seen_at_ms = (now == 0) ? 1U : now; + /* The latch is the truth source now; a preamble timestamp left + * behind would outlive it and be read as a live grace window. */ + data->preamble_seen_at_ms = 0; + *rx_restarted = true; +} + +/* Safety net for a pass that left RX expected but not re-armed. */ +static void lr20xx_dio1_safety_net(struct lr20xx_data *data, uint32_t irq, + lr20xx_status_t rc, bool rx_restarted) +{ /* A zero IRQ word means the edge was already consumed — a duplicate * re-submit, or the restart path cleared it. Nothing failed, so do not * tear RX down and do not count it toward the stuck-DIO1 reset. */ if (irq == 0) { - goto edge_recheck; + return; } /* Error-only wake: the errors were already logged and cleared, so DIO1 @@ -2169,7 +1829,7 @@ safety_check: "out of RX — restarting", irq); lr20xx_restart_rx(data); } - goto edge_recheck; + return; } if (!rx_restarted && data->in_rx_mode && !data->tx_active) { @@ -2177,26 +1837,72 @@ safety_check: "restarting RX", irq, rc); lr20xx_restart_rx(data); } +} -edge_recheck: - /* Edge-triggered DIO1: if still HIGH, re-submit for pending flags. - * Guard against stuck DIO1: after 5 empty cycles, hardware reset. */ - if (gpio_pin_get_dt(&data->hal_ctx.dio1)) { - data->dio1_stuck_count++; - if (data->dio1_stuck_count >= 5) { - LOG_ERR("DIO1 stuck HIGH for %d cycles — " - "hardware reset", data->dio1_stuck_count); - data->dio1_stuck_count = 0; - lr20xx_hardware_reset(data, cfg); - lr20xx_start_rx(data, cfg); - } else { - k_work_submit_to_queue(&data->dio1_wq, - &data->dio1_work); - } - } else { +/* Edge-triggered DIO1: if still HIGH, re-submit for pending flags. + * Guard against stuck DIO1: after 5 empty cycles, hardware reset. */ +static void lr20xx_dio1_recheck_pin(struct lr20xx_data *data, + const struct lr20xx_config *cfg) +{ + if (!gpio_pin_get_dt(&data->hal_ctx.dio1)) { data->dio1_stuck_count = 0; + return; } + data->dio1_stuck_count++; + if (data->dio1_stuck_count >= 5) { + LOG_ERR("DIO1 stuck HIGH for %d cycles — " + "hardware reset", data->dio1_stuck_count); + data->dio1_stuck_count = 0; + lr20xx_hardware_reset(data, cfg); + lr20xx_start_rx(data, cfg); + } else { + k_work_submit_to_queue(&data->dio1_wq, + &data->dio1_work); + } +} + +static void lr20xx_dio1_work_handler(struct k_work *work) +{ + struct lr20xx_data *data = CONTAINER_OF(work, struct lr20xx_data, + dio1_work); + const struct lr20xx_config *cfg = data->dev->config; + bool rx_restarted = false; + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + /* Combined get + clear IRQ status */ + lr20xx_system_irq_mask_t irq = 0; + lr20xx_status_t rc = lr20xx_system_get_and_clear_irq_status(&data->hal_ctx, + &irq); + + if (rc != LR20XX_STATUS_OK) { + LOG_ERR("Failed to read IRQ status (rc=%d)", rc); + } else { + lr20xx_irq_status_notes(data, irq); + + /* Ground truth for a probe Rx: anything proving a transmitter was + * there. A CRC or header error counts as much as a clean packet. */ + if (irq & (LR20XX_SYSTEM_IRQ_RX_DONE | LR20XX_SYSTEM_IRQ_CRC_ERROR | + LR20XX_SYSTEM_IRQ_LORA_HEADER_ERROR)) { + lr20xx_cad_rx_resolve(data, LR20XX_CAD_RX_PACKET); + } + + if (lr20xx_irq_rx_done(data, irq, &rx_restarted) || + lr20xx_irq_cad_done(data, irq, &rx_restarted)) { + return; /* mutex released to deliver a callback */ + } + lr20xx_irq_tx_done(data, cfg, irq, &rx_restarted); + lr20xx_irq_timeout(data, cfg, irq, &rx_restarted); + if (lr20xx_irq_rx_error(data, irq, &rx_restarted)) { + return; + } + lr20xx_irq_header_valid(data, irq, &rx_restarted); + } + + lr20xx_dio1_safety_net(data, irq, rc, rx_restarted); + lr20xx_dio1_recheck_pin(data, cfg); + k_mutex_unlock(&data->spi_mutex); } @@ -2234,14 +1940,8 @@ static int lr20xx_lora_config(const struct device *dev, * ral_lr20xx_init() calibrates at init and never on the Tx/Rx path. */ k_mutex_lock(&data->spi_mutex, K_FOREVER); - /* Standby first — CalibFE "does not work if device is in Rx or Tx mode" - * (DS §6.4.2). On the reconfigure path the chip IS still in Rx: the C++ - * layer's hwCancelReceive() lowers lora_recv_async(NULL)'s bookkeeping - * without commanding the chip, and continuous RX only ends when the host - * changes the mode (DS §6.3.5). Without this the calibration is refused - * for every `set freq/sf/bw/cr`, tempradio set and tempradio clear, and - * refused invisibly — see the note in lr20xx_calibrate_front_end(). - * Free at boot, where hw_init() has already left the chip in STDBY_RC. */ + /* Standby first: CalibFE is refused in Rx/Tx (DS §6.4.2), and on reconfigure + * the chip is still in continuous RX. Free at boot. */ lr20xx_system_set_standby_mode(&data->hal_ctx, LR20XX_SYSTEM_STANDBY_MODE_RC); @@ -2313,26 +2013,17 @@ static uint32_t lr20xx_cad_timeout_ms(struct lr20xx_data *data) return MAX(ms, 200U); } -/* ── Hardware CAD_LBT ─────────────────────────────────────────────────── +/* ── Hardware CAD_LBT ───────────────────────────────────────────────── * - * CadExitMode = 0x10 (DS Table 6-18): the chip runs the CAD and, on a clear - * channel, goes straight to Tx with no host round-trip. Payload and packet - * params must be staged BEFORE SetLoraCAD, and DIO1 must stay enabled across it - * (CAD_DONE and TX_DONE both arrive on it). Busy exits to STDBY_RC, so the host - * still re-arms RX there. - * - * cad_timeout doubles as the Tx timeout: 24 bits of 32 MHz periods = 524 ms max. - * Longer transmits take the classic host path instead of being truncated. - * See ARCHITECTURE.md 5.5.1. */ + * CadExitMode 0x10 (DS Table 6-18): CAD, then on a clear channel straight to + * Tx. Payload and packet params staged before SetLoraCAD; DIO1 stays on. + * cad_timeout is also the Tx timeout: 24 bits of 32 MHz = 524 ms, so longer + * transmits take the classic host path. ARCHITECTURE.md 5.5.1. */ #define LR20XX_CAD_LBT_MAX_TX_TIMEOUT_STEPS 0x00FFFFFFU #define LR20XX_CAD_LBT_STEPS_PER_MS 32000U -/* The OTHER time base on this chip. SetTx/SetRx/SetRxDutyCycle timeouts are - * counted in 32.768 kHz RTC periods (DS §6.3.17: "expressed in periods of the - * 32.768kHz RTC"), not the 32 MHz periods cad_timeout uses above — the two sit - * three lines apart here precisely so nobody reaches for the wrong one. Both - * fields are 24 bits, so the RTC one tops out at 16777215/32768 = 512 s. */ -#define LR20XX_RTC_STEP_MAX 0x00FFFFFFU +/* SetTx/SetRx/SetRxDutyCycle timeouts count 32.768 kHz RTC periods (DS + * §6.3.17), not the 32 MHz periods of cad_timeout above. */ /* Never shorten the Tx safeguard below what shipped before it was scaled. */ #define LR20XX_TX_TIMEOUT_FLOOR_MS 5000U @@ -2491,27 +2182,16 @@ static int lr20xx_lora_send_async(const struct device *dev, bool was_in_rx = data->in_rx_mode; int cad_ret; - /* Discard any stamp left by a CAD that was not this transmit's. - * The CAD_DONE handler stamps cad_done_cycles on every free CAD - * — adaptive-CAD probes every ~15 s, and CAD_LBT, neither of - * which consumes it — but the only reader is the latency log - * below. Without this the reported CAD->TX gap can be measured - * from a probe that ran seconds earlier, which is worse than not - * reporting it: it is a plausible number that is simply wrong. */ + /* Discard a CAD_DONE stamp left by some other CAD (probes, LBT), so the + * CAD->TX latency log measures this transmit. */ data->cad_done_cycles = 0U; cad_ret = lr20xx_lora_cad(dev, K_MSEC(lr20xx_cad_timeout_ms(data))); if (cad_ret > 0) { LOG_DBG("LBT: channel busy"); - /* Re-arm whenever there was anything to re-arm. The - * async_rx_cb condition was safe while a busy CAD only - * cost the RX mode, but lr20xx_dc_takeover() above has - * already stood the duty cycle down by this point — so - * bailing out without restarting leaves the radio - * parked in standby, deaf, with nothing scheduled to - * bring it back. rx_duty_cycle_enabled covers the case - * the callback check would miss. */ + /* Re-arm whenever anything was running: dc_takeover() has already stood + * the duty cycle down, so bailing out would leave the radio deaf. */ if (was_in_rx || data->rx_duty_cycle_enabled) { k_mutex_lock(&data->spi_mutex, K_FOREVER); lr20xx_start_rx(data, cfg); @@ -2589,37 +2269,23 @@ static int lr20xx_lora_send_async(const struct device *dev, data->cad_done_cycles = 0U; } - /* Chip-side Tx safeguard — DS §6.3.6: "Tx timeout can be used as a - * safeguard in case transmission fails and TxDone interrupt never - * occurs", and when it fires "the transmission is stopped prematurely". - * So it has to exceed real airtime, and a fixed budget cannot: at - * SF12/BW62.5 a 31-byte packet already needs more than 5 s, and at - * SF12/BW31.25 so does a 6-byte one. Scaled from the same airtime the - * CAD_LBT path uses, floored at the previous 5000 ms so no preset that - * works today gets a tighter deadline. - * - * Programmed in RTC steps rather than through the SDK's millisecond - * wrapper: that wrapper computes `ms * 32768` in uint32 and overflows - * above 131 071 ms, which is reachable here (SF12/BW7.81 at 255 bytes is - * ~229 s of airtime). Saturating at the 24-bit field maximum is 512 s, - * comfortably past any airtime this chip can produce. */ + /* Chip TX safeguard (DS §6.3.6: it stops the transmission): airtime +25% + * +500 ms, floored at the old 5 s, saturated at 24 bits of RTC steps; the + * SDK's ms helper overflows above 131 s. */ { uint32_t tx_air_ms = lr20xx_lora_airtime(dev, data_len); uint32_t tx_tmo_ms = tx_air_ms + (tx_air_ms / 4U) + 500U; - uint64_t tx_tmo_steps; + uint32_t tx_tmo_steps; if (tx_tmo_ms < LR20XX_TX_TIMEOUT_FLOOR_MS) { tx_tmo_ms = LR20XX_TX_TIMEOUT_FLOOR_MS; } - tx_tmo_steps = ((uint64_t)tx_tmo_ms * LR20XX_RTC_FREQ_HZ) / 1000U; - if (tx_tmo_steps > LR20XX_RTC_STEP_MAX) { - tx_tmo_steps = LR20XX_RTC_STEP_MAX; - } + tx_tmo_steps = zc_lora_ms_to_steps24(tx_tmo_ms, LR20XX_RTC_FREQ_HZ); LOG_DBG("SET_TX: airtime=%u ms, timeout=%u ms (%u steps)", - tx_air_ms, tx_tmo_ms, (uint32_t)tx_tmo_steps); + tx_air_ms, tx_tmo_ms, tx_tmo_steps); lr20xx_radio_common_set_tx_with_timeout_in_rtc_step( - ctx, (uint32_t)tx_tmo_steps); + ctx, tx_tmo_steps); } /* Command status here is SET_TX's own (2=accepted, 1=rejected, @@ -2674,21 +2340,9 @@ static int lr20xx_lora_recv_async(const struct device *dev, k_mutex_lock(&data->spi_mutex, K_FOREVER); - /* Idempotent fast path: the chip is already in continuous RX, so there - * is nothing to program — refresh the callback and leave the receiver on - * air. The usual caller is LoRaRadioBase::startSendRaw()'s failure path: - * the LBT branch of send_async has already run lr20xx_start_rx() before - * returning -EBUSY, so without this every CAD-busy verdict paid a SECOND - * full rebuild (standby -> ClearRxFifo -> apply_modem_config -> SetRx) on - * top of the driver's own. startSendRaw's comment has asserted this was - * "a no-op there" ever since the SX126x gained its Phase 2 fast path; on - * this driver it was not, until now. - * - * rx_duty_cycle_enabled is part of the guard, not decoration. This is - * the CONTINUOUS entry point and it clears that flag below, so a caller - * moving a duty-cycled radio to continuous RX has to take the full path: - * fast-pathing there would leave the cycle running on a chip whose - * bookkeeping claims continuous, and nothing would ever stand it down. */ + /* Already in continuous RX: refresh the callback and stay on air (the usual + * caller is LoRaRadio::startSendRaw()'s failure path, after the LBT branch + * restored RX). Not with a duty cycle: this entry point clears that flag. */ if (data->in_rx_mode && !data->rx_duty_cycle_enabled) { data->async_rx_cb = cb; data->async_rx_user_data = user_data; @@ -2724,19 +2378,9 @@ static int lr20xx_lora_recv(const struct device *dev, uint8_t *buf, /* ── LR20xx extension API ───────────────────────────────────────────── */ -/* Front-end settle to wait after entering Rx before the first GetRssiInst. - * - * DS Table 13-82 puts the RSSI averaging window at ~936 us*kHz / BW and the - * post-Rx-entry delay at 12-15 of those windows; the C++ sampler will not read - * until 16 have passed (rssi_settle_delay_us(), radio_common.h). An Rx entry - * made HERE has to clear the same bar, or the reading it brackets comes from a - * front end the firmware itself considers unsettled. - * - * The flat 1 ms this replaces clears it from BW20.83 up but not below -- - * BW15.63 wants 1008 us, BW10.42 1504 us, BW7.81 2144 us -- and `set radio` - * accepts bandwidths that low. Floored at 1000 so no preset that works today - * waits less than it did. Deliberately the same 936/16 the C++ side uses: two - * settle models that could disagree would be worse than one that is wrong. */ +/* Settle after Rx entry before the first GetRssiInst: 16 RSSI averaging + * windows (DS Table 13-82), the same model as the C++ sampler, floored at + * the old 1 ms. */ static uint32_t lr20xx_rssi_settle_us(struct lr20xx_data *data) { uint32_t bw_khz = (uint32_t)bw_enum_to_khz(data->modem_cfg.bandwidth); @@ -2772,7 +2416,7 @@ int16_t lr20xx_get_rssi_inst(const struct device *dev) int16_t out = -128; /* Non-blocking: a contended bus means the sampler retries next tick. - * -128 is the sentinel LoRaRadioBase::triggerNoiseFloorCalibrate wants. */ + * -128 is the sentinel LoRaRadio::triggerNoiseFloorCalibrate wants. */ if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { return -128; } @@ -2784,13 +2428,8 @@ int16_t lr20xx_get_rssi_inst(const struct device *dev) bool armed = lr20xx_dc_suspend(data); if (armed) { - /* GetRssiInst reads a live receiver; the stand-down left the chip - * in standby, where there is nothing to measure. Enter - * continuous Rx for the reading and hand the cycle back after. - * lr20xx_rssi_settle() sizes the front-end settle from the - * bandwidth (a flat 1 ms was short of it below BW20.83), and - * against a noise-floor interval measured in seconds it costs - * well under a hundredth of a percent of airtime. */ + /* The stand-down left the chip in standby: enter continuous Rx for the + * reading (after the front-end settle), then hand the cycle back. */ lr20xx_radio_common_set_rx_with_timeout_in_rtc_step( &data->hal_ctx, 0xFFFFFF); lr20xx_rssi_settle(data); @@ -2813,16 +2452,10 @@ int16_t lr20xx_get_rssi_inst(const struct device *dev) * + header decode with margin. */ static uint32_t lr20xx_preamble_grace_ms(struct lr20xx_data *data) { - uint8_t sf = (uint8_t)data->modem_cfg.datarate; - uint32_t bw_hz = (uint32_t)(bw_enum_to_khz(data->modem_cfg.bandwidth) * 1000.0f); - uint16_t preamble = data->modem_cfg.preamble_len; - - if (bw_hz == 0 || sf < 5 || sf > 12) { - return 1000; /* safe default ~1 s if config is uninitialised */ - } - uint64_t us = ((uint64_t)(preamble + 8U) << sf) * 1000000ULL / bw_hz; - - return (uint32_t)((us + 999U) / 1000U); + return zc_lora_preamble_grace_ms( + (uint8_t)data->modem_cfg.datarate, + (uint32_t)(bw_enum_to_khz(data->modem_cfg.bandwidth) * 1000.0f), + data->modem_cfg.preamble_len); } /* Upper bound on the payload phase: max-length packet airtime at the current @@ -2832,37 +2465,9 @@ static uint32_t lr20xx_preamble_grace_ms(struct lr20xx_data *data) * gate until reboot. */ static uint32_t lr20xx_max_payload_ms(struct lr20xx_data *data) { - uint8_t sf = (uint8_t)data->modem_cfg.datarate; - uint32_t bw_hz = (uint32_t)(bw_enum_to_khz(data->modem_cfg.bandwidth) * 1000.0f); - - 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 - * The divisor is 4*(SF - 2*DE), not 4*(SF - DE) — same form - * lr20xx_lora_airtime() uses. With DE hard-coded to 1 that is 4*(SF-2), - * which at SF5 is 12, so the divisor is always non-zero. - * - * DE = 1 unconditionally because this is a ceiling, and LDRO on yields - * the larger symbol count: at SF12/BW62.5 it is 416 symbols against 384 - * with the wrong divisor, i.e. 27.3 s of real payload airtime that the - * bound has to clear. */ - 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; + return zc_lora_max_payload_ms( + (uint8_t)data->modem_cfg.datarate, + (uint32_t)(bw_enum_to_khz(data->modem_cfg.bandwidth) * 1000.0f)); } int lr20xx_get_rssi_burst(const struct device *dev, int16_t *out, int n, @@ -2871,22 +2476,8 @@ int lr20xx_get_rssi_burst(const struct device *dev, int16_t *out, int n, struct lr20xx_data *data = dev->data; int got = 0; - /* One stand-down for the WHOLE burst, not one per sample. - * - * lr20xx_get_rssi_inst() has to suspend the duty cycle, enter continuous - * Rx, wait 1 ms for the front end, read, and then hand the cycle back -- - * and lr20xx_dc_resume() clears every IRQ and zeroes the RX-busy latch, - * because a re-armed cycle must start from a known point. The noise-floor - * sampler wants a median of eight, and calling the single-shot read eight - * times therefore cost eight cycle tear-downs, eight latch wipes and 8 ms - * of settle every sampling interval -- on a receiver whose whole job is to - * be listening. That is the same fault the CAD classifier had, in a - * different function; here it fires whether or not a probe follows. - * - * Bracketing once collapses it to one of each. It also holds the SPI - * mutex for a single short span instead of taking it eight times, and the - * reads are spaced by the caller's averaging window exactly as before, so - * they stay independent. */ + /* One stand-down for the whole burst: a per-sample read cost eight cycle + * tear-downs, latch wipes and settles per sampling interval. */ if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { return 0; } @@ -2902,15 +2493,9 @@ int lr20xx_get_rssi_burst(const struct device *dev, int16_t *out, int n, if (armed) { lr20xx_radio_common_set_rx_with_timeout_in_rtc_step( &data->hal_ctx, 0xFFFFFF); - /* Start the window clean so the check after the loop reports - * only what arrived INSIDE it -- these bits are latched (DS 5.7) - * and nothing clears them between duty-cycle re-arms, so a stale - * preamble from earlier in the cycle would otherwise condemn - * every burst. Safe here and nowhere else: the cycle is already - * torn down and dc_resume() clears the lot again on the way out, - * so this destroys no state the poll path could still want. - * CMD_ERROR is deliberately absent -- that is an LR11xx quirk - * with no LR20xx equivalent, as lr20xx_is_receiving() notes. */ + /* Clear the latched reception bits so the check after the loop sees only + * what arrived inside the window (the cycle is already down). No CMD_ERROR + * in the mask: that is an LR11xx quirk. */ lr20xx_system_clear_irq_status(&data->hal_ctx, LR20XX_SYSTEM_IRQ_PREAMBLE_DETECTED | LR20XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID); @@ -2932,22 +2517,9 @@ int lr20xx_get_rssi_burst(const struct device *dev, int16_t *out, int n, got++; } - /* Did a transmitter turn up inside the window? Then these samples - * measure it, not the floor, and the caller must throw them away. - * - * This covers a gap the caller's own guards cannot. cadMaintenance() - * re-checks isReceiving() immediately before its CAD probe, and that - * now works on this family -- but dc_resume() below zeroes - * header_seen_at_ms on its way out, one call earlier, so a reception - * that began inside this bracket is invisible to the re-check by the - * time it runs. Reporting the contamination from in here, where the - * evidence still exists, closes it: the sampler abandons the burst, - * _sample_fresh stays clear, and the probe -- which rides on that flag - * -- does not run this pass. - * - * Only meaningful with a cycle armed. Without one this function makes - * no Rx entry of its own and clears nothing, so the caller's guards see - * the reception unaided. get_status() clears nothing. */ + /* A reception inside the window contaminates the samples: report -EAGAIN. + * Only this bracket can see it; dc_resume() zeroes the latch before the + * caller's own isReceiving() re-check runs. */ if (armed) { lr20xx_system_irq_mask_t irq = 0; @@ -2973,28 +2545,9 @@ bool lr20xx_is_receiving(const struct device *dev) return false; } - /* Sleep-window guard, and the one that matters most: this path is the - * TX gate and runs on every send attempt, far more often than the - * 15 s probe. Asking a sleeping chip "are you receiving?" would end - * the cycle being asked about (DS §6.3.8 — an NSS falling edge - * terminates the loop, and every SPI transaction asserts NSS). A - * sleeping chip is also by definition not mid-packet, so not-receiving - * is the truthful answer, not just the safe one. CAD still gates TX, - * so the channel check is not lost. Same guard the LR11xx applies. - * - * Payload phase, from the latch the DIO1 handler stamps on - * SYNC_WORD_HEADER_VALID. Read before the duty-cycle split because it - * is the one signal both sides share — and the only one the armed side - * has at all. That path cannot bracket itself in suspend/resume the way - * the samplers do (standing the cycle down to ask "am I receiving?" - * would end the very reception being asked about), nor fall back to a - * BUSY read, because BUSY is high through ordinary Rx on this family too - * — a BUSY-high "not receiving" is wrong exactly when it matters, - * mid-packet, with the TX gate asking permission to transmit over it. - * - * That leaves the latch, which is why the header IRQ is routed to DIO1 - * (see the set_dio_irq_cfg call): the handler needs no bus access to keep - * this current, and until it did the armed side had nothing to read. */ + /* The TX gate: never touches a sleeping chip (an NSS edge ends the cycle; + * a sleeping chip is not mid-packet anyway). Answers from the DIO1-stamped + * header latch, never from BUSY (high in sleep and in Rx alike). */ uint32_t hdr_seen = data->header_seen_at_ms; if (hdr_seen != 0 && @@ -3061,14 +2614,9 @@ bool lr20xx_is_receiving(const struct device *dev) return false; } - /* PREAMBLE_DETECTED with SF-aware grace. Within grace, keep reporting - * busy so a real packet has time to land its header; after grace with - * no header, assume noise or a foreign sync word, clear the bit and - * release TX. (Unlike the LR11xx, CMD_ERROR is deliberately not in the - * clear mask: that chip sets it on any ClearIrq whose mask excludes it, - * a quirk with no LR20xx equivalent — this driver already issues - * partial-mask clears elsewhere without provoking one, and folding it in - * here would hide genuine command rejections.) */ + /* PREAMBLE_DETECTED with SF-aware grace: busy until a header lands or the + * grace expires, then clear it and release TX. No CMD_ERROR in the mask + * (LR11xx quirk; here it would hide real command rejections). */ if (irq & LR20XX_SYSTEM_IRQ_PREAMBLE_DETECTED) { uint32_t now = k_uptime_get_32(); uint32_t seen = data->preamble_seen_at_ms; @@ -3262,35 +2810,11 @@ void lr20xx_set_rx_boost(const struct device *dev, bool enable) } } -uint32_t lr20xx_get_random(const struct device *dev) -{ - struct lr20xx_data *data = dev->data; - uint32_t random = 0; - - k_mutex_lock(&data->spi_mutex, K_FOREVER); - lr20xx_system_get_random_number( - &data->hal_ctx, - LR20XX_SYSTEM_RANDOM_ENTROPY_SOURCE_PLL | - LR20XX_SYSTEM_RANDOM_ENTROPY_SOURCE_ADC, - &random); - k_mutex_unlock(&data->spi_mutex); - - return random; -} - /* ── Driver API: CAD ────────────────────────────────────────────────── */ -/* Datasheet Table 6-19 in full: rows = CAD symbol count 1..4, columns = - * SF5..SF12. LoRaRadioBase asks for LORA_CAD_SYMB_4, so the bottom row is - * what we normally use — but cad_symb_nb is overridable from - * mc->cad.symbol_num, and a 1-D table indexed only by SF would silently keep - * handing out 4-symbol peaks for a 2-symbol window. - * - * A higher det_peak is a *less* sensitive CAD. The table this replaced was - * RadioLib's 2-symbol set, which is systematically higher than the 4-symbol - * row, so pairing it with a 4-symbol window made LBT more willing to talk over - * faint traffic. Adaptive CAD offsets from this base, so getting the base - * right shifts the whole operating range. */ +/* DS Table 6-19: rows = CAD symbols 1..4, columns = SF5..SF12 (we normally + * use 4). A higher detPeak is a less sensitive CAD; adaptive CAD offsets + * from this base. */ static const uint8_t lr20xx_cad_peak_table[4][8] = { /* SF5 SF6 SF7 SF8 SF9 SF10 SF11 SF12 */ /* 1 */ { 60, 60, 60, 64, 64, 66, 70, 74 }, @@ -3299,15 +2823,8 @@ static const uint8_t lr20xx_cad_peak_table[4][8] = { /* 4 */ { 51, 51, 51, 54, 56, 60, 60, 64 }, }; -/* The detPeak range this driver will actually program. Exported through - * lr20xx_cad_peak_min/max() so the C++ adaptive-CAD controller can narrow its - * offset window to match: where base+offset falls outside this, several offsets - * collapse onto one peak and the staircase reads sampling noise between - * identical configurations as curvature. - * - * 48 sits three steps below the lowest value DS Table 6-19 documents anywhere - * (51, at SF5-7 with 3-4 symbols), so the range already allows more sensitivity - * than the datasheet tabulates; the datasheet gives no absolute bound. */ +/* The detPeak range this driver programs, exported so the controller's + * window matches. 48 is three below the lowest documented cell (51). */ #define LR20XX_CAD_PEAK_MIN 48 #define LR20XX_CAD_PEAK_MAX 90 @@ -3345,20 +2862,9 @@ static int lr20xx_do_cad(struct lr20xx_data *data) uint8_t symb_nb = lr20xx_cad_symb_nb(mc); lr20xx_radio_lora_cad_params_t cad = { .cad_symb_nb = symb_nb, - /* Best-effort ("fast") CAD. DS §6.3.11: "CadDone time is - * shortened in case of early no detection", and the vendor - * header calls 8 the recommended best-effort value. Most LBT - * CADs find nothing, so this shortens the common pre-TX - * blocking path. 0 would mean the exact symbol count. - * - * Side detectors are already off during CAD, so the datasheet's - * multi-SF caveat ("the main SF determines the not-detect - * condition") does not apply. - * - * If CAD behaviour is ever suspect, `get cad`'s per-level line - * is the instrument: a materially LOWER busy rate is a red flag - * rather than a win, since it more likely means CAD stopped - * detecting than that the channel got quieter. */ + /* Fast CAD (pnr_delta 8, the vendor's recommendation): shortens the common + * no-detection case (DS §6.3.11). A materially lower busy rate in + * `get cad` would mean CAD stopped detecting, not a quieter channel. */ .pnr_delta = 8, /* CAD_LBT hands the CAD->TX transition to the * chip. Note the two CAD commands have different exit-mode @@ -3423,14 +2929,9 @@ static int lr20xx_do_cad(struct lr20xx_data *data) return 0; } -/* Blocking CAD. Leaves the chip in STANDBY on every exit — success, busy, - * timeout and error alike — and deliberately does NOT restore RX: the caller - * knows whether it is about to transmit (in which case re-entering RX would be - * undone immediately) or must go back on air. Both current callers honour - * that: lr20xx_lora_send_async()'s LBT branch re-arms on the busy path, and - * LoRaRadioBase::cadMaintenance() calls startReceive() after every probe. A - * third caller that forgets leaves the node deaf with nothing scheduled to - * recover it, so the contract is stated here rather than left to be inferred. */ +/* Blocking CAD. Leaves the chip in STANDBY on every exit and does not + * restore RX: the caller either transmits or must re-arm (both current + * callers do). A caller that forgets leaves the node deaf. */ static int lr20xx_lora_cad(const struct device *dev, k_timeout_t timeout) { struct lr20xx_data *data = dev->data; @@ -3622,16 +3123,8 @@ static int lr20xx_lora_recv_duty_cycle(const struct device *dev, k_mutex_lock(&data->spi_mutex, K_FOREVER); - /* Idempotent fast path, the duty-cycle counterpart of the one in - * recv_async and the same shape as the SX126x's: a cycle already armed - * on exactly these periods has nothing to re-arm, so refresh the - * callback and stay on air instead of tearing a live cycle down and - * paying a full rebuild for it. Timings are computed before the lock so - * they can be compared here. - * - * Any other combination — continuous RX, or a cycle on different periods - * — falls through to the full path below, which re-arms via - * lr20xx_apply_rx_duty_cycle() and therefore still gets the AGC reset. */ + /* A cycle already armed on these periods: refresh the callback and stay on + * air. Anything else takes the full path (and its AGC reset). */ if (data->in_rx_mode && data->rx_duty_cycle_enabled && data->dc_rx_ms == rx_ms && data->dc_sleep_ms == slp_ms) { data->async_rx_cb = cb; @@ -3710,15 +3203,8 @@ static int lr20xx_hw_init(struct lr20xx_data *data, LOG_INF("LR20xx SDK get_version: major=%u minor=%u", ver.major, ver.minor); - /* The frame A / frame B bring-up dumps lived here: two hexdumps of - * GET_VERSION, one per candidate SPI framing, to settle which protocol - * this chip actually speaks. Answered — the two-window read in - * lr20xx_spi_read_frame() is correct, and TX/RX both work on it. - * Removed rather than left firing on every boot. - * - * The only base FW version the datasheet documents is 1.24 (0x01/0x18). - * Anything else still runs — the mismatch is worth a line in the log - * when a bring-up goes sideways, not a hard failure. */ + /* Only base FW 1.24 (0x01/0x18) is documented; anything else is logged, + * not refused. */ if (ver.major != 0x01 || ver.minor != 0x18) { LOG_WRN("Unexpected LR2021 FW %u.%u (datasheet documents 1.24)", ver.major, ver.minor); @@ -3737,13 +3223,8 @@ static int lr20xx_hw_init(struct lr20xx_data *data, * needed when SetRegMode simo_usage=0x02 (SIMO_NORMAL). * We run in LDO mode (default, simo_usage=0x00). */ - /* A board that declares a TCXO but is actually fitted with a plain - * crystal fails to start its 32 MHz reference once TCXO mode is on, and - * every later command is rejected. The datasheet (S1.9.3) puts the TCXO - * on XTA with the VTCXO regulator, so the two builds are indistinguishable - * from software — the chip's own HF_XOSC_START error is the only signal. - * Fall back to XTAL rather than come up dead, as MeshCore's CustomLR2021 - * does when begin() reports a rejected command. */ + /* A board declaring a TCXO that has a crystal fails HF_XOSC_START and every + * later command is rejected; fall back to XTAL (as MeshCore's CustomLR2021). */ if (cfg->tcxo_voltage_mv > 0 && !data->tcxo_disabled) { uint32_t tcxo_ticks = tcxo_start_time_periods(cfg->tcxo_startup_delay_ms); lr20xx_status_t tcxo_rc = lr20xx_system_set_tcxo_mode(ctx, @@ -3780,13 +3261,7 @@ static int lr20xx_hw_init(struct lr20xx_data *data, lr20xx_system_clear_errors(ctx); lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK); - /* Calibrate all analog blocks: 0x6F = LF_RC|HF_RC|PLL|AAF|MU|PA_OFF - * - * No BUSY poll after this. lr20xx_system_calibrate() returns through - * lr20xx_hal_write(), which already ends in the interrupt-driven - * wait_on_busy() (3 s bound, logs on timeout) — a hand-rolled 1 ms - * polling loop only re-implements it at worse granularity and would - * essentially always find BUSY already low. */ + /* Calibrate all analog blocks (0x6F); hal_write already waits on BUSY. */ st = lr20xx_system_calibrate(ctx, 0x6F); DUMP_CHIP_STATE(data, "post-cal"); @@ -3821,18 +3296,9 @@ static int lr20xx_hw_init(struct lr20xx_data *data, DUMP_CHIP_STATE(data, "init-done"); - /* The status-poll probe lived here, asking whether merely polling - * GetStatus raised CmdError — i.e. whether our own polling was - * manufacturing the CMD_ERROR storm. It reported "is NOT" on every - * run, and the real source is now known: the NSS wake pulse that - * check_device_ready() used to fire at a merely-busy chip, which the - * chip read as a clockless malformed command. Question closed. - * - * DEBUG: what voltage does the chip see on its OWN supply pin? (mV, - * after MU calibration). If this reads low (≪3000mV) while the board - * is powered, the LR2021 VBAT/supply pin is floating or miswired — that - * is why it flags LOW_BATTERY and aborts TX no matter the system rail - * or external battery. ~3000-3300mV ⇒ supply is fine, sag is transient. */ + /* Debug: the chip's own supply voltage after MU calibration. Well below + * 3000 mV while powered means a floating or miswired VBAT pin (the + * LOW_BATTERY + aborted-TX signature). */ { uint16_t vbat_mv = 0; lr20xx_status_t vrc = lr20xx_system_get_vbat(ctx, @@ -3925,25 +3391,25 @@ static DEVICE_API(lora, lr20xx_lora_api) = { SPI_TRANSFER_MSB), \ .reset = GPIO_DT_SPEC_INST_GET(n, reset_gpios), \ .busy = GPIO_DT_SPEC_INST_GET(n, busy_gpios), \ - .dio1 = GPIO_DT_SPEC_INST_GET(n, dio1_gpios), \ + .dio1 = GPIO_DT_SPEC_INST_GET(n, irq_gpios), \ .tcxo_voltage_mv = \ - DT_INST_PROP_OR(n, tcxo_voltage_mv, 0), \ + DT_INST_PROP_OR(n, tcxo_voltage, 0), \ .tcxo_startup_delay_ms = \ - DT_INST_PROP_OR(n, tcxo_startup_delay_ms, 5), \ + DT_INST_PROP_OR(n, tcxo_power_startup_delay_ms, 5), \ .rx_boosted = DT_INST_PROP(n, rx_boosted), \ .irq_dio = DT_INST_PROP_OR(n, irq_dio, 9), \ - .rfswitch_enable = DT_INST_PROP_OR(n, rfswitch_enable, 0), \ - .rfswitch_standby = DT_INST_PROP_OR(n, rfswitch_standby, 0),\ + .rfswitch_enable = DT_INST_PROP_OR(n, rfsw_enable, 0), \ + .rfswitch_standby = DT_INST_PROP_OR(n, rfsw_standby, 0),\ /* rfswitch-rx-lf / -hf are optional and carry no binding \ * default, so an absent one falls back to the combined \ * rfswitch-rx — which keeps every pre-split board (e.g. \ * promicro_lr2021) byte-identical. */ \ - .rfswitch_rx_lf = DT_INST_PROP_OR(n, rfswitch_rx_lf, \ - DT_INST_PROP_OR(n, rfswitch_rx, 0)), \ - .rfswitch_rx_hf = DT_INST_PROP_OR(n, rfswitch_rx_hf, \ - DT_INST_PROP_OR(n, rfswitch_rx, 0)), \ - .rfswitch_tx = DT_INST_PROP_OR(n, rfswitch_tx, 0), \ - .rfswitch_tx_hp = DT_INST_PROP_OR(n, rfswitch_tx_hp, 0), \ + .rfswitch_rx_lf = DT_INST_PROP_OR(n, rfsw_rx_lf, \ + DT_INST_PROP_OR(n, rfsw_rx, 0)), \ + .rfswitch_rx_hf = DT_INST_PROP_OR(n, rfsw_rx_hf, \ + DT_INST_PROP_OR(n, rfsw_rx, 0)), \ + .rfswitch_tx = DT_INST_PROP_OR(n, rfsw_tx, 0), \ + .rfswitch_tx_hp = DT_INST_PROP_OR(n, rfsw_tx_hp, 0), \ }; \ static struct lr20xx_data lr20xx_data_##n; \ DEVICE_DT_INST_DEFINE(n, lr20xx_lora_init, NULL, \ diff --git a/zephcore/patches/zephyr-new/dts/bindings/lora/semtech,lr2021.yaml b/zephcore/patches/zephyr-new/dts/bindings/lora/semtech,lr2021.yaml index 20e7b87..f61f0c1 100644 --- a/zephcore/patches/zephyr-new/dts/bindings/lora/semtech,lr2021.yaml +++ b/zephcore/patches/zephyr-new/dts/bindings/lora/semtech,lr2021.yaml @@ -21,12 +21,12 @@ properties: required: true description: GPIO connected to BUSY (high = busy). - dio1-gpios: + irq-gpios: type: phandle-array required: true description: GPIO connected to DIO5/IRQ line (active-high, pull-down). - tcxo-voltage-mv: + tcxo-voltage: type: int default: 0 description: | @@ -36,7 +36,7 @@ properties: 0 = no TCXO (use XTAL). Allowed: 1600, 1700, 1800, 2200, 2400, 2700, 3000, 3300. - tcxo-startup-delay-ms: + tcxo-power-startup-delay-ms: type: int default: 10 description: | @@ -54,9 +54,9 @@ properties: default: 9 description: | Chip-side DIO wired to the MCU interrupt line, 5..11. This is the DIO - number on the LR2021 itself, not the SoC pin — the SoC side is dio1-gpios. + number on the LR2021 itself, not the SoC pin — the SoC side is irq-gpios. - rfswitch-enable: + rfsw-enable: type: int default: 0 description: | @@ -65,52 +65,52 @@ properties: bit 7 is unused. Set a bit to include that DIO in RF switch control. The DIO named by irq-dio is skipped even if its bit is set here. - rfswitch-standby: + rfsw-standby: type: int default: 0 description: | Bitmask of RF switch DIOs that are HIGH in standby mode. - Bit mapping matches rfswitch-enable. + Bit mapping matches rfsw-enable. - rfswitch-rx: + rfsw-rx: type: int default: 0 description: | Bitmask of RF switch DIOs that are HIGH in RX mode, in BOTH bands. - Bit mapping matches rfswitch-enable. This is the convenient form for - the common single-band board; set rfswitch-rx-lf / rfswitch-rx-hf + Bit mapping matches rfsw-enable. This is the convenient form for + the common single-band board; set rfsw-rx-lf / rfsw-rx-hf instead when the two bands need different masks. - rfswitch-rx-lf: + rfsw-rx-lf: type: int description: | Bitmask of RF switch DIOs that are HIGH in sub-GHz RX only. - Defaults to rfswitch-rx when omitted. Bit mapping matches - rfswitch-enable. + Defaults to rfsw-rx when omitted. Bit mapping matches + rfsw-enable. - rfswitch-rx-hf: + rfsw-rx-hf: type: int description: | Bitmask of RF switch DIOs that are HIGH in 2.4 GHz RX only. - Defaults to rfswitch-rx when omitted. Bit mapping matches - rfswitch-enable. + Defaults to rfsw-rx when omitted. Bit mapping matches + rfsw-enable. - Split from rfswitch-rx for front ends whose two bands are mutually + Split from rfsw-rx for front ends whose two bands are mutually exclusive: the Meshnology W12 must hold the GC1109 sub-GHz FEM in shutdown (DIO11 low) during 2.4 GHz RX, which a single shared mask cannot express. Do NOT give these two a `default:` here — the driver - falls back to rfswitch-rx by testing for the property's absence. + falls back to rfsw-rx by testing for the property's absence. - rfswitch-tx: + rfsw-tx: type: int default: 0 description: | Bitmask of RF switch DIOs that are HIGH in sub-GHz TX mode (LF PA). - Bit mapping matches rfswitch-enable. + Bit mapping matches rfsw-enable. - rfswitch-tx-hp: + rfsw-tx-hp: type: int default: 0 description: | Bitmask of RF switch DIOs that are HIGH in 2.4 GHz / high-power TX mode (HF PA). - Bit mapping matches rfswitch-enable. + Bit mapping matches rfsw-enable. diff --git a/zephcore/patches/zephyr-new/include/zephyr/drivers/lora/lr11xx_lora.h b/zephcore/patches/zephyr-new/include/zephyr/drivers/lora/lr11xx_lora.h index 677983d..cdf3b03 100644 --- a/zephcore/patches/zephyr-new/include/zephyr/drivers/lora/lr11xx_lora.h +++ b/zephcore/patches/zephyr-new/include/zephyr/drivers/lora/lr11xx_lora.h @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: MIT * LR11xx Zephyr LoRa driver — extension API used by the ZephCore adapter - * (LR1110Radio). Everything here takes the driver's SPI mutex; calls that + * (radio_ops_lr1110.cpp). Everything here takes the driver's SPI mutex; calls that * touch the chip bracket a running duty cycle (see lr11xx_lora.c). */ diff --git a/zephcore/patches/zephyr-new/include/zephyr/drivers/lora/lr20xx_lora.h b/zephcore/patches/zephyr-new/include/zephyr/drivers/lora/lr20xx_lora.h index 4067d1f..aac046a 100644 --- a/zephcore/patches/zephyr-new/include/zephyr/drivers/lora/lr20xx_lora.h +++ b/zephcore/patches/zephyr-new/include/zephyr/drivers/lora/lr20xx_lora.h @@ -1,10 +1,8 @@ /* * SPDX-License-Identifier: MIT - * LR20xx Zephyr LoRa driver — extension API - * - * Functions extending the standard Zephyr lora_driver_api with - * LR20xx-specific features (RX boost, RSSI readout, preamble detection, - * duty cycle, AGC reset). + * LR20xx Zephyr LoRa driver — extension API used by the ZephCore adapter + * (radio_ops_lr2021.cpp). Everything here takes the driver's SPI mutex; calls + * that touch the chip bracket a running duty cycle (see lr20xx_lora.c). */ #ifndef LR20XX_LORA_H @@ -18,234 +16,76 @@ extern "C" { #endif -/** - * @brief Get instantaneous RSSI (for noise floor calibration) - * - * @param dev LoRa device - * @return RSSI in dBm, or -128 on error - */ +/* Instantaneous RSSI in dBm, or -128 if the read was refused. */ int16_t lr20xx_get_rssi_inst(const struct device *dev); -/* Read n RSSI samples spaced spacing_us apart, bracketing the duty-cycle - * stand-down ONCE for the whole burst instead of once per sample. Returns the - * number of valid samples written; fewer than n means the read was refused - * partway and the caller should discard the burst. A NEGATIVE return (-EAGAIN) - * means discard for a different reason: the receiver detected a preamble or - * header inside the window, so the samples describe that signal rather than the - * noise floor. The two are worth distinguishing -- one indicts the sampler, - * the other reports the channel. */ +/* n RSSI samples spacing_us apart, one duty-cycle stand-down for the burst. + * Returns the samples written (< n: refused partway, discard), or -EAGAIN when + * a preamble/header landed in the window (a busy channel, not a sampler fault). */ int lr20xx_get_rssi_burst(const struct device *dev, int16_t *out, int n, - uint32_t spacing_us); + uint32_t spacing_us); -/** - * @brief Running carrier-frequency-error statistics - * - * Accumulated from every successfully received packet. A single packet's - * offset is the sum of this node's reference error and the transmitter's, so - * only @c mean_hz over many packets from many peers says anything about our own - * XTAL/TCXO; @c min_hz / @c max_hz show the spread that mean is drawn from. - */ +/* Carrier frequency error over received packets. One packet's offset is this + * node's reference error plus the sender's; only the mean over many peers + * says anything about ours. */ struct lr20xx_freq_offset_stats { - int32_t last_hz; /**< Most recent packet's offset */ - int32_t mean_hz; /**< Arithmetic mean over @c count packets */ - int32_t min_hz; /**< Most negative seen */ - int32_t max_hz; /**< Most positive seen */ - uint32_t count; /**< Packets accumulated (0 = nothing measured) */ + int32_t last_hz; /* most recent packet */ + int32_t mean_hz; /* mean over count packets */ + int32_t min_hz; + int32_t max_hz; + uint32_t count; /* 0 = nothing measured */ }; -/** - * @brief Read the carrier frequency error accumulated since boot or reset - * - * Driver v2.0.2 decodes this from three bytes GetLoraPacketStatus grew beyond - * what DS rev 2.1 Table 9-13 documents, so the driver is ahead of the datasheet - * here — sanity-check against a known-good transmitter before trusting it. - * Readings beyond +/-200 kHz are discarded as implausible for a packet that - * demodulated at all, and warn once. - * - * @param dev LoRa device - * @param out Filled in on return; must not be NULL - * @return number of packets accumulated (0 = nothing measured yet) - */ +/* Fills *out; returns the packet count. Decoded from status bytes the + * datasheet does not document (SDK v2.0.2), so values past +/-200 kHz are + * dropped as implausible. */ uint32_t lr20xx_get_freq_offset(const struct device *dev, struct lr20xx_freq_offset_stats *out); - -/** - * @brief Clear the accumulated frequency-error statistics - * - * @param dev LoRa device - */ void lr20xx_reset_freq_offset(const struct device *dev); -/** - * @brief Duty-cycle re-arms caused by a timeout since boot or reset - * - * Counts the false-preamble case: the preamble-restarted rx_max_time + - * cycle_time window expired with no packet, the chip left the duty-cycle loop, - * and the driver put it back. A climbing rate means the RX window is catching - * noise rather than packets, which is the signal for retuning it. Backs - * `get dc.restarts`, which reported a hardcoded 0 on this radio before anything - * counted them. - * - * @param dev LoRa device - * @return re-arm count - */ +/* Duty-cycle false-preamble re-arms since boot or reset (`get dc.restarts`). */ uint32_t lr20xx_get_dc_timeout_restarts(const struct device *dev); - -/** - * @brief Clear the duty-cycle timeout re-arm counter - * - * @param dev LoRa device - */ void lr20xx_reset_dc_timeout_restarts(const struct device *dev); -/** - * @brief Radio deaf time for one duty-cycle wake transition, in microseconds - * - * DS Table 3-23: warm start (Sleep with retention -> STDBY_RC) 1 ms, plus - * STDBY_RC -> Rx 115 us, plus the TCXO restart where one is fitted — duty-cycle - * sleep powers the VTCXO regulator down, so the oscillator restarts on every - * wake. Counts against the adapter's preamble-catch budget. - * - * @param dev LoRa device - * @return deaf time in microseconds - */ +/* Deaf time per duty-cycle wake in us: warm start 1 ms + STDBY_RC->Rx 115 us + * (DS Table 3-23), plus the TCXO restart where one is fitted. */ uint32_t lr20xx_get_wakeup_time_us(const struct device *dev); -/** - * @brief Check if radio is actively receiving a packet - * - * Checks IRQ status for preamble/sync word detection. - * Uses non-blocking mutex — returns false if SPI is busy. - * - * @param dev LoRa device - * @return true if preamble or sync word detected - */ +/* RX-busy gate: the DIO1-stamped header latch (bounded by the payload deadline) + * and, without a duty cycle, the preamble grace. Never waits for the mutex. */ bool lr20xx_is_receiving(const struct device *dev); -/** - * @brief Enable/disable RX boosted mode - * - * @param dev LoRa device - * @param enable true to enable boost - */ +/* RX boosted gain. */ void lr20xx_set_rx_boost(const struct device *dev, bool enable); -/** - * @brief GPIO-only check for "chip is in a duty-cycle sleep window" (no SPI) - * - * True only while an RX duty cycle is armed and BUSY is high — the chip is in - * its own sleep window (DS Table 4-1: "Sleep: busy=1") and an NSS falling edge - * would end the cycle (DS §6.3.8). - * - * Deliberately gated on the duty cycle rather than reading raw BUSY, but NOT - * for the reason once given here: BUSY is *not* held high during continuous - * RX. DS §5.2 is explicit — "In Rx mode, BUSY goes low as soon as the chip is - * ready to receive data." (The BUSY=1 readings the X1 bring-up logs showed - * came from sampling the pin after issuing SPI, i.e. the dump's own footprint; - * dump_chip_state() now samples before.) The gate is still right: without it - * a stray BUSY would gate TX as well as the probes and mute the node. - * - * @param dev LoRa device - * @return true if the host should not issue commands right now - */ -bool lr20xx_is_chip_busy(const struct device *dev); - -/** - * @brief Configure LoRa side detectors (multi-SF receive) - * - * Programs up to 3 extra spreading factors to be demodulated concurrently - * with the configured one, on the same bandwidth. Applies immediately and - * is re-applied on every RX entry (the chip drops the set on each - * SetModulationParams, and LBT CAD has to switch it off — the datasheet's - * SF constraints for CAD are the inverse of those for RX). - * - * Datasheet constraints, enforced here: every side SF must be greater than - * the configured SF, all SFs distinct, highest - lowest <= 4, and at - * BW >= 500 kHz at most 2 side detectors (1 when the main SF is >= 10). - * - * @param dev LoRa device - * @param sfs Array of side spreading factors (5..12) - * @param num Number of side detectors, 0..3 (0 disables the feature) - * @return 0 on success, -EINVAL if the set violates a chip constraint - */ +/* Up to 3 extra SFs demodulated alongside the configured one, same bandwidth + * (num = 0 disables). Datasheet constraints, enforced: every side SF above the + * main SF, all distinct, highest - lowest <= 4, and at BW >= 500 kHz at most 2 + * (1 when the main SF >= 10). Returns 0, or -EINVAL on a violation. */ int lr20xx_configure_side_detectors(const struct device *dev, const uint8_t *sfs, uint8_t num); -/** - * @brief Get a random number from the radio hardware RNG - * - * @param dev LoRa device - * @return Random 32-bit value - */ -uint32_t lr20xx_get_random(const struct device *dev); - -/** - * @brief Redo the frequency-dependent calibrations (temperature drift path). - * - * Warm sleep, Calibrate, rx-boost re-apply, then CalibFE at the operating - * frequency — the front end being what a temperature swing actually - * invalidates. Deliberately not an AGC reset: that fault, and its remedy, - * belong to the SX126x. Defers if the chip is busy (duty-cycle sleep). - * - * Leaves the driver out of RX — the caller must startReceive() afterwards. - * - * @param dev LoRa device - */ +/* Redo the frequency-dependent calibrations after temperature drift. Leaves the + * driver out of RX: the caller must restart it. */ 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 - * - * Signed delta applied to the per-SF base cadDetPeak on every LBT CAD. - * Takes effect on the next CAD — no reconfigure needed. Clamped - * in-driver to the LR20xx scale (48-90). - * - * @param dev LoRa device - * @param offset Signed offset from the base table value - */ +/* Adaptive CAD. The offset is added to the base detPeak (DS Table 6-19) on + * every LBT CAD from the next one on, clamped to [lr20xx_cad_peak_min(), + * lr20xx_cad_peak_max()]. */ void lr20xx_cad_set_peak_offset(const struct device *dev, int8_t offset); - -/** - * @brief Per-SF base cadDetPeak for the currently configured SF - * - * @param dev LoRa device - * @return Base detPeak (56-68 on this family) - */ uint8_t lr20xx_cad_base_peak(const struct device *dev); - -/** - * @brief Absolute detPeak range this driver will program, inclusive - * - * The adaptive-CAD controller needs these to keep its offset window inside the - * range where every level is a distinct configuration. Outside it the clamp - * folds several offsets onto one peak, and the staircase compares rungs that - * are physically identical. - * - * @return lowest / highest detPeak the driver will ever send - */ uint8_t lr20xx_cad_peak_min(void); uint8_t lr20xx_cad_peak_max(void); -/** - * @brief Run one blocking calibration CAD at base detPeak + peak_offset - * - * Uses the operating modem config (SF/BW/symbol count). Leaves the chip - * in STANDBY — the caller must restart RX afterwards. Mesh loop thread - * only. - * - * @param dev LoRa device - * @param peak_offset Signed offset from the base table value - * @return 1 = activity detected, 0 = channel free, <0 = error - */ +/* One blocking calibration CAD at base + peak_offset. Returns 0 = free (chip in + * standby, the caller restarts Rx), 2 = detected (chip in Rx on the signal; the + * caller must not restart it and reads lr20xx_cad_rx_outcome() later), <0 on + * error. Mesh thread only. */ int lr20xx_cad_probe(const struct device *dev, int8_t peak_offset); +/* The Rx bound a detecting probe programs, in ms. */ uint32_t lr20xx_cad_rx_timeout_ms(const struct device *dev); +/* 1 = a packet arrived, 2 = the Rx timed out with nothing decoded, 0 = nothing + * armed or not resolved yet. Consumes the result. */ int lr20xx_cad_rx_outcome(const struct device *dev); #ifdef __cplusplus diff --git a/zephcore/src/RadioIncludes.h b/zephcore/src/RadioIncludes.h index 37fea0a..cf5428e 100644 --- a/zephcore/src/RadioIncludes.h +++ b/zephcore/src/RadioIncludes.h @@ -19,15 +19,7 @@ #include #include -#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) -#include -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021) -#include -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X) -#include -#else -#include -#endif +#include #define ZEPHCORE_LORA 1 diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 10f4d70..245c31b 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -734,23 +734,8 @@ static mesh::ZephyrBoard zephyr_board; /* Radio is constructed with no prefs pointer; main() binds it to * companion_mesh.prefs via setPrefs() before companion_mesh.begin(). */ -#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) -/* LR1110 via Zephyr LoRa driver */ static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board); -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021) -/* LR2021 via Zephyr LoRa driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::LR2021Radio lora_radio(lora_dev, zephyr_board); -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X) -/* SX127x via Zephyr loramac-node driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::SX127xRadio lora_radio(lora_dev, zephyr_board); -#else -/* SX126x via Zephyr LoRa driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board); -#endif +static mesh::LoRaRadio lora_radio(lora_dev, zephyr_board); static uint16_t get_battery_mv(void) { diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index 30aaffc..1f4ae3c 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -443,23 +443,8 @@ static uint16_t get_battery_mv(void) /* Radio is constructed with no prefs pointer; main() binds it to * repeater_mesh._prefs via setPrefs() before repeater_mesh.begin(). */ -#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) -/* LR1110 via Zephyr LoRa driver */ static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board); -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021) -/* LR2021 via Zephyr LoRa driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::LR2021Radio lora_radio(lora_dev, zephyr_board); -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X) -/* SX127x via Zephyr loramac-node driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::SX127xRadio lora_radio(lora_dev, zephyr_board); -#else -/* SX126x via Zephyr LoRa driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board); -#endif +static mesh::LoRaRadio lora_radio(lora_dev, zephyr_board); static mesh::ZephyrMillisecondClock ms_clock; static mesh::ZephyrRNG zephyr_rng; diff --git a/zephcore/src/main_room_server.cpp b/zephcore/src/main_room_server.cpp index 5aea5dc..0489265 100644 --- a/zephcore/src/main_room_server.cpp +++ b/zephcore/src/main_room_server.cpp @@ -374,23 +374,8 @@ static uint16_t get_battery_mv(void) /* Radio is constructed with no prefs pointer; main() binds it to * room_mesh._prefs via setPrefs() before room_mesh.begin(). */ -#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) -/* LR1110 via Zephyr LoRa driver */ static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board); -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021) -/* LR2021 via Zephyr LoRa driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::LR2021Radio lora_radio(lora_dev, zephyr_board); -#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X) -/* SX127x via Zephyr loramac-node driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::SX127xRadio lora_radio(lora_dev, zephyr_board); -#else -/* SX126x via Zephyr LoRa driver */ -static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); -static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board); -#endif +static mesh::LoRaRadio lora_radio(lora_dev, zephyr_board); static mesh::ZephyrMillisecondClock ms_clock; static mesh::ZephyrRNG zephyr_rng; diff --git a/zephcore/tools/crypto_bench/src/main.cpp b/zephcore/tools/crypto_bench/src/main.cpp index 95f424a..aa5fcfc 100644 --- a/zephcore/tools/crypto_bench/src/main.cpp +++ b/zephcore/tools/crypto_bench/src/main.cpp @@ -20,7 +20,7 @@ * crypto bill in the tens of microseconds is noise, and the whole hardware * exploration closes with a documented "no". The final table does that * division for you, per LoRa preset, using the SAME airtime formula the node - * uses (LoRaRadioBase::getAirtimeMillis) so the comparison is apples to + * uses (LoRaRadio::getAirtimeMillis) so the comparison is apples to * apples. * * It measures the SHIPPING code: src/Utils.cpp, src/Identity.cpp and @@ -141,7 +141,7 @@ static void print_header(const char *section) /* --------------------------------------------------------------- airtime */ -/* Integer port of LoRaRadioBase::getAirtimeMillis(), in microseconds. +/* Integer port of LoRaRadio::getAirtimeMillis(), in microseconds. * Deliberately mirrors that function line for line — including the * preamble-length rule and the LDRO threshold tied to symbol time (not to * SF), because a divergence here would silently distort the only comparison