rework sx and lr1110, improve BLE sync

This commit is contained in:
liquidraver
2026-02-25 14:19:25 +01:00
parent 3b7434be4c
commit 403914d126
15 changed files with 940 additions and 503 deletions
+10 -17
View File
@@ -14,21 +14,19 @@ cmake_minimum_required(VERSION 3.20.0)
# Directory layout:
# patches/zephyr/*.patch - unified diffs applied to the Zephyr tree
# patches/zephyr-new/ - new files copied to the Zephyr tree (no upstream)
# patches/loramac-node/*.patch - unified diffs for modules/lib/loramac-node
# patches/espressif/*.patch - unified diffs for modules/hal/espressif
#
# Zephyr patches:
# 0001-lora-lr11xx-build - CMakeLists.txt + Kconfig (lr11xx subdirectory)
# 0002-lora-sx12xx-common - RX error callback, bandwidth mapping, fast TX/RX
# 0003-lora-sx126x-native - PA module RF switch fix (dio2-tx-enable)
# 0004-lora-sx126x-standalone - DIO1 IRQ error logging
# 0003-lora-sx126x-native - native driver: DIO1 WQ, duty cycle, CRC fix,
# extension API, RF switch fix
# 0005-gnss-air530z-easy - EASY ephemeris prediction (PMTK869) + Kconfig
# 0006-blobs-py - west blobs command fix
# New files (copied, not patched):
# drivers/lora/lr11xx/* - LR11xx Zephyr LoRa driver
# drivers/lora/native/sx126x/sx126x_ext.h - SX126x extension API header
# dts/bindings/lora/semtech,lr1110.yaml - LR1110 DTS binding
# Module patches:
# loramac-node: restore 10-element Bandwidths[] + LDRO fix
# espressif: MCUboot config flexibility (mbedtls, validation, sector count)
#
@@ -108,16 +106,6 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new)
endforeach()
endif()
# Apply unified diff patches to loramac-node module
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/loramac-node)
message(STATUS "Applying ZephCore patches to loramac-node...")
zephcore_apply_patches(
"${CMAKE_CURRENT_SOURCE_DIR}/patches/loramac-node"
"${MODULES_DIR}/lib/loramac-node"
"loramac-node"
)
endif()
# Apply unified diff patches to espressif module
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/espressif)
message(STATUS "Applying ZephCore patches to espressif...")
@@ -367,11 +355,16 @@ if(CONFIG_ZEPHCORE_RADIO_LR1110)
${ZEPHYR_DIR}/drivers/lora/lr11xx
)
else()
# Default: SX126x via Zephyr LoRa driver
message(STATUS "ZephCore Radio: SX126x (Zephyr LoRa driver)")
# Default: SX126x via native Zephyr LoRa driver
message(STATUS "ZephCore Radio: SX126x (native Zephyr driver)")
target_sources(app PRIVATE
adapters/radio/SX126xRadio.cpp
)
# Extension API header (sx126x_ext.h) is in the Zephyr driver tree
get_filename_component(ZEPHYR_DIR_SX ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr ABSOLUTE)
target_include_directories(app PRIVATE
${ZEPHYR_DIR_SX}/drivers/lora/native/sx126x
)
endif()
# ========== Role-Specific Sources ==========
+149 -7
View File
@@ -43,6 +43,11 @@ LOG_MODULE_REGISTER(zephcore_ble, CONFIG_ZEPHCORE_BLE_LOG_LEVEL);
/* TX timeout watchdog - reset ble_tx_in_progress if callback never fires */
#define BLE_TX_TIMEOUT_MS 2000
/* Congestion overflow retry interval — when the TX queue is full, the
* stuck frame retries at this cadence. Slow enough to not hammer the
* BLE stack when the link is marginal, fast enough to recover quickly. */
#define BLE_TX_OVERFLOW_RETRY_MS 250
/* Advertising intervals (Apple Accessory Design Guidelines) */
#define BT_ADV_INTERVAL_FAST CONFIG_ZEPHCORE_BLE_ADV_FAST_INTERVAL
#define BT_ADV_INTERVAL_SLOW CONFIG_ZEPHCORE_BLE_ADV_SLOW_INTERVAL
@@ -78,6 +83,23 @@ K_MSGQ_DEFINE(ble_recv_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4);
static struct frame tx_retry_frame;
static bool tx_retry_pending = false;
/* TX congestion control — flow-control mechanism for queue-full conditions.
*
* When the TX queue is full, instead of blocking or dropping:
* 1. Set ble_tx_congested flag → callers (contact iteration, etc.) stop sending
* 2. Save the stuck frame to overflow_frame → retried every 250ms
* 3. When queue drains below low water mark (1/3) → clear congestion
* 4. On disconnect → clear everything
*
* Water marks (with default FRAME_QUEUE_SIZE=12):
* - Contact iteration pauses: 2/3 = 8 frames (high water)
* - Congestion mode enters: 12/12 = full
* - Congestion mode clears: 1/3 = 4 frames (low water)
*/
static bool ble_tx_congested;
static struct frame overflow_frame;
static bool overflow_pending;
/* Connection state */
static struct bt_conn *current_conn;
static bool nus_notif_enabled;
@@ -95,6 +117,9 @@ static bool adv_is_slow = false;
/* Runtime BLE passkey */
static uint32_t ble_passkey = CONFIG_ZEPHCORE_BLE_PASSKEY;
/* NUS TX characteristic attribute — resolved at init, avoids hard-coded offset */
static const struct bt_gatt_attr *nus_tx_attr;
/* ========== Forward declarations ========== */
static void ble_tx_complete_cb(struct bt_conn *conn, void *user_data);
@@ -140,9 +165,11 @@ STRUCT_SECTION_ITERABLE(bt_nus_inst, secure_nus) = {
static void tx_drain_work_fn(struct k_work *work);
static void adv_slow_work_fn(struct k_work *work);
static void overflow_retry_work_fn(struct k_work *work);
K_WORK_DELAYABLE_DEFINE(tx_drain_work, tx_drain_work_fn);
K_WORK_DELAYABLE_DEFINE(adv_slow_work, adv_slow_work_fn);
K_WORK_DELAYABLE_DEFINE(overflow_retry_work, overflow_retry_work_fn);
/* ========== TX completion callback ========== */
@@ -165,7 +192,7 @@ static void ble_tx_complete_cb(struct bt_conn *conn, void *user_data)
static int secure_nus_send(struct bt_conn *conn, const void *data, uint16_t len)
{
struct bt_gatt_notify_params params = {
.attr = &secure_nus_svc.attrs[2], /* TX characteristic value (not declaration) */
.attr = nus_tx_attr,
.data = data,
.len = len,
.func = ble_tx_complete_cb,
@@ -303,12 +330,15 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
LOG_INF("active_iface = IFACE_NONE");
}
/* Clear queues and retry state */
/* Clear queues, retry state, and congestion */
k_msgq_purge(&ble_send_queue);
k_msgq_purge(&ble_recv_queue);
tx_retry_pending = false;
overflow_pending = false;
ble_tx_congested = false;
k_work_cancel_delayable(&tx_drain_work);
k_work_cancel_delayable(&overflow_retry_work);
/* Notify main of BLE disconnection */
if (ble_cbs && ble_cbs->on_disconnected) {
@@ -379,6 +409,17 @@ static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_
static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param)
{
/* Enforce our configured connection parameters — reject anything
* outside our preferred range. The phone will fall back to our
* PPCP (Peripheral Preferred Connection Parameters) on rejection. */
if (param->interval_min < BLE_DEFAULT_MIN_INTERVAL ||
param->interval_max > BLE_DEFAULT_MAX_INTERVAL) {
LOG_WRN("Rejecting peer conn params: interval %u-%u "
"(our range: %u-%u)",
param->interval_min, param->interval_max,
BLE_DEFAULT_MIN_INTERVAL, BLE_DEFAULT_MAX_INTERVAL);
return false;
}
return true;
}
@@ -464,6 +505,37 @@ static struct bt_conn_auth_info_cb auth_info_cb = {
.pairing_failed = pairing_failed,
};
/* ========== TX congestion overflow retry ========== */
static void overflow_retry_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
if (!overflow_pending) {
return;
}
/* Abandon overflow if connection is gone */
if (!current_conn || active_iface == ZEPHCORE_IFACE_NONE) {
overflow_pending = false;
ble_tx_congested = false;
LOG_INF("overflow cleared (disconnected)");
return;
}
if (k_msgq_put(&ble_send_queue, &overflow_frame, K_NO_WAIT) == 0) {
overflow_pending = false;
LOG_INF("overflow frame queued hdr=0x%02x, kicking drain",
overflow_frame.buf[0]);
kick_tx_drain();
/* Congestion flag cleared by tx_drain at low water mark */
} else {
/* Still full — retry at reduced rate */
LOG_DBG("overflow retry: queue still full, retry in 250ms");
k_work_schedule(&overflow_retry_work, K_MSEC(BLE_TX_OVERFLOW_RETRY_MS));
}
}
/* ========== TX drain work ========== */
static void kick_tx_drain(void)
@@ -503,6 +575,14 @@ static void tx_drain_work_fn(struct k_work *work)
return;
}
/* Take a reference snapshot — prevents use-after-free if
* disconnected() fires from another context between our check
* and use of the connection pointer. */
struct bt_conn *conn = bt_conn_ref(current_conn);
if (!conn) {
return;
}
/* Re-entrancy guard - only one TX in flight at a time */
if (ble_tx_in_progress) {
/* TX timeout watchdog - if callback never fired, reset state */
@@ -512,6 +592,7 @@ static void tx_drain_work_fn(struct k_work *work)
/* Fall through to try next TX */
} else {
LOG_DBG("tx_drain[BLE]: TX in progress, callback will chain");
bt_conn_unref(conn);
return;
}
}
@@ -521,15 +602,17 @@ static void tx_drain_work_fn(struct k_work *work)
LOG_INF("tx_drain[BLE]: retrying len=%u hdr=0x%02x", (unsigned)tx_retry_frame.len, tx_retry_frame.buf[0]);
ble_tx_in_progress = true;
ble_tx_start_time = k_uptime_get();
err = secure_nus_send(current_conn, tx_retry_frame.buf, tx_retry_frame.len);
err = secure_nus_send(conn, tx_retry_frame.buf, tx_retry_frame.len);
if (err == 0) {
tx_retry_pending = false;
LOG_INF("tx_drain[BLE]: retry success");
bt_conn_unref(conn);
return; /* Callback will chain to next */
} else if (err == -EAGAIN || err == -ENOMEM) {
ble_tx_in_progress = false;
LOG_DBG("tx_drain[BLE]: retry still busy, wait %dms", BLE_TX_RETRY_MS);
k_work_schedule(&tx_drain_work, K_MSEC(BLE_TX_RETRY_MS));
bt_conn_unref(conn);
return;
} else {
ble_tx_in_progress = false;
@@ -541,24 +624,41 @@ static void tx_drain_work_fn(struct k_work *work)
/* Get next frame from queue */
if (k_msgq_get(&ble_send_queue, &f, K_NO_WAIT) != 0) {
/* TX queue empty - signal idle */
/* TX queue empty — clear congestion and signal idle */
if (ble_tx_congested) {
ble_tx_congested = false;
LOG_INF("tx_drain: congestion cleared (queue empty)");
}
if (ble_cbs && ble_cbs->on_tx_idle) {
ble_cbs->on_tx_idle();
}
bt_conn_unref(conn);
return;
}
/* Clear congestion at low water mark (1/3 of queue) — gives headroom
* before hitting full again. Hysteresis: ON at full, OFF at 1/3. */
if (ble_tx_congested) {
uint32_t used = k_msgq_num_used_get(&ble_send_queue);
if (used <= FRAME_QUEUE_SIZE / 3) {
ble_tx_congested = false;
LOG_INF("tx_drain: congestion cleared (queue=%u/%u)",
used, (unsigned)FRAME_QUEUE_SIZE);
}
}
LOG_DBG("tx_drain[BLE]: sending len=%u hdr=0x%02x queue=%u",
(unsigned)f.len, f.buf[0], k_msgq_num_used_get(&ble_send_queue));
/* Mark TX in progress before calling notify_cb */
ble_tx_in_progress = true;
ble_tx_start_time = k_uptime_get();
err = secure_nus_send(current_conn, f.buf, f.len);
err = secure_nus_send(conn, f.buf, f.len);
if (err == 0) {
/* Success - callback will chain to next */
LOG_DBG("tx_drain[BLE]: queued for TX");
bt_conn_unref(conn);
return;
} else if (err == -EAGAIN || err == -ENOMEM) {
/* BLE buffer full - save for retry */
@@ -567,11 +667,13 @@ static void tx_drain_work_fn(struct k_work *work)
tx_retry_pending = true;
LOG_DBG("tx_drain[BLE]: BLE busy (err=%d), saved for retry", err);
k_work_schedule(&tx_drain_work, K_MSEC(BLE_TX_RETRY_MS));
bt_conn_unref(conn);
return;
} else {
/* Other error - drop frame */
ble_tx_in_progress = false;
LOG_WRN("tx_drain[BLE]: send failed err=%d, dropped frame", err);
bt_conn_unref(conn);
return;
}
}
@@ -677,6 +779,13 @@ static void adv_slow_work_fn(struct k_work *work)
void zephcore_ble_init(const struct ble_callbacks *cbs)
{
ble_cbs = cbs;
/* Resolve NUS TX characteristic attribute once — avoids hard-coded
* array offset in secure_nus_send(). attrs[2] = TX char value
* (attrs[0]=service, attrs[1]=TX char decl, attrs[2]=TX char value,
* attrs[3]=CCC, attrs[4]=RX char decl, attrs[5]=RX char value). */
nus_tx_attr = &secure_nus_svc.attrs[2];
bt_conn_auth_cb_register(&auth_cb);
bt_conn_auth_info_cb_register(&auth_info_cb);
}
@@ -716,8 +825,36 @@ size_t zephcore_ble_send(const uint8_t *data, uint16_t len)
memcpy(f.buf, data, len);
if (k_msgq_put(&ble_send_queue, &f, K_NO_WAIT) != 0) {
LOG_WRN("queue full!");
return 0;
/* Queue full — enter congestion mode.
*
* Instead of blocking (would stall LoRa) or dropping (loses
* frames), we signal congestion so all senders stop, then
* save this frame and retry at a reduced 250ms cadence until
* the queue drains or the connection drops.
*
* Callers check zephcore_ble_is_congested() and hold off:
* - contact_iter_work: pauses iteration
* - main thread (sendPush): pushes are best-effort signals,
* actual message data is safe in the offline queue
*/
if (!ble_tx_congested) {
LOG_WRN("TX queue full (%u/%u), entering congestion",
k_msgq_num_used_get(&ble_send_queue),
(unsigned)FRAME_QUEUE_SIZE);
ble_tx_congested = true;
}
/* Save to overflow — retried at 250ms intervals.
* If overflow already pending, replace with newest frame
* (push notifications are idempotent MSG_WAITING signals). */
if (overflow_pending) {
LOG_DBG("overflow replaced: 0x%02x → 0x%02x",
overflow_frame.buf[0], data[0]);
}
overflow_frame = f;
overflow_pending = true;
k_work_schedule(&overflow_retry_work, K_MSEC(BLE_TX_OVERFLOW_RETRY_MS));
return len; /* Accepted into overflow — will be retried */
}
LOG_DBG("queued len=%u hdr=0x%02x queue=%u",
@@ -759,6 +896,11 @@ bool zephcore_ble_is_connected(void)
return current_conn != NULL;
}
bool zephcore_ble_is_congested(void)
{
return ble_tx_congested;
}
void zephcore_ble_set_passkey(uint32_t passkey)
{
if (passkey >= 100000 && passkey <= 999999) {
+6
View File
@@ -67,6 +67,12 @@ bool zephcore_ble_is_active(void);
*/
bool zephcore_ble_is_connected(void);
/**
* Check if BLE TX is congested (queue full, overflow retrying).
* Callers should stop sending until this clears.
*/
bool zephcore_ble_is_congested(void);
/**
* Set the BLE passkey at runtime.
*/
+41 -2
View File
@@ -229,8 +229,7 @@ static bool configParamsEqual(const struct lora_modem_config &a,
const struct lora_modem_config &b)
{
/* CRITICAL: a.tx == b.tx MUST be compared — without it, switching
* RX→TX skips RadioSetTxConfig(), leaving TxTimeout=0 which causes
* the loramac-node software timeout to fire after 1ms and break TX. */
* RX→TX skips lora_config() for TX params, breaking transmit. */
return a.frequency == b.frequency &&
a.bandwidth == b.bandwidth &&
a.datarate == b.datarate &&
@@ -242,6 +241,25 @@ static bool configParamsEqual(const struct lora_modem_config &a,
a.public_network == b.public_network;
}
/**
* Check if only the TX/RX direction changed (all radio params identical).
* Used to skip the full lora_config() call on TX↔RX transitions when
* the driver already has valid TX and RX configs from previous calls.
*/
static bool onlyDirectionDiffers(const struct lora_modem_config &a,
const struct lora_modem_config &b)
{
return a.frequency == b.frequency &&
a.bandwidth == b.bandwidth &&
a.datarate == b.datarate &&
a.coding_rate == b.coding_rate &&
a.preamble_len == b.preamble_len &&
a.tx_power == b.tx_power &&
a.iq_inverted == b.iq_inverted &&
a.public_network == b.public_network &&
a.tx != b.tx;
}
void LoRaRadioBase::configureRx()
{
struct lora_modem_config cfg;
@@ -252,6 +270,18 @@ void LoRaRadioBase::configureRx()
return;
}
/* Fast path: if only the TX/RX direction changed, skip the full
* hwConfigure → lora_config() call. The driver already has a valid
* RX config (RadioSetRxConfig) from a previous cycle — Radio.Rx(0)
* in hwStartReceive() will use those register values directly.
* This avoids the modem_acquire → modem_release → Radio.Sleep()
* round-trip that wastes ~5 ms on every TX→RX transition. */
if (_config_cached && onlyDirectionDiffers(cfg, _last_cfg)) {
LOG_DBG("configureRx: direction-only change, skip hwConfigure");
_last_cfg = cfg;
return;
}
LOG_DBG("configureRx: freq=%u bw=%d sf=%d cr=%d pwr=%d",
cfg.frequency, (int)cfg.bandwidth, (int)cfg.datarate,
(int)cfg.coding_rate, cfg.tx_power);
@@ -271,6 +301,15 @@ void LoRaRadioBase::configureTx()
return;
}
/* Fast path: direction-only change (RX→TX). The driver already
* has a valid TX config (RadioSetTxConfig with TxTimeout=4000)
* from a previous cycle — Radio.Send() will use those values. */
if (_config_cached && onlyDirectionDiffers(cfg, _last_cfg)) {
LOG_DBG("configureTx: direction-only change, skip hwConfigure");
_last_cfg = cfg;
return;
}
hwConfigure(cfg);
_last_cfg = cfg;
_config_cached = true;
+11 -154
View File
@@ -1,93 +1,21 @@
/*
* SPDX-License-Identifier: Apache-2.0
* SX126x hardware hooks for LoRaRadioBase.
* SX126x hardware hooks for LoRaRadioBase — native Zephyr driver.
*/
#include "SX126xRadio.h"
#include <mesh/LoRaConfig.h>
#include <zephyr/kernel.h>
/*
* Access loramac-node SX126x functions for advanced features.
* These are defined in the Zephyr LoRa driver (sx126x.c/radio.c).
*/
/* Native SX126x driver extension API */
extern "C" {
uint16_t SX126xGetIrqStatus(void);
int8_t SX126xGetRssiInst(void);
uint32_t SX126xGetRandom(void);
#define RADIO_CALIBRATEIMAGE 0x98
void SX126xWriteCommand(uint8_t opcode, uint8_t *buffer, uint16_t size);
/* Radio struct for direct access to SetRxDutyCycle (SX126x only) */
struct LoRaMacRadio_s {
void (*Init)(void *events);
int (*GetStatus)(void);
void (*SetModem)(int modem);
void (*SetChannel)(uint32_t freq);
bool (*IsChannelFree)(uint32_t freq, uint32_t rxBandwidth, int16_t rssiThresh, uint32_t maxCarrierSenseTime);
uint32_t (*Random)(void);
void (*SetRxConfig)(int modem, uint32_t bandwidth, uint32_t datarate, uint8_t coderate,
uint32_t bandwidthAfc, uint16_t preambleLen, uint16_t symbTimeout,
bool fixLen, uint8_t payloadLen, bool crcOn, bool freqHopOn,
uint8_t hopPeriod, bool iqInverted, bool rxContinuous);
void (*SetTxConfig)(int modem, int8_t power, uint32_t fdev, uint32_t bandwidth,
uint32_t datarate, uint8_t coderate, uint16_t preambleLen,
bool fixLen, bool crcOn, bool freqHopOn, uint8_t hopPeriod,
bool iqInverted, uint32_t timeout);
bool (*CheckRfFrequency)(uint32_t frequency);
uint32_t (*TimeOnAir)(int modem, uint32_t bandwidth, uint32_t datarate, uint8_t coderate,
uint16_t preambleLen, bool fixLen, uint8_t payloadLen, bool crcOn);
void (*Send)(uint8_t *buffer, uint8_t size);
void (*Sleep)(void);
void (*Standby)(void);
void (*Rx)(uint32_t timeout);
void (*StartCad)(void);
void (*SetTxContinuousWave)(uint32_t freq, int8_t power, uint16_t time);
int16_t (*Rssi)(int modem);
void (*Write)(uint32_t addr, uint8_t data);
uint8_t (*Read)(uint32_t addr);
void (*WriteBuffer)(uint32_t addr, uint8_t *buffer, uint8_t size);
void (*ReadBuffer)(uint32_t addr, uint8_t *buffer, uint8_t size);
void (*SetMaxPayloadLength)(int modem, uint8_t max);
void (*SetPublicNetwork)(bool enable);
uint32_t (*GetWakeupTime)(void);
void (*IrqProcess)(void);
void (*RxBoosted)(uint32_t timeout);
void (*SetRxDutyCycle)(uint32_t rxTime, uint32_t sleepTime);
};
extern const struct LoRaMacRadio_s Radio __asm__("Radio");
#include "sx126x_ext.h"
}
/* IRQ status bits */
static const uint16_t kIrqHeaderValid = 0x0010;
static const uint16_t kIrqPreambleDetected = 0x0004;
/* RX gain register */
#define REG_RX_GAIN 0x08AC
#define RX_GAIN_POWER_SAVE 0x94
#define RX_GAIN_BOOSTED 0x96
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_lora, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
/**
* SX126x-specific: Calibrate image rejection for a minimal 4 MHz range.
*/
static void calibrateImageMinimal(uint32_t freq_hz)
{
uint16_t freq_mhz = freq_hz / 1000000;
uint8_t cal_low = (uint8_t)(freq_mhz / 4);
uint8_t cal_high = cal_low + 1;
uint8_t calFreq[2] = { cal_low, cal_high };
SX126xWriteCommand(RADIO_CALIBRATEIMAGE, calFreq, 2);
LOG_DBG("SX126x image calibration: %u-%u MHz", cal_low * 4, cal_high * 4);
}
K_THREAD_STACK_DEFINE(sx126x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board,
@@ -110,8 +38,6 @@ 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);
} else {
calibrateImageMinimal(cfg.frequency);
}
}
@@ -126,9 +52,11 @@ void SX126xRadio::hwStartReceive()
_in_recv_mode = true;
if (_rx_boost_enabled) {
::Radio.Write(REG_RX_GAIN, RX_GAIN_BOOSTED);
sx126x_set_rx_boost(_dev, true);
}
if (_rx_duty_cycle_enabled) {
sx126x_set_rx_duty_cycle(_dev, true);
}
applyRxDutyCycleIfEnabled();
}
void SX126xRadio::hwCancelReceive()
@@ -144,30 +72,22 @@ int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len,
int16_t SX126xRadio::hwGetCurrentRSSI()
{
return SX126xGetRssiInst();
return sx126x_get_rssi_inst(_dev);
}
bool SX126xRadio::hwIsPreambleDetected()
{
uint16_t irq = SX126xGetIrqStatus();
return (irq & kIrqHeaderValid) || (irq & kIrqPreambleDetected);
return sx126x_is_receiving(_dev);
}
void SX126xRadio::hwSetRxBoost(bool enable)
{
::Radio.Write(REG_RX_GAIN, enable ? RX_GAIN_BOOSTED : RX_GAIN_POWER_SAVE);
sx126x_set_rx_boost(_dev, enable);
}
void SX126xRadio::hwSetRxDutyCycle(bool enable)
{
if (enable) {
applyRxDutyCycleIfEnabled();
} else {
::Radio.Rx(0);
if (_rx_boost_enabled) {
::Radio.Write(REG_RX_GAIN, RX_GAIN_BOOSTED);
}
}
sx126x_set_rx_duty_cycle(_dev, enable);
}
void SX126xRadio::hwResetAGC()
@@ -178,67 +98,4 @@ void SX126xRadio::hwResetAGC()
}
}
/* ── SX126x RX Duty Cycle — RadioLib Algorithm ───────────────────────── */
void SX126xRadio::applyRxDutyCycleIfEnabled()
{
if (!_rx_duty_cycle_enabled || !_in_recv_mode) {
return;
}
uint8_t sf = _prefs ? _prefs->sf : LoRaConfig::SPREADING_FACTOR;
float bw_khz = _prefs ? _prefs->bw : (float)LoRaConfig::BANDWIDTH;
uint16_t preamble_len = LoRaConfig::PREAMBLE_LEN;
uint16_t min_symbols = (sf >= 7) ? RADIOLIB_MIN_SYMBOLS_SF7_PLUS
: RADIOLIB_MIN_SYMBOLS_SF6_LESS;
int16_t sleep_symbols = (int16_t)preamble_len - (int16_t)min_symbols;
if (sleep_symbols <= 0) {
LOG_WRN("Preamble too short for duty cycle (need >%d, have %d)",
min_symbols, preamble_len);
_rx_duty_cycle_enabled = false;
return;
}
uint32_t symbol_us = (uint32_t)((float)(1 << sf) * 1000.0f / bw_khz);
/* Shave 2 symbols off the sleep period so the wake window always overlaps
with enough preamble for detection, even in the worst-case arrival timing.
Without this margin the math lands at exactly zero — any TCXO drift,
crystal error, or processing latency causes missed packets. */
int16_t sleep_symbols_safe = sleep_symbols - 2;
if (sleep_symbols_safe < 1) sleep_symbols_safe = 1;
uint32_t sleep_period_us = (uint16_t)sleep_symbols_safe * symbol_us;
uint32_t preamble_total_us = (preamble_len + 1) * symbol_us;
int32_t wake_calc1 = ((int32_t)preamble_total_us -
((int32_t)sleep_period_us - RADIOLIB_TCXO_DELAY_US)) / 2;
uint32_t wake_calc2 = (min_symbols + 1) * symbol_us;
uint32_t wake_period_us = (wake_calc1 > 0 && (uint32_t)wake_calc1 > wake_calc2)
? (uint32_t)wake_calc1 : wake_calc2;
/* SetRxDutyCycle takes times in 15.625us steps */
uint32_t rx_time = (wake_period_us * 64) / 1000;
uint32_t sleep_time = (sleep_period_us * 64) / 1000;
if (rx_time < 64) rx_time = 64;
if (sleep_time < 64) sleep_time = 64;
::Radio.SetRxDutyCycle(rx_time, sleep_time);
uint32_t rx_ms = (rx_time * 1000) / 64000;
uint32_t sleep_ms = (sleep_time * 1000) / 64000;
uint32_t total_ms = rx_ms + sleep_ms;
uint32_t preamble_ms = (preamble_len * symbol_us) / 1000;
uint32_t bw_int = (uint32_t)bw_khz;
uint32_t bw_frac = (uint32_t)((bw_khz - bw_int) * 10);
LOG_DBG("RX duty cycle: SF%d BW%u.%u sym=%uus preamble=%ums",
sf, bw_int, bw_frac, symbol_us, preamble_ms);
LOG_DBG(" -> rx=%ums sleep=%ums period=%ums (minSym=%d sleepSym=%d)",
rx_ms, sleep_ms, total_ms, min_symbols, sleep_symbols);
}
} /* namespace mesh */
+1 -10
View File
@@ -1,17 +1,12 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using Zephyr LoRa driver
* ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using native Zephyr driver
*/
#pragma once
#include "LoRaRadioBase.h"
/* SX126x-specific: RX Duty Cycle power saving - uses RadioLib algorithm */
#define RADIOLIB_MIN_SYMBOLS_SF7_PLUS 8
#define RADIOLIB_MIN_SYMBOLS_SF6_LESS 12
#define RADIOLIB_TCXO_DELAY_US 1000 /* ~1ms startup overhead */
namespace mesh {
class SX126xRadio : public LoRaRadioBase {
@@ -33,10 +28,6 @@ protected:
void hwSetRxBoost(bool enable) override;
void hwSetRxDutyCycle(bool enable) override;
void hwResetAGC() override;
private:
/* SX126x-specific: RadioLib duty cycle timing algorithm */
void applyRxDutyCycleIfEnabled();
};
} /* namespace mesh */
+3 -5
View File
@@ -17,11 +17,9 @@
#define DEFAULT_NOISE_FLOOR 0 /* first calibration accepts all samples */
/* --- RX ring buffer ---
* Sized to be effectively drop-proof: even at the fastest LoRa settings
* (SF7/BW500, ~5ms per packet), 32 slots buffer 160ms+ of back-to-back
* arrivals. The main loop drain takes microseconds per packet, so
* overflow should never occur in practice. Cost: 32 × 260 = ~8.3 KB. */
#define RX_RING_SIZE 32
* 8 slots buffer ~40ms+ of back-to-back arrivals at SF7/BW500.
* Main loop drains in microseconds per packet. Cost: 8 × 260 = ~2 KB. */
#define RX_RING_SIZE 8
/* --- TX wait thread --- */
#define TX_WAIT_THREAD_STACK_SIZE 1024
@@ -35,6 +35,7 @@ CONFIG_REBOOT=y
# ========== LoRa ==========
CONFIG_LORA=y
CONFIG_LORA_MODULE_BACKEND_NATIVE=y
CONFIG_REGULATOR=y
# ========== Display Subsystem ==========
@@ -1,75 +0,0 @@
diff --git a/src/radio/sx126x/radio.c b/src/radio/sx126x/radio.c
index c4a40dab..bf6b4c9b 100644
--- a/src/radio/sx126x/radio.c
+++ b/src/radio/sx126x/radio.c
@@ -421,7 +421,26 @@ const FskBandwidth_t FskBandwidths[] =
{ 500000, 0x00 }, // Invalid Bandwidth
};
-const RadioLoRaBandwidths_t Bandwidths[] = { LORA_BW_125, LORA_BW_250, LORA_BW_500 };
+/*
+ * ZephCore patch: restore full 10-element Bandwidths array.
+ * Upstream Zephyr reduced this to {125, 250, 500} but the Zephyr LoRa API
+ * now defines enums for all bandwidths (BW_7_KHZ through BW_500_KHZ).
+ * sx12xx_common.c maps these enums to indices into this array, so all 10
+ * entries must be present to avoid out-of-bounds access.
+ */
+const RadioLoRaBandwidths_t Bandwidths[] =
+{
+ LORA_BW_007,
+ LORA_BW_010,
+ LORA_BW_015,
+ LORA_BW_020,
+ LORA_BW_031,
+ LORA_BW_041,
+ LORA_BW_062,
+ LORA_BW_125,
+ LORA_BW_250,
+ LORA_BW_500,
+};
uint8_t MaxPayloadLength = 0xFF;
@@ -702,8 +721,14 @@ void RadioSetRxConfig( RadioModems_t modem, uint32_t bandwidth,
SX126x.ModulationParams.Params.LoRa.Bandwidth = Bandwidths[bandwidth];
SX126x.ModulationParams.Params.LoRa.CodingRate = ( RadioLoRaCodingRates_t )coderate;
- if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
- ( ( bandwidth == 1 ) && ( datarate == 12 ) ) )
+ /*
+ * ZephCore patch: LDRO check updated for 10-element Bandwidths[].
+ * Enable LDRO when symbol duration > 16ms.
+ * Old indices (0=125k,1=250k) → new (7=125k,8=250k,6=62.5k, etc.)
+ */
+ if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ ( ( bandwidth == 8 ) && ( datarate == 12 ) ) ||
+ ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) )
{
SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x01;
}
@@ -809,8 +834,10 @@ void RadioSetTxConfig( RadioModems_t modem, int8_t power, uint32_t fdev,
SX126x.ModulationParams.Params.LoRa.Bandwidth = Bandwidths[bandwidth];
SX126x.ModulationParams.Params.LoRa.CodingRate= ( RadioLoRaCodingRates_t )coderate;
- if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
- ( ( bandwidth == 1 ) && ( datarate == 12 ) ) )
+ /* ZephCore patch: LDRO check updated for 10-element Bandwidths[] */
+ if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ ( ( bandwidth == 8 ) && ( datarate == 12 ) ) ||
+ ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) )
{
SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x01;
}
@@ -946,8 +973,10 @@ static uint32_t RadioGetLoRaTimeOnAirNumerator( uint32_t bandwidth,
}
}
- if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
- ( ( bandwidth == 1 ) && ( datarate == 12 ) ) )
+ /* ZephCore patch: LDRO check updated for 10-element Bandwidths[] */
+ if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ ( ( bandwidth == 8 ) && ( datarate == 12 ) ) ||
+ ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) )
{
lowDatareOptimize = true;
}
@@ -27,6 +27,11 @@
LOG_MODULE_REGISTER(lr11xx_lora, CONFIG_LORA_LOG_LEVEL);
/* Dedicated DIO1 work queue — keeps LoRa interrupt processing off the
* system work queue so USB/BLE/timer work items cannot delay packet RX. */
#define LR11XX_DIO1_WQ_STACK_SIZE 1536
K_THREAD_STACK_DEFINE(lr11xx_dio1_wq_stack, LR11XX_DIO1_WQ_STACK_SIZE);
/* ── Driver data structures ─────────────────────────────────────────── */
struct lr11xx_config {
@@ -65,8 +70,9 @@ struct lr11xx_data {
/* Async TX state */
struct k_poll_signal *tx_signal;
/* DIO1 work queue */
/* DIO1 work — runs on dedicated queue, not system work queue */
struct k_work dio1_work;
struct k_work_q dio1_wq;
/* Radio state */
volatile bool tx_active;
@@ -175,9 +181,9 @@ static void lr11xx_hardware_reset(struct lr11xx_data *data,
lr11xx_system_calibrate(ctx, 0x3F);
/* Tight ±2 MHz image calibration around actual frequency */
uint16_t freq_mhz = data->modem_cfg.frequency / 1000000;
uint16_t cal_low = (freq_mhz / 4) * 4;
lr11xx_system_calibrate_image_in_mhz(ctx, cal_low, cal_low + 4);
lr11xx_system_calibrate_image_in_mhz(ctx, freq_mhz - 2, freq_mhz + 2);
lr11xx_radio_set_pkt_type(ctx, LR11XX_RADIO_PKT_TYPE_LORA);
@@ -197,11 +203,16 @@ static void lr11xx_apply_modem_config(struct lr11xx_data *data,
lr11xx_radio_set_rf_freq(ctx, mc->frequency);
/* LDRO must be enabled when symbol time > 16.38ms (SF11+/BW125 etc) */
uint32_t bw_hz = (uint32_t)(bw_enum_to_khz(mc->bandwidth) * 1000.0f);
uint32_t symbol_time_us = ((1U << (uint8_t)mc->datarate) * 1000000U) / bw_hz;
uint8_t ldro = (symbol_time_us > 16380) ? 1 : 0;
lr11xx_radio_mod_params_lora_t mod = {
.sf = (lr11xx_radio_lora_sf_t)mc->datarate,
.bw = bw_enum_to_lr11xx(mc->bandwidth),
.cr = cr_enum_to_lr11xx(mc->coding_rate),
.ldro = 0,
.ldro = ldro,
};
lr11xx_radio_set_lora_mod_params(ctx, &mod);
@@ -238,7 +249,35 @@ static void lr11xx_apply_modem_config(struct lr11xx_data *data,
0);
}
/* ── RX duty cycle (RadioLib algorithm) ─────────────────────────────── */
/* ── CAD det_peak lookup (from Semtech ral_lr11xx reference) ─────────── */
static uint8_t lr11xx_cad_det_peak(uint8_t sf, enum lora_signal_bandwidth bw)
{
static const uint8_t peak_bw500[] = {
/* SF5 SF6 SF7 SF8 SF9 SF10 SF11 SF12 */
65, 70, 77, 85, 78, 80, 79, 82
};
static const uint8_t peak_bw250[] = {
60, 61, 64, 72, 63, 71, 73, 75
};
static const uint8_t peak_bw125[] = {
56, 52, 52, 58, 58, 62, 66, 68
};
if (sf < 5 || sf > 12) return 0x32;
uint8_t idx = sf - 5;
if (bw >= BW_500_KHZ) return peak_bw500[idx];
if (bw >= BW_250_KHZ) return peak_bw250[idx];
return peak_bw125[idx];
}
/* ── RX duty cycle (hardware CAD mode) ──────────────────────────────── */
/* LR1110 native CAD duty cycle: on each wake the chip runs a fast CAD
* (~2 symbols) instead of full RX. Preamble detected auto-RX with
* extended timeout. No activity straight back to sleep. The entire
* loop runs in HW with zero IRQ overhead per cycle. */
static void lr11xx_apply_rx_duty_cycle(struct lr11xx_data *data)
{
@@ -248,43 +287,56 @@ static void lr11xx_apply_rx_duty_cycle(struct lr11xx_data *data)
uint8_t sf = (uint8_t)mc->datarate;
float bw_khz = bw_enum_to_khz(mc->bandwidth);
uint16_t preamble_len = mc->preamble_len;
uint16_t min_symbols = (sf >= 7) ? 8 : 12;
int16_t sleep_symbols = (int16_t)preamble_len - (int16_t)min_symbols;
if (sleep_symbols <= 0) {
LOG_WRN("Preamble too short for duty cycle, using continuous RX");
/* Margin must cover:
* 2 sym CAD scan duration (cad_symb_nb = 2 symbols)
* 4 sym demodulator preamble acquisition (~4.25 sym minimum)
* 4 sym RTC jitter + TCXO startup + state transitions
*
* 10 sym total matches the SX126x driver margin and ensures
* 6 symbols of preamble remain after worst-case wake.
*
* The original margin of 4 left only ~2 usable symbols any TCXO
* startup or timer jitter caused missed packets. */
uint32_t symbol_us = (uint32_t)((float)(1 << sf) * 1000.0f / bw_khz);
int16_t margin = 10;
int16_t sleep_symbols = (int16_t)preamble_len - margin;
if (sleep_symbols < 2) {
LOG_WRN("Preamble too short for CAD duty cycle (%d sym), "
"continuous RX", preamble_len);
data->rx_duty_cycle_enabled = false;
lr11xx_radio_set_rx(ctx, 0xFFFFFF);
return;
}
uint32_t symbol_us = (uint32_t)((float)(1 << sf) * 1000.0f / bw_khz);
/* CAD params: 2 symbols, Semtech det_peak table, auto-RX on detect */
uint8_t det_peak = lr11xx_cad_det_peak(sf, mc->bandwidth);
lr11xx_radio_cad_params_t cad = {
.cad_symb_nb = 1, /* enum: 1 = 2 symbols */
.cad_detect_peak = det_peak,
.cad_detect_min = 10,
.cad_exit_mode = LR11XX_RADIO_CAD_EXIT_MODE_RX,
.cad_timeout = 0,
};
lr11xx_radio_set_cad_params(ctx, &cad);
/* Shave 2 symbols off sleep so the wake window always has enough
preamble overlap for detection. Without this the margin is zero
and any TCXO drift or crystal error causes missed packets. */
int16_t sleep_symbols_safe = sleep_symbols - 2;
if (sleep_symbols_safe < 1) sleep_symbols_safe = 1;
uint32_t sleep_period_us = (uint16_t)sleep_symbols_safe * symbol_us;
uint32_t preamble_total_us = (preamble_len + 1) * symbol_us;
int32_t wake_calc1 = ((int32_t)preamble_total_us -
((int32_t)sleep_period_us - 1000)) / 2;
uint32_t wake_calc2 = (min_symbols + 1) * symbol_us;
uint32_t wake_period_us = (wake_calc1 > 0 &&
(uint32_t)wake_calc1 > wake_calc2)
? (uint32_t)wake_calc1 : wake_calc2;
uint32_t sleep_us = (uint16_t)sleep_symbols * symbol_us;
/* LR1110 API takes milliseconds */
uint32_t rx_ms = (wake_period_us + 500) / 1000;
uint32_t sleep_ms = (sleep_period_us + 500) / 1000;
/* RX period: on CAD detect, chip enters RX for (2*rx + sleep).
* Must cover remaining preamble + sync word + header. */
uint32_t rx_us = (preamble_len + 1) * symbol_us;
uint32_t rx_ms = (rx_us + 500) / 1000;
uint32_t sleep_ms = (sleep_us + 500) / 1000;
if (rx_ms < 1) rx_ms = 1;
if (sleep_ms < 1) sleep_ms = 1;
lr11xx_radio_set_rx_duty_cycle(ctx, rx_ms, sleep_ms,
LR11XX_RADIO_RX_DUTY_CYCLE_MODE_RX);
LR11XX_RADIO_RX_DUTY_CYCLE_MODE_CAD);
LOG_INF("RX duty cycle: rx=%ums sleep=%ums (SF%d BW%.0f)",
rx_ms, sleep_ms, sf, (double)bw_khz);
LOG_INF("RX CAD duty cycle: rx=%ums sleep=%ums peak=%u (SF%d BW%.0f)",
rx_ms, sleep_ms, det_peak, sf, (double)bw_khz);
}
/* ── Start RX (internal) ────────────────────────────────────────────── */
@@ -327,6 +379,49 @@ static void lr11xx_start_rx(struct lr11xx_data *data,
data->tx_active = false;
}
/* ── Lightweight RX restart (no modem reconfig) ─────────────────────── */
/* Used after RX done / CRC error / timeout — frequency/modulation unchanged,
* skip most of lr11xx_apply_modem_config.
* Full lr11xx_start_rx() kept for initial start and TXRX.
*
* Packet params (preamble, pld_len=255) MUST be re-set on every restart:
* the LR1110 can silently corrupt these registers after CRC/header errors
* and through CADRX transitions, causing missed packets especially when
* small and large packets are interleaved on the mesh. Arduino MeshCore
* applies the same workaround (CustomLR1110Wrapper::onSendFinished). */
static void lr11xx_restart_rx(struct lr11xx_data *data)
{
void *ctx = &data->hal_ctx;
struct lora_modem_config *mc = &data->modem_cfg;
lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK);
/* Re-apply packet params — preamble + pld_len=255 for RX */
lr11xx_radio_pkt_params_lora_t pkt = {
.preamble_len_in_symb = mc->preamble_len,
.header_type = LR11XX_RADIO_LORA_PKT_EXPLICIT,
.pld_len_in_bytes = 255,
.crc = mc->packet_crc_disable ? LR11XX_RADIO_LORA_CRC_OFF
: LR11XX_RADIO_LORA_CRC_ON,
.iq = mc->iq_inverted ? LR11XX_RADIO_LORA_IQ_INVERTED
: LR11XX_RADIO_LORA_IQ_STANDARD,
};
lr11xx_radio_set_lora_pkt_params(ctx, &pkt);
if (data->rx_duty_cycle_enabled) {
lr11xx_apply_rx_duty_cycle(data);
} else {
lr11xx_radio_set_rx(ctx, 0xFFFFFF);
}
if (data->rx_boost_enabled) {
lr11xx_radio_cfg_rx_boosted(ctx, true);
}
data->in_rx_mode = true;
}
/* ── DIO1 IRQ handler (work queue, thread context) ──────────────────── */
static void lr11xx_dio1_callback(void *user_data);
@@ -360,8 +455,18 @@ static void lr11xx_dio1_work_handler(struct k_work *work)
rx_stat.buffer_start_pointer,
rx_stat.pld_len_in_bytes);
/* Restart RX BEFORE callback (matches SX126x pattern) */
lr11xx_start_rx(data, cfg);
/* Lightweight RX restart — no reconfig needed */
lr11xx_restart_rx(data);
/* When SNR < 0 the packet RSSI is dominated by
* noise use the signal-only RSSI estimate for a
* more accurate reading on weak links. */
int16_t rssi = pkt_stat.rssi_pkt_in_dbm;
if (pkt_stat.snr_pkt_in_db < 0 &&
pkt_stat.signal_rssi_pkt_in_dbm > rssi) {
rssi = pkt_stat.signal_rssi_pkt_in_dbm;
}
k_mutex_unlock(&data->spi_mutex);
@@ -369,7 +474,7 @@ static void lr11xx_dio1_work_handler(struct k_work *work)
if (data->async_rx_cb) {
data->async_rx_cb(data->dev, data->rx_buf,
rx_stat.pld_len_in_bytes,
pkt_stat.rssi_pkt_in_dbm,
rssi,
pkt_stat.snr_pkt_in_db,
data->async_rx_user_data);
}
@@ -377,7 +482,7 @@ static void lr11xx_dio1_work_handler(struct k_work *work)
}
LOG_WRN("RX: invalid len %d", rx_stat.pld_len_in_bytes);
lr11xx_start_rx(data, cfg);
lr11xx_restart_rx(data);
}
/* ── TX done ── */
@@ -385,7 +490,7 @@ static void lr11xx_dio1_work_handler(struct k_work *work)
LOG_INF("TX done");
data->tx_active = false;
/* Restart RX */
/* Full restart — modem was reconfigured for TX */
lr11xx_start_rx(data, cfg);
/* Raise TX signal */
@@ -396,10 +501,10 @@ static void lr11xx_dio1_work_handler(struct k_work *work)
/* ── Timeout ── */
if (irq & LR11XX_SYSTEM_IRQ_TIMEOUT) {
LOG_INF("Timeout IRQ — restarting RX (duty_cycle=%d)",
LOG_DBG("Timeout IRQ — restarting RX (duty_cycle=%d)",
data->rx_duty_cycle_enabled);
if (!data->tx_active) {
lr11xx_start_rx(data, cfg);
lr11xx_restart_rx(data);
}
}
@@ -418,7 +523,7 @@ static void lr11xx_dio1_work_handler(struct k_work *work)
}
if (!data->tx_active) {
lr11xx_start_rx(data, cfg);
lr11xx_restart_rx(data);
}
k_mutex_unlock(&data->spi_mutex);
@@ -438,7 +543,7 @@ static void lr11xx_dio1_work_handler(struct k_work *work)
static void lr11xx_dio1_callback(void *user_data)
{
struct lr11xx_data *data = (struct lr11xx_data *)user_data;
k_work_submit(&data->dio1_work);
k_work_submit_to_queue(&data->dio1_wq, &data->dio1_work);
}
/* Forward declaration — hw_init is defined after driver API functions */
@@ -465,6 +570,13 @@ static int lr11xx_lora_config(const struct device *dev,
memcpy(&data->modem_cfg, config, sizeof(*config));
data->configured = true;
/* Tight ±2 MHz image calibration for the configured frequency */
k_mutex_lock(&data->spi_mutex, K_FOREVER);
uint16_t freq_mhz = config->frequency / 1000000;
lr11xx_system_calibrate_image_in_mhz(&data->hal_ctx,
freq_mhz - 2, freq_mhz + 2);
k_mutex_unlock(&data->spi_mutex);
LOG_INF("config: %uHz SF%d BW%d CR%d pwr=%d tx=%d",
config->frequency, config->datarate, config->bandwidth,
config->coding_rate, config->tx_power, config->tx);
@@ -797,6 +909,12 @@ static int lr11xx_lora_init(const struct device *dev)
k_mutex_init(&data->spi_mutex);
k_work_init(&data->dio1_work, lr11xx_dio1_work_handler);
/* Start dedicated DIO1 work queue at high priority */
k_work_queue_start(&data->dio1_wq, lr11xx_dio1_wq_stack,
K_THREAD_STACK_SIZEOF(lr11xx_dio1_wq_stack),
K_PRIO_COOP(7), NULL);
k_thread_name_set(&data->dio1_wq.thread, "lr11xx_dio1");
/* Check SPI bus */
if (!spi_is_ready_dt(&cfg->bus)) {
LOG_ERR("SPI bus not ready");
@@ -0,0 +1,68 @@
/*
* 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 duty cycle mode
*
* When enabled, the radio alternates between RX and sleep using the
* RadioLib preamble detection algorithm. Saves ~60-70% RX current.
*
* @param dev LoRa device
* @param enable true to enable, false for continuous RX
*/
void sx126x_set_rx_duty_cycle(const struct device *dev, bool enable);
/**
* @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);
#ifdef __cplusplus
}
#endif
#endif /* SX126X_EXT_H */
@@ -1,173 +0,0 @@
diff --git a/drivers/lora/loramac-node/sx12xx_common.c b/drivers/lora/loramac-node/sx12xx_common.c
index 17689720dd2..85186893fc8 100644
--- a/drivers/lora/loramac-node/sx12xx_common.c
+++ b/drivers/lora/loramac-node/sx12xx_common.c
@@ -3,6 +3,9 @@
* Copyright (c) 2020 Grinn
*
* SPDX-License-Identifier: Apache-2.0
+ *
+ * ZEPHCORE PATCH: Modified sx12xx_ev_rx_error() to notify async callback
+ * with NULL data on RX errors (CRC/header). This allows error counting.
*/
#include <zephyr/drivers/gpio.h>
@@ -38,6 +41,12 @@ static struct sx12xx_data {
struct lora_modem_config tx_cfg;
atomic_t modem_usage;
struct sx12xx_rx_params rx_params;
+ /* Fast TX↔RX switching: cache last full config to detect
+ * direction-only changes and skip redundant SPI reconfigure. */
+ struct lora_modem_config last_cfg;
+ bool last_cfg_valid;
+ bool tx_configured; /* RadioSetTxConfig has been called with current params */
+ bool rx_configured; /* RadioSetRxConfig has been called with current params */
} dev_data;
int __sx12xx_configure_pin(const struct gpio_dt_spec *gpio, gpio_flags_t flags)
@@ -179,6 +188,13 @@ static void sx12xx_ev_rx_error(void)
if (dev_data.async_rx_cb) {
/* Start receiving again */
Radio.Rx(0);
+ /*
+ * ZEPHCORE PATCH: Notify callback with NULL data to indicate
+ * RX error (CRC mismatch, header error). This allows the
+ * application to count receive errors for diagnostics.
+ */
+ dev_data.async_rx_cb(dev_data.dev, NULL, 0, 0, 0,
+ dev_data.async_user_data);
/* Don't run the synchronous code */
return;
}
@@ -195,26 +211,24 @@ static void sx12xx_ev_rx_error(void)
/**
* @brief Convert Zephyr bandwidth enum to loramac-node bandwidth index
*
- * The loramac-node library expects bandwidth as an index (0, 1, 2) into its
- * internal Bandwidths[] array, not the actual kHz value.
- *
- * @param bandwidth Zephyr lora_signal_bandwidth enum value
- * @param bw_idx Pointer to store the resulting bandwidth index
- * @return 0 on success, -EINVAL if bandwidth is not supported
+ * The loramac-node library expects bandwidth as an index into its internal
+ * Bandwidths[] array: {BW_007, BW_010, BW_015, BW_020, BW_031,
+ * BW_041, BW_062, BW_125, BW_250, BW_500}
*/
static int sx12xx_get_bandwidth_idx(enum lora_signal_bandwidth bandwidth,
uint32_t *bw_idx)
{
switch (bandwidth) {
- case BW_125_KHZ:
- *bw_idx = 0;
- break;
- case BW_250_KHZ:
- *bw_idx = 1;
- break;
- case BW_500_KHZ:
- *bw_idx = 2;
- break;
+ case BW_7_KHZ: *bw_idx = 0; break;
+ case BW_10_KHZ: *bw_idx = 1; break;
+ case BW_15_KHZ: *bw_idx = 2; break;
+ case BW_20_KHZ: *bw_idx = 3; break;
+ case BW_31_KHZ: *bw_idx = 4; break;
+ case BW_41_KHZ: *bw_idx = 5; break;
+ case BW_62_KHZ: *bw_idx = 6; break;
+ case BW_125_KHZ: *bw_idx = 7; break;
+ case BW_250_KHZ: *bw_idx = 8; break;
+ case BW_500_KHZ: *bw_idx = 9; break;
default:
return -EINVAL;
}
@@ -225,7 +239,6 @@ uint32_t sx12xx_airtime(const struct device *dev, uint32_t data_len)
{
uint32_t bw_idx;
- /* Translate bandwidth to loramac-node index, default to 0 if invalid */
if (sx12xx_get_bandwidth_idx(dev_data.tx_cfg.bandwidth, &bw_idx) < 0) {
bw_idx = 0;
}
@@ -375,6 +388,21 @@ int sx12xx_lora_recv_async(const struct device *dev, lora_recv_cb cb, void *user
return 0;
}
+/* Check if only the TX/RX direction changed (all radio params identical). */
+static bool sx12xx_only_direction_changed(const struct lora_modem_config *a,
+ const struct lora_modem_config *b)
+{
+ return a->frequency == b->frequency &&
+ a->bandwidth == b->bandwidth &&
+ a->datarate == b->datarate &&
+ a->coding_rate == b->coding_rate &&
+ a->preamble_len == b->preamble_len &&
+ a->tx_power == b->tx_power &&
+ a->iq_inverted == b->iq_inverted &&
+ a->public_network == b->public_network &&
+ a->tx != b->tx;
+}
+
int sx12xx_lora_config(const struct device *dev,
struct lora_modem_config *config)
{
@@ -388,6 +416,39 @@ int sx12xx_lora_config(const struct device *dev,
return ret;
}
+ /* Fast path: if only TX↔RX direction changed and the target direction
+ * was already configured once with the same params, skip the full
+ * RadioSetTxConfig/RadioSetRxConfig (saves ~35ms of SPI traffic).
+ * RadioSetTxConfig MUST have been called at least once to set
+ * TxTimeout=4000; RadioSetRxConfig MUST have been called at least
+ * once to set PayloadLength/SymbTimeout/IQ polarity workaround. */
+ if (dev_data.last_cfg_valid &&
+ sx12xx_only_direction_changed(config, &dev_data.last_cfg)) {
+ if (config->tx && dev_data.tx_configured) {
+ LOG_DBG("lora_config: fast TX switch (skip full reconfig)");
+ if (!modem_acquire(&dev_data)) {
+ return -EBUSY;
+ }
+ memcpy(&dev_data.tx_cfg, config, sizeof(dev_data.tx_cfg));
+ dev_data.last_cfg = *config;
+ modem_release(&dev_data);
+ return 0;
+ }
+ if (!config->tx && dev_data.rx_configured) {
+ LOG_DBG("lora_config: fast RX switch (skip full reconfig)");
+ if (!modem_acquire(&dev_data)) {
+ return -EBUSY;
+ }
+ dev_data.last_cfg = *config;
+ modem_release(&dev_data);
+ return 0;
+ }
+ }
+
+ LOG_INF("lora_config: bw_enum=%d bw_idx=%u tx=%d freq=%u sf=%d",
+ config->bandwidth, bw_idx, config->tx, config->frequency,
+ config->datarate);
+
/* Ensure available, decremented after configuration */
if (!modem_acquire(&dev_data)) {
return -EBUSY;
@@ -403,16 +464,21 @@ int sx12xx_lora_config(const struct device *dev,
bw_idx, config->datarate,
config->coding_rate, config->preamble_len,
false, crc, 0, 0, config->iq_inverted, 4000);
+ dev_data.tx_configured = true;
} else {
/* TODO: Get symbol timeout value from config parameters */
Radio.SetRxConfig(MODEM_LORA, bw_idx,
config->datarate, config->coding_rate,
0, config->preamble_len, 10, false, 0,
crc, false, 0, config->iq_inverted, true);
+ dev_data.rx_configured = true;
}
Radio.SetPublicNetwork(config->public_network);
+ dev_data.last_cfg = *config;
+ dev_data.last_cfg_valid = true;
+
modem_release(&dev_data);
return 0;
}
@@ -1,8 +1,178 @@
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
index 8e0ca45c271..828faaaf37f 100644
index 8e0ca45c271..1816ff8f924 100644
--- a/drivers/lora/native/sx126x/sx126x.c
+++ b/drivers/lora/native/sx126x/sx126x.c
@@ -451,7 +451,14 @@ static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx)
@@ -9,10 +9,19 @@
#include <zephyr/sys/byteorder.h>
#include "sx126x.h"
+#include "sx126x_ext.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sx126x, CONFIG_LORA_LOG_LEVEL);
+/* Dedicated DIO1 work queue — keeps LoRa interrupt processing off the
+ * system work queue so USB/BLE/timer work items cannot delay packet RX. */
+#define SX126X_DIO1_WQ_STACK_SIZE 1536
+K_THREAD_STACK_DEFINE(sx126x_dio1_wq_stack, SX126X_DIO1_WQ_STACK_SIZE);
+
+/* Register not in sx126x_regs.h — only used for §15.3 workaround */
+#define SX126X_REG_EVT_CLR 0x0920
+
static uint8_t bandwidth_to_reg(enum lora_signal_bandwidth bw)
{
switch (bw) {
@@ -172,22 +181,10 @@ static int sx126x_calibrate_image(const struct device *dev, uint32_t freq)
{
uint8_t buf[2];
- if (freq > 900000000) {
- buf[0] = 0xE1;
- buf[1] = 0xE9;
- } else if (freq > 850000000) {
- buf[0] = 0xD7;
- buf[1] = 0xDB;
- } else if (freq > 770000000) {
- buf[0] = 0xC1;
- buf[1] = 0xC5;
- } else if (freq > 460000000) {
- buf[0] = 0x75;
- buf[1] = 0x81;
- } else {
- buf[0] = 0x6B;
- buf[1] = 0x6F;
- }
+ /* Calibrate ±2 MHz around the actual frequency.
+ * CalibrateImage bytes are in 4 MHz steps (freq_Hz / 4000000). */
+ buf[0] = (uint8_t)((freq - 2000000) / 4000000);
+ buf[1] = (uint8_t)((freq + 2000000) / 4000000);
return sx126x_hal_write_cmd(dev, SX126X_CMD_CALIBRATE_IMAGE, buf, 2);
}
@@ -267,6 +264,7 @@ static int sx126x_set_packet_params(const struct device *dev,
uint8_t invert_iq)
{
uint8_t buf[6];
+ int ret;
sys_put_be16(preamble_len, &buf[0]);
buf[2] = header_type;
@@ -274,7 +272,28 @@ static int sx126x_set_packet_params(const struct device *dev,
buf[4] = crc_mode;
buf[5] = invert_iq;
- return sx126x_hal_write_cmd(dev, SX126X_CMD_SET_PACKET_PARAMS, buf, 6);
+ ret = sx126x_hal_write_cmd(dev, SX126X_CMD_SET_PACKET_PARAMS, buf, 6);
+ if (ret < 0) {
+ return ret;
+ }
+
+ /* §15.4 IQ Polarity workaround (datasheet errata).
+ * After SetPacketParams, register 0x0736 bit 2 must be:
+ * SET for standard IQ (non-inverted)
+ * CLEAR for inverted IQ
+ * Without this fix, inverted-IQ packets are not received. */
+ uint8_t iq_val;
+
+ ret = sx126x_hal_read_regs(dev, SX126X_REG_IQ_POLARITY, &iq_val, 1);
+ if (ret < 0) {
+ return ret;
+ }
+ if (invert_iq == SX126X_LORA_IQ_INVERTED) {
+ iq_val &= ~BIT(2);
+ } else {
+ iq_val |= BIT(2);
+ }
+ return sx126x_hal_write_regs(dev, SX126X_REG_IQ_POLARITY, &iq_val, 1);
}
static int sx126x_set_sync_word(const struct device *dev, bool public_network)
@@ -307,6 +326,7 @@ static int sx126x_set_tx(const struct device *dev, uint32_t timeout_ms)
static int sx126x_set_rx(const struct device *dev, uint32_t timeout_ms)
{
uint32_t timeout;
+ int ret;
if (timeout_ms == 0) {
timeout = SX126X_RX_TIMEOUT_CONTINUOUS;
@@ -317,7 +337,23 @@ static int sx126x_set_rx(const struct device *dev, uint32_t timeout_ms)
uint8_t buf[3];
sys_put_be24(timeout, buf);
- return sx126x_hal_write_cmd(dev, SX126X_CMD_SET_RX, buf, 3);
+ ret = sx126x_hal_write_cmd(dev, SX126X_CMD_SET_RX, buf, 3);
+ if (ret < 0) {
+ return ret;
+ }
+
+ /* §15.3 Implicit header mode timeout workaround (datasheet errata).
+ * After SetRx(), clear bit 0 of register 0x0920 to prevent the
+ * RX timer from continuing to run after reception completes.
+ * Applied unconditionally per Semtech reference implementation. */
+ uint8_t evt;
+
+ ret = sx126x_hal_read_regs(dev, SX126X_REG_EVT_CLR, &evt, 1);
+ if (ret < 0) {
+ return ret;
+ }
+ evt &= ~BIT(0);
+ return sx126x_hal_write_regs(dev, SX126X_REG_EVT_CLR, &evt, 1);
}
static int sx126x_get_rx_buffer_status(const struct device *dev,
@@ -341,12 +377,18 @@ static int sx126x_get_packet_status(const struct device *dev,
uint8_t buf[3];
int ret;
- ret = sx126x_hal_read_cmd(dev, SX126X_CMD_GET_PACKET_STATUS, buf, 2);
+ ret = sx126x_hal_read_cmd(dev, SX126X_CMD_GET_PACKET_STATUS, buf, 3);
if (ret == 0) {
/* RSSI is -value/2 dBm */
- *rssi = -((int16_t)buf[0] >> 1);
+ int16_t pkt_rssi = -((int16_t)buf[0] >> 1);
/* SNR is value/4 dB (signed) */
*snr = ((int8_t)buf[1]) >> 2;
+ /* SignalRssiPkt (byte 2) = LoRa signal RSSI excluding noise.
+ * When SNR < 0 the packet RSSI is dominated by noise —
+ * use the signal-only estimate for a more accurate reading. */
+ int16_t sig_rssi = -((int16_t)buf[2] >> 1);
+
+ *rssi = (*snr < 0 && sig_rssi > pkt_rssi) ? sig_rssi : pkt_rssi;
}
return ret;
@@ -419,6 +461,21 @@ static int sx126x_chip_init(const struct device *dev)
return ret;
}
+ /* After TX/RX, fall back to STDBY_XOSC instead of STDBY_RC.
+ * Keeps the crystal warm so the next SetRx skips the ~500 us
+ * XOSC startup, reducing the deaf window between packets. */
+ {
+ uint8_t fallback = 0x40; /* STDBY_XOSC */
+
+ ret = sx126x_hal_write_cmd(dev,
+ SX126X_CMD_SET_RX_TX_FALLBACK_MODE,
+ &fallback, 1);
+ if (ret < 0) {
+ LOG_ERR("Set fallback mode failed: %d", ret);
+ return ret;
+ }
+ }
+
/* Configure IRQs on DIO1: TX done, RX done, timeout */
uint16_t irq_mask = SX126X_IRQ_TX_DONE | SX126X_IRQ_RX_DONE |
SX126X_IRQ_RX_TX_TIMEOUT | SX126X_IRQ_CRC_ERR;
@@ -443,7 +500,7 @@ static void sx126x_dio1_callback(const struct device *dev)
{
struct sx126x_data *data = dev->data;
- k_work_submit(&data->irq_work);
+ k_work_submit_to_queue(&data->dio1_wq, &data->irq_work);
}
static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx)
@@ -451,11 +508,108 @@ static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx)
const struct sx126x_hal_config *config = dev->config;
sx126x_hal_set_antenna_enable(dev, enable);
@@ -18,3 +188,319 @@ index 8e0ca45c271..828faaaf37f 100644
sx126x_hal_set_rf_switch(dev, enable && tx);
}
}
+/* ── RX duty cycle (RadioLib algorithm) ─────────────────────────────── */
+
+#define SX126X_DC_MIN_SYMBOLS_SF7_PLUS 8
+#define SX126X_DC_MIN_SYMBOLS_SF6_LESS 12
+#define SX126X_DC_TCXO_DELAY_US 1000
+
+static void sx126x_apply_rx_duty_cycle(struct sx126x_data *data)
+{
+ const struct device *dev = data->dev;
+ struct lora_modem_config *mc = &data->config;
+
+ uint8_t sf = (uint8_t)mc->datarate;
+ uint32_t bw_hz = bandwidth_to_hz(mc->bandwidth);
+ float bw_khz = (float)bw_hz / 1000.0f;
+ uint16_t preamble_len = mc->preamble_len;
+
+ uint16_t min_symbols = (sf >= 7) ? SX126X_DC_MIN_SYMBOLS_SF7_PLUS
+ : SX126X_DC_MIN_SYMBOLS_SF6_LESS;
+
+ int16_t sleep_symbols = (int16_t)preamble_len - (int16_t)min_symbols;
+ if (sleep_symbols <= 0) {
+ LOG_WRN("Preamble too short for duty cycle (need >%d, have %d)",
+ min_symbols, preamble_len);
+ data->rx_duty_cycle_enabled = false;
+ sx126x_set_rx(dev, 0);
+ return;
+ }
+
+ uint32_t symbol_us = (uint32_t)((float)(1 << sf) * 1000.0f / bw_khz);
+
+ /* Shave 2 symbols off sleep for timing margin */
+ int16_t sleep_symbols_safe = sleep_symbols - 2;
+ if (sleep_symbols_safe < 1) {
+ sleep_symbols_safe = 1;
+ }
+ uint32_t sleep_period_us = (uint16_t)sleep_symbols_safe * symbol_us;
+
+ uint32_t preamble_total_us = (preamble_len + 1) * symbol_us;
+ int32_t wake_calc1 = ((int32_t)preamble_total_us -
+ ((int32_t)sleep_period_us - SX126X_DC_TCXO_DELAY_US)) / 2;
+ uint32_t wake_calc2 = (min_symbols + 1) * symbol_us;
+
+ uint32_t wake_period_us = (wake_calc1 > 0 && (uint32_t)wake_calc1 > wake_calc2)
+ ? (uint32_t)wake_calc1 : wake_calc2;
+
+ /* SetRxDutyCycle takes times in 15.625us steps (multiply by 64/1000) */
+ uint32_t rx_time = (wake_period_us * 64) / 1000;
+ uint32_t sleep_time = (sleep_period_us * 64) / 1000;
+
+ if (rx_time < 64) {
+ rx_time = 64;
+ }
+ if (sleep_time < 64) {
+ sleep_time = 64;
+ }
+
+ /* SPI command: 3 bytes rx_period + 3 bytes sleep_period */
+ uint8_t buf[6];
+
+ sys_put_be24(rx_time, &buf[0]);
+ sys_put_be24(sleep_time, &buf[3]);
+ sx126x_hal_write_cmd(dev, SX126X_CMD_SET_RX_DUTY_CYCLE, buf, 6);
+
+ uint32_t rx_ms = (rx_time * 1000) / 64000;
+ uint32_t sleep_ms = (sleep_time * 1000) / 64000;
+ LOG_DBG("RX duty cycle: SF%d rx=%ums sleep=%ums", sf, rx_ms, sleep_ms);
+}
+
+/* ── Lightweight RX restart ─────────────────────────────────────────── */
+
+/* Restart RX as fast as possible — used for RX→RX transitions in the
+ * IRQ handler. Sends SetRx(continuous) directly, skipping the §15.3
+ * workaround (only needed for implicit-header timed RX). This saves
+ * 2 SPI register transactions (~200 us) on every received packet. */
+static void sx126x_restart_rx(const struct device *dev, struct sx126x_data *data)
+{
+ if (data->rx_duty_cycle_enabled) {
+ sx126x_apply_rx_duty_cycle(data);
+ } else {
+ uint8_t buf[3];
+
+ sys_put_be24(SX126X_RX_TIMEOUT_CONTINUOUS, buf);
+ sx126x_hal_write_cmd(dev, SX126X_CMD_SET_RX, buf, 3);
+ }
+
+ if (data->rx_boost_enabled) {
+ sx126x_set_rx_gain(dev, true);
+ }
+}
+
static void sx126x_handle_irq_tx_done(const struct device *dev)
{
struct sx126x_data *data = dev->data;
@@ -510,12 +664,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) {
- /* Async mode - call callback and restart RX */
- data->rx_cb(dev, data->rx_buf, result.len,
- result.rssi, result.snr,
- data->rx_cb_user_data);
- /* Restart RX for continuous reception */
- sx126x_set_rx(dev, 0);
+ /* Restart RX FIRST — minimise the deaf window.
+ * The callback (mesh processing) can take 100s of us;
+ * doing it before restart would lose back-to-back packets.
+ * Safe: we're on a cooperative work queue so the next
+ * DIO1 work item won't preempt us, and the callback
+ * copies rx_buf before returning. */
+ sx126x_restart_rx(dev, data);
+
+ if (result.status < 0) {
+ data->rx_cb(dev, NULL, 0,
+ result.rssi, result.snr,
+ data->rx_cb_user_data);
+ } else {
+ data->rx_cb(dev, data->rx_buf, result.len,
+ result.rssi, result.snr,
+ data->rx_cb_user_data);
+ }
} else {
/* Sync mode */
atomic_set(&data->state, SX126X_STATE_IDLE);
@@ -621,6 +786,29 @@ static int sx126x_lora_config(const struct device *dev,
goto out;
}
+ /* §15.1 TX Modulation workaround (datasheet errata).
+ * For 500 kHz BW, clear bit 2 of register 0x0889.
+ * For all other bandwidths, set bit 2 (restore default). */
+ {
+ uint8_t txmod;
+
+ ret = sx126x_hal_read_regs(dev, SX126X_REG_TX_MODULATION,
+ &txmod, 1);
+ if (ret < 0) {
+ goto out;
+ }
+ if (config->bandwidth == BW_500_KHZ) {
+ txmod &= ~BIT(2);
+ } else {
+ txmod |= BIT(2);
+ }
+ ret = sx126x_hal_write_regs(dev, SX126X_REG_TX_MODULATION,
+ &txmod, 1);
+ if (ret < 0) {
+ goto out;
+ }
+ }
+
/* Set sync word */
ret = sx126x_set_sync_word(dev, config->public_network);
if (ret < 0) {
@@ -691,6 +879,29 @@ static int sx126x_lora_send_async(const struct device *dev,
/* Enable antenna and set TX path */
sx126x_set_rf_path(dev, true, true);
+ /* §15.2 TX Clamp workaround (datasheet errata) — SX1262 only.
+ * Set bits [4:1] of register 0x08D8 before SetTx to prevent
+ * PA overshoot that can damage the device. */
+ {
+ const struct sx126x_hal_config *hal_cfg = dev->config;
+
+ if (!hal_cfg->is_sx1261) {
+ uint8_t clamp;
+
+ ret = sx126x_hal_read_regs(dev, SX126X_REG_TX_CLAMP_CFG,
+ &clamp, 1);
+ if (ret < 0) {
+ goto out_error;
+ }
+ clamp |= 0x1E;
+ ret = sx126x_hal_write_regs(dev, SX126X_REG_TX_CLAMP_CFG,
+ &clamp, 1);
+ if (ret < 0) {
+ goto out_error;
+ }
+ }
+ }
+
/* Start transmission with 10 second timeout */
ret = sx126x_set_tx(dev, 10000);
if (ret < 0) {
@@ -974,6 +1185,77 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
return 0;
}
+/* ── Extension API (sx126x_ext.h) ──────────────────────────────────── */
+
+int16_t sx126x_get_rssi_inst(const struct device *dev)
+{
+ struct sx126x_data *data = dev->data;
+ uint8_t buf[1];
+ int ret;
+
+ if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) {
+ return -128;
+ }
+
+ ret = sx126x_hal_read_cmd(dev, SX126X_CMD_GET_RSSI_INST, buf, 1);
+ k_mutex_unlock(&data->lock);
+
+ if (ret < 0) {
+ return -128;
+ }
+
+ return -((int16_t)buf[0] >> 1);
+}
+
+bool sx126x_is_receiving(const struct device *dev)
+{
+ struct sx126x_data *data = dev->data;
+ uint16_t irq_status = 0;
+
+ if (k_mutex_lock(&data->lock, K_NO_WAIT) != 0) {
+ return false;
+ }
+
+ sx126x_get_irq_status(dev, &irq_status);
+ k_mutex_unlock(&data->lock);
+
+ return (irq_status & (SX126X_IRQ_PREAMBLE_DETECTED |
+ SX126X_IRQ_HEADER_VALID)) != 0;
+}
+
+void sx126x_set_rx_duty_cycle(const struct device *dev, bool enable)
+{
+ struct sx126x_data *data = dev->data;
+
+ data->rx_duty_cycle_enabled = enable;
+ LOG_DBG("RX duty cycle %s", enable ? "enabled" : "disabled");
+
+ /* If currently in RX, apply immediately */
+ if (atomic_get(&data->state) == SX126X_STATE_RX) {
+ k_mutex_lock(&data->lock, K_FOREVER);
+ if (enable && data->config_valid) {
+ sx126x_apply_rx_duty_cycle(data);
+ } else {
+ sx126x_set_rx(dev, 0);
+ }
+ if (data->rx_boost_enabled) {
+ sx126x_set_rx_gain(dev, true);
+ }
+ k_mutex_unlock(&data->lock);
+ }
+}
+
+void sx126x_set_rx_boost(const struct device *dev, bool enable)
+{
+ struct sx126x_data *data = dev->data;
+
+ data->rx_boost_enabled = enable;
+
+ k_mutex_lock(&data->lock, K_FOREVER);
+ sx126x_set_rx_gain(dev, enable);
+ k_mutex_unlock(&data->lock);
+}
+
static const struct lora_driver_api sx126x_lora_api = {
.config = sx126x_lora_config,
.send = sx126x_lora_send,
@@ -999,6 +1281,14 @@ static int sx126x_init(const struct device *dev)
data->dev = dev;
atomic_set(&data->state, SX126X_STATE_IDLE);
data->config_valid = false;
+ data->rx_duty_cycle_enabled = false;
+ data->rx_boost_enabled = false;
+
+ /* Start dedicated DIO1 work queue */
+ k_work_queue_start(&data->dio1_wq, sx126x_dio1_wq_stack,
+ K_THREAD_STACK_SIZEOF(sx126x_dio1_wq_stack),
+ CONFIG_SYSTEM_WORKQUEUE_PRIORITY, NULL);
+ k_thread_name_set(&data->dio1_wq.thread, "sx126x_dio1");
/* Initialize HAL */
ret = sx126x_hal_init(dev);
diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h
index 1490a010254..ef5b15502db 100644
--- a/drivers/lora/native/sx126x/sx126x.h
+++ b/drivers/lora/native/sx126x/sx126x.h
@@ -59,7 +59,12 @@ struct sx126x_data {
/* Deferred work for interrupt handling */
struct k_work irq_work;
+ struct k_work_q dio1_wq;
const struct device *dev;
+
+ /* Extension features (duty cycle, boost) */
+ bool rx_duty_cycle_enabled;
+ bool rx_boost_enabled;
};
#endif /* ZEPHYR_DRIVERS_LORA_SX126X_SX126X_INTERNAL_H_ */
diff --git a/drivers/lora/native/sx126x/sx126x_hal.c b/drivers/lora/native/sx126x/sx126x_hal.c
index bdf962e3343..de60d18b694 100644
--- a/drivers/lora/native/sx126x/sx126x_hal.c
+++ b/drivers/lora/native/sx126x/sx126x_hal.c
@@ -101,8 +101,16 @@ bool sx126x_hal_is_busy(const struct device *dev)
int sx126x_hal_wait_busy(const struct device *dev, uint32_t timeout_ms)
{
+ /* Fast path: most SX126x commands finish BUSY in <650 us.
+ * Busy-wait first to avoid the ~1 ms k_msleep() scheduler
+ * penalty, which would otherwise dominate RX turnaround. */
+ if (WAIT_FOR(!sx126x_hal_is_busy(dev), 1000, k_busy_wait(10))) {
+ return 0;
+ }
+
+ /* Slow path: long operations like calibration (several ms). */
if (!WAIT_FOR(!sx126x_hal_is_busy(dev),
- timeout_ms * 1000,
+ (timeout_ms * 1000) - 1000,
k_msleep(1))) {
LOG_WRN("Busy timeout after %u ms", timeout_ms);
return -ETIMEDOUT;
@@ -1,17 +0,0 @@
diff --git a/drivers/lora/loramac-node/sx126x_standalone.c b/drivers/lora/loramac-node/sx126x_standalone.c
index c9b16965bc7..3a2ff8b20c9 100644
--- a/drivers/lora/loramac-node/sx126x_standalone.c
+++ b/drivers/lora/loramac-node/sx126x_standalone.c
@@ -40,8 +40,11 @@ uint32_t sx126x_get_dio1_pin_state(struct sx126x_data *dev_data)
void sx126x_dio1_irq_enable(struct sx126x_data *dev_data)
{
- gpio_pin_interrupt_configure_dt(&sx126x_gpio_dio1,
+ int ret = gpio_pin_interrupt_configure_dt(&sx126x_gpio_dio1,
GPIO_INT_EDGE_TO_ACTIVE);
+ if (ret != 0) {
+ LOG_ERR("DIO1 irq enable FAILED: %d", ret);
+ }
}
void sx126x_dio1_irq_disable(struct sx126x_data *dev_data)
+6 -3
View File
@@ -216,9 +216,12 @@ static void contact_iter_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
#ifdef ZEPHCORE_LORA
/* Check if send queue has space before continuing iteration
* Match Arduino's isWriteBusy(): busy when queue >= 2/3 full (8 of 12)
*/
/* Don't iterate while BLE TX is congested — wait for drain to clear.
* Also check 2/3 high-water mark (catches stray frames before full). */
if (zephcore_ble_is_congested()) {
return;
}
uint32_t used = k_msgq_num_used_get(zephcore_ble_get_send_queue());
uint32_t queue_size = CONFIG_ZEPHCORE_BLE_QUEUE_SIZE;
bool has_space = (used < (queue_size * 2 / 3));