// // Created by Kurosu Chan on 2024/1/17. // #ifndef D07297EB_4033_481B_BCFA_1D40899340D0 #define D07297EB_4033_481B_BCFA_1D40899340D0 #include #include #include #include #include #include #include #include "hal/gpio_types.h" #include "hal_gpio.hpp" #include "app_const_llcc68.hpp" #include "radiolib_definitions.hpp" #include "utils/app_clock.hpp" #include "utils/app_utils.hpp" #include "utils/app_result.hpp" #include "utils/app_clock_instant.hpp" #include "llcc68_definitions.hpp" #include "hal_spi.hpp" #include "hal_error.hpp" /** * Both, RxBaseAddr and TxBaseAddr are set using the command SetBufferBaseAddresses(...). * * By default RxBaseAddr and TxBaseAddr are initialized at address 0x00. * * Due to the contiguous nature of the data buffer, the base addresses for Tx and Rx are fully configurable across the 256-byte * memory area. Each pointer can be set independently anywhere within the buffer. To exploit the maximum data buffer size * in transmit or receive mode, the whole data buffer can be used in each mode by setting the base addresses TxBaseAddr and * RxBaseAddr at the bottom of the memory (0x00). * The data buffer is cleared when the device is put into Sleep mode (implying no access). The data is retained in all other * modes of operation. * * The data buffer is acceded via the command WriteBuffer(...) and ReadBuffer(...). In this function the parameter offset defines * the address pointer of the first data to be written or read. Offset zero defines the first position of the data buffer. * Before any read or write operation it is hence necessary to initialize this offset to the corresponding beginning of the buffer. * Upon reading or writing to the data buffer the address pointer will then increment automatically. * * Two possibilities exist to obtain the offset value: * * - First is to use the RxBaseAddr value since the user defines it before receiving a payload. * - Second, offset can be initialized with the value of RxStartBufferPointer returned by GetRxbufferStatus(...) command. */ constexpr auto DEFAULT_TX_BUFFER_ADDRESS = 0x00; constexpr auto DEFAULT_RX_BUFFER_ADDRESS = 0x80; static_assert(DEFAULT_RX_BUFFER_ADDRESS < 0xff, "DEFAULT_RX_BUFFER_ADDRESS must be less than 0xff"); constexpr auto MAX_RX_BUFFER_SIZE = 0xff - DEFAULT_RX_BUFFER_ADDRESS; /** * \brief return if STATEVAR has value, otherwise return the error * \param STATEVAR the variable to check * \note this macro is used to return the error from a function * \sa https://en.cppreference.com/w/cpp/utility/expected/operator_bool */ #define APP_RADIO_RETURN_ERR(STATEVAR) \ { \ if (not(STATEVAR.has_value())) { \ return (ue_t{STATEVAR.error()}); \ } \ } /** * @brief return if STATEVAR has value that is NOT RADIO_TRANS_CMD_PROC_ERR * @note some read operation would still succeed even if the command processing error occurs */ #define APP_RADIO_RETURN_ERR_IGNORE_PROC_ERR(STATEVAR) \ { \ if (not(STATEVAR.has_value())) { \ if (not(STATEVAR.error() == error::RADIO_TRANS_CMD_PROC_ERR)) { \ return ue_t{STATEVAR.error()}; \ } \ } \ } /** * \brief return if STATEVAR has value, otherwise return the error * \param STATEVAR the variable to check * \param ctx the context of the error * \param ... the arguments to pass to the error message * \note this macro is used to return the error from a function and log the error message */ #define APP_RADIO_RETURN_ERR_CTX(STATEVAR, ctx, ...) \ { \ if (not(STATEVAR.has_value())) { \ ESP_LOGE(TAG, ctx, __VA_ARGS__); \ return (ue_t{STATEVAR.error()}); \ } \ } namespace app::driver::llcc68 { constexpr auto TAG = "lora"; using namespace app::driver::hal; using Instant = app::utils::Instant; template using Result = app::utils::Result; using Unit = app::utils::Unit; using millisecond = app::utils::Instant::milliseconds; /** * \brief Whether to use only LDO regulator (true) or DC-DC regulator (false) */ constexpr bool USE_REGULATOR_LDO = true; /*! \brief Whether the module has an XTAL (true) or TCXO (false) TCXO (Temperature Compensated Crystal Oscillator) will be disabled if this is set to true */ constexpr bool USE_TXCO = false; constexpr llcc68::TcxoVoltage DEFAULT_TCXO_VOLTAGE = TcxoVoltage::V_1_6; using error_t = spi::error_t; using ue_t = spi::ue_t; /** * \brief maintaining the transmission state for async transmission */ struct transmit_state_t { bool is_transmitting = false; Instant::time_point expected_end_timestamp{}; void mut_reset() { is_transmitting = false; expected_end_timestamp = Instant::time_point::min(); } }; /** * @brief private namespace; Don't use anything in this namespace outside of this file */ namespace details {} constexpr auto delay_ms = app::utils::delay_ms; /*! \brief Sets the module to standby mode. */ inline Result standby() { // Oscillator to be used in standby mode. // Can be set to RADIOLIB_SX126X_STANDBY_RC (13 MHz RC oscillator) // or RADIOLIB_SX126X_STANDBY_XOSC (32 MHz external crystal oscillator). constexpr uint8_t data[] = {RADIOLIB_SX126X_STANDBY_RC}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_STANDBY, data); } inline Result reset() { if (RST_PIN == GPIO_NUM_NC) { return {}; } gpio::set_mode(RST_PIN, gpio::Mode::OUTPUT); gpio::digital_write(RST_PIN, false); delay_ms(250); gpio::digital_write(RST_PIN, true); delay_ms(1'000); const auto instant = Instant{}; constexpr auto INTERVAL = Instant::milliseconds{spi::DEFAULT_TIMEOUT_MS}; decltype(standby()) res = ue_t{error_t{error::FAILED}}; while (not res && instant.elapsed() < INTERVAL) { res = standby(); if (res.has_value()) { break; } // don't spam the module delay_ms(100); } if (not res) { return res; } else { return {}; } }; inline Result read_register(const uint16_t addr, std::span data) { return spi::read_register(addr, data); } inline Result write_register(const uint16_t addr, std::span data) { return spi::write_register(addr, data); } Result inline write_buffer(std::span data, const uint8_t offset = DEFAULT_TX_BUFFER_ADDRESS) { return spi::write_buffer(offset, data); } struct irq_status_bits_t { // LSB bool tx_done : 1; // 0 bool rx_done : 1; // 1 bool preamble_detected : 1; // 2 bool sync_word_valid : 1; // 3 bool header_valid : 1; // 4 bool header_err : 1; // 5 bool crc_err : 1; // 6 bool cad_done : 1; // 7 bool cad_detected : 1; // 8 bool timeout : 1; // 9 uint8_t reserved_0 : 4; // 10-13 bool lr_fhss_hop : 1; // 14 uint8_t reserved_1 : 1; // 15 // MSB }; union irq_status_t { uint16_t raw; irq_status_bits_t bits; }; static_assert(sizeof(irq_status_t) == 2, "irq_status_t must be 2 bytes"); inline Result get_irq_status() { uint8_t data[] = {0x00, 0x00}; const auto res = spi::read_stream(RADIOLIB_SX126X_CMD_GET_IRQ_STATUS, data); APP_RADIO_RETURN_ERR_IGNORE_PROC_ERR(res); return irq_status_t{.raw = static_cast(static_cast(data[0] << 8) | static_cast(data[1]))}; } inline Result clear_irq_status(const uint16_t mask = RADIOLIB_SX126X_IRQ_ALL) { const uint8_t data[] = {static_cast((mask >> 8) & 0xff), static_cast(mask & 0xff)}; return spi::write_stream(RADIOLIB_SX126X_CMD_CLEAR_IRQ_STATUS, data); } /** * \brief set RF frequency by writing the raw value to registers * \param frf the raw value to write to registers * \note don't use this function directly, use `set_frequency` instead * \sa set_frequency * \sa set_frequency_raw */ inline Result set_rf_frequency(const uint32_t frf) { const uint8_t data[] = {static_cast((frf >> 24) & 0xFF), static_cast((frf >> 16) & 0xFF), static_cast((frf >> 8) & 0xFF), static_cast(frf & 0xFF)}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_RF_FREQUENCY, data); } inline Result set_buffer_base_address(const uint8_t txBaseAddress = DEFAULT_TX_BUFFER_ADDRESS, const uint8_t rxBaseAddress = DEFAULT_RX_BUFFER_ADDRESS) { const uint8_t data[] = {txBaseAddress, rxBaseAddress}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_BUFFER_BASE_ADDRESS, data); } inline Result set_regulator_mode(const enum RegulatorMode mode) { const uint8_t data[] = {mode}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_REGULATOR_MODE, data); } inline Result get_status() { uint8_t data = 0; const auto res = spi::read_stream(RADIOLIB_SX126X_CMD_GET_STATUS, std::span{&data, 1}); APP_RADIO_RETURN_ERR_IGNORE_PROC_ERR(res); return *reinterpret_cast(&data); } inline Result get_packet_status() { uint8_t data[3] = {0, 0, 0}; const auto res = spi::read_stream(RADIOLIB_SX126X_CMD_GET_PACKET_STATUS, data, 3); APP_RADIO_RETURN_ERR_IGNORE_PROC_ERR(res); return packet_status_t{ .raw = {data[0], data[1], data[2]}}; } /** * @note ignore any possible transmission errors */ inline Result get_device_errors() { uint8_t data[] = {0x00, 0x00}; std::ignore = spi::read_stream(RADIOLIB_SX126X_CMD_GET_DEVICE_ERRORS, data); uint16_t device_err_code = data[0] << 8 | data[1]; return *reinterpret_cast(&device_err_code); } /** * \brief This commands clears all the errors recorded in the device. The errors can not be cleared independently. * \note ignore any possible transmission errors */ inline Result clear_device_errors() { constexpr uint8_t data[] = {RADIOLIB_SX126X_CMD_NOP, RADIOLIB_SX126X_CMD_NOP}; std::ignore = spi::write_stream(RADIOLIB_SX126X_CMD_CLEAR_DEVICE_ERRORS, data); return {}; } /** * \brief Set the RF frequency in MHz * \param freq the frequency to set * \sa set_frequency * * \note \text{value}=\frac{f_{\text{XTAL}}}{2^{25}}\cdot f_{\text{RF}} */ inline constexpr uint32_t frequency_raw(const freq_t freq) { uint32_t frf = (freq * (uint32_t(1) << RADIOLIB_SX126X_DIV_EXPONENT)) / RADIOLIB_SX126X_CRYSTAL_FREQ; return frf; } /** * @brief Image calibration is done through the command CalibrateImage(...) for * a given range of frequencies defined by the parameters freq1 * and freq2. Once performed, the calibration is valid for all frequencies between * the two extremes used as parameters. Typically, the user can select the * parameters freq1 and freq2 to cover any specific ISM band. * * @param freq the expected frequency in MHz * @sa 9.2.1 Image Calibration for Specific Frequency Bands */ inline Result calibrate_image(freq_t freq) { if (not valid_freq(freq)) { return ue_t{error_t{error::RADIO_INVALID_FREQUENCY}}; } uint8_t data[2]; if (freq > 900.0) { data[0] = RADIOLIB_SX126X_CAL_IMG_902_MHZ_1; data[1] = RADIOLIB_SX126X_CAL_IMG_902_MHZ_2; } else if (freq > 850.0) { data[0] = RADIOLIB_SX126X_CAL_IMG_863_MHZ_1; data[1] = RADIOLIB_SX126X_CAL_IMG_863_MHZ_2; } else if (freq > 770.0) { data[0] = RADIOLIB_SX126X_CAL_IMG_779_MHZ_1; data[1] = RADIOLIB_SX126X_CAL_IMG_779_MHZ_2; } else if (freq > 460.0) { data[0] = RADIOLIB_SX126X_CAL_IMG_470_MHZ_1; data[1] = RADIOLIB_SX126X_CAL_IMG_470_MHZ_2; } else { data[0] = RADIOLIB_SX126X_CAL_IMG_430_MHZ_1; data[1] = RADIOLIB_SX126X_CAL_IMG_430_MHZ_2; } auto res = spi::write_stream(RADIOLIB_SX126X_CMD_CALIBRATE_IMAGE, data); APP_RADIO_RETURN_ERR(res); delay_ms(5); return {}; } inline Result get_packet_type() { uint8_t data = 0; const auto res = spi::read_stream(RADIOLIB_SX126X_CMD_GET_PACKET_TYPE, std::span{&data, 1}); APP_RADIO_RETURN_ERR(res); return data; } /** * \brief Some sensitivity degradation may be observed on any LoRa device, when receiving signals transmitted by the LLCC68 with a LoRa BW of 500 kHz. * \note should be used before each packet transmission, to properly configure the chip * \param bw the bandwidth * \sa SX1262/SX1268 datasheet, chapter 15 Known Limitations, section 15.1 for details */ inline Result fix_sensitivity(const uint8_t bw) { uint8_t sensitivityConfig = 0; read_register(RADIOLIB_SX126X_REG_SENSITIVITY_CONFIG, std::span{&sensitivityConfig, 1}); // bit 2 constexpr auto HACK_MASK = 0x04; if (bw == RADIOLIB_SX126X_LORA_BW_500_0) { sensitivityConfig &= ~HACK_MASK; } else { sensitivityConfig |= HACK_MASK; } return write_register(RADIOLIB_SX126X_REG_SENSITIVITY_CONFIG, std::span{&sensitivityConfig, 1}); } /** * @brief fixes overly eager PA clamping * * fixes overly eager PA clamping * see SX1262/SX1268 datasheet, chapter 15 Known Limitations, section 15.2 for details * * The LLCC68 platform embeds a Power Amplifier (PA) clamping mechanism, * backing-off the power when over-voltage conditions are detected internally. * This method is put in place to protect the internal devices and ensure * long-term reliability of the chip. Considering a high-power operation of the * LLCC68 (supporting +22dBm on-chip), these "clamping" devices are overly * protective, causing the chip to back-down its output power when even a * reasonable mismatch is detected at the PA output. The observation is * typically 5 to 6 dB less output power than the expected. * * @param enable * @return Result */ inline Result fix_pa_clamping(const bool enable = true) { uint8_t clampConfig = 0; read_register(RADIOLIB_SX126X_REG_TX_CLAMP_CONFIG, std::span{&clampConfig, 1}); // apply or undo workaround if (enable) { clampConfig |= 0x1E; } else { clampConfig = (clampConfig & ~0x1E) | 0x08; } return write_register(RADIOLIB_SX126X_REG_TX_CLAMP_CONFIG, std::span{&clampConfig, 1}); } /** * \brief fixes inverted IQ on SX1262 rev. B * \param iq_config RADIOLIB_SX126X_LORA_IQ_STANDARD or RADIOLIB_SX126X_LORA_IQ_INVERTED * \see SX1262/SX1268 datasheet, chapter 15 Known Limitations, section 15.4 for details */ inline Result fix_inverted_iq(const uint8_t iq_config) { // read current IQ configuration uint8_t iqConfig = 0; const auto res = read_register(RADIOLIB_SX126X_REG_IQ_CONFIG, std::span{&iqConfig, 1}); APP_RADIO_RETURN_ERR(res); // Bit 2 at address 0x0736 must be set to // 0 when using inverted IQ // 1 when using standard IQ if (iqConfig == RADIOLIB_SX126X_LORA_IQ_INVERTED) { iqConfig &= ~0x04; } else { iqConfig |= 0x04; } return write_register(RADIOLIB_SX126X_REG_IQ_CONFIG, std::span{&iqConfig, 1}); } struct irq_params_t { // The IrqMask masks or unmasks the IRQ which can be triggered by the device. By default, all IRQ are masked (all ‘0’) and the // user can enable them one by one (or several at a time) by setting the corresponding mask to ‘1’. uint16_t irqMask = RADIOLIB_SX126X_IRQ_NONE; // The interrupt causes a DIO to be set if the corresponding bit in DioxMask // and the IrqMask are set. As an example, if bit 0 of IrqMask is set to 1 // and bit 0 of DIO1Mask is set to 1 then, a rising edge of IRQ source // TxDone will be logged in the IRQ register and will appear at the same // time on DIO1. // // One IRQ can be mapped to all DIOs, one DIO can be mapped to all IRQs (an // OR operation is done) but some IRQ sources will be available only on // certain modes of operation and frames. uint16_t dio1Mask = RADIOLIB_SX126X_IRQ_NONE; uint16_t dio2Mask = RADIOLIB_SX126X_IRQ_NONE; uint16_t dio3Mask = RADIOLIB_SX126X_IRQ_NONE; }; /** * @brief The IrqMask masks or unmasks the IRQ which can be triggered by the device. * * By default, all IRQ are masked (all ‘0’) and the user can enable them one by one (or several at a time) by setting the corresponding mask to ‘1’. */ inline Result set_dio_irq_params(irq_params_t irq_params) { const auto irqMask = irq_params.irqMask; const auto dio1Mask = irq_params.dio1Mask; const auto dio2Mask = irq_params.dio2Mask; const auto dio3Mask = irq_params.dio3Mask; const uint8_t data[8] = {static_cast((irqMask >> 8) & 0xFF), static_cast(irqMask & 0xFF), static_cast((dio1Mask >> 8) & 0xFF), static_cast(dio1Mask & 0xFF), static_cast((dio2Mask >> 8) & 0xFF), static_cast(dio2Mask & 0xFF), static_cast((dio3Mask >> 8) & 0xFF), static_cast(dio3Mask & 0xFF)}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_DIO_IRQ_PARAMS, data); } /*! \brief Set DIO2 to function as RF switch (default in Semtech example designs). */ inline Result set_dio2_as_rf_switch(const bool en) { const uint8_t data[] = {en ? static_cast(RADIOLIB_SX126X_DIO2_AS_RF_SWITCH) : static_cast(RADIOLIB_SX126X_DIO2_AS_IRQ)}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_DIO2_AS_RF_SWITCH_CTRL, data); } /** * \sa SetPacketType */ inline Result set_packet_type_lora(uint8_t sf) { Result res; constexpr auto mod = RADIOLIB_SX126X_PACKET_TYPE_LORA; uint8_t data[7]; data[0] = mod; res = spi::write_stream(RADIOLIB_SX126X_CMD_SET_PACKET_TYPE, std::span{data, 1}); APP_RADIO_RETURN_ERR(res); // the command SetRxTxFallbackMode() defines into which mode the chip goes // after a successful transmission or after a packet reception. data[0] = RADIOLIB_SX126X_RX_TX_FALLBACK_MODE_STDBY_RC; res = spi::write_stream(RADIOLIB_SX126X_CMD_SET_RX_TX_FALLBACK_MODE, std::span{data, 1}); APP_RADIO_RETURN_ERR(res); data[0] = RADIOLIB_SX126X_CAD_ON_8_SYMB; data[1] = sf + 13; data[2] = RADIOLIB_SX126X_CAD_PARAM_DET_MIN; data[3] = RADIOLIB_SX126X_CAD_GOTO_STDBY; data[4] = 0x00; data[5] = 0x00; data[6] = 0x00; res = spi::write_stream(RADIOLIB_SX126X_CMD_SET_CAD_PARAMS, std::span{data, 7}); APP_RADIO_RETURN_ERR(res); std::ignore = clear_irq_status(); constexpr auto irq_params = irq_params_t{}; res = set_dio_irq_params(irq_params); APP_RADIO_RETURN_ERR(res); while (gpio::digital_read(BUSY_PIN) == gpio::HIGH) {} return {}; } /** * \brief At power up the radio performs calibration of RC64k, RC13M, PLL and * ADC. It is however possible to launch a calibration of one or several * blocks at any time starting in STDBY_RC mode. The calibrate function starts the * calibration of a block defined by calibParam. * \sa 13.1.12 Calibrate Function */ inline Result calibrate(uint8_t calibParam = RADIOLIB_SX126X_CALIBRATE_ALL) { uint8_t data[1]; data[0] = calibParam; auto res = spi::write_stream(RADIOLIB_SX126X_CMD_CALIBRATE, data); APP_RADIO_RETURN_ERR(res); // The total calibration time if all blocks are calibrated is 3.5 ms. The // calibration must be launched in STDBY_RC mode and the BUSY pins will be // high during the calibration process. A falling edge of BUSY indicates the // end of the procedure. delay_ms(5); while (gpio::digital_read(BUSY_PIN) == gpio::HIGH) {} return {}; } /*! \brief Get expected time-on-air for a given size of payload \param len Payload length in bytes. \returns Expected time-on-air in microseconds. */ constexpr uint32_t calc_time_on_air(const size_t len, uint8_t sf, uint8_t bw, uint8_t cr, uint16_t preamble_length, uint8_t header_type) { // everything is in microseconds to allow integer arithmetic // some constants have .25, these are multiplied by 4, and have _x4 postfix to indicate that fact const auto bw_ = float{bw_khz(bw)}; const auto ubw = static_cast(bw_ * 10); const uint32_t symbolLength_us = (static_cast(1000 * 10) << sf) / ubw; uint8_t sfCoeff1_x4 = 17; // (4.25 * 4) uint8_t sfCoeff2 = 8; if (sf == 5 || sf == 6) { sfCoeff1_x4 = 25; // 6.25 * 4 sfCoeff2 = 0; } uint8_t sfDivisor = 4 * sf; if (symbolLength_us >= 16000) { sfDivisor = 4 * (sf - 2); } constexpr int8_t bitsPerCrc = 16; int8_t N_symbol_header = header_type == RADIOLIB_SX126X_LORA_HEADER_EXPLICIT ? 20 : 0; // numerator of equation in section 6.1.4 of SX1268 datasheet v1.1 (might not actually be bitcount, but it has len * 8) int16_t bitCount = static_cast(8) * len + RADIOLIB_SX126X_LORA_CRC_ON * bitsPerCrc - 4 * sf + sfCoeff2 + N_symbol_header; if (bitCount < 0) { bitCount = 0; } // add (sfDivisor) - 1 to the numerator to give integer CEIL(...) const uint16_t nPreCodedSymbols = (bitCount + (sfDivisor - 1)) / (sfDivisor); const auto de = std::get<1>(cr_to_ratio(cr)); // preamble can be 65k, therefore nSymbol_x4 needs to be 32 bit const uint32_t nSymbol_x4 = (preamble_length + 8) * 4 + sfCoeff1_x4 + nPreCodedSymbols * de * 4; return symbolLength_us * nSymbol_x4 / 4; } /** * \brief * \param preamble_length LoRa preamble length in symbols. Allowed values range from 1 to 65535 * \param payload_length implicit header length; 0xff for explicit header * \param crc_type RADIOLIB_SX126X_LORA_CRC_ON or RADIOLIB_SX126X_LORA_CRC_OFF * \param hdr_type RADIOLIB_SX126X_LORA_HEADER_EXPLICIT or RADIOLIB_SX126X_LORA_HEADER_IMPLICIT * \note This is for LoRA packet. setModulationParamsFSK (which is for GFSK) won't be ported. */ inline Result set_packet_params(const uint16_t preamble_length = 8, const uint8_t payload_length = 0xff, const uint8_t crc_type = RADIOLIB_SX126X_LORA_CRC_ON, const uint8_t hdr_type = RADIOLIB_SX126X_LORA_HEADER_EXPLICIT) { if constexpr (DEFAULT_IQ_TYPE == RADIOLIB_SX126X_LORA_IQ_INVERTED) { const auto res = fix_inverted_iq(RADIOLIB_SX126X_LORA_IQ_INVERTED); APP_RADIO_RETURN_ERR(res); } const uint8_t data[] = {static_cast((preamble_length >> 8) & 0xff), static_cast(preamble_length & 0xff), crc_type, payload_length, hdr_type, DEFAULT_IQ_TYPE}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_PACKET_PARAMS, data); } /** * \brief frequency synthesizer calibration */ inline Result set_fs() { // default constructor of std::span is empty return spi::write_stream(RADIOLIB_SX126X_CMD_SET_FS, std::span()); } /** * \param timeout no idea why you need a timeout for TX */ inline Result set_tx(const uint32_t timeout = 0) { const uint8_t data[] = {static_cast((timeout >> 16) & 0xFF), static_cast((timeout >> 8) & 0xFF), static_cast(timeout & 0xFF)}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_TX, data); } /** \brief Interrupt-driven receive method. DIO1 will be activated when full packet is received. \param timeout Receive mode type and/or raw timeout value, expressed as multiples of 15.625 us. \note When set to RADIOLIB_SX126X_RX_TIMEOUT_INF, the timeout will be infinite and the device will remain in Rx mode until explicitly commanded to stop (Rx continuous mode). When set to RADIOLIB_SX126X_RX_TIMEOUT_NONE, there will be no timeout and the device will return to standby when a packet is received (Rx single mode). For any other value, timeout will be applied and signal will be generated on DIO1 for conditions defined by irqFlags and irqMask. */ inline Result set_rx(const uint32_t timeout = RADIOLIB_SX126X_RX_TIMEOUT_INF) { const uint8_t data[] = {static_cast((timeout >> 16) & 0xFF), static_cast((timeout >> 8) & 0xFF), static_cast(timeout & 0xFF)}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_RX, data); } enum class TxRxPinState { TX, RX, }; static constexpr auto tx_rx_en_pin_set = [](TxRxPinState state) { constexpr auto TX_EN_PIN = app::driver::llcc68::TX_EN_PIN; constexpr auto RX_EN_PIN = app::driver::llcc68::RX_EN_PIN; if (TX_EN_PIN == GPIO_NUM_NC or RX_EN_PIN == GPIO_NUM_NC) { return; } switch (state) { case TxRxPinState::TX: ESP_ERROR_CHECK(gpio_set_level(TX_EN_PIN, true)); ESP_ERROR_CHECK(gpio_set_level(RX_EN_PIN, false)); break; case TxRxPinState::RX: ESP_ERROR_CHECK(gpio_set_level(TX_EN_PIN, false)); ESP_ERROR_CHECK(gpio_set_level(RX_EN_PIN, true)); break; } }; enum class CAD_EXIT_MODE : uint8_t { CAD_ONLY = 0x00, CAD_RX = 0x01, }; enum class CAD_SYMB : uint8_t { CAD_ON_1_SYMB = 0x00, CAD_ON_2_SYMB = 0x01, CAD_ON_4_SYMB = 0x02, CAD_ON_8_SYMB = 0x03, CAD_ON_16_SYMB = 0x04, }; struct cad_params_t { CAD_SYMB symbol_num; uint8_t det_peak; uint8_t det_min; CAD_EXIT_MODE exit_mode; // 24-bit // // The parameter cadTimeout is only used when the CAD is performed with // cadExitMode = CAD_RX. Here, the cadTimeout indicates the time the device // will stay in Rx following a successful CAD. // Rx Timeout = cadTimeout * 15.625us uint32_t timeout; }; constexpr std::tuple sf_det_peak_map[6] = { {7, 22}, {8, 22}, {9, 24}, {10, 25}, {11, 26}, {12, 30}}; // Parameters `cadDetPeak` and `cadDetMin` define the sensitivity of the LoRa // modem when trying to correlate to actual LoRa preamble symbols. These // two settings depend on the LoRa spreading factor and Bandwidth, but // also depend on the number of symbols used to validate or not the detection. // // Choosing the right value is not easy and the values selected must be // carefully tested to ensure a good detection at sensitivity level, and // also to limit the number of false detections. Application note AN1200.48 // provides guidance for the selection of these parameters. constexpr inline std::optional get_det_peak(const uint8_t sf) { for (const auto &[r_sf, r_det_peak] : sf_det_peak_map) { if (r_sf == sf) { return r_det_peak; } } return std::nullopt; } /** * \brief Channel Activity Detection (CAD) method */ inline Result set_cad_params(const cad_params_t ¶ms) { uint8_t data[7]; data[0] = static_cast(params.symbol_num); data[1] = params.det_peak; data[2] = params.det_min; data[3] = static_cast(params.exit_mode); data[4] = static_cast((params.timeout >> 16) & 0xFF); data[5] = static_cast((params.timeout >> 8) & 0xFF); data[6] = static_cast(params.timeout & 0xFF); return spi::write_stream(RADIOLIB_SX126X_CMD_SET_CAD_PARAMS, data); } /** * \brief Set the CAD mode * * The command SetCAD() can be used only in LoRa packet type. The Channel * Activity Detection is a LoRa specific mode of operation where the device * searches for the presence of a LoRa preamble signal. After the search has * completed, the device returns in STDBY_RC mode. The length of the search is * configured via the command SetCadParams(...). At the end of the search * period, the device triggers the IRQ CAD DONE if it has been enabled. If a * valid signal has been detected it also generates the IRQ CAD DETECTED. * * The time taken for the channel activity detection is dependent upon the LoRa® * modulation settings used. For a given configuration (SF/BW) the typical CAD * detection time can be selected to be either 1, 2, 4, 8 or 16 symbols. Once * the duration of the selected number of symbols has been done, the radio will * remains for around half a symbol in Rx to post-process the measurement. */ inline Result set_cad() { uint8_t data[] = {spi::SPI_NOP_COMMAND}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_CAD, data); } /** * \brief Set the TX continuous wave mode * \note CW */ inline Result set_tx_continuous_wave() { const uint8_t data[] = {spi::SPI_NOP_COMMAND}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_TX_CONTINUOUS_WAVE, data); } inline Result set_tx_infinite_preamble() { const uint8_t data[] = {spi::SPI_NOP_COMMAND}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_TX_INFINITE_PREAMBLE, data); } /** * \brief Set the PA configuration * \param pa_duty_cycle paDutyCycle controls the duty cycle (conduction angle) of both PAs (SX1261 and SX1262). The maximum output power, the * power consumption, and the harmonics will drastically change with paDutyCycle. The values given across this datasheet * are the recommended settings to achieve the best efficiency of the PA. Changing the paDutyCycle will affect the * distribution of the power in the harmonics and should thus be selected to work in conjunction of a given matching * network. * \param hp_max hpMax selects the size of the PA in the SX1262, this value has no influence on the SX1261. The maximum output power can * be reduced by reducing the value of hpMax. The valid range is between 0x00 and 0x07 and 0x07 is the maximum supported * value for the SX1262 to achieve +22 dBm output power. Increasing hpMax above 0x07 could cause early aging of the device * of could damage the device when used in extreme temperatures. * \param device_sel PA device selection; 0: SX1262, 1: SX1261 */ inline Result set_pa_config(const uint8_t pa_duty_cycle, const uint8_t hp_max = RADIOLIB_SX126X_PA_CONFIG_HP_MAX, const uint8_t device_sel = RADIOLIB_SX126X_PA_CONFIG_SX1262) { // paLut reserved and always 0x01 const uint8_t data[] = {pa_duty_cycle, hp_max, device_sel, RADIOLIB_SX126X_PA_CONFIG_PA_LUT}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_PA_CONFIG, data); } /** * \brief get status from IRQ status register * \return true if channel is free, false if channel is busy, error otherwise */ inline Result channel_scan_result() { const auto res = get_irq_status(); APP_RADIO_RETURN_ERR(res); if (res->bits.cad_detected) { return false; } else if (res->bits.cad_done) { return true; } return ue_t{error_t{error::RADIO_INVALID_CAD_RESULT}}; } template constexpr bool __in_precision(const float v) { using v_t = decltype(v); constexpr auto precision = v_t{0.001}; return v_t{abs(v - v_t{target})} <= precision; } /*! \brief Sets TCXO (Temperature Compensated Crystal Oscillator) configuration. \param voltage TCXO reference voltage in volts. Allowed values are 1.6, 1.7, 1.8, 2.2. 2.4, 2.7, 3.0 and 3.3 V. Set to 0 to disable TCXO. NOTE: After setting this parameter to 0, the module will be reset (since there's no other way to disable TCXO). \param delay TCXO timeout in us. Defaults to 5000 us. \param XTAL Set to true to use XTAL instead of TCXO. Defaults to false. \note will return immediately if XTAL is true; DIO3_AS_TCXO_CTRL; */ inline Result set_TCXO(const TcxoVoltage voltage, const uint32_t delay = 5000, const bool XTAL = false) { if (XTAL) { return ue_t{error_t{error::RADIO_INVALID_TCXO_VOLTAGE}}; } auto res = standby(); APP_RADIO_RETURN_ERR(res); auto err_ = get_device_errors(); APP_RADIO_RETURN_ERR(err_); if (const auto err = *err_; err.XOSC_START_ERR) { clear_device_errors(); } uint8_t data[4]; data[0] = static_cast(voltage); uint32_t delay_val = static_cast(delay / 15.625); data[1] = static_cast((delay_val >> 16) & 0xFF); data[2] = static_cast((delay_val >> 8) & 0xFF); data[3] = static_cast(delay_val & 0xFF); res = spi::write_stream(RADIOLIB_SX126X_CMD_SET_DIO3_AS_TCXO_CTRL, data); APP_RADIO_RETURN_ERR(res); return {delay_val}; } /** * \brief set LoRa modulation parameters * \param sf spread factor (5-12) * \param bw RAW bandwidth (RADIOLIB_SX126X_LORA_BW_*) * \param cr coding rate (RADIOLIB_SX126X_LORA_CR_*) * \param ldro RADIOLIB_SX126X_LORA_LOW_DATA_RATE_OPTIMIZE_ON or RADIOLIB_SX126X_LORA_LOW_DATA_RATE_OPTIMIZE_OFF */ inline Result set_modulation_params(const uint8_t sf, const uint8_t bw = RADIOLIB_SX126X_LORA_BW_500_0, const uint8_t cr = RADIOLIB_SX126X_LORA_CR_4_5, const uint8_t ldro = RADIOLIB_SX126X_LORA_LOW_DATA_RATE_OPTIMIZE_OFF) { if (not valid_bw(bw)) { return ue_t{error_t{error::RADIO_INVALID_BANDWIDTH}}; } if (not valid_sf(bw, sf)) { return ue_t{error_t{error::RADIO_INVALID_SPREADING_FACTOR}}; } if (not valid_cr(cr)) { return ue_t{error_t{error::RADIO_INVALID_CODING_RATE}}; } const uint8_t data[] = {sf, bw, cr, ldro}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_MODULATION_PARAMS, data); } inline Result set_tx_params(const uint8_t pwr, const uint8_t ramp_time = RADIOLIB_SX126X_PA_RAMP_200U) { const uint8_t data[] = {pwr, ramp_time}; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_TX_PARAMS, data); } /*! \brief Sets LoRa sync word. \param sync_word LoRa sync word to be set. \param control_bits Undocumented control bits, required for compatibility purposes. */ inline Result set_lora_sync_word(const uint8_t sync_word = RADIOLIB_SX126X_SYNC_WORD_PRIVATE, const uint8_t control_bits = 0x44) { const auto pkt_type = get_packet_type(); APP_RADIO_RETURN_ERR(pkt_type); if (*pkt_type != RADIOLIB_SX126X_PACKET_TYPE_LORA) { return ue_t{error_t{error::RADIO_WRONG_MODERN}}; } const uint8_t data[2] = {static_cast((sync_word & 0xF0) | ((control_bits & 0xF0) >> 4)), static_cast(((sync_word & 0x0F) << 4) | (control_bits & 0x0F))}; return write_register(RADIOLIB_SX126X_REG_LORA_SYNC_WORD_MSB, data); } inline Result set_packet_type(const PacketType packet_type) { const auto data = std::array{ RADIOLIB_SX126X_CMD_SET_PACKET_TYPE, static_cast(packet_type), }; return spi::write_stream(RADIOLIB_SX126X_CMD_SET_PACKET_TYPE, data); } inline Result set_output_power(const int8_t power) { if (not in_range(power, -9, 22)) { return ue_t{error_t{error::RADIO_INVALID_OUTPUT_POWER}}; } uint8_t ocp = 0; auto res = read_register(RADIOLIB_SX126X_REG_OCP_CONFIGURATION, std::span{&ocp, 1}); APP_RADIO_RETURN_ERR(res); res = set_pa_config(0x04); APP_RADIO_RETURN_ERR(res); res = set_tx_params(power); APP_RADIO_RETURN_ERR(res); // restore OCP configuration return write_register(RADIOLIB_SX126X_REG_OCP_CONFIGURATION, std::span{&ocp, 1}); } /** * \brief checks if BUSY pin is low * \return true if BUSY pin is low, false otherwise */ inline bool busy_low() { return gpio::digital_read(spi::BUSY_PIN) == gpio::LOW; } /** * \brief blocks until BUSY pin is low */ inline void until_busy_low() { while (!busy_low()) {} } template Result set_current_limit() { static_assert(in_range(limit, 0, 140), "limit must be in range [0, 140]"); constexpr uint8_t raw = static_cast(limit / 2.5); return write_register(RADIOLIB_SX126X_REG_OCP_CONFIGURATION, std::span{&raw, 1}); } inline int chip_type_to_max_tx_power(const ChipType &chip) { switch (chip) { case ChipType::LLCC68: return 22; case ChipType::SX1261: return 15; case ChipType::SX1262: return 22; default: return 0; } } constexpr auto MAX_CHIP_CHECK_ATTEMPTS = 10; inline std::tuple find_chip() { uint8_t i = 0; bool ok = false; ChipType chip = ChipType::Unknown; while ((i < MAX_CHIP_CHECK_ATTEMPTS) && !ok) { constexpr auto SX1262_CHIP_TYPE = "SX1262"; constexpr auto LLCC68_CHIP_TYPE = "LLCC68"; constexpr auto SX1261_CHIP_TYPE = "SX1261"; char version[16]{}; auto version_buf = std::span{reinterpret_cast(version), std::size(version)}; spi::read_register(RADIOLIB_SX126X_REG_VERSION_STRING, version_buf); if (strncmp(version, LLCC68_CHIP_TYPE, 6) == 0) { chip = ChipType::LLCC68; } else if (strncmp(version, SX1261_CHIP_TYPE, 6) == 0) { chip = ChipType::SX1261; } else if (strncmp(version, SX1262_CHIP_TYPE, 6) == 0) { chip = ChipType::SX1262; } if (chip != ChipType::Unknown) { ESP_LOGI(TAG, "found chip: %s (%s)", version, chip_type_str(chip)); ok = true; break; } delay_ms(10); i++; } return {ok, chip}; } inline Result, error_t> get_rx_buffer_status() { uint8_t rx_buf_status[] = {0, 0}; auto res = spi::read_stream(RADIOLIB_SX126X_CMD_GET_RX_BUFFER_STATUS, rx_buf_status); APP_RADIO_RETURN_ERR_IGNORE_PROC_ERR(res); return std::make_tuple(rx_buf_status[0], rx_buf_status[1]); } struct read_result_t { std::span data; /*! * \brief user should call this function after reading data (since it will override the internal buffer) */ static Result post_procedure() { Result res; res = set_buffer_base_address(); APP_RADIO_RETURN_ERR(res); res = clear_irq_status(); APP_RADIO_RETURN_ERR(res); return {}; } }; inline Result read_data_internal() { const auto tuple_ = get_rx_buffer_status(); APP_RADIO_RETURN_ERR(tuple_); /** * @see 7.2 Data Buffer in Receive Mode * @see 7.1 Principle of Operation * * In receive mode RxBaseAddr specifies the buffer offset in memory at which * the received packet payload data will be written. The buffer offset of * the last byte written in receive mode is then stored in RxDataPointer * which is initialized to the value of RxBaseAddr at the beginning of the * reception. * * The pointer to the first byte of the last packet received and the packet * length can be read with the command GetRxbufferStatus(). * * In single mode, RxDataPointer is automatically initialized to RxBaseAddr * each time the transceiver enters Rx mode. In continuous mode the pointer * is incremented starting from the previous position. */ const auto [sz, ptr] = *tuple_; // the rx pointer would become 0x80 at first, so we need to adapt that behavior const auto r_ptr = ptr - sz < DEFAULT_RX_BUFFER_ADDRESS ? ptr : ptr - sz; auto res = hal::spi::read_buffer(r_ptr, sz); APP_RADIO_RETURN_ERR(res); return read_result_t{*res}; } /** Circuit Configuration for Basic Rx Operation After power up (battery insertion or hard reset) the chip run automatically a calibration procedure and goes to STDBY_RC mode. This is indicated by a low state on BUSY pin. From this state the steps are: 1. If not in STDBY_RC mode, then set the circuit in this mode with the command SetStandby() 2. Define the protocol (LoRa or FSK) with the command SetPacketType(...) 3. Define the RF frequency with the command SetRfFrequency(...) 4. Define where the data will be stored inside the data buffer in Rx with the command SetBufferBaseAddress(...) 5. Define the modulation parameter according to the chosen protocol with the command SetModulationParams(...)1 6. Define the frame format to be used with the command SetPacketParams(...) 7. Configure DIO and IRQ: use the command SetDioIrqParams(...) to select the IRQ RxDone and map this IRQ to a DIO (DIO1 or DIO2 or DIO3), set IRQ Timeout as well. 8. Define Sync Word value: use the command WriteReg(...) to write the value of the register via direct register access. 9. Set the circuit in reception mode: use the command SetRx(). Set the parameter to enable timeout or continuous mode 10. Wait for IRQ RxDone2 or Timeout: the chip will stay in Rx and look for a new packet if the continuous mode is selected otherwise it will goes to STDBY_RC mode. 11. In case of the IRQ RxDone, check the status to ensure CRC is correct: use the command GetIrqStatus() 12. Clear IRQ flag RxDone or Timeout: use the command ClearIrqStatus(). In case of a valid packet (CRC OK), get the packet length and address of the first byte of the received payload by using the command GetRxBufferStatus(...) 13. In case of a valid packet (CRC OK), start reading the packet \note The IRQ RxDone means that a packet has been received but the CRC could be wrong: the user must check the CRC before validating the packet. */ inline Result kick_inf_rx(const modulation_params_t &mod_params, const packet_params_t &packet_params) { constexpr auto TAG = "llcc68::kick_inf_rx"; Result res; res = standby(); APP_RADIO_RETURN_ERR_CTX(res, "standby"); res = set_buffer_base_address(); APP_RADIO_RETURN_ERR_CTX(res, "set buffer base address"); constexpr auto irq_mask = RADIOLIB_SX126X_IRQ_RX_DEFAULT; constexpr auto irq_params = irq_params_t{ irq_mask, DIO1_PIN == NC_PIN ? RADIOLIB_SX126X_IRQ_NONE : irq_mask, DIO2_PIN == NC_PIN ? RADIOLIB_SX126X_IRQ_NONE : irq_mask, DIO3_PIN == NC_PIN ? RADIOLIB_SX126X_IRQ_NONE : irq_mask, }; res = set_modulation_params(mod_params.sf, mod_params.bw, mod_params.cr, mod_params.ldr_optimize); APP_RADIO_RETURN_ERR_CTX(res, "modulation params"); res = set_packet_params(packet_params.preamble_len, packet_params.payload_len, packet_params.crc_type, packet_params.hdr_type); APP_RADIO_RETURN_ERR_CTX(res, "packet params"); res = set_dio_irq_params(irq_params); APP_RADIO_RETURN_ERR_CTX(res, "dio irq params"); res = clear_irq_status(); APP_RADIO_RETURN_ERR_CTX(res, "clear irq status"); res = set_rx(RADIOLIB_SX126X_RX_TIMEOUT_INF); APP_RADIO_RETURN_ERR_CTX(res, "set rx"); tx_rx_en_pin_set(TxRxPinState::RX); return {}; } /** After power up (battery insertion or hard reset) the chip runs automatically a calibration procedure and goes to STDBY_RC mode. This is indicated by a low state on BUSY pin. From this state the steps are: 1. If not in STDBY_RC mode, then go to this mode with the command SetStandby(...) 2. Define the protocol (LoRa or FSK) with the command SetPacketType(...) 3. Define the RF frequency with the command SetRfFrequency(...) 4. Define the Power Amplifier configuration with the command SetPaConfig(...) 5. Define output power and ramping time with the command SetTxParams(...) 6. Define where the data payload will be stored with the command SetBufferBaseAddress(...) 7. Send the payload to the data buffer with the command WriteBuffer(...) 8. Define the modulation parameter according to the chosen protocol with the command SetModulationParams(...)1 9. Define the frame format to be used with the command SetPacketParams(...)2 10. Configure DIO and IRQ: use the command SetDioIrqParams(...) to select TxDone IRQ and map this IRQ to a DIO (DIO1, DIO2 or DIO3) 11. Define Sync Word value: use the command WriteReg(...) to write the value of the register via direct register access 12. Set the circuit in transmitter mode to start transmission with the command SetTx(). Use the parameter to enable Timeout 13. Wait for the IRQ TxDone or Timeout: once the packet has been sent the chip goes automatically to STDBY_RC mode 14. Clear the IRQ TxDone flag */ inline Result sync_transmit(const std::span data, const lora_parameters_t ¶ms) { // unimplemented std::unreachable(); return {}; } /** * \brief async transmit * \param data the data to transmit * \param len the length of data * \param params the parameters for calculating time on air * \param tx_state current transmit state * \return new transmit state * \see poll_tx_state */ inline Result async_transmit(const std::span data, const lora_parameters_t ¶ms, const transmit_state_t &tx_state) { constexpr auto TAG = "llcc68::async_transmit"; if (tx_state.is_transmitting) { return ue_t{error_t{error::RADIO_BUSY_TX}}; } if (data.size() > RADIOLIB_SX126X_MAX_PACKET_LENGTH) { return ue_t{error_t{error::INVALID_SIZE}}; } const auto &mod_params = params.mod_params; const auto &packet_params = params.packet_params; Result res; res = standby(); APP_RADIO_RETURN_ERR_CTX(res, "standby"); res = set_packet_params(packet_params.preamble_len, data.size(), packet_params.crc_type, packet_params.hdr_type); APP_RADIO_RETURN_ERR_CTX(res, "set packet params"); res = set_buffer_base_address(); APP_RADIO_RETURN_ERR_CTX(res, "set buffer base address"); res = write_buffer(data); APP_RADIO_RETURN_ERR_CTX(res, "write buffer"); res = fix_sensitivity(mod_params.bw); APP_RADIO_RETURN_ERR_CTX(res, "fix sensitivity"); constexpr auto irq_mask = RADIOLIB_SX126X_IRQ_TX_DONE | RADIOLIB_SX126X_IRQ_TIMEOUT; constexpr auto irq_params = irq_params_t{ irq_mask, DIO1_PIN == NC_PIN ? RADIOLIB_SX126X_IRQ_NONE : irq_mask, DIO2_PIN == NC_PIN ? RADIOLIB_SX126X_IRQ_NONE : irq_mask, DIO3_PIN == NC_PIN ? RADIOLIB_SX126X_IRQ_NONE : irq_mask, }; res = set_dio_irq_params(irq_params); APP_RADIO_RETURN_ERR_CTX(res, "set dio irq params"); clear_irq_status(irq_mask); APP_RADIO_RETURN_ERR_CTX(res, "clear irq status"); tx_rx_en_pin_set(TxRxPinState::TX); res = set_tx(); APP_RADIO_RETURN_ERR_CTX(res, "set tx"); constexpr auto TIMEOUT_FACTOR = 11u / 10u; const auto timeout_us = calc_time_on_air(data.size(), mod_params.sf, mod_params.bw, mod_params.cr, packet_params.preamble_len, packet_params.hdr_type) * TIMEOUT_FACTOR; const uint16_t timeout_ms = timeout_us / 1000; return transmit_state_t{ .is_transmitting = true, .expected_end_timestamp = app::utils::clock_t::now() + Instant::milliseconds(timeout_ms), }; } /** * @brief initialize TX & RX enable pins and put them in high level * @note I'm not sure if this is necessary anymore, just in case */ static constexpr auto init_tx_rx_en_pin = [] { constexpr auto TX_EN_PIN = app::driver::llcc68::TX_EN_PIN; constexpr auto RX_EN_PIN = app::driver::llcc68::RX_EN_PIN; // both pins must be defined if (TX_EN_PIN == GPIO_NUM_NC or RX_EN_PIN == GPIO_NUM_NC) { return; } gpio_config_t io_conf = { .pin_bit_mask = (1ULL << TX_EN_PIN) | (1ULL << RX_EN_PIN), .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; ESP_ERROR_CHECK(gpio_config(&io_conf)); ESP_ERROR_CHECK(gpio_set_level(TX_EN_PIN, false)); ESP_ERROR_CHECK(gpio_set_level(RX_EN_PIN, false)); }; static constexpr auto init_pins = [](void (*isr)(void *), void *arg) { if (RST_PIN != GPIO_NUM_NC) { gpio::set_mode(RST_PIN, gpio::Mode::OUTPUT); } { uint64_t pin_bit_mask = 0; for (const auto pin : EXTI_PINS) { pin_bit_mask |= (1ULL << pin); } gpio_config_t io_conf = { .pin_bit_mask = pin_bit_mask, .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; // https://github.com/espressif/esp-idf/blob/v5.4.2/examples/peripherals/gpio/generic_gpio/main/gpio_example_main.c ESP_ERROR_CHECK(gpio_config(&io_conf)); gpio_install_isr_service(0); for (const auto pin : EXTI_PINS) { if (pin == NC_PIN) { continue; } ESP_ERROR_CHECK(gpio_set_intr_type(pin, GPIO_INTR_POSEDGE)); ESP_ERROR_CHECK(gpio_isr_handler_add(pin, isr, arg)); } } init_tx_rx_en_pin(); }; inline Result get_device_error_print_and_clear(const char *tag) { auto res = get_device_errors(); if (not res) { return ue_t{res.error()}; } if (res->has_any_error()) { ESP_LOGE(tag, "%s", res->to_string().c_str()); } else { ESP_LOGI(tag, "no device errors"); } return clear_device_errors(); } static constexpr auto begin = [](const lora_parameters_t ¶ms) -> Result { /** * Most of the commands can be sent in any order except for the radio configuration commands which will set the radio in * the proper operating mode. Indeed, it is mandatory to set the radio protocol using the command SetPacketType(...) as a first * step before issuing any other radio configuration commands. In a second step, the user should define the modulation * parameter according to the chosen protocol with the command SetModulationParams(...). Finally, the user should then * select the packet format with the command SetPacketParams(...). * * \note * If this order is not respected, the behavior of the device could be unexpected. **/ constexpr auto TAG = "llcc68::begin"; Result res; /* explicit not reset the chip by pin, due to unexpected side effect */ res = standby(); APP_RADIO_RETURN_ERR_CTX(res, "standby"); const auto [ok, chip] = find_chip(); if (not ok) { return ue_t{error_t{error::RADIO_CHIP_NOT_FOUND}}; } if constexpr (USE_TXCO) { const auto res = set_TCXO(DEFAULT_TCXO_VOLTAGE); APP_RADIO_RETURN_ERR_CTX(res, "set TCXO"); } const auto &mod_params = params.mod_params; const auto &packet_params = params.packet_params; res = set_packet_type_lora(mod_params.sf); APP_RADIO_RETURN_ERR_CTX(res, "config packet type"); #ifndef APP_RADIO_DISABLE_CALIBRATION res = calibrate(); APP_RADIO_RETURN_ERR_CTX(res, "calibrate"); #endif /* APP_RADIO_DISABLE_CALIBRATION */ res = set_modulation_params(mod_params.sf, mod_params.bw, mod_params.cr, mod_params.ldr_optimize); res = get_device_error_print_and_clear(TAG); APP_RADIO_RETURN_ERR_CTX(res, "set modulation params"); res = set_lora_sync_word(params.sync_word); APP_RADIO_RETURN_ERR_CTX(res, "sync word"); res = set_regulator_mode(USE_REGULATOR_LDO ? RegulatorMode::REGULATOR_LDO : RegulatorMode::REGULATOR_DC_DC); APP_RADIO_RETURN_ERR_CTX(res, "set regulator mode"); res = set_current_limit<60.0f>(); APP_RADIO_RETURN_ERR_CTX(res, "current limit"); res = set_dio2_as_rf_switch(false); APP_RADIO_RETURN_ERR_CTX(res, "set dio2 as rf switch"); res = set_rf_frequency(params.frequency); APP_RADIO_RETURN_ERR_CTX(res, "set frequency"); #ifndef APP_RADIO_DISABLE_CALIBRATION res = calibrate_image(params.frequency); res = get_device_error_print_and_clear(TAG); APP_RADIO_RETURN_ERR_CTX(res, "calibrate image"); #endif /* APP_RADIO_DISABLE_CALIBRATION */ res = set_packet_params(packet_params.preamble_len, packet_params.payload_len, packet_params.crc_type, packet_params.hdr_type); APP_RADIO_RETURN_ERR_CTX(res, "set packet params"); res = set_output_power(chip_type_to_max_tx_power(chip)); APP_RADIO_RETURN_ERR_CTX(res, "set output power"); res = fix_pa_clamping(); APP_RADIO_RETURN_ERR_CTX(res, "fix pa clamping"); res = standby(); APP_RADIO_RETURN_ERR_CTX(res, "standby"); return {}; }; } #undef APP_RADIO_RETURN_ERR #endif /* D07297EB_4033_481B_BCFA_1D40899340D0 */