diff --git a/zephcore/adapters/radio/LR1110Radio.cpp b/zephcore/adapters/radio/LR1110Radio.cpp index d372b6a..c801c32 100644 --- a/zephcore/adapters/radio/LR1110Radio.cpp +++ b/zephcore/adapters/radio/LR1110Radio.cpp @@ -41,23 +41,6 @@ void LR1110Radio::hwConfigure(const struct lora_modem_config &cfg) } } -void LR1110Radio::hwStartReceive() -{ - int ret = lora_recv_async(_dev, rxCallbackStatic, this); - if (ret < 0) { - LOG_ERR("lora_recv_async failed: %d", ret); - atomic_set(&_in_recv_mode, 0); - return; - } - atomic_set(&_in_recv_mode, 1); - - /* RX boost: set once via setRxBoost(), LR1110 SetRxBoosted - * command persists through SetRx calls. */ - if (_rx_duty_cycle_enabled) { - lr11xx_set_rx_duty_cycle(_dev, true); - } -} - void LR1110Radio::hwCancelReceive() { lora_recv_async(_dev, NULL, NULL); @@ -84,11 +67,6 @@ void LR1110Radio::hwSetRxBoost(bool enable) lr11xx_set_rx_boost(_dev, enable); } -void LR1110Radio::hwSetRxDutyCycle(bool enable) -{ - lr11xx_set_rx_duty_cycle(_dev, enable); -} - void LR1110Radio::hwResetAGC() { /* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX boost */ diff --git a/zephcore/adapters/radio/LR1110Radio.h b/zephcore/adapters/radio/LR1110Radio.h index 35df561..3da2d50 100644 --- a/zephcore/adapters/radio/LR1110Radio.h +++ b/zephcore/adapters/radio/LR1110Radio.h @@ -21,14 +21,12 @@ public: protected: /* Hardware primitives */ void hwConfigure(const struct lora_modem_config &cfg) override; - void hwStartReceive() override; void hwCancelReceive() override; int hwSendAsync(uint8_t *buf, uint32_t len, struct k_poll_signal *sig) override; int16_t hwGetCurrentRSSI() override; bool hwIsPreambleDetected() override; void hwSetRxBoost(bool enable) override; - void hwSetRxDutyCycle(bool enable) override; void hwResetAGC() override; }; diff --git a/zephcore/adapters/radio/LR2021Radio.cpp b/zephcore/adapters/radio/LR2021Radio.cpp index 41cb0de..84f689a 100644 --- a/zephcore/adapters/radio/LR2021Radio.cpp +++ b/zephcore/adapters/radio/LR2021Radio.cpp @@ -41,21 +41,6 @@ void LR2021Radio::hwConfigure(const struct lora_modem_config &cfg) } } -void LR2021Radio::hwStartReceive() -{ - int ret = lora_recv_async(_dev, rxCallbackStatic, this); - if (ret < 0) { - LOG_ERR("lora_recv_async failed: %d", ret); - atomic_set(&_in_recv_mode, 0); - return; - } - atomic_set(&_in_recv_mode, 1); - - if (_rx_duty_cycle_enabled) { - lr20xx_set_rx_duty_cycle(_dev, true); - } -} - void LR2021Radio::hwCancelReceive() { lora_recv_async(_dev, NULL, NULL); @@ -82,11 +67,6 @@ void LR2021Radio::hwSetRxBoost(bool enable) lr20xx_set_rx_boost(_dev, enable); } -void LR2021Radio::hwSetRxDutyCycle(bool enable) -{ - lr20xx_set_rx_duty_cycle(_dev, enable); -} - void LR2021Radio::hwResetAGC() { lr20xx_reset_agc(_dev); diff --git a/zephcore/adapters/radio/LR2021Radio.h b/zephcore/adapters/radio/LR2021Radio.h index 06aa6b4..16b1384 100644 --- a/zephcore/adapters/radio/LR2021Radio.h +++ b/zephcore/adapters/radio/LR2021Radio.h @@ -21,14 +21,12 @@ public: protected: /* Hardware primitives */ void hwConfigure(const struct lora_modem_config &cfg) override; - void hwStartReceive() override; void hwCancelReceive() override; int hwSendAsync(uint8_t *buf, uint32_t len, struct k_poll_signal *sig) override; int16_t hwGetCurrentRSSI() override; bool hwIsPreambleDetected() override; void hwSetRxBoost(bool enable) override; - void hwSetRxDutyCycle(bool enable) override; void hwResetAGC() override; }; diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index 99cb57c..bd4f3c8 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -210,6 +210,11 @@ void LoRaRadioBase::buildModemConfig(struct lora_modem_config &cfg, bool tx) cfg.iq_inverted = false; cfg.public_network = false; cfg.packet_crc_disable = false; + + /* LBT: driver performs hardware CAD before TX, returns -EBUSY if busy */ + if (tx) { + cfg.cad.mode = LORA_CAD_MODE_LBT; + } } /** @@ -374,7 +379,63 @@ void LoRaRadioBase::reconfigureWithParams(float freq, float bw, uint8_t sf, uint void LoRaRadioBase::startReceive() { configureRx(); - hwStartReceive(); + + int ret; + + if (_rx_duty_cycle_enabled) { + /* Compute duty cycle timing from modem config (RadioLib algorithm). + * The driver converts these to hardware-specific units. */ + struct lora_modem_config cfg; + buildModemConfig(cfg, false); + + uint8_t sf = (uint8_t)cfg.datarate; + uint32_t bw_hz = bandwidth_to_hz(cfg.bandwidth); + float bw_khz = (float)bw_hz / 1000.0f; + uint16_t preamble_len = cfg.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) { + uint32_t symbol_us = (uint32_t)((float)(1 << sf) * + 1000.0f / bw_khz); + int16_t safe = sleep_symbols - 2; + if (safe < 1) safe = 1; + uint32_t sleep_us = (uint16_t)safe * symbol_us; + + uint32_t preamble_us = (preamble_len + 1) * symbol_us; + int32_t w1 = ((int32_t)preamble_us - + ((int32_t)sleep_us - 1000)) / 2; + uint32_t w2 = (min_symbols + 1) * symbol_us; + uint32_t rx_us = (w1 > 0 && (uint32_t)w1 > w2) + ? (uint32_t)w1 : w2; + + ret = lora_recv_duty_cycle(_dev, + K_USEC(rx_us), + K_USEC(sleep_us), + rxCallbackStatic, this); + if (ret == 0) { + atomic_set(&_in_recv_mode, 1); + return; + } + if (ret != -ENOSYS) { + LOG_ERR("lora_recv_duty_cycle failed: %d", ret); + } + } else { + LOG_WRN("Preamble too short for duty cycle " + "(need >%d, have %d)", + min_symbols, preamble_len); + } + /* Fall through to normal recv_async */ + } + + ret = lora_recv_async(_dev, rxCallbackStatic, this); + if (ret < 0) { + LOG_ERR("lora_recv_async failed: %d", ret); + atomic_set(&_in_recv_mode, 0); + return; + } + atomic_set(&_in_recv_mode, 1); } /* ── RX/TX ────────────────────────────────────────────────────────────── */ @@ -524,18 +585,6 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold) return; } - /* Random delay 0-500 ms before sampling. Breaks phase-lock with - * periodic interference that might be synchronized with our fixed - * 5-second housekeeping cadence. */ - uint32_t jitter; - sys_rand_get(&jitter, sizeof(jitter)); - k_sleep(K_MSEC(jitter % 500)); - - /* Re-check after the delay — a packet may have arrived. */ - if (isReceiving()) { - return; - } - /* Median of multiple RSSI reads (~200 us). Rejects up to N/2-1 * outliers in either direction without the downward bias of min * or the spike sensitivity of average. Insertion sort is fine @@ -663,7 +712,10 @@ void LoRaRadioBase::enableRxDutyCycle(bool enable) _rx_duty_cycle_enabled = enable; LOG_INF("RX duty cycle %s", enable ? "enabled" : "disabled"); if (atomic_get(&_in_recv_mode)) { - hwSetRxDutyCycle(enable); + /* Restart receive to apply new duty cycle state */ + hwCancelReceive(); + atomic_set(&_in_recv_mode, 0); + startReceive(); } } diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index 223bcd7..5d77caa 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -83,9 +83,6 @@ protected: /** Apply lora_modem_config + any chip-specific extras (image cal, etc.) */ virtual void hwConfigure(const struct lora_modem_config &cfg) = 0; - /** Start async receive (lora_recv_async + boost/duty-cycle setup) */ - virtual void hwStartReceive() = 0; - /** Cancel current async receive */ virtual void hwCancelReceive() = 0; @@ -102,9 +99,6 @@ protected: /** Set RX LNA boost on/off */ virtual void hwSetRxBoost(bool enable) = 0; - /** Set RX duty cycle on/off (hardware level) */ - virtual void hwSetRxDutyCycle(bool enable) = 0; - /** Reset AGC (chip-specific, may be no-op) */ virtual void hwResetAGC() = 0; @@ -159,7 +153,7 @@ protected: struct lora_modem_config _last_cfg; bool _config_cached; - /* Static RX callback — passed to lora_recv_async() by subclass hwStartReceive() */ + /* Static RX callback — passed to lora_recv_async() / lora_recv_duty_cycle() */ static void rxCallbackStatic(const struct device *dev, uint8_t *data, uint16_t size, int16_t rssi, int8_t snr, void *user_data); diff --git a/zephcore/adapters/radio/SX126xRadio.cpp b/zephcore/adapters/radio/SX126xRadio.cpp index cb2245b..ebbbf43 100644 --- a/zephcore/adapters/radio/SX126xRadio.cpp +++ b/zephcore/adapters/radio/SX126xRadio.cpp @@ -41,23 +41,6 @@ void SX126xRadio::hwConfigure(const struct lora_modem_config &cfg) } } -void SX126xRadio::hwStartReceive() -{ - int ret = lora_recv_async(_dev, rxCallbackStatic, this); - if (ret < 0) { - LOG_ERR("lora_recv_async failed: %d", ret); - atomic_set(&_in_recv_mode, 0); - return; - } - atomic_set(&_in_recv_mode, 1); - - /* RX boost: set once via setRxBoost(), preserved by SX126x - * hardware retention registers (DS §9.6). */ - if (_rx_duty_cycle_enabled) { - sx126x_set_rx_duty_cycle(_dev, true); - } -} - void SX126xRadio::hwCancelReceive() { lora_recv_async(_dev, NULL, NULL); @@ -84,11 +67,6 @@ void SX126xRadio::hwSetRxBoost(bool enable) sx126x_set_rx_boost(_dev, enable); } -void SX126xRadio::hwSetRxDutyCycle(bool enable) -{ - sx126x_set_rx_duty_cycle(_dev, enable); -} - void SX126xRadio::hwResetAGC() { /* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX settings */ diff --git a/zephcore/adapters/radio/SX126xRadio.h b/zephcore/adapters/radio/SX126xRadio.h index 0df2372..081b667 100644 --- a/zephcore/adapters/radio/SX126xRadio.h +++ b/zephcore/adapters/radio/SX126xRadio.h @@ -19,14 +19,12 @@ public: protected: /* Hardware primitives */ void hwConfigure(const struct lora_modem_config &cfg) override; - void hwStartReceive() override; void hwCancelReceive() override; int hwSendAsync(uint8_t *buf, uint32_t len, struct k_poll_signal *sig) override; int16_t hwGetCurrentRSSI() override; bool hwIsPreambleDetected() override; void hwSetRxBoost(bool enable) override; - void hwSetRxDutyCycle(bool enable) override; void hwResetAGC() override; bool hwIsChipBusy() override; }; diff --git a/zephcore/adapters/radio/radio_common.h b/zephcore/adapters/radio/radio_common.h index 2c90a22..c4fd743 100644 --- a/zephcore/adapters/radio/radio_common.h +++ b/zephcore/adapters/radio/radio_common.h @@ -45,6 +45,24 @@ typedef void (*RadioTxDoneCallback)(void *user_data); /* --- Zephyr enum mapping utilities --- */ +/* Map Zephyr bandwidth enum to Hz */ +static inline uint32_t bandwidth_to_hz(enum lora_signal_bandwidth bw) +{ + switch (bw) { + case BW_7_KHZ: return 7812; + case BW_10_KHZ: return 10417; + case BW_15_KHZ: return 15625; + case BW_20_KHZ: return 20833; + case BW_31_KHZ: return 31250; + case BW_41_KHZ: return 41667; + case BW_62_KHZ: return 62500; + case BW_125_KHZ: return 125000; + case BW_250_KHZ: return 250000; + case BW_500_KHZ: return 500000; + default: return 125000; + } +} + /* Map kHz bandwidth value to Zephyr enum. * Input is (uint16_t)(float_bw) — truncated, e.g. 7.8→7, 10.4→10, 62.5→62. * Zephyr >=4.4 has narrow BWs (7-62 kHz); <=4.3 only has 125/250/500. */ 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 d8196a3..20a0510 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -83,6 +83,13 @@ struct lr11xx_data { bool rx_boost_enabled; bool rx_boost_applied; /* RX boost register written to hardware */ + /* CAD state */ + lora_cad_cb cad_cb; + void *cad_user_data; + struct k_sem cad_sem; + int cad_result; + bool cad_active; + /* Deferred hardware init — heavy SPI/radio work runs on first config() */ bool hw_initialized; @@ -439,6 +446,29 @@ static void lr11xx_dio1_work_handler(struct k_work *work) rx_restarted = true; } + /* ── CAD done ── */ + if (irq & LR11XX_SYSTEM_IRQ_CAD_DONE) { + bool detected = (irq & LR11XX_SYSTEM_IRQ_CAD_DETECTED) != 0; + + LOG_DBG("CAD done: %s", detected ? "activity" : "free"); + data->cad_active = false; + + if (data->cad_cb) { + lora_cad_cb cb = data->cad_cb; + void *ud = data->cad_user_data; + + data->cad_cb = NULL; + data->cad_user_data = NULL; + k_mutex_unlock(&data->spi_mutex); + cb(data->dev, detected, ud); + return; + } + + /* Blocking CAD: signal the semaphore */ + data->cad_result = detected ? 1 : 0; + k_sem_give(&data->cad_sem); + } + /* ── TX done ── */ if (irq & LR11XX_SYSTEM_IRQ_TX_DONE) { LOG_DBG("TX done"); @@ -602,6 +632,9 @@ static uint32_t lr11xx_lora_airtime(const struct device *dev, return (uint32_t)((t_preamble + t_payload) * 1000.0f); } +/* Forward declaration — needed by LBT in send_async */ +static int lr11xx_lora_cad(const struct device *dev, k_timeout_t timeout); + /* ── Driver API: send_async ─────────────────────────────────────────── */ static int lr11xx_lora_send_async(const struct device *dev, @@ -616,6 +649,19 @@ static int lr11xx_lora_send_async(const struct device *dev, if (data->tx_active) return -EBUSY; if (data_len > 255 || data_len == 0) return -EINVAL; + /* LBT: perform blocking CAD before transmitting */ + if (data->modem_cfg.cad.mode == LORA_CAD_MODE_LBT) { + int cad_ret = lr11xx_lora_cad(dev, K_MSEC(200)); + if (cad_ret > 0) { + LOG_DBG("LBT: channel busy"); + return -EBUSY; + } + if (cad_ret < 0 && cad_ret != -ENOSYS) { + LOG_WRN("LBT: CAD failed (%d), proceeding with TX", + cad_ret); + } + } + k_mutex_lock(&data->spi_mutex, K_FOREVER); /* Cancel RX */ @@ -771,13 +817,6 @@ bool lr11xx_is_receiving(const struct device *dev) LR11XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID)) != 0; } -void lr11xx_set_rx_duty_cycle(const struct device *dev, bool enable) -{ - /* LR1110 duty cycle is broken — always continuous RX. Ignore. */ - (void)dev; - (void)enable; -} - void lr11xx_set_rx_boost(const struct device *dev, bool enable) { struct lr11xx_data *data = dev->data; @@ -858,6 +897,131 @@ void lr11xx_reset_agc(const struct device *dev) /* ── Deferred hardware init (runs on first lora_config call) ────────── */ +/* ── Driver API: CAD ────────────────────────────────────────────────── */ + +/* Recommended cad_detect_peak values per SF for 2-symbol CAD. + * From Semtech SX1261/62/68 / LR1110 reference (same silicon IP). */ +static uint8_t lr11xx_cad_detect_peak(uint8_t sf) +{ + switch (sf) { + case 5: case 6: return 56; + case 7: return 56; + case 8: return 58; + case 9: return 58; + case 10: return 60; + case 11: return 64; + case 12: return 68; + default: return 60; + } +} + +static int lr11xx_do_cad(struct lr11xx_data *data) +{ + void *ctx = &data->hal_ctx; + struct lora_modem_config *mc = &data->modem_cfg; + + uint8_t sf = (uint8_t)mc->datarate; + uint8_t symb_nb = 2; + uint8_t detect_peak = lr11xx_cad_detect_peak(sf); + + if (mc->cad.symbol_num != 0) { + symb_nb = (uint8_t)mc->cad.symbol_num; + } + if (mc->cad.detection_peak != 0) { + detect_peak = mc->cad.detection_peak; + } + + lr11xx_radio_cad_params_t cad = { + .cad_symb_nb = symb_nb, + .cad_detect_peak = detect_peak, + .cad_detect_min = mc->cad.detection_minimum ? mc->cad.detection_minimum : 10, + .cad_exit_mode = LR11XX_RADIO_CAD_EXIT_MODE_STANDBYRC, + .cad_timeout = 0, + }; + + lr11xx_radio_set_cad_params(ctx, &cad); + + lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK); + data->cad_active = true; + lr11xx_radio_set_cad(ctx); + + return 0; +} + +static int lr11xx_lora_cad(const struct device *dev, k_timeout_t timeout) +{ + struct lr11xx_data *data = dev->data; + int ret; + + if (!data->configured) { + return -EINVAL; + } + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + bool was_in_rx = data->in_rx_mode; + + if (was_in_rx) { + data->in_rx_mode = false; + lr11xx_system_set_standby(&data->hal_ctx, + LR11XX_SYSTEM_STANDBY_CFG_RC); + } + + k_sem_reset(&data->cad_sem); + data->cad_result = -ETIMEDOUT; + data->cad_cb = NULL; + + ret = lr11xx_do_cad(data); + k_mutex_unlock(&data->spi_mutex); + + if (ret < 0) { + return ret; + } + + ret = k_sem_take(&data->cad_sem, timeout); + if (ret == -EAGAIN) { + data->cad_active = false; + return -ETIMEDOUT; + } + + return data->cad_result; +} + +static int lr11xx_lora_cad_async(const struct device *dev, + lora_cad_cb cb, void *user_data) +{ + struct lr11xx_data *data = dev->data; + + if (cb == NULL) { + data->cad_cb = NULL; + data->cad_user_data = NULL; + data->cad_active = false; + return 0; + } + + if (!data->configured) { + return -EINVAL; + } + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + if (data->in_rx_mode) { + data->in_rx_mode = false; + lr11xx_system_set_standby(&data->hal_ctx, + LR11XX_SYSTEM_STANDBY_CFG_RC); + } + + data->cad_cb = cb; + data->cad_user_data = user_data; + + int ret = lr11xx_do_cad(data); + k_mutex_unlock(&data->spi_mutex); + + return ret; +} + +/* ── Deferred hardware init ─────────────────────────────────────────── */ + static int lr11xx_hw_init(struct lr11xx_data *data, const struct lr11xx_config *cfg) { @@ -975,6 +1139,7 @@ static int lr11xx_lora_init(const struct device *dev) data->hw_initialized = false; k_mutex_init(&data->spi_mutex); + k_sem_init(&data->cad_sem, 0, 1); k_work_init(&data->dio1_work, lr11xx_dio1_work_handler); /* Start dedicated DIO1 work queue at high priority */ @@ -1035,6 +1200,9 @@ static DEVICE_API(lora, lr11xx_lora_api) = { .send_async = lr11xx_lora_send_async, .recv = lr11xx_lora_recv, .recv_async = lr11xx_lora_recv_async, + .cad = lr11xx_lora_cad, + .cad_async = lr11xx_lora_cad_async, + /* .recv_duty_cycle = NULL — LR1110 duty cycle broken */ }; #define LR11XX_INIT(n) \ diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h index 25d2c27..945b68d 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h @@ -40,19 +40,6 @@ int16_t lr11xx_get_rssi_inst(const struct device *dev); */ bool lr11xx_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. - * - * Takes effect on the next RX start (recv_async or after TX). - * - * @param dev LoRa device - * @param enable true to enable, false for continuous RX - */ -void lr11xx_set_rx_duty_cycle(const struct device *dev, bool enable); - /** * @brief Enable/disable RX boosted mode * diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c index 61c2f23..df151c9 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c @@ -86,6 +86,13 @@ struct lr20xx_data { bool rx_boost_enabled; bool rx_boost_applied; + /* CAD state */ + lora_cad_cb cad_cb; + void *cad_user_data; + struct k_sem cad_sem; + int cad_result; /* 0=free, 1=busy, <0=error */ + bool cad_active; + /* Deferred hardware init */ bool hw_initialized; @@ -675,6 +682,29 @@ static void lr20xx_dio1_work_handler(struct k_work *work) rx_restarted = true; } + /* ── CAD done ── */ + if (irq & LR20XX_SYSTEM_IRQ_CAD_DONE) { + bool detected = (irq & LR20XX_SYSTEM_IRQ_CAD_DETECTED) != 0; + + LOG_DBG("CAD done: %s", detected ? "activity" : "free"); + data->cad_active = false; + + if (data->cad_cb) { + lora_cad_cb cb = data->cad_cb; + void *ud = data->cad_user_data; + + data->cad_cb = NULL; + data->cad_user_data = NULL; + k_mutex_unlock(&data->spi_mutex); + cb(data->dev, detected, ud); + return; + } + + /* Blocking CAD: signal the semaphore */ + data->cad_result = detected ? 1 : 0; + k_sem_give(&data->cad_sem); + } + /* ── TX done ── */ if (irq & LR20XX_SYSTEM_IRQ_TX_DONE) { LOG_DBG("TX done"); @@ -826,6 +856,8 @@ static uint32_t lr20xx_lora_airtime(const struct device *dev, /* ── Driver API: send_async ─────────────────────────────────────────── */ +static int lr20xx_lora_cad(const struct device *dev, k_timeout_t timeout); + static int lr20xx_lora_send_async(const struct device *dev, uint8_t *buf, uint32_t data_len, struct k_poll_signal *async) @@ -838,6 +870,19 @@ static int lr20xx_lora_send_async(const struct device *dev, if (data->tx_active) return -EBUSY; if (data_len > 255 || data_len == 0) return -EINVAL; + /* LBT: perform blocking CAD before transmitting */ + if (data->modem_cfg.cad.mode == LORA_CAD_MODE_LBT) { + int cad_ret = lr20xx_lora_cad(dev, K_MSEC(200)); + if (cad_ret > 0) { + LOG_DBG("LBT: channel busy"); + return -EBUSY; + } + if (cad_ret < 0 && cad_ret != -ENOSYS) { + LOG_WRN("LBT: CAD failed (%d), proceeding with TX", + cad_ret); + } + } + k_mutex_lock(&data->spi_mutex, K_FOREVER); data->async_rx_cb = NULL; @@ -956,15 +1001,13 @@ static int lr20xx_lora_recv_async(const struct device *dev, data->async_rx_cb = cb; data->async_rx_user_data = user_data; + data->rx_duty_cycle_enabled = false; lr20xx_start_rx(data, cfg); k_mutex_unlock(&data->spi_mutex); - LOG_INF("recv_async: %s%s", - data->rx_duty_cycle_enabled - ? "RX duty cycle" - : "continuous RX", + LOG_INF("recv_async: continuous RX%s", data->rx_boost_enabled ? ", boosted" : ""); return 0; @@ -1022,19 +1065,6 @@ bool lr20xx_is_receiving(const struct device *dev) LR20XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID)) != 0; } -void lr20xx_set_rx_duty_cycle(const struct device *dev, bool enable) -{ - struct lr20xx_data *data = dev->data; - - if (data->rx_duty_cycle_enabled == enable) { - return; - } - - data->rx_duty_cycle_enabled = enable; - LOG_INF("RX duty cycle %s (takes effect on next RX start)", - enable ? "enabled" : "disabled"); -} - void lr20xx_set_rx_boost(const struct device *dev, bool enable) { struct lr20xx_data *data = dev->data; @@ -1113,6 +1143,188 @@ void lr20xx_reset_agc(const struct device *dev) k_mutex_unlock(&data->spi_mutex); } +/* ── Driver API: CAD ────────────────────────────────────────────────── */ + +/* Recommended cad_detect_peak values per SF for 2-symbol CAD. + * From Semtech LR20xx datasheet table. Using 2 symbols as a + * good balance between speed (~2 symbol durations) and reliability. */ +static uint8_t lr20xx_cad_detect_peak(uint8_t sf) +{ + switch (sf) { + case 5: case 6: return 56; + case 7: return 56; + case 8: return 58; + case 9: return 58; + case 10: return 60; + case 11: return 64; + case 12: return 68; + default: return 60; + } +} + +static int lr20xx_do_cad(struct lr20xx_data *data) +{ + void *ctx = &data->hal_ctx; + struct lora_modem_config *mc = &data->modem_cfg; + + uint8_t sf = (uint8_t)mc->datarate; + lr20xx_radio_lora_cad_params_t cad = { + .cad_symb_nb = 2, + .pnr_delta = 0, /* exact symbol count, no best-effort */ + .cad_exit_mode = LR20XX_RADIO_LORA_CAD_EXIT_MODE_STANDBYRC, + .cad_timeout_in_pll_step = 0, + .cad_detect_peak = lr20xx_cad_detect_peak(sf), + }; + + /* Override from modem config if caller set non-zero values */ + if (mc->cad.symbol_num != 0) { + cad.cad_symb_nb = (uint8_t)mc->cad.symbol_num; + } + if (mc->cad.detection_peak != 0) { + cad.cad_detect_peak = mc->cad.detection_peak; + } + + lr20xx_radio_lora_configure_cad_params(ctx, &cad); + + /* Clear any pending IRQ flags, then start CAD */ + lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK); + data->cad_active = true; + lr20xx_radio_lora_set_cad(ctx); + + return 0; +} + +static int lr20xx_lora_cad(const struct device *dev, k_timeout_t timeout) +{ + struct lr20xx_data *data = dev->data; + int ret; + + if (!data->configured) { + return -EINVAL; + } + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + /* Stop async RX if active — CAD needs the radio */ + bool was_in_rx = data->in_rx_mode; + + if (was_in_rx) { + data->in_rx_mode = false; + lr20xx_system_set_standby_mode(&data->hal_ctx, + LR20XX_SYSTEM_STANDBY_MODE_RC); + } + + k_sem_reset(&data->cad_sem); + data->cad_result = -ETIMEDOUT; + data->cad_cb = NULL; + + ret = lr20xx_do_cad(data); + k_mutex_unlock(&data->spi_mutex); + + if (ret < 0) { + return ret; + } + + /* Wait for DIO1 handler to signal CAD_DONE */ + ret = k_sem_take(&data->cad_sem, timeout); + if (ret == -EAGAIN) { + data->cad_active = false; + return -ETIMEDOUT; + } + + return data->cad_result; +} + +static int lr20xx_lora_cad_async(const struct device *dev, + lora_cad_cb cb, void *user_data) +{ + struct lr20xx_data *data = dev->data; + + if (cb == NULL) { + /* Cancel pending CAD */ + data->cad_cb = NULL; + data->cad_user_data = NULL; + data->cad_active = false; + return 0; + } + + if (!data->configured) { + return -EINVAL; + } + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + bool was_in_rx = data->in_rx_mode; + + if (was_in_rx) { + data->in_rx_mode = false; + lr20xx_system_set_standby_mode(&data->hal_ctx, + LR20XX_SYSTEM_STANDBY_MODE_RC); + } + + data->cad_cb = cb; + data->cad_user_data = user_data; + + int ret = lr20xx_do_cad(data); + k_mutex_unlock(&data->spi_mutex); + + return ret; +} + +/* ── Driver API: recv_duty_cycle ────────────────────────────────────── */ + +static int lr20xx_lora_recv_duty_cycle(const struct device *dev, + k_timeout_t rx_period, + k_timeout_t sleep_period, + lora_recv_cb cb, void *user_data) +{ + struct lr20xx_data *data = dev->data; + const struct lr20xx_config *cfg = dev->config; + + if (cb == NULL) { + /* Cancel — same as recv_async(NULL) */ + k_mutex_lock(&data->spi_mutex, K_FOREVER); + data->async_rx_cb = NULL; + data->async_rx_user_data = NULL; + data->in_rx_mode = false; + k_mutex_unlock(&data->spi_mutex); + return 0; + } + + if (!data->configured) { + return -EINVAL; + } + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + data->async_rx_cb = cb; + data->async_rx_user_data = user_data; + + void *ctx = &data->hal_ctx; + + lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC); + lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK); + lr20xx_radio_fifo_clear_rx(ctx); + lr20xx_apply_modem_config(data, cfg, false); + + uint32_t rx_ms = k_ticks_to_ms_ceil32(rx_period.ticks); + uint32_t slp_ms = k_ticks_to_ms_ceil32(sleep_period.ticks); + if (rx_ms < 1) rx_ms = 1; + if (slp_ms < 1) slp_ms = 1; + + data->rx_duty_cycle_enabled = true; + lr20xx_radio_common_set_rx_duty_cycle(ctx, rx_ms, slp_ms, + LR20XX_RADIO_COMMON_RX_DUTY_CYCLE_MODE_RX); + LOG_INF("recv_duty_cycle: rx=%ums sleep=%ums", rx_ms, slp_ms); + + lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK); + data->in_rx_mode = true; + data->tx_active = false; + + k_mutex_unlock(&data->spi_mutex); + return 0; +} + /* ── Deferred hardware init ─────────────────────────────────────────── */ static int lr20xx_hw_init(struct lr20xx_data *data, @@ -1285,6 +1497,7 @@ static int lr20xx_lora_init(const struct device *dev) data->hw_initialized = false; k_mutex_init(&data->spi_mutex); + k_sem_init(&data->cad_sem, 0, 1); k_work_init(&data->dio1_work, lr20xx_dio1_work_handler); k_work_queue_start(&data->dio1_wq, lr20xx_dio1_wq_stack, @@ -1327,12 +1540,15 @@ static int lr20xx_lora_init(const struct device *dev) /* ── Device instantiation ───────────────────────────────────────────── */ static DEVICE_API(lora, lr20xx_lora_api) = { - .config = lr20xx_lora_config, - .airtime = lr20xx_lora_airtime, - .send = lr20xx_lora_send, - .send_async = lr20xx_lora_send_async, - .recv = lr20xx_lora_recv, - .recv_async = lr20xx_lora_recv_async, + .config = lr20xx_lora_config, + .airtime = lr20xx_lora_airtime, + .send = lr20xx_lora_send, + .send_async = lr20xx_lora_send_async, + .recv = lr20xx_lora_recv, + .recv_async = lr20xx_lora_recv_async, + .cad = lr20xx_lora_cad, + .cad_async = lr20xx_lora_cad_async, + .recv_duty_cycle = lr20xx_lora_recv_duty_cycle, }; #define LR20XX_INIT(n) \ diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h index 96b08f7..92cd72d 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.h @@ -37,17 +37,6 @@ int16_t lr20xx_get_rssi_inst(const struct device *dev); */ bool lr20xx_is_receiving(const struct device *dev); -/** - * @brief Enable/disable RX duty cycle mode - * - * When enabled, radio alternates between RX and sleep. - * Takes effect on the next RX start. - * - * @param dev LoRa device - * @param enable true to enable, false for continuous RX - */ -void lr20xx_set_rx_duty_cycle(const struct device *dev, bool enable); - /** * @brief Enable/disable RX boosted mode * 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 index 265cb9e..10a376f 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h +++ b/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h @@ -40,17 +40,6 @@ int16_t sx126x_get_rssi_inst(const struct device *dev); */ 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 * diff --git a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch index e5a80bd..d3cb1e5 100644 --- a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch +++ b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch @@ -1,5 +1,5 @@ diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c -index 7b0b67f89d5..a6c1ceba162 100644 +index 7b0b67f89d5..0225ff4360d 100644 --- a/drivers/lora/native/sx126x/sx126x.c +++ b/drivers/lora/native/sx126x/sx126x.c @@ -9,10 +9,19 @@ @@ -177,7 +177,7 @@ index 7b0b67f89d5..a6c1ceba162 100644 } static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx) -@@ -405,11 +466,133 @@ static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx) +@@ -405,11 +466,70 @@ 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); @@ -194,74 +194,6 @@ index 7b0b67f89d5..a6c1ceba162 100644 } } -+/* ── 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 @@ -290,7 +222,12 @@ index 7b0b67f89d5..a6c1ceba162 100644 + sx126x_set_dio2_as_rf_switch(dev, true); + } + -+ sx126x_apply_rx_duty_cycle(data); ++ /* Re-apply duty cycle with stored timing */ ++ uint8_t dc_buf[6]; ++ sys_put_be24(data->dc_rx_time, &dc_buf[0]); ++ sys_put_be24(data->dc_sleep_time, &dc_buf[3]); ++ sx126x_hal_write_cmd(dev, SX126X_CMD_SET_RX_DUTY_CYCLE, ++ dc_buf, 6); + } else { + uint8_t buf[3]; + @@ -312,7 +249,7 @@ index 7b0b67f89d5..a6c1ceba162 100644 static int sx126x_set_sleep(const struct device *dev) { struct sx126x_data *data = dev->data; -@@ -521,19 +704,23 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -521,19 +641,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) { @@ -345,7 +282,33 @@ index 7b0b67f89d5..a6c1ceba162 100644 } else { /* Sync mode */ sx126x_set_sleep(dev); -@@ -653,6 +840,29 @@ static int sx126x_lora_config(const struct device *dev, +@@ -592,6 +716,25 @@ static void sx126x_irq_work_handler(struct k_work *work) + sx126x_handle_irq_timeout(dev); + } + ++ if (irq_status & SX126X_IRQ_CAD_DONE) { ++ bool detected = (irq_status & SX126X_IRQ_CAD_ACTIVITY_DETECTED) != 0; ++ ++ LOG_DBG("CAD done: %s", detected ? "activity" : "free"); ++ data->cad_active = false; ++ ++ if (data->cad_cb) { ++ lora_cad_cb cb = data->cad_cb; ++ void *ud = data->cad_user_data; ++ ++ data->cad_cb = NULL; ++ data->cad_user_data = NULL; ++ cb(dev, detected, ud); ++ } else { ++ data->cad_result = detected ? 1 : 0; ++ k_sem_give(&data->cad_sem); ++ } ++ } ++ + /* Re-enable the DIO1 interrupt for the next event (unless sleeping) */ + if (atomic_get(&data->state) != SX126X_REST_STATE) { + sx126x_hal_dio1_irq_enable(dev); +@@ -653,6 +796,29 @@ static int sx126x_lora_config(const struct device *dev, goto out; } @@ -375,7 +338,51 @@ index 7b0b67f89d5..a6c1ceba162 100644 /* Set sync word */ ret = sx126x_set_sync_word(dev, config->public_network); if (ret < 0) { -@@ -732,6 +942,29 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -676,6 +842,8 @@ out: + return ret; + } + ++static int sx126x_lora_cad(const struct device *dev, k_timeout_t timeout); ++ + static int sx126x_lora_send_async(const struct device *dev, + uint8_t *data_buf, uint32_t data_len, + struct k_poll_signal *async) +@@ -707,6 +875,34 @@ static int sx126x_lora_send_async(const struct device *dev, + return ret; + } + ++ /* LBT: perform blocking CAD before transmitting */ ++ if (data->config.cad.mode == LORA_CAD_MODE_LBT) { ++ k_mutex_unlock(&data->lock); ++ atomic_set(&data->state, SX126X_REST_STATE); ++ int cad_ret = sx126x_lora_cad(dev, K_MSEC(200)); ++ if (cad_ret > 0) { ++ LOG_DBG("LBT: channel busy"); ++ return -EBUSY; ++ } ++ if (cad_ret < 0 && cad_ret != -ENOSYS) { ++ LOG_WRN("LBT: CAD failed (%d), proceeding with TX", ++ cad_ret); ++ } ++ /* Re-acquire state and lock for TX */ ++ if (!atomic_cas(&data->state, SX126X_REST_STATE, ++ SX126X_STATE_TX)) { ++ LOG_ERR("Busy after CAD"); ++ return -EBUSY; ++ } ++ k_mutex_lock(&data->lock, K_FOREVER); ++ ret = sx126x_ensure_ready(dev); ++ if (ret < 0) { ++ k_mutex_unlock(&data->lock); ++ atomic_set(&data->state, SX126X_REST_STATE); ++ return ret; ++ } ++ } ++ + data->tx_async_signal = async; + k_msgq_purge(&data->tx_msgq); + +@@ -732,6 +928,29 @@ static int sx126x_lora_send_async(const struct device *dev, /* Enable antenna and set TX path */ sx126x_set_rf_path(dev, true, true); @@ -405,7 +412,15 @@ index 7b0b67f89d5..a6c1ceba162 100644 /* Start transmission with 10 second timeout */ ret = sx126x_set_tx(dev, 10000); if (ret < 0) { -@@ -1038,6 +1271,126 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, +@@ -902,6 +1121,7 @@ static int sx126x_lora_recv_async(const struct device *dev, + + data->rx_cb = cb; + data->rx_cb_user_data = user_data; ++ data->rx_duty_cycle_enabled = false; + + /* Set packet parameters */ + ret = sx126x_set_packet_params(dev, +@@ -1038,14 +1258,407 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, return 0; } @@ -452,25 +467,7 @@ index 7b0b67f89d5..a6c1ceba162 100644 + 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); -+ } -+ /* RX gain preserved by hardware retention (§9.6) */ -+ k_mutex_unlock(&data->lock); -+ } -+} + +void sx126x_set_rx_boost(const struct device *dev, bool enable) +{ @@ -528,11 +525,321 @@ index 7b0b67f89d5..a6c1ceba162 100644 + + k_mutex_unlock(&data->lock); +} ++ ++/* ── Driver API: CAD ────────────────────────────────────────────────── */ ++ ++/* Recommended cad_detect_peak values per SF for 2-symbol CAD. ++ * From Semtech SX1261/62/68 datasheet AN1200.48. */ ++static uint8_t sx126x_cad_detect_peak(uint8_t sf) ++{ ++ switch (sf) { ++ case 5: case 6: return 56; ++ case 7: return 56; ++ case 8: return 58; ++ case 9: return 58; ++ case 10: return 60; ++ case 11: return 64; ++ case 12: return 68; ++ default: return 60; ++ } ++} ++ ++static int sx126x_do_cad(const struct device *dev, struct sx126x_data *data) ++{ ++ struct lora_modem_config *mc = &data->config; ++ uint8_t sf = (uint8_t)mc->datarate; ++ uint8_t symb_nb = 2; ++ uint8_t detect_peak = sx126x_cad_detect_peak(sf); ++ ++ if (mc->cad.symbol_num != 0) { ++ symb_nb = (uint8_t)mc->cad.symbol_num; ++ } ++ if (mc->cad.detection_peak != 0) { ++ detect_peak = mc->cad.detection_peak; ++ } ++ ++ /* SetCadParams: symb_nb, detect_peak, detect_min, exit_mode, ++ * timeout (3 bytes, unused for STANDBYRC exit) */ ++ uint8_t buf[7]; ++ ++ buf[0] = symb_nb; ++ buf[1] = detect_peak; ++ buf[2] = mc->cad.detection_minimum ? mc->cad.detection_minimum : 10; ++ buf[3] = 0x00; /* CAD_ONLY (exit to STANDBYRC) */ ++ buf[4] = 0; /* timeout[23:16] */ ++ buf[5] = 0; /* timeout[15:8] */ ++ buf[6] = 0; /* timeout[7:0] */ ++ ++ sx126x_hal_write_cmd(dev, SX126X_CMD_SET_CAD_PARAMS, buf, 7); ++ ++ sx126x_clear_irq_status(dev, SX126X_IRQ_CAD_DONE | ++ SX126X_IRQ_CAD_ACTIVITY_DETECTED); ++ data->cad_active = true; ++ ++ /* Enable antenna in RX mode for CAD */ ++ sx126x_set_rf_path(dev, true, false); ++ sx126x_hal_write_cmd(dev, SX126X_CMD_SET_CAD, NULL, 0); ++ ++ return 0; ++} ++ ++static int sx126x_lora_cad(const struct device *dev, k_timeout_t timeout) ++{ ++ struct sx126x_data *data = dev->data; ++ int ret; ++ ++ if (!data->config_valid) { ++ return -EINVAL; ++ } ++ ++ /* Transition from REST → IDLE for the CAD operation */ ++ if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_IDLE)) { ++ /* If we're in RX, force to IDLE */ ++ if (!atomic_cas(&data->state, SX126X_STATE_RX, SX126X_STATE_IDLE)) { ++ return -EBUSY; ++ } ++ } ++ ++ k_mutex_lock(&data->lock, K_FOREVER); ++ ++ ret = sx126x_ensure_ready(dev); ++ if (ret < 0) { ++ k_mutex_unlock(&data->lock); ++ atomic_set(&data->state, SX126X_REST_STATE); ++ return ret; ++ } ++ ++ k_sem_reset(&data->cad_sem); ++ data->cad_result = -ETIMEDOUT; ++ data->cad_cb = NULL; ++ ++ ret = sx126x_do_cad(dev, data); ++ k_mutex_unlock(&data->lock); ++ ++ if (ret < 0) { ++ atomic_set(&data->state, SX126X_REST_STATE); ++ return ret; ++ } ++ ++ ret = k_sem_take(&data->cad_sem, timeout); ++ if (ret == -EAGAIN) { ++ data->cad_active = false; ++ atomic_set(&data->state, SX126X_REST_STATE); ++ return -ETIMEDOUT; ++ } ++ ++ atomic_set(&data->state, SX126X_REST_STATE); ++ return data->cad_result; ++} ++ ++static int sx126x_lora_cad_async(const struct device *dev, ++ lora_cad_cb cb, void *user_data) ++{ ++ struct sx126x_data *data = dev->data; ++ ++ if (cb == NULL) { ++ data->cad_cb = NULL; ++ data->cad_user_data = NULL; ++ data->cad_active = false; ++ return 0; ++ } ++ ++ if (!data->config_valid) { ++ return -EINVAL; ++ } ++ ++ if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_IDLE)) { ++ if (!atomic_cas(&data->state, SX126X_STATE_RX, SX126X_STATE_IDLE)) { ++ return -EBUSY; ++ } ++ } ++ ++ k_mutex_lock(&data->lock, K_FOREVER); ++ ++ int ret = sx126x_ensure_ready(dev); ++ ++ if (ret < 0) { ++ k_mutex_unlock(&data->lock); ++ atomic_set(&data->state, SX126X_REST_STATE); ++ return ret; ++ } ++ ++ data->cad_cb = cb; ++ data->cad_user_data = user_data; ++ ++ ret = sx126x_do_cad(dev, data); ++ k_mutex_unlock(&data->lock); ++ ++ if (ret < 0) { ++ atomic_set(&data->state, SX126X_REST_STATE); ++ } ++ ++ return ret; ++} ++ ++/* ── Driver API: recv_duty_cycle ────────────────────────────────────── */ ++ ++static int sx126x_lora_recv_duty_cycle(const struct device *dev, ++ k_timeout_t rx_period, ++ k_timeout_t sleep_period, ++ lora_recv_cb cb, void *user_data) ++{ ++ struct sx126x_data *data = dev->data; ++ int ret; ++ ++ k_mutex_lock(&data->lock, K_FOREVER); ++ ++ if (cb == NULL) { ++ data->rx_cb = NULL; ++ data->rx_cb_user_data = NULL; ++ if (atomic_cas(&data->state, SX126X_STATE_RX, SX126X_STATE_IDLE)) { ++ sx126x_set_standby(dev, SX126X_STANDBY_RC); ++ sx126x_set_sleep(dev); ++ } ++ k_mutex_unlock(&data->lock); ++ return 0; ++ } ++ ++ if (!data->config_valid) { ++ k_mutex_unlock(&data->lock); ++ return -EINVAL; ++ } ++ ++ if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_RX)) { ++ k_mutex_unlock(&data->lock); ++ return -EBUSY; ++ } ++ ++ ret = sx126x_ensure_ready(dev); ++ if (ret < 0) { ++ k_mutex_unlock(&data->lock); ++ atomic_set(&data->state, SX126X_REST_STATE); ++ return ret; ++ } ++ ++ data->rx_cb = cb; ++ data->rx_cb_user_data = user_data; ++ ++ /* Set packet parameters (same as recv_async) */ ++ ret = sx126x_set_packet_params(dev, ++ data->config.preamble_len, ++ SX126X_LORA_HEADER_EXPLICIT, ++ SX126X_MAX_PAYLOAD_LEN, ++ data->config.packet_crc_disable ? ++ SX126X_LORA_CRC_OFF : SX126X_LORA_CRC_ON, ++ data->config.iq_inverted ? ++ SX126X_LORA_IQ_INVERTED : SX126X_LORA_IQ_STANDARD); ++ if (ret < 0) { ++ data->rx_cb = NULL; ++ sx126x_set_sleep(dev); ++ k_mutex_unlock(&data->lock); ++ return ret; ++ } ++ ++ sx126x_set_rf_path(dev, true, false); ++ ++ uint32_t rx_time, sleep_time; ++ ++ if (K_TIMEOUT_EQ(rx_period, K_FOREVER) || ++ K_TIMEOUT_EQ(sleep_period, K_FOREVER)) { ++ /* Auto-compute from modem config (RadioLib algorithm) */ ++ 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) ? 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 " ++ "(need >%d, have %d)", ++ min_symbols, preamble_len); ++ data->rx_cb = NULL; ++ sx126x_set_sleep(dev); ++ k_mutex_unlock(&data->lock); ++ atomic_set(&data->state, SX126X_REST_STATE); ++ return -EINVAL; ++ } ++ ++ uint32_t symbol_us = (uint32_t)((float)(1 << sf) * ++ 1000.0f / bw_khz); ++ ++ int16_t sleep_symbols_safe = sleep_symbols - 2; ++ if (sleep_symbols_safe < 1) sleep_symbols_safe = 1; ++ uint32_t sleep_us = (uint16_t)sleep_symbols_safe * symbol_us; ++ ++ uint32_t preamble_us = (preamble_len + 1) * symbol_us; ++ int32_t wake1 = ((int32_t)preamble_us - ++ ((int32_t)sleep_us - 1000)) / 2; ++ uint32_t wake2 = (min_symbols + 1) * symbol_us; ++ uint32_t rx_us = (wake1 > 0 && (uint32_t)wake1 > wake2) ++ ? (uint32_t)wake1 : wake2; ++ ++ /* Convert us to 15.625us steps */ ++ rx_time = (rx_us * 64) / 1000; ++ sleep_time = (sleep_us * 64) / 1000; ++ } else { ++ /* Explicit timing from caller */ ++ uint32_t rx_us = k_ticks_to_us_ceil32(rx_period.ticks); ++ uint32_t slp_us = k_ticks_to_us_ceil32(sleep_period.ticks); ++ rx_time = (rx_us * 64) / 1000; ++ sleep_time = (slp_us * 64) / 1000; ++ } ++ ++ if (rx_time < 64) rx_time = 64; ++ if (sleep_time < 64) sleep_time = 64; ++ ++ /* Store for restart_rx to re-use */ ++ data->dc_rx_time = rx_time; ++ data->dc_sleep_time = sleep_time; ++ data->rx_duty_cycle_enabled = true; ++ ++ 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); ++ ++ /* Apply RX gain */ ++ uint8_t gain = data->rx_boost_enabled ? SX126X_RX_GAIN_BOOSTED ++ : SX126X_RX_GAIN_POWER_SAVING; ++ sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &gain, 1); ++ ++ k_mutex_unlock(&data->lock); ++ ++ LOG_DBG("recv_duty_cycle: rx=%uus sleep=%uus", ++ (rx_time * 1000) / 64, (sleep_time * 1000) / 64); ++ return 0; ++} + static DEVICE_API(lora, sx126x_lora_api) = { - .config = sx126x_lora_config, - .send = sx126x_lora_send, -@@ -1063,6 +1416,14 @@ static int sx126x_init(const struct device *dev) +- .config = sx126x_lora_config, +- .send = sx126x_lora_send, +- .send_async = sx126x_lora_send_async, +- .recv = sx126x_lora_recv, +- .recv_async = sx126x_lora_recv_async, +- .airtime = sx126x_lora_airtime, +- .test_cw = sx126x_lora_test_cw, ++ .config = sx126x_lora_config, ++ .send = sx126x_lora_send, ++ .send_async = sx126x_lora_send_async, ++ .recv = sx126x_lora_recv, ++ .recv_async = sx126x_lora_recv_async, ++ .airtime = sx126x_lora_airtime, ++ .cad = sx126x_lora_cad, ++ .cad_async = sx126x_lora_cad_async, ++ .recv_duty_cycle = sx126x_lora_recv_duty_cycle, ++ .test_cw = sx126x_lora_test_cw, + }; + + static int sx126x_init(const struct device *dev) +@@ -1060,9 +1673,18 @@ static int sx126x_init(const struct device *dev) + k_msgq_init(&data->rx_msgq, (char *)&data->rx_result, + sizeof(struct sx126x_rx_result), 1); + k_work_init(&data->irq_work, sx126x_irq_work_handler); ++ k_sem_init(&data->cad_sem, 0, 1); data->dev = dev; atomic_set(&data->state, SX126X_STATE_IDLE); data->config_valid = false; @@ -548,10 +855,10 @@ index 7b0b67f89d5..a6c1ceba162 100644 /* Initialize HAL */ ret = sx126x_hal_init(dev); diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h -index 9dbf3f26586..dabe5495853 100644 +index 9dbf3f26586..3aa0c7e588c 100644 --- a/drivers/lora/native/sx126x/sx126x.h +++ b/drivers/lora/native/sx126x/sx126x.h -@@ -62,7 +62,12 @@ struct sx126x_data { +@@ -62,7 +62,21 @@ struct sx126x_data { /* Deferred work for interrupt handling */ struct k_work irq_work; @@ -561,6 +868,15 @@ index 9dbf3f26586..dabe5495853 100644 + /* Extension features (duty cycle, boost) */ + bool rx_duty_cycle_enabled; + bool rx_boost_enabled; ++ uint32_t dc_rx_time; /* stored duty cycle rx period (15.625us steps) */ ++ uint32_t dc_sleep_time; /* stored duty cycle sleep period (15.625us steps) */ ++ ++ /* CAD state */ ++ lora_cad_cb cad_cb; ++ void *cad_user_data; ++ struct k_sem cad_sem; ++ int cad_result; ++ bool cad_active; }; #endif /* ZEPHYR_DRIVERS_LORA_SX126X_SX126X_INTERNAL_H_ */