From 1c3626d58b0a3b9fd62ad293717bbf925acecf40 Mon Sep 17 00:00:00 2001 From: crosstyan Date: Mon, 1 Jun 2026 18:05:31 +0800 Subject: [PATCH] feat(llcc68): add configurable RF switch modes Add an explicit rf-switch-mode devicetree property for LLCC68 instances, covering no switch handling, TXEN/RXEN complementary GPIO control, and DIO2 single-pin control for PE4259-style RF switches. Preserve the existing default behavior with an auto Kconfig default that only enables complementary GPIO handling when both TXEN and RXEN GPIOs are present. Resolve the RF switch mode into llcc68_config at build time and validate incompatible devicetree combinations with BUILD_ASSERT checks. Configure optional RXEN GPIO handling for DIO2 single-pin mode and keep DIO2 RF switch control disabled unless that mode is selected. Replace the old fire-and-forget TX/RX GPIO helper with a result-returning mode-aware RF switch state helper, and apply it across standby, sleep, CAD, TX, RX, continuous wave, infinite preamble, and modem init paths. Add SetRxDutyCycle support with explicit raw 24-bit LLCC68 period units, plus helpers and a millisecond wrapper for callers that work in time units. Select the RX RF path before issuing the duty-cycle command so RXEN stays valid for duty-cycle listen windows. --- drivers/llcc68/Kconfig | 36 +++++++- drivers/llcc68/llcc68_raw.c | 41 ++++++++- dts/bindings/semtech,llcc68-weihua.yaml | 22 +++++ include/llcc68.hpp | 17 +++- include/llcc68_definitions.hpp | 39 +++++++- include/llcc68_raw.h | 7 ++ src/llcc68.cpp | 113 ++++++++++++++++++++---- 7 files changed, 250 insertions(+), 25 deletions(-) diff --git a/drivers/llcc68/Kconfig b/drivers/llcc68/Kconfig index 92c75ba..02dc4b7 100644 --- a/drivers/llcc68/Kconfig +++ b/drivers/llcc68/Kconfig @@ -1,9 +1,9 @@ -DT_COMPAT_SEMTECH_LLCC68 := "semtech,llcc68" +DT_COMPAT_SEMTECH_LLCC68_WEIHUA := "semtech,llcc68-weihua" config LLCC68 bool "Semtech LLCC68 LoRa Radio Driver" depends on SPI && GPIO - default $(dt_compat_enabled,$(DT_COMPAT_SEMTECH_LLCC68)) + default $(dt_compat_enabled,$(DT_COMPAT_SEMTECH_LLCC68_WEIHUA)) help Enable the Semtech LLCC68 LoRa Radio Driver. @@ -32,6 +32,38 @@ config LLCC68_ALWAYS_USE_SX1262_HIGH_PA When enabled, the LLCC68/SX1262/SX1261 driver always chooses high power amplifier settings instead of selecting them from chip version. +choice LLCC68_RF_SWITCH_DEFAULT + prompt "Default LLCC68 RF switch mode" + default LLCC68_RF_SWITCH_DEFAULT_AUTO + help + Default RF switch mode for LLCC68 devicetree nodes that do not set + rf-switch-mode. + +config LLCC68_RF_SWITCH_DEFAULT_AUTO + bool "Auto" + help + Preserve legacy behavior: use complementary GPIO control when both + tx-enable-gpios and rx-enable-gpios are present, otherwise disable RF + switch control. + +config LLCC68_RF_SWITCH_DEFAULT_NONE + bool "None" + help + Disable RF switch control unless rf-switch-mode is set in devicetree. + +config LLCC68_RF_SWITCH_DEFAULT_GPIO_COMPLEMENTARY + bool "TXEN/RXEN complementary GPIO" + help + Use MCU GPIOs for complementary TXEN/RXEN RF switch control by default. + +config LLCC68_RF_SWITCH_DEFAULT_DIO2_SINGLE + bool "DIO2 single-pin" + help + Use LLCC68 DIO2 RF switch control by default. DIO2 drives TXEN, while + RXEN is held active externally or by rx-enable-gpios. + +endchoice + module = LLCC68 module-str = llcc68 diff --git a/drivers/llcc68/llcc68_raw.c b/drivers/llcc68/llcc68_raw.c index 4980391..cbf2e69 100644 --- a/drivers/llcc68/llcc68_raw.c +++ b/drivers/llcc68/llcc68_raw.c @@ -1,10 +1,30 @@ #include "llcc68_raw.h" #include +#include #include #define DT_DRV_COMPAT semtech_llcc68_weihua +#define LLCC68_AUTO_RF_SWITCH_MODE(inst) \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, tx_enable_gpios), \ + (COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, rx_enable_gpios), \ + (LLCC68_RF_SWITCH_GPIO_COMPLEMENTARY), (LLCC68_RF_SWITCH_NONE))), \ + (LLCC68_RF_SWITCH_NONE)) + +#if defined(CONFIG_LLCC68_RF_SWITCH_DEFAULT_GPIO_COMPLEMENTARY) +#define LLCC68_DEFAULT_RF_SWITCH_MODE(inst) LLCC68_RF_SWITCH_GPIO_COMPLEMENTARY +#elif defined(CONFIG_LLCC68_RF_SWITCH_DEFAULT_DIO2_SINGLE) +#define LLCC68_DEFAULT_RF_SWITCH_MODE(inst) LLCC68_RF_SWITCH_DIO2_SINGLE +#elif defined(CONFIG_LLCC68_RF_SWITCH_DEFAULT_NONE) +#define LLCC68_DEFAULT_RF_SWITCH_MODE(inst) LLCC68_RF_SWITCH_NONE +#else +#define LLCC68_DEFAULT_RF_SWITCH_MODE(inst) LLCC68_AUTO_RF_SWITCH_MODE(inst) +#endif + +#define LLCC68_RF_SWITCH_MODE(inst) \ + DT_ENUM_IDX_OR(DT_DRV_INST(inst), rf_switch_mode, LLCC68_DEFAULT_RF_SWITCH_MODE(inst)) + static void dio1_irq_trampoline(const struct device *port, struct gpio_callback *cb, uint32_t pins) { ARG_UNUSED(port); @@ -20,14 +40,21 @@ int llcc68_init(const struct device *dev) { const struct llcc68_config *config = dev->config; struct llcc68_data *data = dev->data; - if (config->tx_enable_gpio.port != NULL) { + if (config->rf_switch_mode == LLCC68_RF_SWITCH_GPIO_COMPLEMENTARY && + config->tx_enable_gpio.port != NULL) { gpio_pin_configure_dt(&config->tx_enable_gpio, GPIO_OUTPUT_INACTIVE); } - if (config->rx_enable_gpio.port != NULL) { + if (config->rf_switch_mode == LLCC68_RF_SWITCH_GPIO_COMPLEMENTARY && + config->rx_enable_gpio.port != NULL) { gpio_pin_configure_dt(&config->rx_enable_gpio, GPIO_OUTPUT_INACTIVE); } + if (config->rf_switch_mode == LLCC68_RF_SWITCH_DIO2_SINGLE && + config->rx_enable_gpio.port != NULL) { + gpio_pin_configure_dt(&config->rx_enable_gpio, GPIO_OUTPUT_ACTIVE); + } + gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT_INACTIVE); gpio_pin_configure_dt(&config->busy_gpio, GPIO_INPUT); gpio_pin_configure_dt(&config->dio1_gpio, GPIO_INPUT); @@ -43,6 +70,15 @@ int llcc68_init(const struct device *dev) { } #define LLCC68_DEFINE(inst) \ + BUILD_ASSERT(LLCC68_RF_SWITCH_MODE(inst) != LLCC68_RF_SWITCH_GPIO_COMPLEMENTARY || \ + (DT_INST_NODE_HAS_PROP(inst, tx_enable_gpios) && \ + DT_INST_NODE_HAS_PROP(inst, rx_enable_gpios)), \ + "LLCC68 gpio-complementary RF switch mode requires tx-enable-gpios " \ + "and rx-enable-gpios"); \ + BUILD_ASSERT(LLCC68_RF_SWITCH_MODE(inst) != LLCC68_RF_SWITCH_DIO2_SINGLE || \ + !DT_INST_NODE_HAS_PROP(inst, tx_enable_gpios), \ + "LLCC68 dio2-single RF switch mode uses DIO2 for TXEN and must not " \ + "define tx-enable-gpios"); \ static struct llcc68_data llcc68_data_##inst; \ static const struct llcc68_config llcc68_config_##inst = \ { \ @@ -52,6 +88,7 @@ int llcc68_init(const struct device *dev) { .dio1_gpio = GPIO_DT_SPEC_INST_GET(inst, dio1_gpios), \ .tx_enable_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, tx_enable_gpios, {.port = NULL}), \ .rx_enable_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, rx_enable_gpios, {.port = NULL}), \ + .rf_switch_mode = LLCC68_RF_SWITCH_MODE(inst), \ }; \ DEVICE_DT_INST_DEFINE(inst, \ llcc68_init, \ diff --git a/dts/bindings/semtech,llcc68-weihua.yaml b/dts/bindings/semtech,llcc68-weihua.yaml index b7905d6..8b86a40 100644 --- a/dts/bindings/semtech,llcc68-weihua.yaml +++ b/dts/bindings/semtech,llcc68-weihua.yaml @@ -48,6 +48,28 @@ properties: Antenna switch RX enable GPIO. If set, the driver tracks the state of the radio and controls the RF switch. + rf-switch-mode: + type: string + enum: + - "none" + - "gpio-complementary" + - "dio2-single" + description: | + Optional RF switch control mode. + + "none" disables RF switch handling. + + "gpio-complementary" controls TXEN/RXEN from MCU GPIOs using the + complementary table: + idle: TXEN=0, RXEN=0 + RX: TXEN=0, RXEN=1 + TX: TXEN=1, RXEN=0 + This mode requires tx-enable-gpios and rx-enable-gpios. + + "dio2-single" enables LLCC68 DIO2-as-RF-switch control for TXEN. + RXEN must be externally pulled active or supplied as rx-enable-gpios, + which the driver holds active. This mode must not use tx-enable-gpios. + spi-cs-setup-delay-ns: default: 100000 diff --git a/include/llcc68.hpp b/include/llcc68.hpp index b3cef7c..489974e 100644 --- a/include/llcc68.hpp +++ b/include/llcc68.hpp @@ -120,7 +120,7 @@ struct LLCC68 { uint8_t offset, std::span data_from_host, timeout_ms_t busy_timeout = DEFAULT_BUSY_TIMEOUT_MS); - void tx_rx_en_pin_set(TxRxPinState state); + expected set_rf_switch_state(RfSwitchState state); /** LLCC68 DataSheet Function */ @@ -239,6 +239,21 @@ struct LLCC68 { expected set_tx(uint32_t timeout = TIMEOUT_NONE); expected set_rx(uint32_t timeout = TIMEOUT_INF); + /** + * @brief Start LLCC68 RX duty-cycle/listen mode. + * + * rx_period and sleep_period are raw 24-bit LLCC68 RTC periods, not + * milliseconds. Datasheet section 13.1.7 defines one period as 15.625 us: + * RX duration = rx_period * 15.625 us + * sleep duration = sleep_period * 15.625 us + * + * Use rx_duty_cycle_period_from_ms() or set_rx_duty_cycle_ms() when caller + * inputs are in milliseconds. + */ + expected set_rx_duty_cycle(uint32_t rx_period, + uint32_t sleep_period); + expected set_rx_duty_cycle_ms(uint32_t rx_period_ms, + uint32_t sleep_period_ms); expected set_sleep(sleep_config_t config); expected set_tx_continuous_wave(); expected set_tx_infinite_preamble(); diff --git a/include/llcc68_definitions.hpp b/include/llcc68_definitions.hpp index 2267c49..a70263a 100644 --- a/include/llcc68_definitions.hpp +++ b/include/llcc68_definitions.hpp @@ -17,6 +17,42 @@ struct LLCC68; using airtime_t = std::chrono::microseconds; using error_code = std::error_code; +constexpr uint32_t RX_DUTY_CYCLE_PERIOD_MAX = 0x00FFFFFFU; +constexpr uint32_t RX_DUTY_CYCLE_PERIOD_UNIT_US_NUMERATOR = 125U; +constexpr uint32_t RX_DUTY_CYCLE_PERIOD_UNIT_US_DENOMINATOR = 8U; + +/** + * @brief Convert microseconds to a raw SetRxDutyCycle period. + * + * LLCC68 datasheet section 13.1.7 defines rxPeriod and sleepPeriod as raw + * 24-bit RTC periods, not milliseconds. One raw period is 15.625 us. + */ +constexpr std::optional rx_duty_cycle_period_from_us(uint64_t us) { + constexpr uint64_t scale = RX_DUTY_CYCLE_PERIOD_UNIT_US_DENOMINATOR; + constexpr uint64_t divisor = RX_DUTY_CYCLE_PERIOD_UNIT_US_NUMERATOR; + if (us > (UINT64_MAX - (divisor - 1U)) / scale) { + return std::nullopt; + } + const uint64_t raw = ((us * scale) + (divisor - 1U)) / divisor; + if (raw > RX_DUTY_CYCLE_PERIOD_MAX) { + return std::nullopt; + } + return static_cast(raw); +} + +/** + * @brief Convert milliseconds to a raw SetRxDutyCycle period. + * + * Returns std::nullopt if the requested duration does not fit the LLCC68 + * 24-bit period field. + */ +constexpr std::optional rx_duty_cycle_period_from_ms(uint64_t ms) { + if (ms > UINT64_MAX / 1000U) { + return std::nullopt; + } + return rx_duty_cycle_period_from_us(ms * 1000U); +} + enum class Errc : uint8_t { FailureToExecuteCommand = 1, CommandTimeout = 2, @@ -985,7 +1021,8 @@ struct irq_status_bits_t { // MSB }; -enum class TxRxPinState : uint8_t { +enum class RfSwitchState : uint8_t { + Idle, TX, RX, }; diff --git a/include/llcc68_raw.h b/include/llcc68_raw.h index c1695e5..966c616 100644 --- a/include/llcc68_raw.h +++ b/include/llcc68_raw.h @@ -14,6 +14,12 @@ extern "C" { typedef void (*llcc68_user_dio1_handler_t)(const struct device *dev, void *user_data); +enum llcc68_rf_switch_mode { + LLCC68_RF_SWITCH_NONE = 0, + LLCC68_RF_SWITCH_GPIO_COMPLEMENTARY = 1, + LLCC68_RF_SWITCH_DIO2_SINGLE = 2, +}; + struct llcc68_config { struct spi_dt_spec spi; struct gpio_dt_spec reset_gpio; @@ -21,6 +27,7 @@ struct llcc68_config { struct gpio_dt_spec dio1_gpio; struct gpio_dt_spec tx_enable_gpio; struct gpio_dt_spec rx_enable_gpio; + enum llcc68_rf_switch_mode rf_switch_mode; }; struct llcc68_data { diff --git a/src/llcc68.cpp b/src/llcc68.cpp index 927e583..6304c7a 100644 --- a/src/llcc68.cpp +++ b/src/llcc68.cpp @@ -172,18 +172,46 @@ int wait_for_not_busy(const gpio_dt_spec &busy_gpio, uint16_t timeout_ms) { } } // namespace -void LLCC68::tx_rx_en_pin_set(TxRxPinState state) { - if (not tx_enable_gpio() or (not rx_enable_gpio())) { - return; +expected LLCC68::set_rf_switch_state(RfSwitchState state) { + auto set_gpio = [](const gpio_dt_spec &gpio, int value) + -> expected { + if (not device_is_ready(gpio.port)) { + return ue(-ENODEV); + } + const int ret = gpio_pin_set_dt(&gpio, value); + if (ret < 0) { + return ue(ret); + } + return unit{}; + }; + + switch (config().rf_switch_mode) { + case LLCC68_RF_SWITCH_NONE: + return unit{}; + case LLCC68_RF_SWITCH_GPIO_COMPLEMENTARY: { + const auto tx = tx_enable_gpio(); + const auto rx = rx_enable_gpio(); + if (not tx or not rx) { + return ue(-ENODEV); + } + + expected r; + if (state == RfSwitchState::TX) { + r = set_gpio(*rx, 0); + APP_RADIO_RETURN_ERR(r); + return set_gpio(*tx, 1); + } + r = set_gpio(*tx, 0); + APP_RADIO_RETURN_ERR(r); + return set_gpio(*rx, state == RfSwitchState::RX ? 1 : 0); } - auto t = *tx_enable_gpio(); - auto r = *rx_enable_gpio(); - if (state == TxRxPinState::TX) { - gpio_pin_set_dt(&t, 1); - gpio_pin_set_dt(&r, 0); - } else if (state == TxRxPinState::RX) { - gpio_pin_set_dt(&t, 0); - gpio_pin_set_dt(&r, 1); + case LLCC68_RF_SWITCH_DIO2_SINGLE: + if (auto rx = rx_enable_gpio()) { + return set_gpio(*rx, 1); + } + return unit{}; + default: + return ue(-EINVAL); } } @@ -643,7 +671,9 @@ expected LLCC68::reset() { expected LLCC68::set_standby() { const uint8_t data[] = {RADIOLIB_SX126X_STANDBY_RC}; - return write_stream(RADIOLIB_SX126X_CMD_SET_STANDBY, data); + auto r = write_stream(RADIOLIB_SX126X_CMD_SET_STANDBY, data); + APP_RADIO_RETURN_ERR(r); + return set_rf_switch_state(RfSwitchState::Idle); } expected LLCC68::hal_get_chip_type() { @@ -1080,6 +1110,8 @@ expected LLCC68::set_cad_params(cad_params_t params) { expected LLCC68::set_cad() { auto dummy = std::span{}; + auto r = set_rf_switch_state(RfSwitchState::RX); + APP_RADIO_RETURN_ERR(r); return write_stream(RADIOLIB_SX126X_CMD_SET_CAD, dummy); } @@ -1101,21 +1133,56 @@ expected LLCC68::set_rx(uint32_t timeout) { return write_stream(RADIOLIB_SX126X_CMD_SET_RX, data); } +expected LLCC68::set_rx_duty_cycle(uint32_t rx_period, + uint32_t sleep_period) { + if (rx_period > RX_DUTY_CYCLE_PERIOD_MAX || + sleep_period > RX_DUTY_CYCLE_PERIOD_MAX) { + return ue(-EINVAL); + } + auto r = set_rf_switch_state(RfSwitchState::RX); + APP_RADIO_RETURN_ERR(r); + const uint8_t data[] = { + static_cast((rx_period >> 16) & 0xFF), + static_cast((rx_period >> 8) & 0xFF), + static_cast(rx_period & 0xFF), + static_cast((sleep_period >> 16) & 0xFF), + static_cast((sleep_period >> 8) & 0xFF), + static_cast(sleep_period & 0xFF), + }; + return write_stream(RADIOLIB_SX126X_CMD_SET_RX_DUTY_CYCLE, data); +} + +expected +LLCC68::set_rx_duty_cycle_ms(uint32_t rx_period_ms, uint32_t sleep_period_ms) { + const auto rx_period = rx_duty_cycle_period_from_ms(rx_period_ms); + const auto sleep_period = rx_duty_cycle_period_from_ms(sleep_period_ms); + if (not rx_period or not sleep_period) { + return ue(-EINVAL); + } + return set_rx_duty_cycle(*rx_period, *sleep_period); +} + expected LLCC68::set_sleep(sleep_config_t config) { auto c = *reinterpret_cast(&config); const uint8_t data[] = {c}; - return write_stream(RADIOLIB_SX126X_CMD_SET_SLEEP, data); + auto r = write_stream(RADIOLIB_SX126X_CMD_SET_SLEEP, data); + APP_RADIO_RETURN_ERR(r); + return set_rf_switch_state(RfSwitchState::Idle); } expected LLCC68::set_tx_continuous_wave() { auto dummy = std::span{}; // const uint8_t dummy[] = {RADIOLIB_SX126X_CMD_NOP}; + auto r = set_rf_switch_state(RfSwitchState::TX); + APP_RADIO_RETURN_ERR(r); return write_stream(RADIOLIB_SX126X_CMD_SET_TX_CONTINUOUS_WAVE, dummy); } expected LLCC68::set_tx_infinite_preamble() { auto dummy = std::span{}; // const uint8_t dummy[] = {RADIOLIB_SX126X_CMD_NOP}; + auto r = set_rf_switch_state(RfSwitchState::TX); + APP_RADIO_RETURN_ERR(r); return write_stream(RADIOLIB_SX126X_CMD_SET_TX_INFINITE_PREAMBLE, dummy); } @@ -1222,7 +1289,9 @@ expected LLCC68::hal_modem_init(lora_parameters_t params) { "modem_init::set_modulation_params"); APP_RADIO_RETURN_ERR_CTX(set_lora_sync_word(params.sync_word), "modem_init::set_lora_sync_word"); - APP_RADIO_RETURN_ERR_CTX(set_dio2_as_rf_switch(false), + APP_RADIO_RETURN_ERR_CTX(set_dio2_as_rf_switch( + config().rf_switch_mode == + LLCC68_RF_SWITCH_DIO2_SINGLE), "modem_init::set_dio2_as_rf_switch"); APP_RADIO_RETURN_ERR_CTX(set_rf_frequency(params.frequency_mhz), "modem_init::set_rf_frequency"); @@ -1278,7 +1347,9 @@ LLCC68::hal_gfsk_modem_init(gfsk_parameters_t params) { params.broadcast_address.value_or(0)), "gfsk_init::set_address_filtering"); } - APP_RADIO_RETURN_ERR_CTX(set_dio2_as_rf_switch(false), + APP_RADIO_RETURN_ERR_CTX(set_dio2_as_rf_switch( + config().rf_switch_mode == + LLCC68_RF_SWITCH_DIO2_SINGLE), "gfsk_init::set_dio2_as_rf_switch"); APP_RADIO_RETURN_ERR_CTX(set_rf_frequency(params.frequency_mhz), "gfsk_init::set_rf_frequency"); @@ -1330,7 +1401,8 @@ LLCC68::hal_async_flush(lora_parameters_t params) { "tx::set_dio_irq_params"); APP_RADIO_RETURN_ERR_CTX(clear_irq_status(irq_params.irqMask), "tx::clear_irq_status"); - tx_rx_en_pin_set(TxRxPinState::TX); + APP_RADIO_RETURN_ERR_CTX(set_rf_switch_state(RfSwitchState::TX), + "tx::set_rf_switch"); APP_RADIO_RETURN_ERR_CTX(set_tx(), "tx::set_tx_params"); auto air = calc_time_on_air( data().tx_xfer_size, params.mod_params.sf, params.mod_params.bw, @@ -1387,7 +1459,8 @@ LLCC68::hal_gfsk_async_flush(gfsk_parameters_t params) { "gfsk_tx::set_dio_irq_params"); APP_RADIO_RETURN_ERR_CTX(clear_irq_status(irq_params.irqMask), "gfsk_tx::clear_irq_status"); - tx_rx_en_pin_set(TxRxPinState::TX); + APP_RADIO_RETURN_ERR_CTX(set_rf_switch_state(RfSwitchState::TX), + "gfsk_tx::set_rf_switch"); APP_RADIO_RETURN_ERR_CTX(set_tx(), "gfsk_tx::set_tx"); auto air_estimated = calc_gfsk_time_on_air(params, static_cast(data().tx_xfer_size)); @@ -1431,7 +1504,8 @@ expected LLCC68::hal_async_rx(lora_parameters_t params) { APP_RADIO_RETURN_ERR_CTX(clear_irq_status(irq_params.irqMask), "rx::clear_irq_status"); - tx_rx_en_pin_set(TxRxPinState::RX); + APP_RADIO_RETURN_ERR_CTX(set_rf_switch_state(RfSwitchState::RX), + "rx::set_rf_switch"); APP_RADIO_RETURN_ERR_CTX(set_rx(), "rx::set_rx"); return unit{}; } @@ -1457,7 +1531,8 @@ expected LLCC68::hal_gfsk_async_rx(gfsk_parameters_t params) { APP_RADIO_RETURN_ERR_CTX(clear_irq_status(irq_params.irqMask), "gfsk_rx::clear_irq_status"); - tx_rx_en_pin_set(TxRxPinState::RX); + APP_RADIO_RETURN_ERR_CTX(set_rf_switch_state(RfSwitchState::RX), + "gfsk_rx::set_rf_switch"); APP_RADIO_RETURN_ERR_CTX(set_rx(), "gfsk_rx::set_rx"); return unit{}; }