Files
esp_llcc68_driver/inc/llcc68.hpp
2025-08-08 12:16:21 +08:00

1355 lines
49 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_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 <typename T, typename E>
using Result = app::utils::Result<T, E>;
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<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(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<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);
}
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<irq_status_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_IGNORE_PROC_ERR(res);
return irq_status_t{.raw = static_cast<uint16_t>(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 enum RegulatorMode 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_IGNORE_PROC_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_IGNORE_PROC_ERR(res);
return packet_status_t{
.raw = {data[0], data[1], data[2]}};
}
/**
* @note ignore any possible transmission errors
*/
inline Result<DeviceErrors, error_t>
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<DeviceErrors *>(&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<Unit, error_t>
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<Unit, error_t>
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<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(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);
}
/**
* \sa SetPacketType
*/
inline Result<Unit, error_t>
set_packet_type_lora(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);
// 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<const uint8_t>{data, 1});
APP_RADIO_RETURN_ERR(res);
std::ignore = clear_irq_status();
constexpr auto irq_params = irq_params_t{};
res = set_dio_irq_params(irq_params);
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<Unit, error_t>
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<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> set_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> set_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> set_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 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<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 TX continuous wave mode
* \note CW
*/
inline Result<Unit, error_t>
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<Unit, error_t>
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<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->bits.cad_detected) {
return false;
} else if (res->bits.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 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_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<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});
}
/**
* \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_IGNORE_PROC_ERR(res);
return std::make_tuple(rx_buf_status[0], rx_buf_status[1]);
}
struct read_result_t {
std::span<const uint8_t> data;
/*!
* \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 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<Unit, error_t>
kick_inf_rx(const modulation_params_t &mod_params, const packet_params_t &packet_params) {
constexpr auto TAG = "llcc68::kick_inf_rx";
Result<Unit, error_t> 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<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) {
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<Unit, error_t> 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<Unit, error_t> 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 &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.
**/
constexpr auto TAG = "llcc68::begin";
Result<Unit, error_t> 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 */