Three driver fixes for RX duty cycle, all applied to the existing patch

stolen from Zephyr main:

  1. Issue StopTimerOnPreamble=1 before SetRxDutyCycle so the chip's
     timer is not reset on every preamble detect (per §13.1 of the
     datasheet). Without this, duty cycle effectively never sleeps in
     noisy RF and current draw spikes.
  2. On IRQ_RX_TX_TIMEOUT during duty-cycle RX, re-arm via
     sx126x_restart_rx() instead of falling through to set_sleep().
     The old path silently killed duty cycle after the first preamble
     false-positive.
  3. On recv_duty_cycle(NULL) cancel, wake the radio before issuing
     SetStandby — BUSY stays asserted during the sleep phase and the
     standby command was being dropped.

Also adds a dc_timeout_restarts atomic counter incremented on the Fix 2
path, exposed end-to-end: sx126x_ext.h accessors → LoRaRadioBase vtable
→ SX126xRadio override → CommonCLICallbacks → RepeaterMesh. Query via
`get dc.restarts` on the repeater CLI; cleared by `clear stats`. High
values indicate a noisy environment or a too-loose preamble threshold.

(+increase ESP BT stack because future zephyr pin advance will trip that mine)
This commit is contained in:
liquidraver
2026-04-20 13:36:45 +02:00
parent f685f6c095
commit a3244e2dc2
11 changed files with 380 additions and 232 deletions
+1
View File
@@ -194,6 +194,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
| `get loop.detect` | Loop detection level: `off`, `minimal`, `moderate`, or `strict` |
| `get radio.rxgain` | RX gain boost: `0` or `1` |
| `get rxduty` | RX duty cycle mode: `0` or `1` |
| `get dc.restarts` | Duty-cycle preamble false-positive re-arm counter. High values mean the preamble detector is tripping on noise/interference without real packets arriving — inflates RX-on time and drains battery. Reset by `clear stats`. |
| `get adc.multiplier` | Battery voltage ADC calibration multiplier |
| `get bootloader.ver` | Bootloader version string |
| `get public.key` | *(USB only)* Node's public key as hex |
+10
View File
@@ -71,6 +71,16 @@ public:
void setRxBoost(bool enable);
bool isRxBoostEnabled() const { return _rx_boost_enabled; }
/* Duty-cycle preamble false-positive counter.
* Incremented by the driver whenever RX_TX_TIMEOUT fires in
* duty-cycle mode and the chip is silently re-armed. High
* values indicate a noisy RF environment or too-loose preamble
* detection — each event extends real RX time past the nominal
* duty cycle, inflating current draw.
* Default returns 0 on radios that don't support the stat. */
virtual uint32_t getDutyCycleTimeoutRestarts() const { return 0; }
virtual void resetDutyCycleTimeoutRestarts() {}
/* Adaptive Power Control */
void setTxPowerReduction(int8_t reduction_db) override { _tx_power_reduction_db = reduction_db; }
int8_t getTxPowerReduction() const override { return _tx_power_reduction_db; }
+98 -88
View File
@@ -1,88 +1,98 @@
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x hardware hooks for LoRaRadioBase — native Zephyr driver.
*/
#include "SX126xRadio.h"
#include <zephyr/kernel.h>
/* Native SX126x driver extension API */
extern "C" {
#include "sx126x_ext.h"
}
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sx126x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(sx126x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
}
void SX126xRadio::begin()
{
startTxThread(sx126x_tx_wait_stack,
K_THREAD_STACK_SIZEOF(sx126x_tx_wait_stack));
LoRaRadioBase::begin();
#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH)
/* Apply undocumented register 0x8B5 RX improvement (MeshCore PR#1398).
* Must run after lora_config() has been called (via startReceive above). */
sx126x_apply_heltec_reg_patch(_dev);
LOG_INF("Applied Heltec reg 0x8B5 RX patch");
#endif
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
void SX126xRadio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
}
}
void SX126xRadio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t SX126xRadio::hwGetCurrentRSSI()
{
return sx126x_get_rssi_inst(_dev);
}
bool SX126xRadio::hwIsPreambleDetected()
{
return sx126x_is_receiving(_dev);
}
void SX126xRadio::hwSetRxBoost(bool enable)
{
sx126x_set_rx_boost(_dev, enable);
}
void SX126xRadio::hwResetAGC()
{
/* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX settings */
sx126x_reset_agc(_dev);
}
bool SX126xRadio::hwIsChipBusy()
{
return sx126x_is_chip_busy(_dev);
}
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x hardware hooks for LoRaRadioBase — native Zephyr driver.
*/
#include "SX126xRadio.h"
#include <zephyr/kernel.h>
/* Native SX126x driver extension API */
extern "C" {
#include "sx126x_ext.h"
}
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sx126x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(sx126x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
{
}
void SX126xRadio::begin()
{
startTxThread(sx126x_tx_wait_stack,
K_THREAD_STACK_SIZEOF(sx126x_tx_wait_stack));
LoRaRadioBase::begin();
#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH)
/* Apply undocumented register 0x8B5 RX improvement (MeshCore PR#1398).
* Must run after lora_config() has been called (via startReceive above). */
sx126x_apply_heltec_reg_patch(_dev);
LOG_INF("Applied Heltec reg 0x8B5 RX patch");
#endif
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
void SX126xRadio::hwConfigure(const struct lora_modem_config &cfg)
{
int ret = lora_config(_dev, const_cast<struct lora_modem_config *>(&cfg));
if (ret < 0) {
LOG_ERR("lora_config failed: %d", ret);
}
}
void SX126xRadio::hwCancelReceive()
{
lora_recv_async(_dev, NULL, NULL);
}
int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig)
{
return lora_send_async(_dev, buf, len, sig);
}
int16_t SX126xRadio::hwGetCurrentRSSI()
{
return sx126x_get_rssi_inst(_dev);
}
bool SX126xRadio::hwIsPreambleDetected()
{
return sx126x_is_receiving(_dev);
}
void SX126xRadio::hwSetRxBoost(bool enable)
{
sx126x_set_rx_boost(_dev, enable);
}
void SX126xRadio::hwResetAGC()
{
/* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX settings */
sx126x_reset_agc(_dev);
}
bool SX126xRadio::hwIsChipBusy()
{
return sx126x_is_chip_busy(_dev);
}
uint32_t SX126xRadio::getDutyCycleTimeoutRestarts() const
{
return sx126x_get_dc_timeout_restarts(_dev);
}
void SX126xRadio::resetDutyCycleTimeoutRestarts()
{
sx126x_reset_dc_timeout_restarts(_dev);
}
} /* namespace mesh */
+36 -32
View File
@@ -1,32 +1,36 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using native Zephyr driver
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class SX126xRadio : public LoRaRadioBase {
public:
SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
protected:
/* Hardware primitives */
void hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
int16_t hwGetCurrentRSSI() override;
bool hwIsPreambleDetected() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
bool hwIsChipBusy() override;
};
} /* namespace mesh */
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using native Zephyr driver
*/
#pragma once
#include "LoRaRadioBase.h"
namespace mesh {
class SX126xRadio : public LoRaRadioBase {
public:
SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
/* Duty-cycle preamble false-positive stats (SX126x-specific) */
uint32_t getDutyCycleTimeoutRestarts() const override;
void resetDutyCycleTimeoutRestarts() override;
protected:
/* Hardware primitives */
void hwConfigure(const struct lora_modem_config &cfg) override;
void hwCancelReceive() override;
int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) override;
int16_t hwGetCurrentRSSI() override;
bool hwIsPreambleDetected() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
bool hwIsChipBusy() override;
};
} /* namespace mesh */
+9
View File
@@ -1137,10 +1137,19 @@ void RepeaterMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
void RepeaterMesh::clearStats() {
auto& radio_driver = getRadioDriver(_radio);
radio_driver.resetStats();
radio_driver.resetDutyCycleTimeoutRestarts();
resetStats();
((mesh::SimpleMeshTables *)getTables())->resetStats();
}
uint32_t RepeaterMesh::getDutyCycleTimeoutRestarts() const {
return getRadioDriver(_radio).getDutyCycleTimeoutRestarts();
}
void RepeaterMesh::resetDutyCycleTimeoutRestarts() {
getRadioDriver(_radio).resetDutyCycleTimeoutRestarts();
}
void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
if (region_load_active) {
if (StrHelper::isBlank(command)) {
+5
View File
@@ -234,6 +234,11 @@ public:
getContentionTracker().setBackoffMultiplier(m);
}
/* Duty-cycle preamble false-positive stats (SX126x only;
* other radios return 0 from the base class). */
uint32_t getDutyCycleTimeoutRestarts() const override;
void resetDutyCycleTimeoutRestarts() override;
#ifdef CONFIG_ZEPHCORE_APC
/* Adaptive Power Control callbacks */
int8_t getAPCReduction() const override {
+15 -4
View File
@@ -8,12 +8,23 @@
# DLE managed internally by Espressif blob — no CONFIG_BT_CTLR_DATA_LENGTH_MAX
# TX power managed internally by Espressif blob — no CONFIG_BT_CTLR_TX_PWR_*
# Host privacy: required for reliable iOS SMP on Espressif controller (Zephyr #84182-class).
# Android MeshCore may need in-app bond or system pairing when using RPA.
# Host privacy: ESP32-only override. Required for reliable iOS SMP on the
# Espressif controller (Zephyr #84182-class). nRF/MG24 keep privacy off
# because it breaks Android companion "connect from app" (manual pairing
# from system BT settings works, but the app flow silently fails).
CONFIG_BT_PRIVACY=y
# BLE thread stacks — ESP32 software BLE controller needs larger stacks
CONFIG_BT_TX_PROCESSOR_STACK_SIZE=2048
# BLE thread stacks — ESP32 software BLE controller needs larger stacks.
# Zephyr 2026-Q1 added a synchronous sc_store() on the RX WQ in the
# identity-resolved path (commit 2c6c80ddf65) which pushes the LittleFS
# settings write onto the SMP callback chain. Default 2048 overflowed.
CONFIG_BT_TX_PROCESSOR_STACK_SIZE=4096
CONFIG_BT_RX_STACK_SIZE=4096
# Stack-overflow detection — xtensa has no MPU stack guard in Zephyr, so
# use the software sentinel. ARM boards rely on MPU_STACK_GUARD (already
# on via HW_STACK_PROTECTION) which is incompatible with this symbol.
CONFIG_STACK_SENTINEL=y
# BLE TX buffers — the Espressif BLE controller blob bursts more aggressively
# than the nRF softdevice and will deadlock the system workqueue if ACL TX
+3
View File
@@ -507,6 +507,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_boost);
} else if (memcmp(config, "rxduty", 6) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_duty_cycle);
} else if (memcmp(config, "dc.restarts", 11) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u",
(uint32_t)_callbacks->getDutyCycleTimeoutRestarts());
} else {
snprintf(reply, CLI_REPLY_SIZE, "??: %s", config);
}
+4
View File
@@ -52,6 +52,10 @@ public:
virtual float getFloodDelayFactor() const { return 0.5f; }
virtual void setBackoffMultiplier(float m) { (void)m; }
// Duty-cycle preamble false-positive counter (SX126x only)
virtual uint32_t getDutyCycleTimeoutRestarts() const { return 0; }
virtual void resetDutyCycleTimeoutRestarts() {}
// Adaptive Power Control
virtual int8_t getAPCReduction() const { return 0; }
virtual float getAPCMargin() const { return 0.0f; }
@@ -1,95 +1,116 @@
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x native driver extension API
*
* Functions extending the standard Zephyr lora_driver_api with
* SX126x-specific features (duty cycle, RX boost, RSSI readout,
* preamble detection).
*/
#ifndef SX126X_EXT_H
#define SX126X_EXT_H
#include <zephyr/device.h>
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Get instantaneous RSSI (for noise floor calibration)
*
* Reads the current RSSI from the radio while in RX mode.
* Uses non-blocking mutex returns -128 if SPI is busy.
*
* @param dev LoRa device
* @return RSSI in dBm, or -128 on error
*/
int16_t sx126x_get_rssi_inst(const struct device *dev);
/**
* @brief Check if radio is actively receiving a packet
*
* Checks IRQ status for preamble/header detection.
* Uses non-blocking mutex returns false if SPI is busy.
*
* @param dev LoRa device
* @return true if preamble or header detected
*/
bool sx126x_is_receiving(const struct device *dev);
/**
* @brief Enable/disable RX boosted mode
*
* Boosted mode increases LNA gain for +3dB sensitivity at +2mA cost.
*
* @param dev LoRa device
* @param enable true to enable boost
*/
void sx126x_set_rx_boost(const struct device *dev, bool enable);
/**
* @brief Check if the radio chip is busy (cannot accept SPI commands)
*
* Reads the BUSY GPIO pin directly no SPI, no blocking.
* Returns true when the chip is in its duty-cycle sleep phase.
* Safe to call at any time.
*
* @param dev LoRa device
* @return true if BUSY pin is high (chip sleeping / processing)
*/
bool sx126x_is_chip_busy(const struct device *dev);
/**
* @brief Apply undocumented register 0x8B5 RX improvement for Heltec V4
*
* Sets the LSB of register 0x8B5 which consistently improves RX reception
* on boards with GC1109 or KCT8103L PA (Heltec V4/V4.3). Described by
* Heltec engineer @Quency-D in MeshCore PR#1398.
*
* Must be called after the first lora_config() completes.
*
* @param dev LoRa device
*/
void sx126x_apply_heltec_reg_patch(const struct device *dev);
/**
* @brief Reset AGC by performing warm sleep + full recalibration
*
* Warm sleep powers down the analog frontend (resets AGC gain state),
* then Calibrate(0x7F) refreshes all blocks (ADC, PLL, image, oscillators).
* Re-applies DIO2 RF switch, RX boosted gain, and image calibration afterward.
*
* Must be called while NOT actively receiving a packet.
*
* @param dev LoRa device
*/
void sx126x_reset_agc(const struct device *dev);
#ifdef __cplusplus
}
#endif
#endif /* SX126X_EXT_H */
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x native driver extension API
*
* Functions extending the standard Zephyr lora_driver_api with
* SX126x-specific features (duty cycle, RX boost, RSSI readout,
* preamble detection).
*/
#ifndef SX126X_EXT_H
#define SX126X_EXT_H
#include <zephyr/device.h>
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Get instantaneous RSSI (for noise floor calibration)
*
* Reads the current RSSI from the radio while in RX mode.
* Uses non-blocking mutex returns -128 if SPI is busy.
*
* @param dev LoRa device
* @return RSSI in dBm, or -128 on error
*/
int16_t sx126x_get_rssi_inst(const struct device *dev);
/**
* @brief Check if radio is actively receiving a packet
*
* Checks IRQ status for preamble/header detection.
* Uses non-blocking mutex returns false if SPI is busy.
*
* @param dev LoRa device
* @return true if preamble or header detected
*/
bool sx126x_is_receiving(const struct device *dev);
/**
* @brief Enable/disable RX boosted mode
*
* Boosted mode increases LNA gain for +3dB sensitivity at +2mA cost.
*
* @param dev LoRa device
* @param enable true to enable boost
*/
void sx126x_set_rx_boost(const struct device *dev, bool enable);
/**
* @brief Check if the radio chip is busy (cannot accept SPI commands)
*
* Reads the BUSY GPIO pin directly no SPI, no blocking.
* Returns true when the chip is in its duty-cycle sleep phase.
* Safe to call at any time.
*
* @param dev LoRa device
* @return true if BUSY pin is high (chip sleeping / processing)
*/
bool sx126x_is_chip_busy(const struct device *dev);
/**
* @brief Apply undocumented register 0x8B5 RX improvement for Heltec V4
*
* Sets the LSB of register 0x8B5 which consistently improves RX reception
* on boards with GC1109 or KCT8103L PA (Heltec V4/V4.3). Described by
* Heltec engineer @Quency-D in MeshCore PR#1398.
*
* Must be called after the first lora_config() completes.
*
* @param dev LoRa device
*/
void sx126x_apply_heltec_reg_patch(const struct device *dev);
/**
* @brief Reset AGC by performing warm sleep + full recalibration
*
* Warm sleep powers down the analog frontend (resets AGC gain state),
* then Calibrate(0x7F) refreshes all blocks (ADC, PLL, image, oscillators).
* Re-applies DIO2 RF switch, RX boosted gain, and image calibration afterward.
*
* Must be called while NOT actively receiving a packet.
*
* @param dev LoRa device
*/
void sx126x_reset_agc(const struct device *dev);
/**
* @brief Get duty-cycle preamble false-positive counter
*
* Returns the number of times duty-cycle RX tripped IRQ_RX_TX_TIMEOUT
* and was silently re-armed. High values mean the preamble detector is
* firing on noise/neighbour interference without a real packet arriving,
* which inflates RX-on time beyond the nominal duty cycle and shortens
* battery life.
*
* @param dev LoRa device
* @return Cumulative re-arm count since last reset
*/
uint32_t sx126x_get_dc_timeout_restarts(const struct device *dev);
/**
* @brief Reset the duty-cycle preamble false-positive counter to zero.
*
* @param dev LoRa device
*/
void sx126x_reset_dc_timeout_restarts(const struct device *dev);
#ifdef __cplusplus
}
#endif
#endif /* SX126X_EXT_H */
@@ -68,7 +68,7 @@ index 17689720dd2..09982dc2e70 100644
return -EINVAL;
}
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
index 30243ba5dc7..ddc4ce19660 100644
index 30243ba5dc7..4f2254c1f79 100644
--- a/drivers/lora/native/sx126x/sx126x.c
+++ b/drivers/lora/native/sx126x/sx126x.c
@@ -10,10 +10,19 @@
@@ -262,7 +262,7 @@ index 30243ba5dc7..ddc4ce19660 100644
sx126x_hal_set_rf_switch(dev, enable, tx);
}
}
@@ -455,6 +523,58 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev)
@@ -455,6 +523,67 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev)
}
#endif /* CONFIG_PM_DEVICE */
@@ -294,6 +294,15 @@ index 30243ba5dc7..ddc4ce19660 100644
+ sx126x_set_dio2_as_rf_switch(dev, true);
+ }
+
+ /* StopTimerOnPreamble: without it, the duty-cycle RX timer
+ * keeps running even after a preamble is detected — a
+ * preamble arriving at the tail of the RX window can get
+ * cut off when the sleep phase begins. Must be re-issued
+ * every time because Calibrate(ALL) above resets it. */
+ uint8_t stop_on_preamble = 1;
+ sx126x_hal_write_cmd(dev, SX126X_CMD_STOP_TIMER_ON_PREAMBLE,
+ &stop_on_preamble, 1);
+
+ /* Re-apply duty cycle with stored timing */
+ uint8_t dc_buf[6];
+ sys_put_be24(data->dc_rx_time, &dc_buf[0]);
@@ -321,7 +330,7 @@ index 30243ba5dc7..ddc4ce19660 100644
static int sx126x_set_sleep(const struct device *dev)
{
struct sx126x_data *data = dev->data;
@@ -566,19 +686,23 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
@@ -566,19 +695,23 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
/* Handle async callback or signal sync receiver */
if (data->rx_cb != NULL) {
@@ -354,7 +363,28 @@ index 30243ba5dc7..ddc4ce19660 100644
} else {
/* Sync mode */
sx126x_set_sleep(dev);
@@ -637,6 +761,25 @@ static void sx126x_irq_work_handler(struct k_work *work)
@@ -591,6 +724,20 @@ static void sx126x_handle_irq_timeout(const struct device *dev)
struct sx126x_data *data = dev->data;
LOG_DBG("Timeout");
+
+ /* Duty-cycle RX: a preamble false-positive (noise, partial preamble
+ * from another modulation) can fire RxTimeout — but the chip has left
+ * duty cycle and is parked in STDBY_RC. Without this re-arm the
+ * driver silently falls out of duty cycle after the first noisy
+ * window and goes to sleep. Re-entering via sx126x_restart_rx()
+ * preserves the stored rx/sleep timing and re-applies AGC reset +
+ * StopTimerOnPreamble. */
+ if (data->rx_duty_cycle_enabled && data->rx_cb != NULL) {
+ atomic_inc(&data->dc_timeout_restarts);
+ sx126x_restart_rx(dev, data);
+ return;
+ }
+
sx126x_set_sleep(dev);
if (data->tx_async_signal != NULL) {
@@ -637,6 +784,25 @@ static void sx126x_irq_work_handler(struct k_work *work)
sx126x_handle_irq_timeout(dev);
}
@@ -380,7 +410,7 @@ index 30243ba5dc7..ddc4ce19660 100644
/* Re-enable the DIO1 interrupt for the next event (unless sleeping) */
if (atomic_get(&data->state) != SX126X_REST_STATE) {
sx126x_hal_dio1_irq_enable(dev);
@@ -698,6 +841,29 @@ static int sx126x_lora_config(const struct device *dev,
@@ -698,6 +864,29 @@ static int sx126x_lora_config(const struct device *dev,
goto out;
}
@@ -410,7 +440,7 @@ index 30243ba5dc7..ddc4ce19660 100644
/* Set sync word */
ret = sx126x_set_sync_word(dev, config->public_network);
if (ret < 0) {
@@ -721,6 +887,8 @@ out:
@@ -721,6 +910,8 @@ out:
return ret;
}
@@ -419,7 +449,7 @@ index 30243ba5dc7..ddc4ce19660 100644
static int sx126x_lora_send_async(const struct device *dev,
uint8_t *data_buf, uint32_t data_len,
struct k_poll_signal *async)
@@ -752,6 +920,34 @@ static int sx126x_lora_send_async(const struct device *dev,
@@ -752,6 +943,34 @@ static int sx126x_lora_send_async(const struct device *dev,
return ret;
}
@@ -454,7 +484,7 @@ index 30243ba5dc7..ddc4ce19660 100644
data->tx_async_signal = async;
k_msgq_purge(&data->tx_msgq);
@@ -777,6 +973,29 @@ static int sx126x_lora_send_async(const struct device *dev,
@@ -777,6 +996,29 @@ static int sx126x_lora_send_async(const struct device *dev,
/* Enable antenna and set TX path */
sx126x_set_rf_path(dev, true, true);
@@ -484,7 +514,7 @@ index 30243ba5dc7..ddc4ce19660 100644
/* Start transmission with 10 second timeout */
ret = sx126x_set_tx(dev, 10000);
if (ret < 0) {
@@ -947,6 +1166,7 @@ static int sx126x_lora_recv_async(const struct device *dev,
@@ -947,6 +1189,7 @@ static int sx126x_lora_recv_async(const struct device *dev,
data->rx_cb = cb;
data->rx_cb_user_data = user_data;
@@ -492,7 +522,7 @@ index 30243ba5dc7..ddc4ce19660 100644
/* Set packet parameters */
ret = sx126x_set_packet_params(dev,
@@ -1083,14 +1303,423 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
@@ -1083,14 +1326,454 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
return 0;
}
@@ -598,6 +628,20 @@ index 30243ba5dc7..ddc4ce19660 100644
+ k_mutex_unlock(&data->lock);
+}
+
+uint32_t sx126x_get_dc_timeout_restarts(const struct device *dev)
+{
+ struct sx126x_data *data = dev->data;
+
+ return (uint32_t)atomic_get(&data->dc_timeout_restarts);
+}
+
+void sx126x_reset_dc_timeout_restarts(const struct device *dev)
+{
+ struct sx126x_data *data = dev->data;
+
+ atomic_set(&data->dc_timeout_restarts, 0);
+}
+
+void sx126x_apply_heltec_reg_patch(const struct device *dev)
+{
+ struct sx126x_data *data = dev->data;
@@ -780,7 +824,16 @@ index 30243ba5dc7..ddc4ce19660 100644
+ if (cb == NULL) {
+ data->rx_cb = NULL;
+ data->rx_cb_user_data = NULL;
+ data->rx_duty_cycle_enabled = false;
+ if (atomic_cas(&data->state, SX126X_STATE_RX, SX126X_STATE_IDLE)) {
+ /* If we're called during the duty-cycle sleep phase the
+ * chip is actually asleep and BUSY is asserted — issuing
+ * SetStandby straight away stalls the HAL in
+ * wait_busy(). Poke CS via sx126x_hal_wakeup() first so
+ * BUSY drops before we try to talk to the chip. */
+ if (sx126x_hal_is_busy(dev)) {
+ sx126x_hal_wakeup(dev);
+ }
+ sx126x_set_standby(dev, SX126X_STANDBY_RC);
+ sx126x_set_sleep(dev);
+ }
@@ -884,6 +937,14 @@ index 30243ba5dc7..ddc4ce19660 100644
+ data->dc_sleep_time = sleep_time;
+ data->rx_duty_cycle_enabled = true;
+
+ /* StopTimerOnPreamble: keep the RX timer from expiring mid-packet
+ * when a preamble is detected late in the RX window. Without it,
+ * a packet whose preamble lands near the tail of the RX window
+ * gets cut off by the sleep transition. */
+ uint8_t stop_on_preamble = 1;
+ sx126x_hal_write_cmd(dev, SX126X_CMD_STOP_TIMER_ON_PREAMBLE,
+ &stop_on_preamble, 1);
+
+ uint8_t buf[6];
+
+ sys_put_be24(rx_time, &buf[0]);
@@ -923,7 +984,7 @@ index 30243ba5dc7..ddc4ce19660 100644
};
#ifdef CONFIG_PM_DEVICE
@@ -1121,9 +1750,18 @@ static int sx126x_init(const struct device *dev)
@@ -1121,9 +1804,19 @@ static int sx126x_init(const struct device *dev)
k_msgq_init(&data->rx_msgq, (char *)&data->rx_result,
sizeof(struct sx126x_rx_result), 1);
k_work_init(&data->irq_work, sx126x_irq_work_handler);
@@ -933,6 +994,7 @@ index 30243ba5dc7..ddc4ce19660 100644
data->config_valid = false;
+ data->rx_duty_cycle_enabled = false;
+ data->rx_boost_enabled = false;
+ atomic_set(&data->dc_timeout_restarts, 0);
+
+ /* Start dedicated DIO1 work queue */
+ k_work_queue_start(&data->dio1_wq, sx126x_dio1_wq_stack,
@@ -943,10 +1005,10 @@ index 30243ba5dc7..ddc4ce19660 100644
/* Initialize HAL */
ret = sx126x_hal_init(dev);
diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h
index 9dbf3f26586..3aa0c7e588c 100644
index 9dbf3f26586..79f34b67e19 100644
--- a/drivers/lora/native/sx126x/sx126x.h
+++ b/drivers/lora/native/sx126x/sx126x.h
@@ -62,7 +62,21 @@ struct sx126x_data {
@@ -62,7 +62,29 @@ struct sx126x_data {
/* Deferred work for interrupt handling */
struct k_work irq_work;
@@ -959,6 +1021,14 @@ index 9dbf3f26586..3aa0c7e588c 100644
+ 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) */
+
+ /* Duty-cycle preamble false-positive counter: incremented whenever
+ * IRQ_RX_TX_TIMEOUT fires during duty-cycle RX (preamble detector
+ * tripped but no valid sync/header followed — re-arm triggered).
+ * High values indicate a noisy RF environment or preamble-detect
+ * threshold too loose; each event extends the real RX time past
+ * the nominal rx_period, increasing current draw. */
+ atomic_t dc_timeout_restarts;
+
+ /* CAD state */
+ lora_cad_cb cad_cb;
+ void *cad_user_data;