From 348754e83885638b36432cec7b1b70199c72f9eb Mon Sep 17 00:00:00 2001 From: ZiB Date: Sat, 7 Nov 2026 15:08:00 -0800 Subject: [PATCH 3/3] drivers: lora: native: sx126x: implement driver-level LBT send Adds sx126x_lora_send_lbt(), an SX126x-specific extension that does CAD then, only if clear, transmits. Uses a new SX126X_STATE_CAD_TX state covering the whole CAD->TX op. Packet params/FIFO/TX-modulation workaround are staged before SetCAD, not after - CAD doesn't touch any of those registers, so this is free. On a clear CadDone, the IRQ handler goes straight from "channel is clear" to RF-path + SetTx, skipping the sleep that CAD_ONLY's generic completion path (sx126x_handle_irq_cad_done()) otherwise always takes. The two outcomes (busy vs. sent) are delivered over a new dedicated lbt_msgq rather than overloading tx_msgq/cad_msgq, since only this state's completion needs to carry both. A caller's cad_timeout bounds only the CAD phase: lbt_committed tracks whether CAD already went clear and SetTx was issued, so a timeout arriving after that point waits out the actual transmission (bounded by SetTx's own 10s hardware timeout) instead of aborting an in-flight TX. Retry/backoff policy is deliberately not pulled into the driver. CAD exit mode stays CAD_ONLY: the SX126x has no CAD-to-TX exit mode in hardware (only CAD-to-RX), so this is done by skipping the driver's own sleep rather than anything the radio does autonomously. Signed-off-by: ZiB --- drivers/lora/native/sx126x/sx126x.c | 255 +++++++++++++++++++++++++-- drivers/lora/native/sx126x/sx126x.h | 17 ++ include/zephyr/drivers/lora/sx126x.h | 24 +++ 3 files changed, 277 insertions(+), 19 deletions(-) diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c index 8ba8416ed0c..16037a3714d 100644 --- a/drivers/lora/native/sx126x/sx126x.c +++ b/drivers/lora/native/sx126x/sx126x.c @@ -817,9 +817,24 @@ static int sx126x_ensure_ready(const struct device *dev) static void sx126x_handle_irq_tx_done(const struct device *dev) { struct sx126x_data *data = dev->data; - struct sx126x_tx_result result = { .status = 0 }; LOG_DBG("TX done"); + + /* A send_lbt() TX-after-clear-CAD never changes data->state away + * from SX126X_STATE_CAD_TX (see sx126x_handle_irq_cad_done_lbt()), + * so that state alone tells a TX_DONE here apart from one started + * by lora_send()/lora_send_async(). + */ + if (atomic_get(&data->state) == SX126X_STATE_CAD_TX) { + struct sx126x_lbt_result result = { .status = 0 }; + + sx126x_set_sleep(dev); + k_msgq_put(&data->lbt_msgq, &result, K_NO_WAIT); + return; + } + + struct sx126x_tx_result result = { .status = 0 }; + sx126x_set_sleep(dev); if (data->tx_async_signal != NULL) { @@ -905,14 +920,61 @@ out: } } +/* + * CadDone branch for a send_lbt() op (data->state == SX126X_STATE_CAD_TX). + * On activity, this is the whole op's result: sleep and report busy. On a + * clear channel, sx126x_lora_send_lbt() already staged packet params/FIFO/ + * TX-modulation workaround before issuing SetCAD (none of those registers + * matter to CAD) - so the only things left between "channel is clear" and + * "radio is transmitting" are RF-path + SetTx, issued right here, with no + * intervening sleep/wake round-trip. data->state is deliberately left at + * SX126X_STATE_CAD_TX (not folded back to SLEEP, not advanced to + * SX126X_STATE_TX) so sx126x_handle_irq_tx_done()/sx126x_handle_irq_timeout() + * can tell the eventual TX_DONE/timeout apart from one started by + * lora_send()/lora_send_async() and route it back to lbt_msgq instead of + * tx_msgq. + */ +static void sx126x_handle_irq_cad_done_lbt(const struct device *dev, bool activity_detected) +{ + struct sx126x_data *data = dev->data; + struct sx126x_lbt_result result; + int ret; + + if (activity_detected) { + LOG_DBG("LBT: channel busy, not transmitting"); + sx126x_set_sleep(dev); + result.status = 1; + k_msgq_put(&data->lbt_msgq, &result, K_NO_WAIT); + return; + } + + sx126x_set_rf_path(dev, true, true); + + ret = sx126x_set_tx(dev, 10000); + if (ret < 0) { + LOG_ERR("LBT: SetTx failed: %d", ret); + sx126x_set_sleep(dev); + result.status = ret; + k_msgq_put(&data->lbt_msgq, &result, K_NO_WAIT); + return; + } + + atomic_set(&data->lbt_committed, 1); +} + static void sx126x_handle_irq_cad_done(const struct device *dev, uint16_t irq_status) { struct sx126x_data *data = dev->data; - struct sx126x_cad_result result = { - .activity_detected = (irq_status & SX126X_IRQ_CAD_ACTIVITY_DETECTED) != 0, - }; + bool activity_detected = (irq_status & SX126X_IRQ_CAD_ACTIVITY_DETECTED) != 0; - LOG_DBG("CAD done: activity=%d", result.activity_detected); + LOG_DBG("CAD done: activity=%d", activity_detected); + + if (atomic_get(&data->state) == SX126X_STATE_CAD_TX) { + sx126x_handle_irq_cad_done_lbt(dev, activity_detected); + return; + } + + struct sx126x_cad_result result = { .activity_detected = activity_detected }; /* CAD_ONLY already left the chip in STDBY_RC; this just folds it * back into the driver's own rest state. @@ -947,6 +1009,18 @@ static void sx126x_handle_irq_timeout(const struct device *dev) return; } + if (atomic_get(&data->state) == SX126X_STATE_CAD_TX) { + /* SetTx's own 10s HW timeout, firing on a send_lbt() TX that + * committed after a clear CAD - see + * sx126x_handle_irq_cad_done_lbt(). + */ + struct sx126x_lbt_result result = { .status = -ETIMEDOUT }; + + sx126x_set_sleep(dev); + k_msgq_put(&data->lbt_msgq, &result, K_NO_WAIT); + return; + } + sx126x_set_sleep(dev); if (data->tx_async_signal != NULL) { @@ -1179,6 +1253,33 @@ out: return ret; } +/* + * Packet params + FIFO + TX-modulation workaround: everything a + * transmission needs besides RF-path and SetTx themselves. Shared by + * sx126x_lora_send_async() and sx126x_lora_send_lbt(), the latter calling + * it *before* SetCAD so that, once CAD comes back clear, only RF-path and + * SetTx stand between CadDone and a transmitting radio - see + * sx126x_handle_irq_cad_done_lbt(). + */ +static int sx126x_stage_tx(const struct device *dev, uint8_t *data_buf, uint32_t data_len) +{ + struct sx126x_data *data = dev->data; + int ret; + + ret = sx126x_set_configured_packet_params(dev, data_len); + if (ret < 0) { + return ret; + } + + ret = sx126x_hal_write_buffer(dev, 0x00, data_buf, data_len); + if (ret < 0) { + return ret; + } + + return sx126x_apply_tx_modulation_workaround( + dev, !SX126X_IS_GFSK(data) && data->config.bandwidth == BW_500_KHZ); +} + static int sx126x_lora_send_async(const struct device *dev, uint8_t *data_buf, uint32_t data_len, struct k_poll_signal *async) @@ -1223,20 +1324,7 @@ static int sx126x_lora_send_async(const struct device *dev, data->tx_async_signal = async; k_msgq_purge(&data->tx_msgq); - /* Set packet parameters */ - ret = sx126x_set_configured_packet_params(dev, data_len); - if (ret < 0) { - goto out_error; - } - - /* Write payload to buffer */ - ret = sx126x_hal_write_buffer(dev, 0x00, data_buf, data_len); - if (ret < 0) { - goto out_error; - } - - ret = sx126x_apply_tx_modulation_workaround( - dev, !SX126X_IS_GFSK(data) && data->config.bandwidth == BW_500_KHZ); + ret = sx126x_stage_tx(dev, data_buf, data_len); if (ret < 0) { goto out_error; } @@ -1847,6 +1935,133 @@ static int sx126x_lora_cad_async(const struct device *dev, lora_cad_cb cb, void return 0; } +/* + * Listen-before-talk, done in the driver so the gap between "channel is + * clear" and "radio is transmitting" is just RF-path + SetTx, with no + * sleep/wake round-trip in between (see sx126x_handle_irq_cad_done_lbt() + * and sx126x_stage_tx()). Not part of the generic lora.h API - exposed via + * zephyr/drivers/lora/sx126x.h, same as the other SX126x-specific + * extensions. + * + * @p cad_timeout bounds only the CAD phase. Once CAD comes back clear and + * the radio commits to transmitting, the wait for TX_DONE is bounded + * separately (by SetTx's own 10s hardware timeout) so a short + * @p cad_timeout can't abort an in-flight transmission - see + * data->lbt_committed below. + * + * @return 1 if the channel was busy (nothing was sent), 0 if the frame + * was sent, or a negative error code. + */ +int sx126x_lora_send_lbt(const struct device *dev, uint8_t *data_buf, uint32_t data_len, + k_timeout_t cad_timeout) +{ + struct sx126x_data *data = dev->data; + struct sx126x_cad_defaults defaults; + enum lora_cad_symbol_num symbol_num; + uint8_t det_peak, det_min, symbol_num_reg; + struct sx126x_lbt_result result; + int ret; + + if (!data->config_valid) { + LOG_ERR("Not configured"); + return -EINVAL; + } + + if (SX126X_IS_GFSK(data)) { + LOG_ERR("CAD is a LoRa-only operation"); + return -ENOTSUP; + } + + if (data_len > SX126X_MAX_PAYLOAD_LEN) { + LOG_ERR("Payload too long: %u", data_len); + return -EINVAL; + } + + symbol_num = data->config.cad.symbol_num; + det_peak = data->config.cad.detection_peak; + det_min = data->config.cad.detection_minimum; + + if (symbol_num == 0 || det_peak == 0 || det_min == 0) { + ret = sx126x_cad_lookup_defaults(data->config.bandwidth, data->config.datarate, + &defaults); + if (ret < 0) { + LOG_ERR("No CAD defaults for this bandwidth/SF; " + "set cad.symbol_num/detection_peak/detection_minimum explicitly"); + return ret; + } + symbol_num = symbol_num ? symbol_num : defaults.symbol_num; + det_peak = det_peak ? det_peak : defaults.det_peak; + det_min = det_min ? det_min : defaults.det_min; + } + + ret = sx126x_cad_symbol_num_to_reg(symbol_num, &symbol_num_reg); + if (ret < 0) { + LOG_ERR("Invalid CAD symbol count: %d", symbol_num); + return ret; + } + + if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_CAD_TX)) { + LOG_ERR("Busy"); + return -EBUSY; + } + + k_mutex_lock(&data->lock, K_FOREVER); + k_msgq_purge(&data->lbt_msgq); + atomic_set(&data->lbt_committed, 0); + + ret = sx126x_ensure_ready(dev); + if (ret < 0) { + goto out_error; + } + + /* Stage the payload now, while the chip is awake in STDBY_RC and + * before SetCAD - CAD doesn't touch packet params/FIFO/TX-mod, so + * none of this needs to happen again after CadDone. + */ + ret = sx126x_stage_tx(dev, data_buf, data_len); + if (ret < 0) { + goto out_error; + } + + ret = sx126x_set_cad_params(dev, symbol_num_reg, det_peak, det_min, + SX126X_CAD_EXIT_MODE_CAD_ONLY, 0); + if (ret < 0) { + goto out_error; + } + + ret = sx126x_set_cad(dev); + if (ret < 0) { + goto out_error; + } + + k_mutex_unlock(&data->lock); + + ret = k_msgq_get(&data->lbt_msgq, &result, cad_timeout); + if (ret < 0 && atomic_get(&data->lbt_committed)) { + /* CAD already came back clear and TX is underway; the short + * CAD-phase wait above isn't the right bound for that - wait + * out the transmission itself instead of aborting it. + */ + ret = k_msgq_get(&data->lbt_msgq, &result, K_SECONDS(15)); + } + + if (ret < 0) { + LOG_DBG("LBT: timeout"); + k_mutex_lock(&data->lock, K_FOREVER); + sx126x_set_standby(dev, SX126X_STANDBY_RC); + sx126x_set_sleep(dev); + k_mutex_unlock(&data->lock); + return -ETIMEDOUT; + } + + return result.status; + +out_error: + sx126x_set_sleep(dev); + k_mutex_unlock(&data->lock); + return ret; +} + static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, int8_t tx_power, uint16_t duration) { @@ -1951,6 +2166,8 @@ static int sx126x_init(const struct device *dev) sizeof(struct sx126x_rx_result), 1); k_msgq_init(&data->cad_msgq, (char *)&data->cad_result, sizeof(struct sx126x_cad_result), 1); + k_msgq_init(&data->lbt_msgq, (char *)&data->lbt_result, + sizeof(struct sx126x_lbt_result), 1); k_work_init(&data->irq_work, sx126x_irq_work_handler); data->dev = dev; atomic_set(&data->state, SX126X_STATE_IDLE); diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h index 5f22f620f35..d831d62aaa8 100644 --- a/drivers/lora/native/sx126x/sx126x.h +++ b/drivers/lora/native/sx126x/sx126x.h @@ -20,6 +20,7 @@ enum sx126x_state { SX126X_STATE_RX, SX126X_STATE_RX_DUTY_CYCLE, SX126X_STATE_CAD, + SX126X_STATE_CAD_TX, }; struct sx126x_tx_result { @@ -37,6 +38,11 @@ struct sx126x_cad_result { bool activity_detected; }; +struct sx126x_lbt_result { + /* 1 = channel busy, nothing transmitted; 0 = sent; <0 = error */ + int status; +}; + struct sx126x_data { struct sx126x_hal_data hal; @@ -76,6 +82,17 @@ struct sx126x_data { lora_cad_cb cad_cb; void *cad_cb_user_data; + /* sx126x_lora_send_lbt() completion via message queue */ + struct k_msgq lbt_msgq; + struct sx126x_lbt_result lbt_result; + /* + * Set once a send_lbt() op's CAD comes back clear and SetTx has + * been issued, so a caller whose CAD-phase wait times out knows + * the radio is already transmitting rather than aborting it - see + * sx126x_lora_send_lbt(). + */ + atomic_t lbt_committed; + /* RX duty cycle parameters (in SX126x timeout ticks) */ struct { uint32_t rx_period; diff --git a/include/zephyr/drivers/lora/sx126x.h b/include/zephyr/drivers/lora/sx126x.h index 4052f86aa41..0a2dc9c13c1 100644 --- a/include/zephyr/drivers/lora/sx126x.h +++ b/include/zephyr/drivers/lora/sx126x.h @@ -9,6 +9,7 @@ #define ZEPHYR_INCLUDE_DRIVERS_LORA_SX126X_H_ #include +#include #include #include #include @@ -51,6 +52,29 @@ extern "C" { */ bool sx126x_irq_timestamp_get(const struct device *dev, uint16_t irq_bit, uint64_t *ns_out); +/** + * @brief Listen-before-talk send: CAD, then transmit only if clear. + * + * This issues a single CAD attempt; retry/backoff policy belongs at the + * caller (e.g. a TX queue). + * + * @param dev SX126x LoRa device, already configured via + * @ref lora_config + * @param data Payload to send if the channel is clear + * @param len Payload length + * @param cad_timeout Bound on the CAD phase only. Once CAD comes back + * clear and the radio commits to transmitting, the + * wait for TX completion is bounded separately (by the + * radio's own hardware TX timeout) so a short + * @p cad_timeout here can never abort an in-flight + * transmission. + * @return 1 if the channel was busy and nothing was sent, 0 if the frame + * was sent, or a negative error code (including -ETIMEDOUT if + * CAD itself never completed within @p cad_timeout) + */ +int sx126x_lora_send_lbt(const struct device *dev, uint8_t *data, uint32_t len, + k_timeout_t cad_timeout); + #ifdef __cplusplus } #endif -- 2.54.0 (Apple Git-157)