refactor patches and bump to 1.17.2

This commit is contained in:
liquidraver
2026-08-20 17:50:48 +02:00
parent 9ab9a9b950
commit 5c4ac21bd4
28 changed files with 1382 additions and 856 deletions
@@ -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.
+57 -4
View File
@@ -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). |
+1 -1
View File
@@ -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)
+15
View File
@@ -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);
+3
View File
@@ -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;
+17
View File
@@ -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:
+3
View File
@@ -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 */
+98
View File
@@ -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
+31
View File
@@ -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;
+21
View File
@@ -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);
+2
View File
@@ -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;
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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;
+5
View File
@@ -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;
}
+5
View File
@@ -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) {