feat: implement string conversion functions for CommandStatus and ChipMode enums
1284 lines
47 KiB
C++
1284 lines
47 KiB
C++
//
|
||
// 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);
|
||
}
|
||
|
||
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(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 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 ¶ms) {
|
||
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->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 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 {
|
||
irq_status_t irq_status;
|
||
std::span<const uint8_t> data;
|
||
|
||
bool is_crc_err() const {
|
||
return (irq_status.bits.crc_err) != 0;
|
||
}
|
||
|
||
bool is_header_err() const {
|
||
return (irq_status.bits.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.bits.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 ¶ms) {
|
||
// unimplemented
|
||
std::unreachable();
|
||
return {};
|
||
}
|
||
|
||
/**
|
||
* \brief async transmit
|
||
* \param data the data to transmit
|
||
* \param len the length of data
|
||
* \param params the parameters for calculating time on air
|
||
* \param tx_state current transmit state
|
||
* \return new transmit state
|
||
* \see poll_tx_state
|
||
*/
|
||
inline Result<transmit_state_t, error_t>
|
||
async_transmit(const std::span<const uint8_t> data,
|
||
const lora_parameters_t ¶ms,
|
||
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 ¶ms) -> 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 */
|