diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index ab9745c..d0f305c 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -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 ========== diff --git a/zephcore/adapters/ble/ZephyrBLE.cpp b/zephcore/adapters/ble/ZephyrBLE.cpp index 0666a3d..547d2ae 100644 --- a/zephcore/adapters/ble/ZephyrBLE.cpp +++ b/zephcore/adapters/ble/ZephyrBLE.cpp @@ -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) { diff --git a/zephcore/adapters/ble/ZephyrBLE.h b/zephcore/adapters/ble/ZephyrBLE.h index 81425e6..aab05b1 100644 --- a/zephcore/adapters/ble/ZephyrBLE.h +++ b/zephcore/adapters/ble/ZephyrBLE.h @@ -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. */ diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index 4e0fc5e..5fd5660 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -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; diff --git a/zephcore/adapters/radio/SX126xRadio.cpp b/zephcore/adapters/radio/SX126xRadio.cpp index df4b3e8..f6a4754 100644 --- a/zephcore/adapters/radio/SX126xRadio.cpp +++ b/zephcore/adapters/radio/SX126xRadio.cpp @@ -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 #include -/* - * 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 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(&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 */ diff --git a/zephcore/adapters/radio/SX126xRadio.h b/zephcore/adapters/radio/SX126xRadio.h index 9bd5977..8c2da5b 100644 --- a/zephcore/adapters/radio/SX126xRadio.h +++ b/zephcore/adapters/radio/SX126xRadio.h @@ -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 */ diff --git a/zephcore/adapters/radio/radio_common.h b/zephcore/adapters/radio/radio_common.h index 9113e13..fa18f59 100644 --- a/zephcore/adapters/radio/radio_common.h +++ b/zephcore/adapters/radio/radio_common.h @@ -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 diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index fe0bea5..e08fe34 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -35,6 +35,7 @@ CONFIG_REBOOT=y # ========== LoRa ========== CONFIG_LORA=y +CONFIG_LORA_MODULE_BACKEND_NATIVE=y CONFIG_REGULATOR=y # ========== Display Subsystem ========== diff --git a/zephcore/patches/loramac-node/0001-bandwidth-array-ldro.patch b/zephcore/patches/loramac-node/0001-bandwidth-array-ldro.patch deleted file mode 100644 index c9ae812..0000000 --- a/zephcore/patches/loramac-node/0001-bandwidth-array-ldro.patch +++ /dev/null @@ -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; - } diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c index d3520ae..565b473 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -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 TX→RX. + * + * 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 CAD→RX 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"); diff --git a/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h b/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h new file mode 100644 index 0000000..5f74515 --- /dev/null +++ b/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h @@ -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 +#include +#include + +#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 */ diff --git a/zephcore/patches/zephyr/0002-lora-sx12xx-common.patch b/zephcore/patches/zephyr/0002-lora-sx12xx-common.patch deleted file mode 100644 index 0efca66..0000000 --- a/zephcore/patches/zephyr/0002-lora-sx12xx-common.patch +++ /dev/null @@ -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 -@@ -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; - } diff --git a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch index 3e67c92..3b04af2 100644 --- a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch +++ b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch @@ -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 + + #include "sx126x.h" ++#include "sx126x_ext.h" + + #include + 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; diff --git a/zephcore/patches/zephyr/0004-lora-sx126x-standalone.patch b/zephcore/patches/zephyr/0004-lora-sx126x-standalone.patch deleted file mode 100644 index 8f1df59..0000000 --- a/zephcore/patches/zephyr/0004-lora-sx126x-standalone.patch +++ /dev/null @@ -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) diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 317ad88..6e6650b 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -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));