mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 22:38:19 +00:00
refactor patches and bump to 1.17.2
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# ZephCore 1.17.2-zephcore
|
||||
|
||||
A **minor release.** Three small changes, two of which only affect specific hardware. If you are running
|
||||
1.17.1 on anything other than a XIAO nRF52840 or an Ikoka Nano 30 dBm, there is little reason to flash
|
||||
this.
|
||||
|
||||
> [!NOTE]
|
||||
> **Most people can skip this.** The one group that should not is **XIAO nRF52840 and Ikoka Nano 30 dBm**
|
||||
> owners — those boards get twice the charge current and a battery-sense pin that no longer sits at the
|
||||
> nRF52840's absolute maximum. Everyone else: safe to skip and pick these up with the next real release.
|
||||
>
|
||||
> Straight upgrade from 1.17.1 either way — bonds and data survive, no re-pairing.
|
||||
|
||||
---
|
||||
|
||||
## XIAO nRF52840 / Ikoka Nano 30 dBm: 100 mA charging, and a pin fix
|
||||
|
||||
The BQ25100 on the XIAO module selects its charge current from the HICHG pin, and left floating — the
|
||||
reset default — it charges at **50 mA**. That pin is now driven, so these boards charge at **100 mA**,
|
||||
roughly halving the time to fill a pack. Matches Arduino MeshCore's `variants/xiao_nrf52` and
|
||||
`variants/ikoka_nano_nrf`.
|
||||
|
||||
The battery ADC divider enable (P0.14) is now held low permanently instead of toggled per reading.
|
||||
Driving it high puts about **3.6 V on the AIN7 node with a 4.2 V cell** — the nRF52840 GPIO absolute
|
||||
maximum, with no margin, and over it on a warm pack. Holding it low parks the node near 1.4 V and costs
|
||||
about 2.8 µA, which is nothing beside BLE and LoRa receive.
|
||||
|
||||
> [!WARNING]
|
||||
> **If your XIAO sits on a carrier board with its own charger** — a TP4056 module, for example — delete
|
||||
> the `hichg` GPIO hog from the board DTS before flashing. Two chargers must not drive the same pack.
|
||||
|
||||
## Less receiver downtime when the channel is busy — LR2021 only
|
||||
|
||||
On the MeshTracker X1, every transmit that listen-before-talk refused rebuilt the receiver **twice**
|
||||
before retrying, because the driver restored RX and then the layer above it did too. One of those is now
|
||||
a no-op, halving the time the node spends deaf per refused attempt.
|
||||
|
||||
No effect on any other board.
|
||||
|
||||
## A muted node now says so
|
||||
|
||||
If listen-before-talk keeps refusing to transmit, that is reported after four seconds of continuous
|
||||
refusal and flagged in the node's error status. Previously it was logged below debug level with no
|
||||
counter, so a node that could hear the mesh but never got a word in looked completely idle.
|
||||
|
||||
All roles, all boards.
|
||||
|
||||
---
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **The two radio changes are not on-air validated** — both are build-verified and reasoned from the
|
||||
datasheet, not measured on a live mesh.
|
||||
- **The reported X1 difficulty logging in to repeaters is not fixed here** and remains under
|
||||
investigation. The listen-before-talk reporting above will make that failure visible in the log if it
|
||||
is the cause, which is one of several open hypotheses.
|
||||
@@ -20,6 +20,7 @@
|
||||
12. [BLE Protocol Reference](#12-ble-protocol-reference)
|
||||
13. [Data Storage](#13-data-storage)
|
||||
14. [Key Call Flows](#14-key-call-flows)
|
||||
15. [Watchdogs and Recovery Mechanisms](#15-watchdogs-and-recovery-mechanisms)
|
||||
|
||||
---
|
||||
|
||||
@@ -477,6 +478,7 @@ The custom `lr11xx_lora.c` driver handles several LR1110 firmware bugs:
|
||||
- **RX buffer drift**: Buffer base shifts 4 bytes per packet → `clear_rxbuffer()` after every RX
|
||||
- **Header error**: Can shift buffer pointer → standby before RX restart
|
||||
- **DIO1 stuck HIGH**: 5-cycle detection → full hardware reset + recovery
|
||||
- **BUSY-high wedge**: a command racing the autonomous `SetRxDutyCycle` sleep phase can leave the chip BUSY-high with DIO1 low. No IRQ ever fires, so the event-driven driver never re-arms and the node goes permanently deaf. A wedge-recovery watchdog on its own work queue (`lr11xx_wedge`, `K_PRIO_COOP(7)`) checks every 3 s: after 12 s of DIO1 silence it polls the BUSY GPIO continuously for 250 ms, and a dwell with no low edge means a genuine wedge → hardware reset + RX restart. False-positive free by construction — a healthy chip, continuous or duty-cycled, always drops BUSY low within one cycle. Reads the GPIO only (no SPI), so it cannot itself disturb the chip or race the autonomous DC state machine.
|
||||
- **RX duty cycle**: wired via `SetRxDutyCycle` MODE_RX, sized by the shared adapter math (same as SX126x). The earlier "broken, 23-40% loss" verdict was a window-sizing bug (over-sleep + no header budget), not a chip defect — default-off, HW-verify before production use.
|
||||
|
||||
### 5.5 SX127x and LR2021 Paths
|
||||
@@ -926,15 +928,23 @@ Applied automatically at CMake configure time; a failed patch aborts the configu
|
||||
|
||||
| Patch | Risk | Purpose |
|
||||
|-------|------|---------|
|
||||
| 0001-lora-lr11xx-build | LOW | Integrates LR11xx driver into Zephyr LoRa build |
|
||||
| 0002-lora-lr20xx-build | LOW | Integrates LR20xx driver into Zephyr LoRa build |
|
||||
| 0003-lora-sx126x-native | **HIGH** | DIO1 work queue, duty cycle, RX-busy gating, extension API, errata workarounds |
|
||||
| 0001-lora-lr11xx-lr20xx-build | LOW | Registers the LR11xx and LR20xx drivers in the Zephyr LoRa build |
|
||||
| 0003-lora-sx126x-native | **HIGH** | DIO1 work queue, duty cycle, CAD, RX-busy gating, band RSSI/AGC calibration, PA/OCP tuning, extension API |
|
||||
| 0004-lora-sx127x-62k5-bandwidth | LOW | Adds 62.5 kHz bandwidth to the loramac-node backend |
|
||||
| 0005-gnss-air530z-easy | MEDIUM | EASY ephemeris + removes PM (prevents deadlocks) |
|
||||
| 0005-gnss-config-and-version-query | MEDIUM | Air530Z nav-rate config + `$PCAS06` version query; NMEA generic dump |
|
||||
| 0006-blobs-py | LOW | Fix `west blobs fetch` KeyError |
|
||||
| 0007-spi-gpio-native-linux | LOW | Wires native-Linux SPI/GPIO drivers into the Zephyr build |
|
||||
| 0008-flash-sim-per-node-file | LOW | Flash simulator defaults to per-node settings file (native Linux) |
|
||||
| 0009-display-ssd16xx-fill-ram-white | LOW | E-paper full-refresh-to-white anti-ghosting helper |
|
||||
| 0010-uarte-pm-suspend-bounded-rxto-wait | MEDIUM | Bounds the nRF UARTE STOPRX/RXTO spin on PM suspend; unbounded upstream, wedges the mesh thread |
|
||||
|
||||
**One patch per file.** No upstream file is touched by more than one patch, so
|
||||
apply order is irrelevant and no patch can be anchored inside another's added
|
||||
lines. Consolidated 2026-08-20 (15 → 9): `0002` folded into `0001`, and
|
||||
`0011`–`0015` folded into `0003`, each keeping its rationale as a `== section ==`
|
||||
in that patch's preamble. `0002` is a deliberate numbering gap. Add a new
|
||||
sx126x fix by regenerating `0003`, never by stacking an `0016` on it — see
|
||||
`WEST_UPDATE.md`.
|
||||
|
||||
New drivers in `patches/zephyr-new/` (LR11xx, LR20xx, native-Linux SPI/GPIO, DTS bindings) are copied — not patched — into the Zephyr tree at configure time.
|
||||
|
||||
@@ -1173,3 +1183,46 @@ main event loop (every 5s) → Dispatcher::maintenanceLoop()
|
||||
→ EMA: floor += round((sample - floor) / 8)
|
||||
→ clamp [-120, -50] dBm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Watchdogs and Recovery Mechanisms
|
||||
|
||||
**There is no hardware watchdog.** No `CONFIG_WATCHDOG`, no `task_wdt`, no `wdt`
|
||||
node enabled on any board — the `wdt` nodes visible in board `.dts` files are
|
||||
inherited SoC definitions, and the `RTCWDT` references in the TTGO board configs
|
||||
concern the ESP32 ROM bootloader's own watchdog, not something ZephCore arms.
|
||||
The only consumer of the concept is the boot breadcrumb in `main_companion.cpp`,
|
||||
which reads `RESET_WATCHDOG` out of `hwinfo_get_reset_cause()` and reports it in
|
||||
the "Restarted:" v-contact message (see [6.2.1](#621-v-contact-loopback-admin-contact)).
|
||||
|
||||
Everything below is software: bounded stall detection in the layer that owns the
|
||||
state machine. Each entry names what it recovers, because several are
|
||||
deliberately diagnostic-only and recover nothing.
|
||||
|
||||
### 15.1 Named watchdogs
|
||||
|
||||
| Watchdog | Location | Period | Trigger → action |
|
||||
|----------|----------|--------|------------------|
|
||||
| SX126x parked-RX | `patches/zephyr/0003-lora-sx126x-native.patch` (`sx126x_dc_watchdog_handler`) | `2×(preamble+8)` symbols, floor 250 ms | Duty-cycle only. Two consecutive samples showing the chip parked in full RX after a false preamble detect → re-arm the DC cycle. Two-strike so a sighting can never fall inside one real packet's preamble→header gap and abort a live reception. Counted by `get dc.restarts`. |
|
||||
| LR11xx wedge-recovery | `lr11xx_lora.c` (`lr11xx_wedge_watchdog_handler`, own `lr11xx_wedge` queue) | 3 s | DIO1 silent >12 s **and** BUSY continuously high for a 250 ms confirm poll → hardware reset + RX restart. See [5.4](#54-lr1110-driver-errata-workarounds). |
|
||||
| Radio stall | `Dispatcher::maintenanceLoop()` | `RADIO_STALL_THRESHOLD_MS` (8 s) | Radio neither in RX nor mid-TX for the whole window → latch `ERR_EVENT_STARTRX_TIMEOUT`. **Diagnostic only.** The bit is surfaced everywhere: repeater/room-server `stats`, binary telemetry, MQTT uplink, and the companion's BLE device-status response. |
|
||||
| Contact-dump stall | `main_companion.cpp` housekeeping | housekeeping tick | Dump active and the iterator cursor unmoved across a whole tick → re-post `MESH_EVENT_CONTACT_ITER`. The dump is pumped solely by the BLE/USB tx-idle callback, so one lost kick would strand it silently. |
|
||||
| BLE advertising | `main_companion.cpp` housekeeping | housekeeping tick | Enabled, not connected, not advertising → `zephcore_ble_set_enabled(true)`. Covers transient `bt_le_adv_start` failure, which would otherwise leave the node undiscoverable until reboot. |
|
||||
| BLE TX timeout | `ZephyrBLE.cpp` (`BLE_TX_TIMEOUT_MS`) | 2 s | `ble_tx_in_progress` set with no completion callback → clear the flag and proceed to the next TX. Sits 3 s inside the 5 s supervision timeout. |
|
||||
| USB partial-input | `ZephyrCompanionUSB.cpp` (`USB_FRAME_TIMEOUT_MS`) | byte-driven | Mid-frame or mid-text-line too long → reset parser to `USB_RX_IDLE`. **Not a timer** — it only runs when bytes arrive, so it never wakes a sleeping node. |
|
||||
| Buzzer safety | `helpers/ui/buzzer.c` (`BUZZER_TONE_MAX_MS`) | 2 s | Note handler stalls → silence PWM + amp off. The PWM block is autonomous and would otherwise drive the pin forever after a crash or work-queue stall. |
|
||||
|
||||
### 15.2 Unnamed, same job
|
||||
|
||||
Timeouts and deadlines that are watchdogs in everything but name:
|
||||
|
||||
| Mechanism | Location | Bounds |
|
||||
|-----------|----------|--------|
|
||||
| RX-latch payload deadline | all three custom radio paths: SX126x `patch 0013`, `lr11xx_lora.c`, `lr20xx_lora.c` (`header_seen_at_ms` + `*_max_payload_ms()`) | A `HEADER_VALID` whose packet never completes would pin the TX gate closed and silently mute the node — continuous RX has no symbol timer. Released at 255-byte airtime +25% +100 ms. See [5.2.1](#521-rx-busy-gate-tx-during-rx-prevention). |
|
||||
| Stuck-DIO1 counter | `lr11xx_lora.c` and `lr20xx_lora.c` | 5 empty DIO1 cycles → hardware reset. Counting rather than timing; on the LR11xx it complements the wedge watchdog rather than replacing it. |
|
||||
| CAD timeout | `Dispatcher::checkSend()` | 4 s (~20 retry attempts) → `ERR_EVENT_CAD_TIMEOUT` + `recoverRxState()`, rather than falling through to TX. See [5.2.2](#522-cad-timeout-recovery). |
|
||||
| Chip-side TX timeout | SX126x `SetTx` deadline (`patch 0014`, airtime +25% +500 ms, floored at 10 s, clamped 262143 ms); LR2021 `TIMEOUT` IRQ handler | The chip stops the transmission when this fires, so a fixed value is a truncation, not a safeguard — at SF12/BW62.5 the old flat 10 s cut every packet from 76 bytes up. |
|
||||
| Serial partial-frame resync | `SerialCompanionTransport.c` (`FRAME_PARTIAL_TIMEOUT_MS`) | 2 s. Parser-level only — deliberately **not** a session or idle timeout; an idle-but-connected companion sits in `RX_IDLE` indefinitely. |
|
||||
| TCP send timeout | `LinuxTCPTransport.c` | Native sim only. A peer that can't accept a frame in the window is wedged → close it, rather than hang the whole queue. |
|
||||
| Bounded RXTO wait | `patches/zephyr/0010-uarte-pm-suspend-bounded-rxto-wait.patch` | `uarte_pm_suspend()` busy-waits for RXTO with no timeout upstream. Landing in the STOPRX race with bytes in flight spins forever on the main thread and wedges the entire mesh (observed: RAK3401 1W repeater on 1.16.6, CLI answering only `-> busy`). Backstop for the GPS UART PM path in [7.3](#73-gps-adaptersgps). |
|
||||
|
||||
@@ -525,7 +525,7 @@ add_definitions(-DFIRMWARE_BUILD_EPOCH=${ZEPHCORE_BUILD_EPOCH}u)
|
||||
# release is tagged/named, and what the Mesh America catalog uses as its version
|
||||
# key — so the configurator can match a running device against the catalog. Keep
|
||||
# all four identical; the release workflow reads this value directly.
|
||||
set(ZEPHCORE_FIRMWARE_VERSION "1.17.1-zephcore")
|
||||
set(ZEPHCORE_FIRMWARE_VERSION "1.17.2-zephcore")
|
||||
add_definitions(-DFIRMWARE_VERSION="${ZEPHCORE_FIRMWARE_VERSION}")
|
||||
|
||||
add_subdirectory(lib/monocypher)
|
||||
|
||||
@@ -78,6 +78,21 @@ bool LR1110Radio::hwIsReceiving()
|
||||
return lr11xx_is_receiving(_dev);
|
||||
}
|
||||
|
||||
void LR1110Radio::hwResetAgc()
|
||||
{
|
||||
lr11xx_reset_agc(_dev);
|
||||
}
|
||||
|
||||
void LR1110Radio::hwRecalibrate()
|
||||
{
|
||||
lr11xx_recalibrate(_dev);
|
||||
}
|
||||
|
||||
int16_t LR1110Radio::hwGetChipTempC()
|
||||
{
|
||||
return lr11xx_get_chip_temp_c(_dev);
|
||||
}
|
||||
|
||||
void LR1110Radio::hwSetRxBoost(bool enable)
|
||||
{
|
||||
lr11xx_set_rx_boost(_dev, enable);
|
||||
|
||||
@@ -34,6 +34,9 @@ protected:
|
||||
int16_t hwGetCurrentRSSI() override;
|
||||
bool hwIsReceiving() override;
|
||||
void hwSetRxBoost(bool enable) override;
|
||||
void hwResetAgc() override;
|
||||
void hwRecalibrate() override;
|
||||
int16_t hwGetChipTempC() override;
|
||||
uint32_t hwWakeupTimeUs() override;
|
||||
int hwCadProbe(int8_t level) override;
|
||||
void hwCadSetPeakOffset(int8_t offset) override;
|
||||
|
||||
@@ -167,6 +167,23 @@ uint8_t LR2021Radio::hwCadPeakMax()
|
||||
return lr20xx_cad_peak_max();
|
||||
}
|
||||
|
||||
void LR2021Radio::hwResetAgc()
|
||||
{
|
||||
lr20xx_reset_agc(_dev);
|
||||
}
|
||||
|
||||
void LR2021Radio::hwRecalibrate()
|
||||
{
|
||||
/* Front-end calibration included: this is the temperature-drift path, and
|
||||
* image/FE cal is the part a temperature swing actually invalidates. */
|
||||
lr20xx_recalibrate(_dev);
|
||||
}
|
||||
|
||||
int16_t LR2021Radio::hwGetChipTempC()
|
||||
{
|
||||
return lr20xx_get_chip_temp_c(_dev);
|
||||
}
|
||||
|
||||
uint32_t LR2021Radio::hwWakeupTimeUs()
|
||||
{
|
||||
/* Per-device, because the TCXO term dominates and is board-specific:
|
||||
|
||||
@@ -52,6 +52,9 @@ protected:
|
||||
uint8_t hwCadPeakMin() override;
|
||||
uint8_t hwCadPeakMax() override;
|
||||
uint32_t hwWakeupTimeUs() override;
|
||||
void hwResetAgc() override;
|
||||
void hwRecalibrate() override;
|
||||
int16_t hwGetChipTempC() override;
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
|
||||
@@ -60,6 +60,8 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
|
||||
_rx_duty_cycle_enabled(IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)),
|
||||
_rx_boost_enabled(true),
|
||||
_dc_last_rx_us(0), _dc_last_sleep_us(0),
|
||||
_agc_rx_count_shadow(0), _agc_last_activity_ms(0),
|
||||
_agc_last_cal_temp_c(INT16_MIN),
|
||||
_config_cached(false),
|
||||
_has_radio_override(false),
|
||||
_override_freq(0), _override_bw(0),
|
||||
@@ -1125,6 +1127,102 @@ bool LoRaRadioBase::isReceiving()
|
||||
return isChannelActive();
|
||||
}
|
||||
|
||||
/* ── Receiver hygiene ─────────────────────────────────────────────────
|
||||
*
|
||||
* Two independent faults, two independent triggers, neither on the packet path.
|
||||
*
|
||||
* 1. STUCK AGC. Semtech's remedy is a warm sleep plus recalibration; it is not
|
||||
* in the datasheets, so it is not up for removal on datasheet reasoning.
|
||||
* What IS a design choice is when to fire it, and the honest answer is that
|
||||
* a periodic reset gets it backwards: a timer fires most often on a busy
|
||||
* channel, which is exactly where a received packet has just PROVED the AGC
|
||||
* is working, and it fires no more often on a silent one, where a
|
||||
* desensitised receiver is invisible and nothing else will reveal it.
|
||||
*
|
||||
* So trigger on silence instead. If nothing has been heard for
|
||||
* AGC_IDLE_RESET_MS the receiver is either genuinely idle — in which case
|
||||
* the reset costs nothing, there is no traffic to miss — or it is deaf, in
|
||||
* which case this is the only thing that will recover it. Self-targeting in
|
||||
* both directions: a busy node essentially never resets, and the resets a
|
||||
* quiet node does perform are free. Arduino MeshCore's periodic
|
||||
* `agc_reset_interval` addressed the same fault and shipped defaulted to 0
|
||||
* (off), which is a fair summary of how well a plain timer serves it.
|
||||
*
|
||||
* Deliberately NOT reset here: the noise floor. The previous periodic
|
||||
* implementation (removed in fe6e585) zeroed it on every fire, forcing a
|
||||
* fresh seed and a full EMA warmup each time — that, not the AGC work, is
|
||||
* what made it a net loss. Arduino zeroes it too; we do not.
|
||||
*
|
||||
* 2. CALIBRATION DRIFT. Image/front-end and PLL/AAF calibration are valid for
|
||||
* a temperature range, not forever: "Image calibration is necessary if there
|
||||
* is a frequency change > 10MHz, or a temperature change > 10 C", and the
|
||||
* LR2021 additionally advises redoing PLL and AAF beyond +/-20 C. A node
|
||||
* that boots on a hot afternoon and runs into a cold night crosses both.
|
||||
* Nothing does this automatically: the LR2021's temperature-compensation
|
||||
* block (DS 6.12) only corrects crystal drift from TX self-heating, and it
|
||||
* refuses to run at all when a TCXO is fitted.
|
||||
*
|
||||
* AGC_RECAL_TEMP_DELTA_C is deliberately tighter than either datasheet
|
||||
* figure — cheap insurance, and the reading is a junction temperature that
|
||||
* lags ambient. */
|
||||
#define AGC_IDLE_RESET_MS 60000U
|
||||
#define AGC_RECAL_TEMP_DELTA_C 5
|
||||
|
||||
void LoRaRadioBase::agcMaintenance()
|
||||
{
|
||||
/* Never mid-transmit: both operations sleep the chip. */
|
||||
if (atomic_get(&_tx_active)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = (uint32_t)k_uptime_get_32();
|
||||
|
||||
/* Any demodulation activity counts as proof of life, errored frames
|
||||
* included — a CRC failure still means RF reached the demodulator, which
|
||||
* is precisely what a stuck AGC would prevent. Counting only good
|
||||
* packets would fire resets on a node that is merely out of range. */
|
||||
uint32_t rx_total = (uint32_t)atomic_get(&_packets_recv) +
|
||||
(uint32_t)atomic_get(&_packets_recv_errors);
|
||||
|
||||
if (rx_total != _agc_rx_count_shadow || _agc_last_activity_ms == 0) {
|
||||
_agc_rx_count_shadow = rx_total;
|
||||
_agc_last_activity_ms = now ? now : 1;
|
||||
} else if ((now - _agc_last_activity_ms) >= AGC_IDLE_RESET_MS) {
|
||||
LOG_INF("agc: %u ms without RX activity — resetting AGC",
|
||||
(unsigned)(now - _agc_last_activity_ms));
|
||||
hwResetAgc();
|
||||
/* hwResetAgc() leaves the chip out of RX by contract, so this is
|
||||
* a genuine re-entry and re-arms the duty cycle if one is set. */
|
||||
startReceive();
|
||||
_agc_last_activity_ms = now ? now : 1;
|
||||
}
|
||||
|
||||
/* Temperature drift. Sampled every pass because the read is one cheap
|
||||
* command; the expensive recalibration is gated on the delta. */
|
||||
int16_t temp_c = hwGetChipTempC();
|
||||
|
||||
if (temp_c == INT16_MIN) {
|
||||
return; /* backend cannot measure — drift handling disabled */
|
||||
}
|
||||
if (_agc_last_cal_temp_c == INT16_MIN) {
|
||||
_agc_last_cal_temp_c = temp_c; /* first reading is the baseline */
|
||||
return;
|
||||
}
|
||||
|
||||
int delta = (int)temp_c - (int)_agc_last_cal_temp_c;
|
||||
|
||||
if (delta < 0) {
|
||||
delta = -delta;
|
||||
}
|
||||
if (delta >= AGC_RECAL_TEMP_DELTA_C) {
|
||||
LOG_INF("agc: chip temp moved %d C (%d -> %d) — recalibrating",
|
||||
delta, (int)_agc_last_cal_temp_c, (int)temp_c);
|
||||
hwRecalibrate();
|
||||
startReceive();
|
||||
_agc_last_cal_temp_c = temp_c;
|
||||
}
|
||||
}
|
||||
|
||||
void LoRaRadioBase::recoverRxState()
|
||||
{
|
||||
/* Called by the Dispatcher on CAD timeout when isReceiving() has been
|
||||
|
||||
@@ -121,6 +121,9 @@ public:
|
||||
void setCadParams(bool auto_enabled, int8_t offset,
|
||||
uint16_t probe_interval_s, uint8_t busycap_pct) override;
|
||||
void cadMaintenance() override;
|
||||
/** Deaf-aware AGC unstick + temperature-drift recalibration.
|
||||
* Called from Dispatcher::maintenanceLoop(); never on the packet path. */
|
||||
void agcMaintenance() override;
|
||||
uint32_t msUntilNextMaintenance() override;
|
||||
int8_t getCadOffset() const override { return _cad_offset; }
|
||||
|
||||
@@ -175,6 +178,26 @@ protected:
|
||||
virtual uint8_t hwCadPeakMin() { return 0; }
|
||||
virtual uint8_t hwCadPeakMax() { return 0; }
|
||||
|
||||
/* ── Receiver hygiene — see LoRaRadioBase::agcMaintenance() ────── */
|
||||
|
||||
/** Unstick a jammed AGC: warm sleep to drop the analog front end, then
|
||||
* recalibrate on the way back up. Semtech's stated remedy; the
|
||||
* datasheets do not describe it, so do not "simplify" it away on
|
||||
* datasheet grounds alone. Must leave the driver out of RX so the
|
||||
* caller's startReceive() performs a real re-entry rather than hitting
|
||||
* an idempotent fast path. Default: unsupported, no-op. */
|
||||
virtual void hwResetAgc() {}
|
||||
|
||||
/** Redo the frequency-dependent calibrations (image / front end, and
|
||||
* PLL+AAF where the part separates them) at the current operating
|
||||
* frequency. Called on temperature drift, never on the packet path.
|
||||
* Same RX-state contract as hwResetAgc(). Default: unsupported. */
|
||||
virtual void hwRecalibrate() {}
|
||||
|
||||
/** Chip junction temperature in whole degrees C, or INT16_MIN when the
|
||||
* backend cannot measure it (which disables drift recalibration). */
|
||||
virtual int16_t hwGetChipTempC() { return INT16_MIN; }
|
||||
|
||||
/** Radio deaf time per duty-cycle wake transition (context restore +
|
||||
* PLL lock + TCXO startup where fitted), in microseconds. Counts
|
||||
* against the duty-cycle preamble-catch budget: per SX126x DS rev 2.2
|
||||
@@ -298,8 +321,16 @@ protected:
|
||||
* changes once at INF instead of on every RX restart. 0/0 = never
|
||||
* computed; UINT32_MAX rx = continuous-RX fallback active. */
|
||||
uint32_t _dc_last_rx_us;
|
||||
|
||||
uint32_t _dc_last_sleep_us;
|
||||
|
||||
/* agcMaintenance() bookkeeping. RX activity is inferred by sampling the
|
||||
* existing packet counters rather than timestamping in the RX callback,
|
||||
* so nothing is added to the ISR path. */
|
||||
uint32_t _agc_rx_count_shadow;
|
||||
uint32_t _agc_last_activity_ms;
|
||||
int16_t _agc_last_cal_temp_c;
|
||||
|
||||
/* Config cache — skip redundant hwConfigure() */
|
||||
struct lora_modem_config _last_cfg;
|
||||
bool _config_cached;
|
||||
|
||||
@@ -75,6 +75,27 @@ bool SX126xRadio::hwIsReceiving()
|
||||
return sx126x_is_receiving(_dev);
|
||||
}
|
||||
|
||||
void SX126xRadio::hwResetAgc()
|
||||
{
|
||||
/* Leaves the chip in STANDBY and the driver state at IDLE, so the
|
||||
* caller's startReceive() performs a real re-entry. */
|
||||
sx126x_reset_agc(_dev);
|
||||
}
|
||||
|
||||
void SX126xRadio::hwRecalibrate()
|
||||
{
|
||||
/* Calibrate(ALL) on this part already includes image rejection and
|
||||
* sx126x_reset_agc() re-issues CalibrateImage for the operating
|
||||
* frequency, so the drift path needs nothing extra. */
|
||||
sx126x_reset_agc(_dev);
|
||||
}
|
||||
|
||||
/* No hwGetChipTempC(): the SX126x exposes no junction-temperature readout —
|
||||
* no command in the datasheet, and RadioLib has no getTemperature() for it
|
||||
* either. Drift recalibration is therefore inactive on this family, which
|
||||
* costs little: unlike the LR parts, its datasheet gives no temperature
|
||||
* threshold for image calibration in the first place. */
|
||||
|
||||
void SX126xRadio::hwSetRxBoost(bool enable)
|
||||
{
|
||||
sx126x_set_rx_boost(_dev, enable);
|
||||
|
||||
@@ -29,6 +29,8 @@ protected:
|
||||
int16_t hwGetCurrentRSSI() override;
|
||||
bool hwIsReceiving() override;
|
||||
void hwSetRxBoost(bool enable) override;
|
||||
void hwResetAgc() override;
|
||||
void hwRecalibrate() override;
|
||||
bool hwIsChipBusy() override;
|
||||
uint32_t hwWakeupTimeUs() override;
|
||||
int hwCadProbe(int8_t level) override;
|
||||
|
||||
@@ -156,7 +156,7 @@ CONFIG_BT_DIS_FW_REV=y
|
||||
# of truth for the C side, injected as -DFIRMWARE_VERSION). This Kconfig value can't
|
||||
# read a C macro, so it must be bumped here too. Phones that read DIS and phones
|
||||
# that query CMD_DEVICE_QUERY should see the same version.
|
||||
CONFIG_BT_DIS_FW_REV_STR="1.17.1-zephcore"
|
||||
CONFIG_BT_DIS_FW_REV_STR="1.17.2-zephcore"
|
||||
CONFIG_BT_DIS_SW_REV=y
|
||||
CONFIG_BT_DIS_SW_REV_STR="Zephyr"
|
||||
CONFIG_BT_DIS_PNP=n
|
||||
|
||||
@@ -482,7 +482,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
zephcore_buzzer_mode_name(mode));
|
||||
#endif
|
||||
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
|
||||
strcpy(reply, "Removed - use rxduty instead");
|
||||
strcpy(reply, "Removed - Automatic AGC reset is on");
|
||||
} else if (memcmp(config, "multi.acks", 10) == 0) {
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->multi_acks);
|
||||
} else if (memcmp(config, "allow.read.only", 15) == 0) {
|
||||
@@ -742,7 +742,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
* The prefs BYTE is retained (read/written, never acted on) — the
|
||||
* on-disk layout is byte-exact and shifting it would corrupt every
|
||||
* existing node's prefs. */
|
||||
strcpy(reply, "Removed - use rxduty instead");
|
||||
strcpy(reply, "Removed - Automatic AGC reset is on");
|
||||
} else if (memcmp(config, "cad.auto ", 9) == 0) {
|
||||
if (memcmp(&config[9], "on", 2) == 0 || memcmp(&config[9], "off", 3) == 0) {
|
||||
_prefs->cad_auto = (config[9] == 'o' && config[10] == 'n') ? 1 : 0;
|
||||
|
||||
@@ -50,6 +50,11 @@ public:
|
||||
* update stats, maybe step the staircase (auto mode). */
|
||||
virtual void cadMaintenance() {}
|
||||
|
||||
/** Receiver hygiene: deaf-aware AGC unstick and temperature-drift
|
||||
* recalibration. Both sleep the radio, so this is called only from the
|
||||
* maintenance tick, never from a packet path. Default: no-op. */
|
||||
virtual void agcMaintenance() {}
|
||||
|
||||
/* Milliseconds until this radio's periodic work (noise floor sampling,
|
||||
* CAD probing/decay) next needs a call, or MAINTENANCE_IDLE when it has
|
||||
* nothing pending. Radios with no periodic work keep the default. */
|
||||
|
||||
@@ -1427,6 +1427,102 @@ uint32_t lr11xx_get_random(const struct device *dev)
|
||||
return random;
|
||||
}
|
||||
|
||||
/* ── Extension API: receiver hygiene ─────────────────────────────────
|
||||
*
|
||||
* Warm sleep to drop the analog front end, then recalibrate on the way back up
|
||||
* — Semtech's stated remedy for a jammed AGC, and the same sequence as Arduino
|
||||
* MeshCore's lr11x0ResetAGC() (helpers/radiolib/LR11x0Reset.h). Driven from
|
||||
* LoRaRadioBase::agcMaintenance() on RX silence, never on the packet path.
|
||||
*
|
||||
* Unlike the LR2021, calibrate(0x3F) here DOES include image rejection and
|
||||
* reverts it to the 902-928 MHz default, so the image cal must be re-issued —
|
||||
* exactly what the Arduino helper does. That makes recal_fe moot on this part:
|
||||
* both entry points pay it because the calibration forces it.
|
||||
*/
|
||||
static void lr11xx_agc_reset_locked(struct lr11xx_data *data)
|
||||
{
|
||||
void *ctx = &data->hal_ctx;
|
||||
lr11xx_system_sleep_cfg_t sleep_cfg = {
|
||||
.is_warm_start = true,
|
||||
.is_rtc_timeout = false,
|
||||
};
|
||||
|
||||
lr11xx_system_set_sleep(ctx, sleep_cfg, 0);
|
||||
k_sleep(K_USEC(500));
|
||||
data->hal_ctx.radio_is_sleeping = true;
|
||||
|
||||
lr11xx_system_set_standby(ctx, LR11XX_SYSTEM_STANDBY_CFG_RC);
|
||||
lr11xx_system_calibrate(ctx, 0x3F);
|
||||
|
||||
if (data->configured) {
|
||||
uint16_t freq_mhz = data->modem_cfg.frequency / 1000000;
|
||||
|
||||
lr11xx_system_calibrate_image_in_mhz(ctx, freq_mhz - 4,
|
||||
freq_mhz + 4);
|
||||
}
|
||||
|
||||
if (data->rx_boost_enabled) {
|
||||
lr11xx_radio_cfg_rx_boosted(ctx, true);
|
||||
data->rx_boost_applied = true;
|
||||
}
|
||||
|
||||
/* Contract with agcMaintenance(): leave the driver out of RX so the
|
||||
* caller's startReceive() performs a real re-entry. */
|
||||
data->in_rx_mode = false;
|
||||
lr11xx_reset_rx_busy_signals(data);
|
||||
}
|
||||
|
||||
void lr11xx_reset_agc(const struct device *dev)
|
||||
{
|
||||
struct lr11xx_data *data = dev->data;
|
||||
|
||||
if (!data->configured) {
|
||||
return;
|
||||
}
|
||||
k_mutex_lock(&data->spi_mutex, K_FOREVER);
|
||||
lr11xx_agc_reset_locked(data);
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
}
|
||||
|
||||
void lr11xx_recalibrate(const struct device *dev)
|
||||
{
|
||||
/* Same sequence: calibrate(0x3F) already carries the image cal on this
|
||||
* part, so there is nothing extra for the drift path to do. */
|
||||
lr11xx_reset_agc(dev);
|
||||
}
|
||||
|
||||
/* Junction temperature in whole degrees C, INT16_MIN if unavailable.
|
||||
* UM: T = (Temp(10:0)/2047 * Vana - Vbe25) * 1000/VbeSlope + 25, with typicals
|
||||
* Vana 1.35 V, Vbe25 0.7295 V, VbeSlope -1.7 mV/C. Runs once per maintenance
|
||||
* pass, so the float is free. */
|
||||
int16_t lr11xx_get_chip_temp_c(const struct device *dev)
|
||||
{
|
||||
struct lr11xx_data *data = dev->data;
|
||||
uint16_t raw = 0;
|
||||
lr11xx_status_t rc;
|
||||
|
||||
if (!data->configured) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
rc = lr11xx_system_get_temp(&data->hal_ctx, &raw);
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
|
||||
if (rc != LR11XX_STATUS_OK) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
|
||||
float v = ((float)(raw & 0x07FF) / 2047.0f) * 1.35f - 0.7295f;
|
||||
float t = v * (1000.0f / -1.7f) + 25.0f;
|
||||
|
||||
if (t < -128.0f || t > 127.0f) {
|
||||
return INT16_MIN; /* implausible — treat as unavailable */
|
||||
}
|
||||
return (int16_t)t;
|
||||
}
|
||||
|
||||
/* ── Deferred hardware init (runs on first lora_config call) ────────── */
|
||||
|
||||
/* ── Driver API: CAD ────────────────────────────────────────────────── */
|
||||
|
||||
@@ -95,6 +95,32 @@ void lr11xx_reset_dc_timeout_restarts(const struct device *dev);
|
||||
*/
|
||||
uint32_t lr11xx_get_random(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief Unstick a jammed AGC: warm sleep, then recalibrate.
|
||||
*
|
||||
* Leaves the driver out of RX — the caller must startReceive() afterwards.
|
||||
*
|
||||
* @param dev LoRa device
|
||||
*/
|
||||
void lr11xx_reset_agc(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief Redo the frequency-dependent calibrations (temperature drift path).
|
||||
*
|
||||
* On this part calibrate(0x3F) already includes image rejection, so this is
|
||||
* the same sequence as lr11xx_reset_agc().
|
||||
*
|
||||
* @param dev LoRa device
|
||||
*/
|
||||
void lr11xx_recalibrate(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief Junction temperature in whole degrees C, or INT16_MIN if unavailable.
|
||||
*
|
||||
* @param dev LoRa device
|
||||
*/
|
||||
int16_t lr11xx_get_chip_temp_c(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief Set the adaptive-CAD operating detPeak offset
|
||||
*
|
||||
|
||||
@@ -95,6 +95,7 @@ struct lr20xx_data {
|
||||
bool rx_boost_enabled;
|
||||
bool rx_boost_applied;
|
||||
|
||||
|
||||
/* Duty-cycle re-arms triggered by a timeout, i.e. the false-preamble
|
||||
* case: the preamble-restarted rx_max_time + cycle_time window expired
|
||||
* with no packet, so the chip left the loop and the host put it back.
|
||||
@@ -963,6 +964,134 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
* lr20xx_lora_recv_duty_cycle(). Returns true on success, false if no
|
||||
* timing has been provided yet (falls back to continuous RX).
|
||||
*/
|
||||
/* Unstick the AGC, per Semtech's stated remedy: warm sleep to power the analog
|
||||
* front end down, then recalibrate on the way back up. Same shape as Arduino
|
||||
* MeshCore's sx126xResetAGC() / lr11x0ResetAGC() (helpers/radiolib/SX126xReset.h,
|
||||
* LR11x0Reset.h) and
|
||||
* as our own SX126x duty-cycle re-arm. Caller must hold spi_mutex.
|
||||
*
|
||||
* Two LR2021-specific differences from those ports, both saving real time:
|
||||
*
|
||||
* 1. NO CalibFE afterwards. On the SX126x and LR11x0, Calibrate(ALL) includes
|
||||
* image calibration and resets it to the 902-928 MHz default, which is why
|
||||
* both Arduino helpers re-issue calibrateImage()/calibrateImageRejection().
|
||||
* On the LR2021 the front end is a separate command: DS Table 6-28 lists the
|
||||
* Calibrate blocks as LF_RC, HF_RC, PLL, AAF, MU and PA_OFF — no image, no
|
||||
* front end. CalibFE (§6.4.2) owns those and its values survive retention
|
||||
* sleep, so re-running it here would cost ~9 ms to reproduce what is already
|
||||
* on the chip. The old (dead, never-called) lr20xx_reset_agc() ran it
|
||||
* unconditionally, which is most of why it looked expensive.
|
||||
*
|
||||
* 2. No fixed post-Calibrate delay. DS §6.4.1: "Upon completion of the
|
||||
* calibration, the chip automatically enters Standby RC mode" — BUSY
|
||||
* deasserts and the next command's check_device_ready() waits on it, so the
|
||||
* SX126x's 5 ms busy-wait has no counterpart here.
|
||||
*
|
||||
* Budget: 1 ms for the HAL's post-sleep-opcode wait, 1 ms for the warm start
|
||||
* (DS Table 3-23 TSPDRCW, "retrieve calibration from retained registers"), plus
|
||||
* the Calibrate itself, which the datasheet does not put a number on. */
|
||||
static void lr20xx_agc_reset_locked(struct lr20xx_data *data, bool recal_fe)
|
||||
{
|
||||
void *ctx = &data->hal_ctx;
|
||||
lr20xx_system_sleep_cfg_t sleep_cfg = {
|
||||
.is_clk_32k_enabled = false,
|
||||
/* Retention: keeps the modem config and the PRAM image (DS
|
||||
* §22.3). A cold sleep here would drop both. */
|
||||
.is_ram_retention_enabled = true,
|
||||
};
|
||||
|
||||
lr20xx_system_set_sleep_mode(ctx, &sleep_cfg, 0);
|
||||
lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC);
|
||||
|
||||
/* 0x6F = every block in Table 6-28. Legal only outside Rx/Tx, which the
|
||||
* standby above guarantees. */
|
||||
lr20xx_system_calibrate(ctx, 0x6F);
|
||||
|
||||
/* Re-assert the Rx path after calibrating AAF and MU, both of which are
|
||||
* receive-chain blocks. Retention preserves the setting across the sleep
|
||||
* (see the note in lr20xx_apply_rx_duty_cycle), but nothing documents what
|
||||
* Calibrate leaves behind — and both Arduino helpers re-apply boost here
|
||||
* for the same reason. One command. */
|
||||
if (data->rx_boost_enabled) {
|
||||
lr20xx_radio_common_set_rx_path(
|
||||
ctx, LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_7);
|
||||
data->rx_boost_applied = true;
|
||||
}
|
||||
|
||||
/* Front end only on the drift path. Image/FE calibration is valid for a
|
||||
* temperature range rather than forever (DS: "necessary if there is a
|
||||
* frequency change > 10MHz, or a temperature change > 10 C"), so it is
|
||||
* exactly what a temperature swing invalidates — and exactly what a stuck
|
||||
* AGC does not, which is why the AGC path skips its ~9 ms. */
|
||||
if (recal_fe && data->configured) {
|
||||
lr20xx_calibrate_front_end(ctx, data->modem_cfg.frequency);
|
||||
}
|
||||
|
||||
/* Contract with LoRaRadioBase::agcMaintenance(): leave the driver OUT of
|
||||
* RX. The chip is parked in STDBY_RC here, and the caller's
|
||||
* startReceive() must perform a real re-entry — without this the
|
||||
* idempotent fast paths in recv_async / recv_duty_cycle would see
|
||||
* in_rx_mode still set, refresh the callback and return, leaving the
|
||||
* receiver switched off. */
|
||||
data->in_rx_mode = false;
|
||||
lr20xx_reset_rx_busy_signals(data);
|
||||
}
|
||||
|
||||
/* ── Extension API: receiver hygiene ─────────────────────────────────── */
|
||||
|
||||
void lr20xx_reset_agc(const struct device *dev)
|
||||
{
|
||||
struct lr20xx_data *data = dev->data;
|
||||
|
||||
if (!data->configured) {
|
||||
return;
|
||||
}
|
||||
k_mutex_lock(&data->spi_mutex, K_FOREVER);
|
||||
lr20xx_agc_reset_locked(data, false);
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
}
|
||||
|
||||
void lr20xx_recalibrate(const struct device *dev)
|
||||
{
|
||||
struct lr20xx_data *data = dev->data;
|
||||
|
||||
if (!data->configured) {
|
||||
return;
|
||||
}
|
||||
k_mutex_lock(&data->spi_mutex, K_FOREVER);
|
||||
lr20xx_agc_reset_locked(data, true);
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
}
|
||||
|
||||
/* Junction temperature in whole degrees C, INT16_MIN if unavailable.
|
||||
* VALUE_FORMAT_UNIT returns 13.5sb: integer part in the high byte, fraction in
|
||||
* the low. 13-bit resolution costs nothing here — this runs once per
|
||||
* maintenance tick, never on the packet path. */
|
||||
int16_t lr20xx_get_chip_temp_c(const struct device *dev)
|
||||
{
|
||||
struct lr20xx_data *data = dev->data;
|
||||
uint16_t raw = 0;
|
||||
lr20xx_status_t rc;
|
||||
|
||||
if (!data->configured || lr20xx_is_chip_busy(dev)) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
rc = lr20xx_system_get_temp(&data->hal_ctx,
|
||||
LR20XX_SYSTEM_VALUE_FORMAT_UNIT,
|
||||
LR20XX_SYSTEM_MEAS_RES_13_BITS,
|
||||
LR20XX_SYSTEM_TEMP_SRC_VBE, &raw);
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
|
||||
if (rc != LR20XX_STATUS_OK) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
return (int16_t)((int8_t)(raw >> 8));
|
||||
}
|
||||
|
||||
static bool lr20xx_apply_rx_duty_cycle(struct lr20xx_data *data)
|
||||
{
|
||||
void *ctx = &data->hal_ctx;
|
||||
@@ -975,31 +1104,28 @@ static bool lr20xx_apply_rx_duty_cycle(struct lr20xx_data *data)
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Re-apply RX boost before re-arming — same warm-start caution as
|
||||
* lr11xx_restart_rx(), which has always done this on its duty-cycle
|
||||
* path. A duty cycle sleeps the chip between every window (DS §6.3.8),
|
||||
* and DS §6.3.4 (SetAdditionalRegToRetain, "specifies the address of an
|
||||
* additional register to retain in Sleep mode") establishes that not
|
||||
* every register survives that on its own. This driver holds no
|
||||
* retention entries at all, and the only place set_rx_path was ever
|
||||
* issued is apply_modem_config() — which the re-arm path never calls, so
|
||||
* before this the boost was programmed once at RX entry and never again.
|
||||
* If it does not survive the sleep, every window after the first ran
|
||||
* 3 dB down: the same failure the SX126x hit, where the fix was pinning
|
||||
* RX gain into the warm-start retention list.
|
||||
/* Deliberately NO RX-boost re-apply here, unlike lr11xx_restart_rx().
|
||||
* The LR2021 retains it across the duty-cycle sleep and the datasheet is
|
||||
* explicit on every step of that:
|
||||
* §4.4.1 — retention "save[s] all previously programmed chip settings"
|
||||
* §6.3.8 — "In RxDutyCycleMode, the transceiver context is retained
|
||||
* during sleep operations", and the inter-window sleep is
|
||||
* always the with-retention kind
|
||||
* §7.3.4 — the Rx gain tables, whose entries are G1..G11 plus
|
||||
* G12_boost0..7 and G13_boost0..7, are "automatically stored
|
||||
* in the retention memory"
|
||||
* SetRxPath is an ordinary command, so the boost is part of that retained
|
||||
* context. SetAdditionalRegToRetain (§6.3.4) is not a counter-argument:
|
||||
* it takes "the address of an additional peripheral register", for
|
||||
* directly-poked internals outside the command-programmed context — the
|
||||
* datasheet's only worked example is the SX1276 freq-hop compatibility
|
||||
* register 0xF30A24, set by a workaround rather than by a command.
|
||||
*
|
||||
* One SPI command, correct either way. Whether the LR2021 actually needs
|
||||
* it — or wants a retention entry instead — is still an open question;
|
||||
* read the gain back after a window, or A/B it on PDR.
|
||||
*
|
||||
* Not conditioned on rx_boost_applied: that flag tracks what we
|
||||
* programmed, not what survived. */
|
||||
if (data->rx_boost_enabled) {
|
||||
lr20xx_radio_common_set_rx_path(
|
||||
ctx, LR20XX_RADIO_COMMON_RX_PATH_LF,
|
||||
LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_7);
|
||||
data->rx_boost_applied = true;
|
||||
}
|
||||
* This is where the SX126x and the LR2021 genuinely differ: the SX126x
|
||||
* does NOT retain Rx gain (DS §9.6 requires pinning it into the warm-start
|
||||
* retention list, and skipping that costs 3 dB on every wake after the
|
||||
* first). Do not port that fix here by analogy — it is a real gap on that
|
||||
* chip and a no-op write on this one. */
|
||||
|
||||
/* Clear the CMD_PERR the HAL's clockless wake frame provokes: it is
|
||||
* self-inflicted by the wake and says nothing about the command below.
|
||||
|
||||
@@ -168,6 +168,30 @@ int lr20xx_configure_side_detectors(const struct device *dev,
|
||||
*/
|
||||
uint32_t lr20xx_get_random(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief Unstick a jammed AGC: warm sleep, then recalibrate (no front end).
|
||||
*
|
||||
* Leaves the driver out of RX — the caller must startReceive() afterwards.
|
||||
*
|
||||
* @param dev LoRa device
|
||||
*/
|
||||
void lr20xx_reset_agc(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief As lr20xx_reset_agc(), plus a front-end calibration at the operating
|
||||
* frequency. For temperature drift, not for the packet path.
|
||||
*
|
||||
* @param dev LoRa device
|
||||
*/
|
||||
void lr20xx_recalibrate(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief Junction temperature in whole degrees C, or INT16_MIN if unavailable.
|
||||
*
|
||||
* @param dev LoRa device
|
||||
*/
|
||||
int16_t lr20xx_get_chip_temp_c(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief Set the adaptive-CAD operating detPeak offset
|
||||
*
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
diff --git a/drivers/lora/CMakeLists.txt b/drivers/lora/CMakeLists.txt
|
||||
index 9cd035fcf2b..cda2f6dbaf3 100644
|
||||
--- a/drivers/lora/CMakeLists.txt
|
||||
+++ b/drivers/lora/CMakeLists.txt
|
||||
@@ -1,7 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
+# ZephCore patch: adds lr11xx driver subdirectory.
|
||||
|
||||
zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c)
|
||||
|
||||
+add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx)
|
||||
+
|
||||
# zephyr-keep-sorted-start
|
||||
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node)
|
||||
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORA_BASICS_MODEM lora-basics-modem)
|
||||
diff --git a/drivers/lora/Kconfig b/drivers/lora/Kconfig
|
||||
index 657bdb9ce28..0ebee4246f3 100644
|
||||
--- a/drivers/lora/Kconfig
|
||||
+++ b/drivers/lora/Kconfig
|
||||
@@ -5,6 +5,7 @@
|
||||
#
|
||||
|
||||
# Top-level configuration file for LORA drivers.
|
||||
+# ZephCore patch: adds lr11xx driver Kconfig.
|
||||
|
||||
menuconfig LORA
|
||||
bool "LoRa drivers"
|
||||
@@ -60,6 +61,7 @@ config LORA_INIT_PRIORITY
|
||||
|
||||
rsource "Kconfig.sx12xx"
|
||||
rsource "Kconfig.rylrxxx"
|
||||
+rsource "lr11xx/Kconfig"
|
||||
rsource "lora-basics-modem/Kconfig"
|
||||
rsource "native/Kconfig"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
Subject: [PATCH] lora: register the lr11xx and lr20xx drivers
|
||||
|
||||
Hooks the two out-of-tree LoRa drivers into the drivers/lora build --
|
||||
add_subdirectory_ifdef() in CMakeLists.txt, rsource in Kconfig. The drivers
|
||||
themselves have no upstream counterpart and are copied into the tree from
|
||||
patches/zephyr-new/drivers/lora/{lr11xx,lr20xx}/, so there is nothing to patch
|
||||
on that side.
|
||||
|
||||
Sole owner of drivers/lora/CMakeLists.txt and drivers/lora/Kconfig. Was 0001
|
||||
(lr11xx) plus 0002 (lr20xx). 0003 carried both hunks as well, having been
|
||||
regenerated at some point as a diff of the whole drivers/lora tree -- so on
|
||||
every configure 0003 failed git apply --check, the stale-file fallback in
|
||||
zephcore_apply_patches() reset these two files, and 0003 re-supplied the lines
|
||||
0001 and 0002 had just added. Now that each file has exactly one owner, both
|
||||
patches apply first time.
|
||||
|
||||
diff --git a/drivers/lora/CMakeLists.txt b/drivers/lora/CMakeLists.txt
|
||||
index 9cd035fcf2b..18d04ec099a 100644
|
||||
--- a/drivers/lora/CMakeLists.txt
|
||||
+++ b/drivers/lora/CMakeLists.txt
|
||||
@@ -1,7 +1,11 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
+# ZephCore patch: adds lr11xx driver subdirectory.
|
||||
|
||||
zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c)
|
||||
|
||||
+add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx)
|
||||
+add_subdirectory_ifdef(CONFIG_LORA_LR20XX lr20xx)
|
||||
+
|
||||
# zephyr-keep-sorted-start
|
||||
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node)
|
||||
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORA_BASICS_MODEM lora-basics-modem)
|
||||
diff --git a/drivers/lora/Kconfig b/drivers/lora/Kconfig
|
||||
index 657bdb9ce28..f8513b37044 100644
|
||||
--- a/drivers/lora/Kconfig
|
||||
+++ b/drivers/lora/Kconfig
|
||||
@@ -5,6 +5,7 @@
|
||||
#
|
||||
|
||||
# Top-level configuration file for LORA drivers.
|
||||
+# ZephCore patch: adds lr11xx driver Kconfig.
|
||||
|
||||
menuconfig LORA
|
||||
bool "LoRa drivers"
|
||||
@@ -60,6 +61,8 @@ config LORA_INIT_PRIORITY
|
||||
|
||||
rsource "Kconfig.sx12xx"
|
||||
rsource "Kconfig.rylrxxx"
|
||||
+rsource "lr11xx/Kconfig"
|
||||
+rsource "lr20xx/Kconfig"
|
||||
rsource "lora-basics-modem/Kconfig"
|
||||
rsource "native/Kconfig"
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
diff --git a/drivers/lora/CMakeLists.txt b/drivers/lora/CMakeLists.txt
|
||||
index cda2f6dbaf3..18d04ec099a 100644
|
||||
--- a/drivers/lora/CMakeLists.txt
|
||||
+++ b/drivers/lora/CMakeLists.txt
|
||||
@@ -5,5 +5,6 @@ zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c)
|
||||
|
||||
add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx)
|
||||
+add_subdirectory_ifdef(CONFIG_LORA_LR20XX lr20xx)
|
||||
|
||||
# zephyr-keep-sorted-start
|
||||
add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node)
|
||||
diff --git a/drivers/lora/Kconfig b/drivers/lora/Kconfig
|
||||
index 0ebee4246f3..f8513b37044 100644
|
||||
--- a/drivers/lora/Kconfig
|
||||
+++ b/drivers/lora/Kconfig
|
||||
@@ -62,5 +62,6 @@ config LORA_INIT_PRIORITY
|
||||
rsource "Kconfig.sx12xx"
|
||||
rsource "Kconfig.rylrxxx"
|
||||
rsource "lr11xx/Kconfig"
|
||||
+rsource "lr20xx/Kconfig"
|
||||
rsource "lora-basics-modem/Kconfig"
|
||||
rsource "native/Kconfig"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,18 @@
|
||||
Subject: [PATCH] lora: sx127x: 62.5 kHz bandwidth
|
||||
|
||||
Adds BW_62_KHZ to sx12xx_get_bandwidth_idx(), the Zephyr-enum to
|
||||
loramac-node-index mapping. The SX127x hardware supports sub-125 kHz LoRa
|
||||
bandwidths; index 3 is 62.5 kHz (register BW=6). The radio-side half of this --
|
||||
the index-3 register mapping and the LDRO rules that come with a >16 ms symbol
|
||||
time -- lives in the loramac-node module and is added by
|
||||
patches/modules/loramac-node/0001-sx1276-bw62k5.patch.
|
||||
|
||||
Sole owner of drivers/lora/loramac-node/sx12xx_common.c. A byte-identical copy
|
||||
of this hunk was also carried in 0003, which meant this patch failed
|
||||
git apply --check on every configure and only landed because the stale-file
|
||||
fallback reset the file first. That copy is gone; see 0003's "Why this is one
|
||||
patch".
|
||||
|
||||
diff --git a/drivers/lora/loramac-node/sx12xx_common.c b/drivers/lora/loramac-node/sx12xx_common.c
|
||||
index 17689720dd2..09982dc2e70 100644
|
||||
--- a/drivers/lora/loramac-node/sx12xx_common.c
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
Subject: [PATCH] lora: sx126x: band-aware RSSI/AGC calibration (DS Sec 6.1.6)
|
||||
|
||||
The SX126x RSSI and AGC gain-tune registers are band-specific. Semtech ships
|
||||
the chip calibrated for 868-915 MHz on their EVK and publishes a second value
|
||||
set for 470-490 MHz (DS rev 2.2 Table 6-4, section added Dec 2024). We wrote
|
||||
none of them, so sub-GHz builds ran the 868-915 calibration.
|
||||
|
||||
That is not cosmetic. Per DS Sec 6.1.6 an incorrect RSSI produces "an
|
||||
incorrect gain selection in LoRa and GFSK mode", which "can result in a missed
|
||||
detection (packet loss) or decreased resistance to interference".
|
||||
|
||||
Register 0x08AC is shared: bits 7:2 are AgcSensiAdjust, bits 1:0 select the RX
|
||||
gain mode. The driver's 0x94/0x96 constants therefore also wrote the 868-915
|
||||
AgcSensiAdjust (0x94 >> 2 == 0x25) at every RX-gain re-apply, which would have
|
||||
clobbered any calibration installed elsewhere. All four write sites now build
|
||||
the byte from the cached per-band value.
|
||||
|
||||
The low-band column is the datasheet's 470-490 MHz EVK calibration, applied
|
||||
below 600 MHz. It is NOT a 433 MHz calibration -- Semtech publishes no such
|
||||
column -- but it is the nearest published low-band data and beats leaving the
|
||||
868-915 tunes in place. A true 433 figure needs the DS Sec 6.1.6 generator
|
||||
sweep on real hardware.
|
||||
|
||||
Applied from sx126x_config() after SetRfFrequency so a runtime band change
|
||||
re-selects it, and re-applied after each Calibrate(ALL) alongside the existing
|
||||
CalibrateImage re-issue.
|
||||
|
||||
--- a/drivers/lora/native/sx126x/sx126x_regs.h
|
||||
+++ b/drivers/lora/native/sx126x/sx126x_regs.h
|
||||
@@ -175,10 +175,34 @@
|
||||
#define SX126X_LORA_SYNC_WORD_PUBLIC 0x3444
|
||||
#define SX126X_LORA_SYNC_WORD_PRIVATE 0x1424
|
||||
|
||||
-/* RX Gain */
|
||||
+/* RX Gain.
|
||||
+ *
|
||||
+ * 0x08AC is a shared register: bits 7:2 are AgcSensiAdjust (DS Table 6-4,
|
||||
+ * band-dependent RSSI/AGC calibration) and bits 1:0 select the RX gain mode.
|
||||
+ * The legacy 0x94/0x96 constants are the 868-915 MHz band values -- they
|
||||
+ * happen to encode AgcSensiAdjust 0x25 (0x94 >> 2 == 0x25). Writing them
|
||||
+ * verbatim in a 470-490 MHz build silently installs the wrong AGC
|
||||
+ * calibration, so build the byte with SX126X_RX_GAIN_VAL() instead.
|
||||
+ */
|
||||
#define SX126X_REG_RX_GAIN 0x08AC
|
||||
#define SX126X_RX_GAIN_POWER_SAVING 0x94
|
||||
#define SX126X_RX_GAIN_BOOSTED 0x96
|
||||
+#define SX126X_RX_GAIN_BOOST_BITS 0x02
|
||||
+#define SX126X_RX_GAIN_VAL(sensi, boosted) \
|
||||
+ (uint8_t)(((sensi) << 2) | ((boosted) ? SX126X_RX_GAIN_BOOST_BITS : 0U))
|
||||
+
|
||||
+/* RSSI / AGC calibration registers (DS Sec 6.1.6, Table 6-4).
|
||||
+ * The chip ships calibrated for 868-915 MHz on the Semtech EVK; the
|
||||
+ * datasheet publishes a second column for 470-490 MHz. */
|
||||
+#define SX126X_REG_AGC_RSSI_MEAS_CAL_H 0x089C /* bits 4:0 */
|
||||
+#define SX126X_REG_AGC_RSSI_MEAS_CAL_L 0x089D
|
||||
+#define SX126X_REG_AGC_GFORST_POWTHR 0x08B9
|
||||
+#define SX126X_REG_AGC_GAIN_TUNE_1_2 0x08F5 /* .. 0x08FB, 7 bytes */
|
||||
+#define SX126X_AGC_GAIN_TUNE_LEN 7
|
||||
+
|
||||
+/* AgcSensiAdjust (0x08AC bits 7:2) per band */
|
||||
+#define SX126X_AGC_SENSI_ADJUST_HIGH_BAND 0x25 /* 868-915 MHz (default) */
|
||||
+#define SX126X_AGC_SENSI_ADJUST_LOW_BAND 0x22 /* 470-490 MHz */
|
||||
|
||||
/* RX Gain Retention (DS Sec 9.6) -- tells chip to preserve 0x08AC across
|
||||
* mode transitions (sleep/standby/TX -> RX). Without retention, the
|
||||
--- a/drivers/lora/native/sx126x/sx126x.h
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.h
|
||||
@@ -70,6 +70,13 @@
|
||||
bool rx_duty_cycle_enabled;
|
||||
bool rx_boost_enabled;
|
||||
|
||||
+ /* AgcSensiAdjust value for the configured band (DS Table 6-4), cached
|
||||
+ * because it shares register 0x08AC with the RX gain bits and so has
|
||||
+ * to be re-applied by every RX-gain write. Defaults to the 868-915
|
||||
+ * value the chip ships with; sx126x_config() re-selects it per
|
||||
+ * frequency. */
|
||||
+ uint8_t agc_sensi_adjust;
|
||||
+
|
||||
/* RX-busy latch: set by the work handler when HEADER_VALID fires for
|
||||
* a real packet, cleared on RX_DONE / CRC_ERR / RX_TX_TIMEOUT / RX
|
||||
* (re)start / TX-state entry. Read by sx126x_is_receiving() as the
|
||||
--- a/drivers/lora/native/sx126x/sx126x.c
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.c
|
||||
@@ -307,9 +307,117 @@
|
||||
return sx126x_hal_write_regs(dev, SX126X_REG_LORA_SYNC_WORD_MSB, buf, 2);
|
||||
}
|
||||
|
||||
+/* RSSI / AGC band calibration (DS Sec 6.1.6, Table 6-4).
|
||||
+ *
|
||||
+ * Semtech: "The power seen by the SX1261/2 analog front-end is affected by
|
||||
+ * external components such as the matching network and RF switches. An
|
||||
+ * incorrect RSSI results in a sensitivity degradation in (G)FSK mode and an
|
||||
+ * incorrect gain selection in LoRa and GFSK mode. An incorrect gain can
|
||||
+ * result in a missed detection (packet loss) or decreased resistance to
|
||||
+ * interference." So this is an RX-performance register set, not telemetry
|
||||
+ * cosmetics.
|
||||
+ *
|
||||
+ * The chip ships calibrated for 868-915 MHz on the Semtech EVK, which is why
|
||||
+ * the high-band tunes are all zero -- writing them is a no-op against a fresh
|
||||
+ * chip and exists only so that a runtime band change (`set freq`) restores
|
||||
+ * them after a low-band build has installed the other set.
|
||||
+ *
|
||||
+ * IMPORTANT: the low-band column is the datasheet's 470-490 MHz EVK
|
||||
+ * calibration. It is NOT a 433 MHz calibration -- Semtech publishes no such
|
||||
+ * column. Applying it below 600 MHz is "the nearest published values for a
|
||||
+ * low band" and is expected to beat leaving the 868-915 calibration in place,
|
||||
+ * but a true 433 MHz figure needs the DS Sec 6.1.6 generator sweep on real
|
||||
+ * hardware. Do not present these numbers as calibrated for 433.
|
||||
+ */
|
||||
+struct sx126x_agc_band_cal {
|
||||
+ uint8_t rssi_meas_cal_h; /* 0x089C, bits 4:0 */
|
||||
+ uint8_t rssi_meas_cal_l; /* 0x089D */
|
||||
+ uint8_t gforst_powthr; /* 0x08B9 */
|
||||
+ uint8_t sensi_adjust; /* 0x08AC, bits 7:2 */
|
||||
+ uint8_t gain_tune[SX126X_AGC_GAIN_TUNE_LEN]; /* 0x08F5 .. 0x08FB */
|
||||
+};
|
||||
+
|
||||
+static const struct sx126x_agc_band_cal sx126x_agc_cal_high = {
|
||||
+ .rssi_meas_cal_h = 0x01,
|
||||
+ .rssi_meas_cal_l = 0x53,
|
||||
+ .gforst_powthr = 0x0A,
|
||||
+ .sensi_adjust = SX126X_AGC_SENSI_ADJUST_HIGH_BAND,
|
||||
+ .gain_tune = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
|
||||
+};
|
||||
+
|
||||
+static const struct sx126x_agc_band_cal sx126x_agc_cal_low = {
|
||||
+ .rssi_meas_cal_h = 0x01,
|
||||
+ .rssi_meas_cal_l = 0x27,
|
||||
+ .gforst_powthr = 0x04,
|
||||
+ .sensi_adjust = SX126X_AGC_SENSI_ADJUST_LOW_BAND,
|
||||
+ /* GainTune_1_2, _3_4, _5_6, _7_8, _9_10, _11_12, _13 */
|
||||
+ .gain_tune = { 0xDE, 0xE2, 0x32, 0x44, 0x33, 0x34, 0x04 },
|
||||
+};
|
||||
+
|
||||
+/* Band split. The datasheet only characterises 470-490 and 868-915, so any
|
||||
+ * threshold between them is a judgement call; 600 MHz sits clear of both the
|
||||
+ * 433/470 group and the 779/868/915 group that sx126x_calibrate_image()
|
||||
+ * already distinguishes. */
|
||||
+#define SX126X_AGC_LOW_BAND_MAX_HZ 600000000
|
||||
+
|
||||
+static const struct sx126x_agc_band_cal *sx126x_agc_cal_for(uint32_t freq)
|
||||
+{
|
||||
+ return (freq < SX126X_AGC_LOW_BAND_MAX_HZ) ? &sx126x_agc_cal_low
|
||||
+ : &sx126x_agc_cal_high;
|
||||
+}
|
||||
+
|
||||
+/* Apply the band's RSSI/AGC calibration. Caches AgcSensiAdjust for the RX
|
||||
+ * gain writes, which share register 0x08AC, but does NOT touch 0x08AC itself
|
||||
+ * -- sx126x_set_rx_gain() owns that register and runs later in
|
||||
+ * sx126x_config(). */
|
||||
+static int sx126x_apply_agc_band_cal(const struct device *dev, uint32_t freq)
|
||||
+{
|
||||
+ struct sx126x_data *data = dev->data;
|
||||
+ const struct sx126x_agc_band_cal *cal = sx126x_agc_cal_for(freq);
|
||||
+ uint8_t val;
|
||||
+ int ret;
|
||||
+
|
||||
+ data->agc_sensi_adjust = cal->sensi_adjust;
|
||||
+
|
||||
+ /* Bits 4:0 only -- preserve the rest of 0x089C. */
|
||||
+ ret = sx126x_hal_read_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_H, &val, 1);
|
||||
+ if (ret < 0) {
|
||||
+ return ret;
|
||||
+ }
|
||||
+ val = (val & ~0x1FU) | (cal->rssi_meas_cal_h & 0x1FU);
|
||||
+ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_H, &val, 1);
|
||||
+ if (ret < 0) {
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ val = cal->rssi_meas_cal_l;
|
||||
+ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_RSSI_MEAS_CAL_L, &val, 1);
|
||||
+ if (ret < 0) {
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ val = cal->gforst_powthr;
|
||||
+ ret = sx126x_hal_write_regs(dev, SX126X_REG_AGC_GFORST_POWTHR, &val, 1);
|
||||
+ if (ret < 0) {
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ return sx126x_hal_write_regs(dev, SX126X_REG_AGC_GAIN_TUNE_1_2,
|
||||
+ cal->gain_tune,
|
||||
+ SX126X_AGC_GAIN_TUNE_LEN);
|
||||
+}
|
||||
+
|
||||
+/* Byte to write to 0x08AC: band AgcSensiAdjust in bits 7:2, gain mode in 1:0. */
|
||||
+static uint8_t sx126x_rx_gain_byte(const struct device *dev, bool boosted)
|
||||
+{
|
||||
+ struct sx126x_data *data = dev->data;
|
||||
+
|
||||
+ return SX126X_RX_GAIN_VAL(data->agc_sensi_adjust, boosted);
|
||||
+}
|
||||
+
|
||||
static int sx126x_set_rx_gain(const struct device *dev, bool boosted)
|
||||
{
|
||||
- uint8_t val = boosted ? SX126X_RX_GAIN_BOOSTED : SX126X_RX_GAIN_POWER_SAVING;
|
||||
+ uint8_t val = sx126x_rx_gain_byte(dev, boosted);
|
||||
|
||||
/* Host-driven RX paths do not rely on retention: the chip resets
|
||||
* 0x08AC to power-saving (0x94) on every SetRx command, and
|
||||
@@ -661,6 +769,11 @@
|
||||
* EU868 / 433 / 779 MHz operation. */
|
||||
if (data->config_valid) {
|
||||
sx126x_calibrate_image(dev, data->config.frequency);
|
||||
+ /* Same reasoning for the AGC band calibration (DS
|
||||
+ * Sec 6.1.6): cheap insurance against Calibrate(ALL)
|
||||
+ * disturbing the RSSI / gain-tune registers. 0x08AC
|
||||
+ * itself is restored by the RX-gain write below. */
|
||||
+ sx126x_apply_agc_band_cal(dev, data->config.frequency);
|
||||
}
|
||||
|
||||
/* Re-apply DIO2 as RF switch -- Calibrate resets it */
|
||||
@@ -715,8 +828,8 @@
|
||||
* after SetRx) before writing. The gain is therefore applied after the radio
|
||||
* has entered RX but before any preamble can arrive -- giving a clean AGC
|
||||
* state for each new packet regardless of the previous packet's signal level. */
|
||||
- uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED
|
||||
- : SX126X_RX_GAIN_POWER_SAVING;
|
||||
+ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled);
|
||||
+
|
||||
sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1);
|
||||
return 0;
|
||||
}
|
||||
@@ -1156,6 +1269,15 @@
|
||||
goto out;
|
||||
}
|
||||
|
||||
+ /* Install the band's RSSI/AGC calibration (DS Sec 6.1.6) before the
|
||||
+ * RX gain write below, which consumes the AgcSensiAdjust this caches.
|
||||
+ * Re-run on every config so a runtime frequency change switches bands
|
||||
+ * instead of leaving the previous band's tunes installed. */
|
||||
+ ret = sx126x_apply_agc_band_cal(dev, config->frequency);
|
||||
+ if (ret < 0) {
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
/* Configure PA and TX power based on chip variant and frequency */
|
||||
ret = sx126x_hal_configure_tx_params(dev, config->tx_power,
|
||||
config->frequency,
|
||||
@@ -1607,8 +1729,8 @@
|
||||
* SetRx without retention, and lora_config sets gain before going to
|
||||
* sleep, so without this the first packet would be received in
|
||||
* power-saving gain regardless of DTS rx-boosted. */
|
||||
- uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED
|
||||
- : SX126X_RX_GAIN_POWER_SAVING;
|
||||
+ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled);
|
||||
+
|
||||
sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1);
|
||||
|
||||
k_mutex_unlock(&data->lock);
|
||||
@@ -2281,6 +2403,11 @@
|
||||
* (902-928 MHz band) which kills RX sensitivity on
|
||||
* EU868 / 433 / 779 MHz operation. */
|
||||
sx126x_calibrate_image(dev, data->config.frequency);
|
||||
+ /* Same reasoning for the AGC band calibration (DS Sec 6.1.6):
|
||||
+ * cheap insurance against Calibrate(ALL) disturbing the RSSI /
|
||||
+ * gain-tune registers. 0x08AC itself is restored by the
|
||||
+ * RX-gain write on the restart path. */
|
||||
+ sx126x_apply_agc_band_cal(dev, data->config.frequency);
|
||||
|
||||
/* Re-apply DIO2 as RF switch -- Calibrate resets it. */
|
||||
if (hal_cfg->dio2_tx_enable) {
|
||||
@@ -2352,8 +2479,8 @@
|
||||
* chip-internal sleep->RX wakes restore it (DS sec 9.6: mandatory for
|
||||
* SetRxDutyCycle -- without this every window after the first sleep
|
||||
* listens at power-saving gain, a silent -3 dB sensitivity loss). */
|
||||
- uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED
|
||||
- : SX126X_RX_GAIN_POWER_SAVING;
|
||||
+ uint8_t gain = sx126x_rx_gain_byte(dev, data->rx_boost_enabled);
|
||||
+
|
||||
sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1);
|
||||
sx126x_retain_rx_gain(dev);
|
||||
|
||||
@@ -2427,6 +2554,10 @@
|
||||
* pick up the boost setting even when the user never calls the
|
||||
* sx126x_set_rx_boost() extension API. */
|
||||
data->rx_boost_enabled = config->rx_boosted;
|
||||
+ /* Match the calibration the chip ships with, so any 0x08AC write that
|
||||
+ * happens before the first sx126x_config() keeps the factory value
|
||||
+ * rather than zeroing AgcSensiAdjust. */
|
||||
+ data->agc_sensi_adjust = SX126X_AGC_SENSI_ADJUST_HIGH_BAND;
|
||||
atomic_set(&data->dc_timeout_restarts, 0);
|
||||
k_work_init_delayable(&data->dc_watchdog_work,
|
||||
sx126x_dc_watchdog_handler);
|
||||
@@ -1,43 +0,0 @@
|
||||
Mark DIO1 as a system wakeup source.
|
||||
|
||||
Without this, enabling CONFIG_PM on an ESP32 gives a node that light-sleeps
|
||||
correctly and is deaf: the SX1262 asserts DIO1 on a received packet, but the
|
||||
SoC is not armed to wake on that pin, so the frame is dropped and the CPU only
|
||||
resumes on the next RTC timer deadline. The failure is silent and looks like a
|
||||
success on a current meter, which is the worst possible shape for it.
|
||||
|
||||
The Zephyr ESP32 GPIO driver arms the wake in its CONFIGURE path --
|
||||
gpio_esp32_config() reads (flags & GPIO_INT_WAKEUP) at drivers/gpio/gpio_esp32.c
|
||||
and calls esp_sleep_enable_ext1_wakeup_io() -- not in the interrupt path, which
|
||||
strips the bit. So the flag has to go on gpio_pin_configure_dt(), not on the
|
||||
gpio_pin_interrupt_configure_dt() call below it.
|
||||
|
||||
GPIO_INT_WAKEUP is deliberately NOT part of GPIO_INT_MASK (see
|
||||
include/zephyr/drivers/gpio.h), precisely so it can be passed to
|
||||
gpio_pin_configure() without tripping its "Interrupt flags are not supported"
|
||||
assert. Drivers that do not implement wakeup ignore the bit, so this stays a
|
||||
no-op on nRF/STM32/MG24 rather than needing a per-platform guard.
|
||||
|
||||
Board caveat, not fixable here: on ESP32-S3 esp_sleep_is_valid_wakeup_gpio()
|
||||
reduces to RTC_GPIO_IS_VALID_GPIO(), i.e. GPIO 0-21 only. Boards wiring DIO1
|
||||
above that (xiao_esp32s3 GPIO39, station_g2 GPIO48, thinknode_m9 GPIO42) log
|
||||
"Pin N is not wakeup capable" and cannot use light sleep with LoRa RX at all.
|
||||
Heltec V3/V4/V43 and the Wireless Trackers put DIO1 on GPIO14 and are fine.
|
||||
|
||||
--- a/drivers/lora/native/sx126x/sx126x_hal.c
|
||||
+++ b/drivers/lora/native/sx126x/sx126x_hal.c
|
||||
@@ -150,7 +150,13 @@
|
||||
LOG_ERR("DIO1 GPIO not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
- ret = gpio_pin_configure_dt(&config->dio1, GPIO_INPUT);
|
||||
+ /* GPIO_INT_WAKEUP: let a DIO1 assertion wake the SoC from a system
|
||||
+ * low-power state, so an inbound packet is not slept through when
|
||||
+ * CONFIG_PM is enabled. Outside GPIO_INT_MASK by design, so this is
|
||||
+ * legal here and ignored by drivers without wakeup support.
|
||||
+ */
|
||||
+ ret = gpio_pin_configure_dt(&config->dio1,
|
||||
+ GPIO_INPUT | GPIO_INT_WAKEUP);
|
||||
if (ret < 0) {
|
||||
LOG_ERR("Failed to configure DIO1 GPIO: %d", ret);
|
||||
return ret;
|
||||
@@ -1,175 +0,0 @@
|
||||
Bound the lifetime of the RX-busy latch (payload-phase deadline).
|
||||
|
||||
sx126x_is_receiving() reports "busy" for the whole payload phase from the
|
||||
rx_packet_active latch, which patch 0003 sets when HEADER_VALID fires and clears
|
||||
only on a terminal IRQ (RX_DONE / CRC_ERR / RX_TX_TIMEOUT) or an RX (re)start.
|
||||
|
||||
In continuous RX (SX126X_RX_TIMEOUT_CONTINUOUS) there is no symbol timer, so
|
||||
none of those events is guaranteed to arrive. A HEADER_VALID whose packet never
|
||||
completes therefore pins the TX gate true forever: the node keeps receiving but
|
||||
never transmits again until reboot, with nothing in the logs pointing at it. The
|
||||
duty-cycle parked-RX watchdog does not cover this case -- it is DC-only and
|
||||
deliberately treats rx_packet_active as a legitimate in-flight packet.
|
||||
|
||||
Add header_seen_at_ms, stamped when the latch is promoted, and release the latch
|
||||
(clearing the sticky PREAMBLE_DETECTED / SYNC_WORD_VALID / HEADER_VALID /
|
||||
HEADER_ERR bits) once sx126x_max_payload_ms() has elapsed. That bound is the
|
||||
airtime of a 255-byte explicit-header packet at the current SF/BW with CR 4/8
|
||||
and LDRO pinned on, plus 25% and 100 ms -- deliberately generous, since firing
|
||||
early would let TX start on top of a packet that is still arriving.
|
||||
|
||||
Arduino MeshCore added the equivalent deadline in 79ef74ea (SX1262) and
|
||||
0bd871cd (LR11X0); their preamble-phase timeout is already covered here by the
|
||||
SF-aware grace in patch 0003.
|
||||
|
||||
Kept separate from 0003 because that patch cannot be regenerated from the tree:
|
||||
0011 and 0012 also modify this driver and would be folded into it.
|
||||
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
|
||||
index 822d0f5..00faca6 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x.c
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.c
|
||||
@@ -82,14 +82,15 @@ static uint32_t bandwidth_to_hz(enum lora_signal_bandwidth bw)
|
||||
}
|
||||
|
||||
/* Reset all software state that indicates "we are currently receiving":
|
||||
- * the rx_packet_active latch and the preamble-grace timestamp. Paired
|
||||
- * write so the two fields never drift out of sync. Called from every
|
||||
- * RX (re)start site, every terminal-event handler (RX_DONE / CRC_ERR /
|
||||
- * RX_TX_TIMEOUT), and on TX-state entry. */
|
||||
+ * the rx_packet_active latch (and its deadline timestamp) and the
|
||||
+ * preamble-grace timestamp. Paired write so the fields never drift out of
|
||||
+ * sync. Called from every RX (re)start site, every terminal-event handler
|
||||
+ * (RX_DONE / CRC_ERR / RX_TX_TIMEOUT), and on TX-state entry. */
|
||||
static inline void sx126x_reset_rx_busy_signals(struct sx126x_data *data)
|
||||
{
|
||||
data->rx_packet_active = false;
|
||||
atomic_set(&data->preamble_seen_at_ms, 0);
|
||||
+ atomic_set(&data->header_seen_at_ms, 0);
|
||||
}
|
||||
|
||||
/* Grace period for the PREAMBLE_DETECTED -> HEADER_VALID gap, SF/BW-aware.
|
||||
@@ -113,6 +114,53 @@ static uint32_t sx126x_preamble_grace_ms(struct sx126x_data *data)
|
||||
return (uint32_t)((us + 999U) / 1000U);
|
||||
}
|
||||
|
||||
+/* Upper bound on the payload phase: airtime of a maximum-length (255 byte)
|
||||
+ * explicit-header packet at the current SF/BW, worst-case coding rate 4/8,
|
||||
+ * plus margin. Used by sx126x_is_receiving() to bound the lifetime of the
|
||||
+ * rx_packet_active latch.
|
||||
+ *
|
||||
+ * Why the latch needs a deadline at all: it is cleared only by a terminal IRQ
|
||||
+ * (RX_DONE / CRC_ERR / RX_TX_TIMEOUT) or an RX (re)start. In continuous RX
|
||||
+ * (SX126X_RX_TIMEOUT_CONTINUOUS) there is no symbol timer, so none of those is
|
||||
+ * guaranteed to arrive -- a HEADER_VALID that never completes would pin the TX
|
||||
+ * gate true forever and silently mute the node until reboot. (The parked-RX
|
||||
+ * watchdog cannot cover this: it is duty-cycle only and deliberately treats
|
||||
+ * rx_packet_active as a legitimate in-flight packet.) Mirrors the payload
|
||||
+ * deadline Arduino MeshCore added in 79ef74ea / 0bd871cd.
|
||||
+ *
|
||||
+ * Deliberately generous: this is a stuck-state safety net, and clearing the
|
||||
+ * latch early would let TX start on top of a packet that is still arriving.
|
||||
+ * The low-data-rate-optimisation term is pinned at 1 (rather than read from
|
||||
+ * the modem config) because DE=1 yields the larger symbol count of the two,
|
||||
+ * i.e. the safer bound. */
|
||||
+static uint32_t sx126x_max_payload_ms(struct sx126x_data *data)
|
||||
+{
|
||||
+ uint8_t sf = (uint8_t)data->config.datarate;
|
||||
+ uint32_t bw_hz = bandwidth_to_hz(data->config.bandwidth);
|
||||
+
|
||||
+ if (bw_hz == 0 || sf < 5 || sf > 12) {
|
||||
+ return 30000; /* safe default if config is uninitialised */
|
||||
+ }
|
||||
+
|
||||
+ /* Semtech payload-symbol count with PL=255, CRC on, explicit header,
|
||||
+ * CR = 4/8 (coded_bits = 8), DE = 1:
|
||||
+ * n = 8 + ceil((8*PL - 4*SF + 28 + 16) / (4*(SF - 2*DE))) * 8
|
||||
+ * DE=1 so the divisor is 4*(SF-2); at SF5 that is 12, never zero. */
|
||||
+ uint32_t numer = 8U * 255U + 28U + 16U;
|
||||
+ uint32_t denom = 4U * (uint32_t)(sf - 2U);
|
||||
+
|
||||
+ if (numer > 4U * (uint32_t)sf) {
|
||||
+ numer -= 4U * (uint32_t)sf;
|
||||
+ }
|
||||
+ uint32_t n_sym = 8U + ((numer + denom - 1U) / denom) * 8U;
|
||||
+
|
||||
+ /* n_sym * 2^sf * 1000000 / bw_hz -> us, then +25% and +100 ms margin. */
|
||||
+ uint64_t us = ((uint64_t)n_sym << sf) * 1000000ULL / bw_hz;
|
||||
+ uint32_t ms = (uint32_t)((us + 999U) / 1000U);
|
||||
+
|
||||
+ return ms + (ms / 4U) + 100U;
|
||||
+}
|
||||
+
|
||||
static bool should_enable_ldro(enum lora_datarate sf, enum lora_signal_bandwidth bw,
|
||||
const struct sx126x_hal_config *config)
|
||||
{
|
||||
@@ -1225,6 +1273,14 @@ static void sx126x_irq_work_handler(struct k_work *work)
|
||||
* is stale and we don't want a future is_receiving() to treat
|
||||
* it as live when the latch eventually clears. */
|
||||
atomic_set(&data->preamble_seen_at_ms, 0);
|
||||
+ /* Start the payload deadline (see sx126x_max_payload_ms). Use 1
|
||||
+ * as the "set" sentinel if k_uptime is 0 right after boot. */
|
||||
+ {
|
||||
+ uint32_t now = k_uptime_get_32();
|
||||
+
|
||||
+ atomic_set(&data->header_seen_at_ms,
|
||||
+ (atomic_val_t)(now == 0 ? 1U : now));
|
||||
+ }
|
||||
}
|
||||
|
||||
/* Re-enable the DIO1 interrupt for the next event (unless sleeping) */
|
||||
@@ -1886,9 +1942,33 @@ bool sx126x_is_receiving(const struct device *dev)
|
||||
/* Primary source of truth: software latch set by the work handler on
|
||||
* HEADER_VALID, cleared by terminal events (RX_DONE / CRC_ERR /
|
||||
* RX_TX_TIMEOUT) and RX (re)start sites. Covers the entire payload
|
||||
- * phase without an SPI access. */
|
||||
+ * phase without an SPI access.
|
||||
+ *
|
||||
+ * Bounded by a payload deadline: in continuous RX no terminal IRQ is
|
||||
+ * guaranteed, so a HEADER_VALID that never completes would otherwise pin
|
||||
+ * this gate true forever and mute TX until reboot. */
|
||||
if (data->rx_packet_active) {
|
||||
- return true;
|
||||
+ uint32_t seen = (uint32_t)atomic_get(&data->header_seen_at_ms);
|
||||
+ uint32_t now = k_uptime_get_32();
|
||||
+
|
||||
+ if (seen == 0 || (now - seen) < sx126x_max_payload_ms(data)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) {
|
||||
+ /* Chip is busy elsewhere; re-check on the next poll. */
|
||||
+ return true;
|
||||
+ }
|
||||
+ LOG_WRN("RX latch stuck %u ms after HEADER_VALID, releasing TX gate",
|
||||
+ now - seen);
|
||||
+ /* Drop the sticky reception bits so the next poll starts clean. */
|
||||
+ sx126x_clear_irq_status(dev, SX126X_IRQ_PREAMBLE_DETECTED |
|
||||
+ SX126X_IRQ_SYNC_WORD_VALID |
|
||||
+ SX126X_IRQ_HEADER_VALID |
|
||||
+ SX126X_IRQ_HEADER_ERR);
|
||||
+ sx126x_reset_rx_busy_signals(data);
|
||||
+ k_mutex_unlock(&data->lock);
|
||||
+ return false;
|
||||
}
|
||||
|
||||
if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) {
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h
|
||||
index 02bbaac..762f8eb 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x.h
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.h
|
||||
@@ -98,6 +98,14 @@ struct sx126x_data {
|
||||
* terminal-event handler, and on TX-state entry. */
|
||||
atomic_t preamble_seen_at_ms;
|
||||
|
||||
+ /* Timestamp (k_uptime_get_32() units, ms) at which rx_packet_active was
|
||||
+ * promoted by HEADER_VALID. Zero means "no payload phase being timed".
|
||||
+ * Bounds the latch's lifetime: continuous RX has no symbol timer, so a
|
||||
+ * HEADER_VALID whose packet never completes produces no terminal IRQ and
|
||||
+ * would keep the TX gate closed forever. sx126x_is_receiving() releases
|
||||
+ * the latch once sx126x_max_payload_ms() has elapsed. */
|
||||
+ atomic_t header_seen_at_ms;
|
||||
+
|
||||
uint32_t dc_rx_time; /* stored duty cycle rx period (15.625us steps) */
|
||||
uint32_t dc_sleep_time; /* stored duty cycle sleep period (15.625us steps) */
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
Scale the chip-side Tx timeout from airtime instead of a fixed 10 s.
|
||||
|
||||
sx126x_lora_send_async() programmed SetTx with a constant 10000 ms. DS section
|
||||
13.1.5 is explicit that when the Tx timer fires the transmission is stopped, so
|
||||
that constant is not a safeguard on slow presets -- it is a truncation.
|
||||
|
||||
Airtime of a 255-byte packet at CR 4/8 reaches 17.7 s at SF10/BW31.25, 15.9 s at
|
||||
SF11/BW62.5 and 28.6 s at SF12/BW62.5. Against a 10 s ceiling those presets lost
|
||||
every packet from 136 B, 154 B and 76 B up respectively, cut mid-transmission, with
|
||||
nothing in the log connecting the loss to this line: TX_DONE never arrives, the
|
||||
timeout handler re-arms RX, and the host wait is what eventually reports it.
|
||||
|
||||
Scale as airtime + 25% + 500 ms, floored at the previous 10000 so no preset that
|
||||
works today gets a tighter deadline. Clamp to 262143 ms: sx126x_set_tx() converts
|
||||
via SX126X_MS_TO_TIMEOUT (64 steps/ms) and sys_put_be24(), which truncates
|
||||
silently above that. The ceiling is 262 s against a worst reachable airtime of
|
||||
~229 s (SF12/BW7.81 at 255 B), so it never binds on a real packet.
|
||||
|
||||
Kept separate from 0003 because that patch cannot be regenerated from the tree,
|
||||
and the line being changed is upstream context within it rather than an added
|
||||
line -- same reason 0011, 0012 and 0013 are separate.
|
||||
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
|
||||
--- a/drivers/lora/native/sx126x/sx126x.c
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.c
|
||||
@@ -1432,6 +1432,7 @@
|
||||
|
||||
static int sx126x_lora_cad(const struct device *dev, k_timeout_t timeout);
|
||||
static uint32_t sx126x_cad_timeout_ms(struct sx126x_data *data);
|
||||
+static uint32_t sx126x_lora_airtime(const struct device *dev, uint32_t data_len);
|
||||
|
||||
static int sx126x_lora_send_async(const struct device *dev,
|
||||
uint8_t *data_buf, uint32_t data_len,
|
||||
@@ -1614,8 +1615,34 @@
|
||||
}
|
||||
#endif /* CONFIG_LORA_SX126X_NATIVE_STANDALONE */
|
||||
|
||||
- /* Start transmission with 10 second timeout */
|
||||
- ret = sx126x_set_tx(dev, 10000);
|
||||
+ /* Chip-side Tx safeguard, scaled from airtime.
|
||||
+ *
|
||||
+ * DS §13.1.5: when the Tx timer fires the transmission is stopped, so
|
||||
+ * this has to exceed real airtime — and a fixed 10 s cannot. A 255-byte
|
||||
+ * packet at CR 4/8 is 17.7 s at SF10/BW31.25 and 28.6 s at SF12/BW62.5;
|
||||
+ * on those presets the old constant truncated every packet from 136 B
|
||||
+ * and 76 B up respectively, mid-transmission and with nothing in the log
|
||||
+ * tying the loss to this line.
|
||||
+ *
|
||||
+ * Floored at the previous 10000 so no preset that works today gets a
|
||||
+ * tighter deadline, and clamped to the 24-bit field: sx126x_set_tx()
|
||||
+ * converts with SX126X_MS_TO_TIMEOUT (64 steps/ms at 15.625 us) and then
|
||||
+ * sys_put_be24(), which would silently truncate above 262143 ms. That
|
||||
+ * ceiling is 262 s against a worst reachable airtime of ~229 s
|
||||
+ * (SF12/BW7.81, 255 B), so the clamp never binds on a real packet. */
|
||||
+ {
|
||||
+ uint32_t air_ms = sx126x_lora_airtime(dev, data_len);
|
||||
+ uint32_t tmo_ms = air_ms + (air_ms / 4U) + 500U;
|
||||
+
|
||||
+ if (tmo_ms < 10000U) {
|
||||
+ tmo_ms = 10000U;
|
||||
+ }
|
||||
+ if (tmo_ms > 262143U) {
|
||||
+ tmo_ms = 262143U;
|
||||
+ }
|
||||
+ LOG_DBG("SET_TX: airtime=%u ms, timeout=%u ms", air_ms, tmo_ms);
|
||||
+ ret = sx126x_set_tx(dev, tmo_ms);
|
||||
+ }
|
||||
if (ret < 0) {
|
||||
goto out_error;
|
||||
}
|
||||
@@ -181,6 +181,11 @@ void Dispatcher::maintenanceLoop()
|
||||
* we only surface offset changes so the app layer can persist them. */
|
||||
_radio->cadMaintenance();
|
||||
|
||||
/* Receiver hygiene: deaf-aware AGC unstick + temperature-drift
|
||||
* recalibration. Both sleep the chip, so they live here rather than on
|
||||
* any packet path. */
|
||||
_radio->agcMaintenance();
|
||||
|
||||
int8_t cad_off = _radio->getCadOffset();
|
||||
|
||||
if (!cad_offset_shadow_valid) {
|
||||
|
||||
Reference in New Issue
Block a user