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{}; }