Files
esp_llcc68_driver/inc/llcc68.hpp

1259 lines
46 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Created by Kurosu Chan on 2024/1/17.
//
#ifndef D07297EB_4033_481B_BCFA_1D40899340D0
#define D07297EB_4033_481B_BCFA_1D40899340D0
#include <cstddef>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <optional>
#include <esp_log.h>
#include "hal_gpio.hpp"
#include "app_const_llcc68.hpp"
#include "radiolib_definitions.hpp"
#include "utils/app_utils.hpp"
#include "utils/app_result.hpp"
#include "utils/app_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, 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;
template <typename T = int64_t>
using Instant = app::utils::Instant<T>;
template <typename T, typename E>
using Result = app::utils::Result<T, E>;
using Unit = app::utils::Unit;
using millisecond = std::chrono::duration<size_t, std::milli>;
/**
* \brief Whether to use only LDO regulator (true) or DC-DC regulator (false)
*/
constexpr bool USE_REGULATOR_LDO = false;
/*!
\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_XTAL = true;
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<> start{};
uint16_t expected_duration_ms{};
};
/**
* @brief private namespace; Don't use anything in this namespace outside of this file
*/
namespace details {
extern uint32_t __tcxo_delay__;
}
constexpr auto delay_ms = app::utils::delay_ms;
/*!
\brief Sets the module to standby mode.
*/
inline Result<Unit, error_t> 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<Unit, error_t>
reset() {
if (RST_PIN == GPIO_NUM_NC) {
return {};
}
gpio::set_mode(RST_PIN, gpio::Mode::OUTPUT);
gpio::digital_write(RST_PIN, false);
delay_ms(100);
gpio::digital_write(RST_PIN, true);
delay_ms(100);
const auto instant = Instant<>{};
constexpr auto INTERVAL = std::chrono::duration<uint16_t, std::milli>{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 (!res) {
return res;
} else {
return {};
}
};
inline Result<Unit, error_t>
read_register(const uint16_t addr, std::span<uint8_t> data) {
return spi::read_register(addr, data);
}
inline Result<Unit, error_t>
write_register(const uint16_t addr, std::span<const uint8_t> data) {
return spi::write_register(addr, data);
}
Result<Unit, error_t> inline write_buffer(std::span<const uint8_t> data, const uint8_t offset = DEFAULT_TX_BUFFER_ADDRESS) {
return spi::write_buffer(offset, data);
}
inline Result<uint16_t, error_t>
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(res);
return static_cast<uint16_t>(data[0] << 8) | static_cast<uint16_t>(data[1]);
}
inline Result<Unit, error_t>
clear_irq_status(const uint16_t mask = RADIOLIB_SX126X_IRQ_ALL) {
const uint8_t data[] = {static_cast<uint8_t>((mask >> 8) & 0xff), static_cast<uint8_t>(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<Unit, error_t>
set_rf_frequency(const uint32_t frf) {
const uint8_t data[] = {static_cast<uint8_t>((frf >> 24) & 0xFF), static_cast<uint8_t>((frf >> 16) & 0xFF), static_cast<uint8_t>((frf >> 8) & 0xFF), static_cast<uint8_t>(frf & 0xFF)};
return spi::write_stream(RADIOLIB_SX126X_CMD_SET_RF_FREQUENCY, data);
}
inline Result<Unit, error_t>
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<Unit, error_t>
set_regulator_mode(const uint8_t mode) {
const uint8_t data[] = {mode};
return spi::write_stream(RADIOLIB_SX126X_CMD_SET_REGULATOR_MODE, data);
}
inline Result<status_t, error_t>
get_status() {
uint8_t data = 0;
const auto res = spi::read_stream(RADIOLIB_SX126X_CMD_GET_STATUS, std::span<uint8_t>{&data, 1});
APP_RADIO_RETURN_ERR(res);
return *reinterpret_cast<status_t *>(&data);
}
inline Result<packet_status_t, error_t>
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(res);
return packet_status_t{
.raw = {data[0], data[1], data[2]}};
}
inline Result<op_error_t, error_t>
get_device_errors() {
uint8_t data[] = {0x00, 0x00};
const auto res = spi::read_stream(RADIOLIB_SX126X_CMD_GET_DEVICE_ERRORS, data);
APP_RADIO_RETURN_ERR(res);
// assuming little endian (as most microcontrollers are)
uint16_t opError_le = data[0] << 8 | data[1];
auto opError = *reinterpret_cast<op_error_t *>(&opError_le);
return opError;
}
/**
* \brief This commands clears all the errors recorded in the device. The errors can not be cleared independently.
*/
inline Result<Unit, error_t>
clear_device_errors() {
constexpr uint8_t data[] = {RADIOLIB_SX126X_CMD_NOP, RADIOLIB_SX126X_CMD_NOP};
return spi::write_stream(RADIOLIB_SX126X_CMD_CLEAR_DEVICE_ERRORS, data);
}
/**
* \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 Set the RF frequency in MHz, with necessary calibration and checks
* \param freq the frequency to set, which must be in range [150, 960]
* \param calibrate whether to calibrate the image
* \sa set_frequency_raw
*/
inline Result<Unit, error_t>
set_frequency(freq_t freq, const bool calibrate = true) {
using f_t = decltype(freq);
if (!valid_freq(freq)) {
return ue_t{error_t{error::RADIO_INVALID_FREQUENCY}};
}
if (calibrate) {
uint8_t data[2];
if (freq > f_t{900}) {
data[0] = RADIOLIB_SX126X_CAL_IMG_902_MHZ_1;
data[1] = RADIOLIB_SX126X_CAL_IMG_902_MHZ_2;
} else if (freq > f_t{850.0}) {
data[0] = RADIOLIB_SX126X_CAL_IMG_863_MHZ_1;
data[1] = RADIOLIB_SX126X_CAL_IMG_863_MHZ_2;
} else if (freq > f_t{770.0}) {
data[0] = RADIOLIB_SX126X_CAL_IMG_779_MHZ_1;
data[1] = RADIOLIB_SX126X_CAL_IMG_779_MHZ_2;
} else if (freq > f_t{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);
}
static freq_t last_freq = 0;
static uint32_t raw_last_freq = 0;
if (last_freq != freq) {
last_freq = freq;
raw_last_freq = frequency_raw(freq);
}
return set_rf_frequency(raw_last_freq);
}
inline Result<uint8_t, error_t>
get_packet_type() {
uint8_t data = 0;
const auto res = spi::read_stream(RADIOLIB_SX126X_CMD_GET_PACKET_TYPE, std::span<uint8_t>{&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<Unit, error_t>
fix_sensitivity(const uint8_t bw) {
uint8_t sensitivityConfig = 0;
read_register(RADIOLIB_SX126X_REG_SENSITIVITY_CONFIG, std::span<uint8_t>{&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<const uint8_t>{&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<Unit, error_t>
*/
inline Result<Unit, error_t>
fix_pa_clamping(const bool enable = true) {
uint8_t clampConfig = 0;
read_register(RADIOLIB_SX126X_REG_TX_CLAMP_CONFIG, std::span<uint8_t>{&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<const uint8_t>{&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<Unit, error_t>
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<uint8_t>{&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<const uint8_t>{&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<Unit, error_t>
set_dio_irq_params(const 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<uint8_t>((irqMask >> 8) & 0xFF), static_cast<uint8_t>(irqMask & 0xFF),
static_cast<uint8_t>((dio1Mask >> 8) & 0xFF), static_cast<uint8_t>(dio1Mask & 0xFF),
static_cast<uint8_t>((dio2Mask >> 8) & 0xFF), static_cast<uint8_t>(dio2Mask & 0xFF),
static_cast<uint8_t>((dio3Mask >> 8) & 0xFF), static_cast<uint8_t>(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<Unit, error_t>
set_dio2_as_rf_switch(const bool en) {
const uint8_t data[] = {en ? static_cast<uint8_t>(RADIOLIB_SX126X_DIO2_AS_RF_SWITCH) : static_cast<uint8_t>(RADIOLIB_SX126X_DIO2_AS_IRQ)};
return spi::write_stream(RADIOLIB_SX126X_CMD_SET_DIO2_AS_RF_SWITCH_CTRL, data);
}
/**
* \brief set packet type and do the calibration
*/
inline Result<Unit, error_t> config_packet_type(uint8_t sf) {
Result<Unit, error_t> 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<const uint8_t>{data, 1});
APP_RADIO_RETURN_ERR(res);
data[0] = RADIOLIB_SX126X_RX_TX_FALLBACK_MODE_STDBY_RC;
res = spi::write_stream(RADIOLIB_SX126X_CMD_SET_RX_TX_FALLBACK_MODE, std::span<const uint8_t>{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<const uint8_t>{data, 7});
APP_RADIO_RETURN_ERR(res);
clear_irq_status();
// default init to be non-interrupt
constexpr auto irq_params = irq_params_t{};
res = set_dio_irq_params(irq_params);
APP_RADIO_RETURN_ERR(res);
data[0] = RADIOLIB_SX126X_CALIBRATE_ALL;
res = spi::write_stream(RADIOLIB_SX126X_CMD_CALIBRATE, std::span<const uint8_t>{data, 1});
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<uint32_t>(bw_ * 10);
const uint32_t symbolLength_us = (static_cast<uint32_t>(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<int16_t>(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<Unit, error_t>
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<uint8_t>((preamble_length >> 8) & 0xff),
static_cast<uint8_t>(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<Unit, error_t> fs() {
// default constructor of std::span is empty
return spi::write_stream(RADIOLIB_SX126X_CMD_SET_FS, std::span<uint8_t>());
}
/**
* \param timeout no idea why you need a timeout for TX
*/
inline Result<Unit, error_t> tx(const uint32_t timeout = 0) {
const uint8_t data[] = {static_cast<uint8_t>((timeout >> 16) & 0xFF), static_cast<uint8_t>((timeout >> 8) & 0xFF), static_cast<uint8_t>(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<Unit, error_t> rx(const uint32_t timeout = RADIOLIB_SX126X_RX_TIMEOUT_INF) {
const uint8_t data[] = {static_cast<uint8_t>((timeout >> 16) & 0xFF), static_cast<uint8_t>((timeout >> 8) & 0xFF), static_cast<uint8_t>(timeout & 0xFF)};
return spi::write_stream(RADIOLIB_SX126X_CMD_SET_RX, data);
}
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<uint8_t, uint8_t> 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<uint8_t> 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<Unit, error_t>
set_cad_params(const cad_params_t &params) {
uint8_t data[7];
data[0] = static_cast<uint8_t>(params.symbol_num);
data[1] = params.det_peak;
data[2] = params.det_min;
data[3] = static_cast<uint8_t>(params.exit_mode);
data[4] = static_cast<uint8_t>((params.timeout >> 16) & 0xFF);
data[5] = static_cast<uint8_t>((params.timeout >> 8) & 0xFF);
data[6] = static_cast<uint8_t>(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<Unit, error_t>
set_cad() {
uint8_t data[] = {spi::SPI_NOP_COMMAND};
return spi::write_stream(RADIOLIB_SX126X_CMD_SET_CAD, 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<Unit, error_t>
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<bool, error_t>
channel_scan_result() {
const auto res = get_irq_status();
APP_RADIO_RETURN_ERR(res);
if (*res & RADIOLIB_SX126X_IRQ_CAD_DETECTED) {
return false;
} else if (*res & RADIOLIB_SX126X_IRQ_CAD_DONE) {
return true;
}
return ue_t{error_t{error::RADIO_INVALID_CAD_RESULT}};
}
template <float target>
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<uint32_t, error_t>
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<uint8_t>(voltage);
uint32_t delay_val = static_cast<uint32_t>(delay / 15.625);
data[1] = static_cast<uint8_t>((delay_val >> 16) & 0xFF);
data[2] = static_cast<uint8_t>((delay_val >> 8) & 0xFF);
data[3] = static_cast<uint8_t>(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 if the symbol (length := 2^sf / bw_khz) > 16, then we're using LDR
*/
template <uint8_t bw, uint32_t sf>
bool static_is_use_ldr() {
constexpr auto khz = bw_khz(bw);
constexpr auto symbol_length = static_cast<uint32_t>(std::pow(2, sf) / khz);
return symbol_length > 16;
}
/**
* \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<Unit, error_t>
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<Unit, error_t>
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<Unit, error_t> set_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<uint8_t>((sync_word & 0xF0) | ((control_bits & 0xF0) >> 4)),
static_cast<uint8_t>(((sync_word & 0x0F) << 4) | (control_bits & 0x0F))};
return write_register(RADIOLIB_SX126X_REG_LORA_SYNC_WORD_MSB, data);
}
inline Result<Unit, error_t>
set_packet_type(const PacketType packet_type) {
const auto data = std::array<uint8_t, 2>{
RADIOLIB_SX126X_CMD_SET_PACKET_TYPE,
static_cast<uint8_t>(packet_type),
};
return spi::write_stream(RADIOLIB_SX126X_CMD_SET_PACKET_TYPE, data);
}
inline Result<Unit, error_t>
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<uint8_t>{&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<const uint8_t>{&ocp, 1});
}
inline Result<Unit, error_t>
set_regulator_ldo() {
return set_regulator_mode(RADIOLIB_SX126X_REGULATOR_LDO);
}
inline Result<Unit, error_t>
set_regulator_dc_dc() {
return set_regulator_mode(RADIOLIB_SX126X_REGULATOR_DC_DC);
}
/**
* \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 <float limit>
Result<Unit, error_t>
set_current_limit() {
static_assert(in_range(limit, 0, 140), "limit must be in range [0, 140]");
constexpr uint8_t raw = static_cast<uint8_t>(limit / 2.5);
return write_register(RADIOLIB_SX126X_REG_OCP_CONFIGURATION, std::span<const uint8_t>{&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<bool, ChipType> 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<uint8_t>{reinterpret_cast<uint8_t *>(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<std::tuple<uint8_t, uint8_t>, 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(res);
return std::make_tuple(rx_buf_status[0], rx_buf_status[1]);
}
struct read_result_t {
uint16_t irq_status;
std::span<const uint8_t> data;
bool is_crc_err() const {
return (irq_status & RADIOLIB_SX126X_IRQ_CRC_ERR) != 0;
}
bool is_header_err() const {
return (irq_status & RADIOLIB_SX126X_IRQ_HEADER_ERR) != 0;
}
/*!
* \brief user should call this function after reading data (since it will override the internal buffer)
*/
static Result<Unit, error_t> post_procedure() {
Result<Unit, error_t> res;
res = set_buffer_base_address();
APP_RADIO_RETURN_ERR(res);
res = clear_irq_status();
APP_RADIO_RETURN_ERR(res);
return {};
}
};
inline Result<read_result_t, error_t>
read_data_internal() {
const auto irq_ = get_irq_status();
APP_RADIO_RETURN_ERR(irq_);
const auto st_ = get_status();
APP_RADIO_RETURN_ERR(st_);
const auto irq = *irq_;
const auto st = *st_;
if (irq & RADIOLIB_SX126X_IRQ_TIMEOUT || st.command_status == CommandStatus::COMMAND_TIMEOUT) {
return ue_t{error_t{error::RADIO_RX_TIMEOUT}};
}
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{irq, *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<Unit, error_t>
kick_inf_rx(const modulation_params_t &mod_params, const packet_params_t &packet_params) {
Result<Unit, error_t> res;
res = standby();
APP_RADIO_RETURN_ERR_CTX(res, "failed to standby");
res = set_buffer_base_address();
APP_RADIO_RETURN_ERR_CTX(res, "failed to 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, "failed to set 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, "failed to set packet params");
res = set_dio_irq_params(irq_params);
APP_RADIO_RETURN_ERR_CTX(res, "failed to set dio irq params");
res = clear_irq_status();
APP_RADIO_RETURN_ERR_CTX(res, "failed to clear irq status");
res = rx(RADIOLIB_SX126X_RX_TIMEOUT_INF);
APP_RADIO_RETURN_ERR_CTX(res, "failed to set 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<Unit, error_t>
sync_transmit(const std::span<const uint8_t> data, const lora_parameters_t &params) {
// 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<transmit_state_t, error_t>
async_transmit(const std::span<const uint8_t> data,
const lora_parameters_t &params,
const transmit_state_t &tx_state) {
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<Unit, error_t> res;
res = standby();
APP_RADIO_RETURN_ERR_CTX(res, "failed to 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, "failed to set packet params");
res = set_buffer_base_address();
APP_RADIO_RETURN_ERR_CTX(res, "failed to set buffer base address");
res = write_buffer(data);
APP_RADIO_RETURN_ERR_CTX(res, "failed to write buffer");
res = fix_sensitivity(mod_params.bw);
APP_RADIO_RETURN_ERR_CTX(res, "failed to 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, "failed to set dio irq params");
clear_irq_status(irq_mask);
APP_RADIO_RETURN_ERR_CTX(res, "failed to clear irq status");
res = tx();
APP_RADIO_RETURN_ERR_CTX(res, "failed to transmit");
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;
auto instant = Instant<>{};
return transmit_state_t{
.is_transmitting = true,
.start = std::move(instant),
.expected_duration_ms = timeout_ms,
};
}
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_POSEDGE,
};
// https://github.com/espressif/esp-idf/blob/v5.3.2/examples/peripherals/gpio/generic_gpio/main/gpio_example_main.c
ESP_ERROR_CHECK(gpio_config(&io_conf));
gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
for (const auto pin : EXTI_PINS) {
if (pin == NC_PIN) {
continue;
}
gpio_isr_handler_add(pin, isr, arg);
}
};
static constexpr auto begin = [](const lora_parameters_t &params) -> Result<Unit, error_t> {
/**
* 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.
**/
Result<Unit, error_t> res;
/* res = reset(); */
/* explicit not reset the chip by pin, due to unexpected side effect */
res = standby();
APP_RADIO_RETURN_ERR_CTX(res, "failed to standby");
const auto [ok, chip] = find_chip();
if (not ok) {
return ue_t{error_t{error::RADIO_CHIP_NOT_FOUND}};
}
if (not USE_XTAL) {
const auto voltage_ = set_TCXO(DEFAULT_TCXO_VOLTAGE);
details::__tcxo_delay__ = *voltage_;
APP_RADIO_RETURN_ERR_CTX(res, "failed to set TCXO");
}
const auto &mod_params = params.mod_params;
const auto &packet_params = params.packet_params;
// SetPacketType
res = config_packet_type(mod_params.sf);
APP_RADIO_RETURN_ERR_CTX(res, "failed to config packet type");
res = set_modulation_params(mod_params.sf,
mod_params.bw,
mod_params.cr,
mod_params.ldr_optimize);
APP_RADIO_RETURN_ERR_CTX(res, "failed to set modulation params");
res = set_sync_word(params.sync_word);
APP_RADIO_RETURN_ERR_CTX(res, "failed to set sync word");
if constexpr (USE_REGULATOR_LDO) {
res = set_regulator_ldo();
APP_RADIO_RETURN_ERR_CTX(res, "failed to set regulator LDO");
} else {
res = set_regulator_dc_dc();
APP_RADIO_RETURN_ERR_CTX(res, "failed to set regulator DC-DC");
}
res = set_current_limit<60.0f>();
APP_RADIO_RETURN_ERR_CTX(res, "failed to set current limit");
res = set_dio2_as_rf_switch(false);
APP_RADIO_RETURN_ERR_CTX(res, "failed to set dio2 as rf switch");
res = set_frequency(params.frequency);
APP_RADIO_RETURN_ERR_CTX(res, "failed to set frequency");
// SetPacketParams
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, "failed to set packet params");
res = set_output_power(chip_type_to_max_tx_power(chip));
APP_RADIO_RETURN_ERR_CTX(res, "failed to set output power");
res = fix_pa_clamping();
APP_RADIO_RETURN_ERR_CTX(res, "failed to fix pa clamping");
res = standby();
APP_RADIO_RETURN_ERR_CTX(res, "failed to standby");
return {};
};
}
#undef APP_RADIO_RETURN_ERR
#endif /* D07297EB_4033_481B_BCFA_1D40899340D0 */