1c3626d58b
Add an explicit rf-switch-mode devicetree property for LLCC68 instances, covering no switch handling, TXEN/RXEN complementary GPIO control, and DIO2 single-pin control for PE4259-style RF switches. Preserve the existing default behavior with an auto Kconfig default that only enables complementary GPIO handling when both TXEN and RXEN GPIOs are present. Resolve the RF switch mode into llcc68_config at build time and validate incompatible devicetree combinations with BUILD_ASSERT checks. Configure optional RXEN GPIO handling for DIO2 single-pin mode and keep DIO2 RF switch control disabled unless that mode is selected. Replace the old fire-and-forget TX/RX GPIO helper with a result-returning mode-aware RF switch state helper, and apply it across standby, sleep, CAD, TX, RX, continuous wave, infinite preamble, and modem init paths. Add SetRxDutyCycle support with explicit raw 24-bit LLCC68 period units, plus helpers and a millisecond wrapper for callers that work in time units. Select the RX RF path before issuing the duty-cycle command so RXEN stays valid for duty-cycle listen windows.
1150 lines
28 KiB
C++
1150 lines
28 KiB
C++
#ifndef ECC594CF_EDF0_42B5_8518_0EB3B3583727
|
||
#define ECC594CF_EDF0_42B5_8518_0EB3B3583727
|
||
|
||
#include <array>
|
||
#include <chrono>
|
||
#include <cstdint>
|
||
#include <optional>
|
||
#include <sstream>
|
||
#include <string>
|
||
#include <system_error>
|
||
#include <tuple>
|
||
#include <type_traits>
|
||
#include <variant>
|
||
|
||
namespace app::driver::llcc68 {
|
||
struct LLCC68;
|
||
using airtime_t = std::chrono::microseconds;
|
||
using error_code = std::error_code;
|
||
|
||
constexpr uint32_t RX_DUTY_CYCLE_PERIOD_MAX = 0x00FFFFFFU;
|
||
constexpr uint32_t RX_DUTY_CYCLE_PERIOD_UNIT_US_NUMERATOR = 125U;
|
||
constexpr uint32_t RX_DUTY_CYCLE_PERIOD_UNIT_US_DENOMINATOR = 8U;
|
||
|
||
/**
|
||
* @brief Convert microseconds to a raw SetRxDutyCycle period.
|
||
*
|
||
* LLCC68 datasheet section 13.1.7 defines rxPeriod and sleepPeriod as raw
|
||
* 24-bit RTC periods, not milliseconds. One raw period is 15.625 us.
|
||
*/
|
||
constexpr std::optional<uint32_t> rx_duty_cycle_period_from_us(uint64_t us) {
|
||
constexpr uint64_t scale = RX_DUTY_CYCLE_PERIOD_UNIT_US_DENOMINATOR;
|
||
constexpr uint64_t divisor = RX_DUTY_CYCLE_PERIOD_UNIT_US_NUMERATOR;
|
||
if (us > (UINT64_MAX - (divisor - 1U)) / scale) {
|
||
return std::nullopt;
|
||
}
|
||
const uint64_t raw = ((us * scale) + (divisor - 1U)) / divisor;
|
||
if (raw > RX_DUTY_CYCLE_PERIOD_MAX) {
|
||
return std::nullopt;
|
||
}
|
||
return static_cast<uint32_t>(raw);
|
||
}
|
||
|
||
/**
|
||
* @brief Convert milliseconds to a raw SetRxDutyCycle period.
|
||
*
|
||
* Returns std::nullopt if the requested duration does not fit the LLCC68
|
||
* 24-bit period field.
|
||
*/
|
||
constexpr std::optional<uint32_t> rx_duty_cycle_period_from_ms(uint64_t ms) {
|
||
if (ms > UINT64_MAX / 1000U) {
|
||
return std::nullopt;
|
||
}
|
||
return rx_duty_cycle_period_from_us(ms * 1000U);
|
||
}
|
||
|
||
enum class Errc : uint8_t {
|
||
FailureToExecuteCommand = 1,
|
||
CommandTimeout = 2,
|
||
CommandProcessing = 3,
|
||
InvalidState = 4,
|
||
};
|
||
|
||
class Llcc68ErrorCategory : public std::error_category {
|
||
public:
|
||
[[nodiscard]]
|
||
const char *name() const noexcept override {
|
||
return "llcc68";
|
||
}
|
||
|
||
[[nodiscard]]
|
||
std::string message(int condition) const override {
|
||
switch (static_cast<Errc>(condition)) {
|
||
case Errc::FailureToExecuteCommand:
|
||
return "failure to execute command";
|
||
case Errc::CommandTimeout:
|
||
return "command timeout";
|
||
case Errc::CommandProcessing:
|
||
return "command processing error";
|
||
case Errc::InvalidState:
|
||
return "invalid state";
|
||
default:
|
||
return "unknown llcc68 error";
|
||
}
|
||
}
|
||
};
|
||
|
||
inline const std::error_category &error_category() {
|
||
static const Llcc68ErrorCategory category{};
|
||
return category;
|
||
}
|
||
|
||
inline error_code make_error_code(Errc error) {
|
||
return {static_cast<int>(error), error_category()};
|
||
}
|
||
|
||
inline error_code make_zephyr_error_code(int zephyr_ret) {
|
||
if (zephyr_ret >= 0) {
|
||
return {};
|
||
}
|
||
|
||
return {static_cast<int>(-zephyr_ret), std::generic_category()};
|
||
}
|
||
|
||
using freq_t = float;
|
||
enum LoRaBandwidth : uint8_t {
|
||
BW_7_8 = 0x00,
|
||
BW_10_4 = 0x08,
|
||
BW_15_6 = 0x01,
|
||
BW_20_8 = 0x09,
|
||
BW_31_25 = 0x02,
|
||
BW_41_7 = 0x0A,
|
||
BW_62_5 = 0x03,
|
||
BW_125_0 = 0x04,
|
||
BW_250_0 = 0x05,
|
||
BW_500_0 = 0x06,
|
||
};
|
||
|
||
enum LoRaHeaderType : uint8_t {
|
||
HEADER_EXPLICIT = 0x00,
|
||
HEADER_IMPLICIT = 0x01,
|
||
};
|
||
|
||
enum LoRaCodingRate : uint8_t {
|
||
CR_4_5 = 0x01,
|
||
CR_4_6 = 0x02,
|
||
CR_4_7 = 0x03,
|
||
CR_4_8 = 0x04,
|
||
};
|
||
|
||
enum LoRaCrcType : uint8_t {
|
||
CRC_OFF = 0x00,
|
||
CRC_ON = 0x01,
|
||
};
|
||
|
||
enum LoRaIQType : uint8_t {
|
||
IQ_STANDARD = 0x00,
|
||
IQ_INVERTED = 0x01,
|
||
};
|
||
|
||
// LoRa LDR (Low Data Rate) types
|
||
enum LoRaLowDataRateType : uint8_t {
|
||
LDR_OFF = 0x00,
|
||
LDR_ON = 0x01,
|
||
};
|
||
|
||
// - RampTime Value RampTime (µs)
|
||
// - SET_RAMP_10U 0x00 10
|
||
// - SET_RAMP_20U 0x01 20
|
||
// - SET_RAMP_ 40U 0x02 40
|
||
// - SET_RAMP_80U 0x03 80
|
||
// - SET_RAMP_200U 0x04 200
|
||
// - SET_RAMP_800U 0x05 800
|
||
// - SET_RAMP_1700U 0x06 1700
|
||
// - SET_RAMP_3400U 0x07 3400
|
||
enum LoRaTxRampTime : uint8_t {
|
||
SET_RAMP_10U = 0x00,
|
||
SET_RAMP_20U = 0x01,
|
||
SET_RAMP_40U = 0x02,
|
||
SET_RAMP_80U = 0x03,
|
||
SET_RAMP_200U = 0x04,
|
||
SET_RAMP_800U = 0x05,
|
||
SET_RAMP_1700U = 0x06,
|
||
SET_RAMP_3400U = 0x07,
|
||
};
|
||
|
||
enum RegulatorMode : uint8_t {
|
||
REGULATOR_LDO = 0x00,
|
||
REGULATOR_DC_DC = 0x01,
|
||
};
|
||
|
||
enum LoRaSyncWord : uint16_t {
|
||
SYNC_WORD_PRIVATE = 0x1424, // Private sync word
|
||
SYNC_WORD_PUBLIC = 0x3444, // Public sync word
|
||
};
|
||
|
||
inline const char *to_str(LoRaBandwidth bw) {
|
||
switch (bw) {
|
||
case LoRaBandwidth::BW_7_8:
|
||
return "7.8kHz";
|
||
case LoRaBandwidth::BW_10_4:
|
||
return "10.4kHz";
|
||
case LoRaBandwidth::BW_15_6:
|
||
return "15.6kHz";
|
||
case LoRaBandwidth::BW_20_8:
|
||
return "20.8kHz";
|
||
case LoRaBandwidth::BW_31_25:
|
||
return "31.25kHz";
|
||
case LoRaBandwidth::BW_41_7:
|
||
return "41.7kHz";
|
||
case LoRaBandwidth::BW_62_5:
|
||
return "62.5kHz";
|
||
case LoRaBandwidth::BW_125_0:
|
||
return "125.0kHz";
|
||
case LoRaBandwidth::BW_250_0:
|
||
return "250.0kHz";
|
||
case LoRaBandwidth::BW_500_0:
|
||
return "500.0kHz";
|
||
default:
|
||
return "Unknown";
|
||
}
|
||
}
|
||
|
||
inline const char *to_str(LoRaCodingRate cr) {
|
||
switch (cr) {
|
||
case LoRaCodingRate::CR_4_5:
|
||
return "4/5";
|
||
case LoRaCodingRate::CR_4_6:
|
||
return "4/6";
|
||
case LoRaCodingRate::CR_4_7:
|
||
return "4/7";
|
||
case LoRaCodingRate::CR_4_8:
|
||
return "4/8";
|
||
default:
|
||
return "Unknown";
|
||
}
|
||
}
|
||
|
||
inline std::optional<uint8_t> sf_from_uint8_t(const uint8_t sf) {
|
||
if (sf < 6 || sf > 12) {
|
||
return std::nullopt;
|
||
}
|
||
return sf;
|
||
}
|
||
|
||
struct modulation_params_t {
|
||
LoRaBandwidth bw;
|
||
uint8_t sf;
|
||
LoRaCodingRate cr;
|
||
LoRaLowDataRateType ldr_optimize;
|
||
|
||
static modulation_params_t Default() {
|
||
return modulation_params_t{
|
||
.bw = LoRaBandwidth::BW_250_0,
|
||
.sf = 7,
|
||
.cr = LoRaCodingRate::CR_4_7,
|
||
.ldr_optimize = LoRaLowDataRateType::LDR_OFF,
|
||
};
|
||
}
|
||
};
|
||
|
||
struct packet_params_t {
|
||
uint16_t preamble_len;
|
||
uint8_t payload_len;
|
||
LoRaCrcType crc_type;
|
||
LoRaHeaderType hdr_type;
|
||
LoRaIQType iq_type;
|
||
|
||
static packet_params_t Default() {
|
||
return packet_params_t{
|
||
.preamble_len = 8,
|
||
.payload_len = 0xff,
|
||
.crc_type = LoRaCrcType::CRC_OFF,
|
||
.hdr_type = LoRaHeaderType::HEADER_IMPLICIT,
|
||
.iq_type = LoRaIQType::IQ_STANDARD,
|
||
};
|
||
}
|
||
};
|
||
|
||
struct pa_setting_t {
|
||
// 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.
|
||
uint8_t pa_duty_cycle;
|
||
// 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.
|
||
uint8_t hp_max;
|
||
uint8_t device_sel;
|
||
|
||
/**
|
||
* @note SetTxParams should be set to +22dBm
|
||
*/
|
||
static constexpr pa_setting_t Default22dBmHp() { return {0x04, 0x07, 0x00}; }
|
||
|
||
/**
|
||
* @note SetTxParams should be set to +22dBm
|
||
*/
|
||
static constexpr pa_setting_t Default20dBmHp() { return {0x03, 0x05, 0x00}; }
|
||
/**
|
||
* @note SetTxParams should be set to +22dBm
|
||
*/
|
||
static constexpr pa_setting_t Default17dBmHp() { return {0x02, 0x03, 0x00}; }
|
||
/**
|
||
* @note SetTxParams should be set to +22dBm
|
||
* @note High Power; SX1262/LLCC68
|
||
*/
|
||
static constexpr pa_setting_t Default14dBmHp() { return {0x02, 0x02, 0x00}; }
|
||
|
||
/**
|
||
* @note SetTxParams should be set to +14dBm
|
||
* @note Low Power; SX1261
|
||
*/
|
||
static constexpr pa_setting_t Default14dBmLp() { return {0x04, 0x00, 0x01}; }
|
||
|
||
/**
|
||
* @note SetTxParams should be set to +14dBm
|
||
* @note Low Power; SX1261
|
||
*/
|
||
static constexpr pa_setting_t Default10dBmLp() { return {0x01, 0x00, 0x01}; }
|
||
|
||
static constexpr pa_setting_t DefaultHp() { return Default22dBmHp(); }
|
||
static constexpr pa_setting_t DefaultLp() { return Default14dBmLp(); }
|
||
};
|
||
|
||
inline const char *to_str(llcc68::LoRaTxRampTime ramp_time) {
|
||
switch (ramp_time) {
|
||
case llcc68::SET_RAMP_10U:
|
||
return "10us";
|
||
case llcc68::SET_RAMP_20U:
|
||
return "20us";
|
||
case llcc68::SET_RAMP_40U:
|
||
return "40us";
|
||
case llcc68::SET_RAMP_80U:
|
||
return "80us";
|
||
case llcc68::SET_RAMP_200U:
|
||
return "200us";
|
||
case llcc68::SET_RAMP_800U:
|
||
return "800us";
|
||
case llcc68::SET_RAMP_1700U:
|
||
return "1700us";
|
||
case llcc68::SET_RAMP_3400U:
|
||
return "3400us";
|
||
default:
|
||
return "Unknown";
|
||
}
|
||
}
|
||
|
||
struct tx_params_t {
|
||
/**
|
||
* @brief magic number for automatic max TX power depending on
|
||
* chip type
|
||
*/
|
||
static constexpr auto MAX_POWER_HIGH_PA = 22;
|
||
static constexpr auto MAX_POWER_LOW_PA = 14;
|
||
|
||
int8_t power;
|
||
LoRaTxRampTime ramp_time;
|
||
|
||
static constexpr tx_params_t Default() {
|
||
return tx_params_t{
|
||
.power = MAX_POWER_HIGH_PA,
|
||
.ramp_time = SET_RAMP_200U,
|
||
};
|
||
}
|
||
[[nodiscard]]
|
||
std::string to_string() const {
|
||
#ifdef __cpp_lib_format
|
||
return std::format("tx_params[power={}, ramp_time={}]", power,
|
||
to_str(ramp_time));
|
||
#else
|
||
return "tx_params[power=" + std::to_string(power) +
|
||
", ramp_time=" + to_str(ramp_time) + "]";
|
||
#endif
|
||
}
|
||
};
|
||
|
||
struct lora_parameters_t {
|
||
modulation_params_t mod_params;
|
||
packet_params_t packet_params;
|
||
tx_params_t tx_params;
|
||
freq_t frequency_mhz;
|
||
LoRaSyncWord sync_word;
|
||
|
||
static lora_parameters_t Default() {
|
||
return lora_parameters_t{
|
||
.mod_params = modulation_params_t::Default(),
|
||
.packet_params = packet_params_t::Default(),
|
||
.tx_params = tx_params_t::Default(),
|
||
.frequency_mhz = 434.75,
|
||
.sync_word = LoRaSyncWord::SYNC_WORD_PRIVATE,
|
||
};
|
||
}
|
||
void log(const char *tag) const;
|
||
};
|
||
|
||
enum class GfskPulseShape : uint8_t {
|
||
Off = 0x00,
|
||
Gauss03 = 0x08,
|
||
Gauss05 = 0x09,
|
||
Gauss07 = 0x0A,
|
||
Gauss1 = 0x0B,
|
||
};
|
||
|
||
enum class GfskRxBandwidth : uint8_t {
|
||
Bw4800 = 0x1F,
|
||
Bw5800 = 0x17,
|
||
Bw7300 = 0x0F,
|
||
Bw9700 = 0x1E,
|
||
Bw11700 = 0x16,
|
||
Bw14600 = 0x0E,
|
||
Bw19500 = 0x1D,
|
||
Bw23400 = 0x15,
|
||
Bw29300 = 0x0D,
|
||
Bw39000 = 0x1C,
|
||
Bw46900 = 0x14,
|
||
Bw58600 = 0x0C,
|
||
Bw78200 = 0x1B,
|
||
Bw93800 = 0x13,
|
||
Bw117300 = 0x0B,
|
||
Bw156200 = 0x1A,
|
||
Bw187200 = 0x12,
|
||
Bw234300 = 0x0A,
|
||
Bw312000 = 0x19,
|
||
Bw373600 = 0x11,
|
||
Bw467000 = 0x09,
|
||
};
|
||
|
||
inline uint32_t rx_bandwidth_hz(GfskRxBandwidth bandwidth) {
|
||
using enum GfskRxBandwidth;
|
||
switch (bandwidth) {
|
||
case Bw4800:
|
||
return 4800;
|
||
case Bw5800:
|
||
return 5800;
|
||
case Bw7300:
|
||
return 7300;
|
||
case Bw9700:
|
||
return 9700;
|
||
case Bw11700:
|
||
return 11700;
|
||
case Bw14600:
|
||
return 14600;
|
||
case Bw19500:
|
||
return 19500;
|
||
case Bw23400:
|
||
return 23400;
|
||
case Bw29300:
|
||
return 29300;
|
||
case Bw39000:
|
||
return 39000;
|
||
case Bw46900:
|
||
return 46900;
|
||
case Bw58600:
|
||
return 58600;
|
||
case Bw78200:
|
||
return 78200;
|
||
case Bw93800:
|
||
return 93800;
|
||
case Bw117300:
|
||
return 117300;
|
||
case Bw156200:
|
||
return 156200;
|
||
case Bw187200:
|
||
return 187200;
|
||
case Bw234300:
|
||
return 234300;
|
||
case Bw312000:
|
||
return 312000;
|
||
case Bw373600:
|
||
return 373600;
|
||
case Bw467000:
|
||
return 467000;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
enum class GfskPreambleDetector : uint8_t {
|
||
Off = 0x00,
|
||
Bits8 = 0x04,
|
||
Bits16 = 0x05,
|
||
Bits24 = 0x06,
|
||
Bits32 = 0x07,
|
||
};
|
||
|
||
enum class GfskAddressFiltering : uint8_t {
|
||
Disabled = 0x00,
|
||
Node = 0x01,
|
||
NodeAndBroadcast = 0x02,
|
||
};
|
||
|
||
enum class GfskPacketLengthMode : uint8_t {
|
||
Fixed = 0x00,
|
||
Variable = 0x01,
|
||
};
|
||
|
||
enum class GfskCrcType : uint8_t {
|
||
Off = 0x01,
|
||
Crc1Byte = 0x00,
|
||
Crc2Byte = 0x02,
|
||
Crc1ByteInverted = 0x04,
|
||
Crc2ByteInverted = 0x06,
|
||
};
|
||
|
||
enum class GfskWhitening : uint8_t {
|
||
Off = 0x00,
|
||
On = 0x01,
|
||
};
|
||
|
||
struct gfsk_modulation_params_t {
|
||
uint32_t bitrate_bps;
|
||
uint32_t frequency_deviation_hz;
|
||
GfskPulseShape pulse_shape;
|
||
GfskRxBandwidth rx_bandwidth;
|
||
|
||
static constexpr gfsk_modulation_params_t Default() {
|
||
return {
|
||
.bitrate_bps = 4800,
|
||
.frequency_deviation_hz = 5000,
|
||
.pulse_shape = GfskPulseShape::Gauss05,
|
||
.rx_bandwidth = GfskRxBandwidth::Bw23400,
|
||
};
|
||
}
|
||
};
|
||
|
||
struct gfsk_packet_params_t {
|
||
uint16_t preamble_bits;
|
||
GfskPreambleDetector detector_length;
|
||
uint8_t sync_length_bits;
|
||
GfskAddressFiltering address_filtering;
|
||
GfskPacketLengthMode packet_length_mode;
|
||
uint8_t payload_length;
|
||
GfskCrcType crc_type;
|
||
GfskWhitening whitening;
|
||
|
||
static constexpr gfsk_packet_params_t Default() {
|
||
return {
|
||
.preamble_bits = 32,
|
||
.detector_length = GfskPreambleDetector::Bits16,
|
||
.sync_length_bits = 32,
|
||
.address_filtering = GfskAddressFiltering::Disabled,
|
||
.packet_length_mode = GfskPacketLengthMode::Variable,
|
||
.payload_length = 0xff,
|
||
.crc_type = GfskCrcType::Crc2Byte,
|
||
.whitening = GfskWhitening::On,
|
||
};
|
||
}
|
||
};
|
||
|
||
struct gfsk_parameters_t {
|
||
gfsk_modulation_params_t mod_params;
|
||
gfsk_packet_params_t packet_params;
|
||
tx_params_t tx_params;
|
||
freq_t frequency_mhz;
|
||
std::array<uint8_t, 8> sync_word;
|
||
uint8_t sync_word_length;
|
||
uint16_t crc_seed;
|
||
uint16_t crc_polynomial;
|
||
uint16_t whitening_seed;
|
||
std::optional<uint8_t> node_address;
|
||
std::optional<uint8_t> broadcast_address;
|
||
|
||
static constexpr gfsk_parameters_t Default() {
|
||
return {
|
||
.mod_params = gfsk_modulation_params_t::Default(),
|
||
.packet_params = gfsk_packet_params_t::Default(),
|
||
.tx_params = tx_params_t::Default(),
|
||
.frequency_mhz = 434.75,
|
||
.sync_word = {0x2D, 0xD4, 0, 0, 0, 0, 0, 0},
|
||
.sync_word_length = 2,
|
||
.crc_seed = 0x1D0F,
|
||
.crc_polynomial = 0x1021,
|
||
.whitening_seed = 0x0100,
|
||
.node_address = std::nullopt,
|
||
.broadcast_address = std::nullopt,
|
||
};
|
||
}
|
||
|
||
static constexpr gfsk_parameters_t
|
||
Gfsk100kFixed6(freq_t frequency_mhz = 434.18,
|
||
tx_params_t tx_params = tx_params_t::Default()) {
|
||
return {
|
||
.mod_params =
|
||
{
|
||
.bitrate_bps = 100'000,
|
||
.frequency_deviation_hz = 25'000,
|
||
.pulse_shape = GfskPulseShape::Gauss05,
|
||
.rx_bandwidth = GfskRxBandwidth::Bw187200,
|
||
},
|
||
.packet_params =
|
||
{
|
||
.preamble_bits = 64,
|
||
.detector_length = GfskPreambleDetector::Bits32,
|
||
.sync_length_bits = 32,
|
||
.address_filtering = GfskAddressFiltering::Disabled,
|
||
.packet_length_mode = GfskPacketLengthMode::Fixed,
|
||
.payload_length = 6,
|
||
.crc_type = GfskCrcType::Off,
|
||
.whitening = GfskWhitening::On,
|
||
},
|
||
.tx_params = tx_params,
|
||
.frequency_mhz = frequency_mhz,
|
||
.sync_word = {0x2D, 0xD4, 0xA1, 0x7E, 0, 0, 0, 0},
|
||
.sync_word_length = 4,
|
||
.crc_seed = 0x1D0F,
|
||
.crc_polynomial = 0x1021,
|
||
.whitening_seed = 0x0100,
|
||
.node_address = std::nullopt,
|
||
.broadcast_address = std::nullopt,
|
||
};
|
||
}
|
||
void log(const char *tag) const;
|
||
};
|
||
|
||
enum class ChipType : std::uint8_t {
|
||
Unknown,
|
||
LLCC68,
|
||
SX1261,
|
||
SX1262,
|
||
};
|
||
|
||
inline const char *to_str(ChipType chip) {
|
||
switch (chip) {
|
||
case ChipType::LLCC68:
|
||
return "LLCC68";
|
||
case ChipType::SX1261:
|
||
return "SX1261";
|
||
case ChipType::SX1262:
|
||
return "SX1262";
|
||
default:
|
||
return "Unknown";
|
||
}
|
||
}
|
||
|
||
/// @note suitable SX1262/LLCC68
|
||
enum class PaOptimalPresetHigh : uint8_t {
|
||
DBM_14,
|
||
DBM_17,
|
||
DBM_20,
|
||
DBM_22,
|
||
};
|
||
|
||
/// @note suitable SX1261
|
||
enum class PaOptimalPresetLow : uint8_t {
|
||
DBM_10,
|
||
DBM_14,
|
||
DBM_15,
|
||
};
|
||
|
||
using PaOptimalPreset = std::variant<PaOptimalPresetHigh, PaOptimalPresetLow>;
|
||
|
||
struct pa_config_t {
|
||
uint8_t pa_duty_cycle;
|
||
uint8_t hp_max;
|
||
uint8_t device_sel;
|
||
};
|
||
|
||
enum class PacketType : uint8_t {
|
||
FSK = 0x00,
|
||
LORA = 0x01,
|
||
LR_FHSS = 0x03,
|
||
};
|
||
|
||
enum class TcxoVoltage : uint8_t {
|
||
V_1_6 = 0x00,
|
||
V_1_7 = 0x01,
|
||
V_1_8 = 0x02,
|
||
V_2_2 = 0x03,
|
||
V_2_4 = 0x04,
|
||
V_2_7 = 0x05,
|
||
V_3_0 = 0x06,
|
||
V_3_3 = 0x07,
|
||
};
|
||
|
||
enum class CommandStatus : uint8_t {
|
||
RESERVED = 0x00, // 0b000
|
||
RFU = 0x01, // 0b001
|
||
DATA_AVAILABLE = 0x02, // 0b010
|
||
COMMAND_TIMEOUT = 0x03, // 0b011 i.e. TIMEOUT
|
||
COMMAND_PROCESSING_ERROR = 0x04, // 0b100 i.e. INVALID
|
||
FAILURE_TO_EXECUTE_COMMAND = 0x05, // 0b101 i.e. FAILED
|
||
COMMAND_TX_DONE = 0x06, // 0b110
|
||
};
|
||
|
||
inline const char *to_str(const CommandStatus &status) {
|
||
switch (status) {
|
||
case CommandStatus::RESERVED:
|
||
return "RESERVED";
|
||
case CommandStatus::RFU:
|
||
return "RFU";
|
||
case CommandStatus::DATA_AVAILABLE:
|
||
return "DATA_AVAILABLE";
|
||
case CommandStatus::COMMAND_TIMEOUT:
|
||
return "COMMAND_TIMEOUT";
|
||
case CommandStatus::COMMAND_PROCESSING_ERROR:
|
||
return "COMMAND_PROCESSING_ERROR";
|
||
case CommandStatus::FAILURE_TO_EXECUTE_COMMAND:
|
||
return "FAILURE_TO_EXECUTE_COMMAND";
|
||
case CommandStatus::COMMAND_TX_DONE:
|
||
return "COMMAND_TX_DONE";
|
||
default:
|
||
return "Unknown";
|
||
}
|
||
}
|
||
|
||
enum class ChipMode : uint8_t {
|
||
UNUSED = 0x00, // 0b000
|
||
RFU = 0x01, // 0b001
|
||
STBY_RC = 0x02, // 0b010
|
||
STBY_XOSC = 0x03, // 0b011
|
||
FS = 0x04, // 0b100
|
||
RX = 0x05, // 0b101
|
||
TX = 0x06, // 0b110
|
||
};
|
||
|
||
inline const char *to_str(const ChipMode &mode) {
|
||
switch (mode) {
|
||
case ChipMode::UNUSED:
|
||
return "UNUSED";
|
||
case ChipMode::RFU:
|
||
return "RFU";
|
||
case ChipMode::STBY_RC:
|
||
return "STBY_RC";
|
||
case ChipMode::STBY_XOSC:
|
||
return "STBY_XOSC";
|
||
case ChipMode::FS:
|
||
return "FS";
|
||
case ChipMode::RX:
|
||
return "RX";
|
||
case ChipMode::TX:
|
||
return "TX";
|
||
default:
|
||
return "Unknown";
|
||
}
|
||
}
|
||
|
||
struct __attribute__((packed)) status_t {
|
||
// LSB
|
||
uint8_t reserved_1 : 1;
|
||
CommandStatus command_status : 3;
|
||
ChipMode chip_mode : 3;
|
||
uint8_t reserved_2 : 1;
|
||
// MSB
|
||
};
|
||
static_assert(sizeof(status_t) == 1);
|
||
|
||
struct __attribute__((packed)) lora_packet_status_t {
|
||
// Average over last packet received of RSSI
|
||
// Actual signal power is –RssiPkt/2 (dBm)
|
||
uint8_t rssi_pkt;
|
||
// Estimation of SNR on last packet received in two’s compliment format
|
||
// multiplied by 4. Actual SNR in dB =SnrPkt/4
|
||
int8_t snr_pkt;
|
||
// Estimation of RSSI of the LoRa® signal (after despreading) on last packet
|
||
// received. Actual Rssi in dB = -SignalRssiPkt/2
|
||
uint8_t signal_rssi_pkt;
|
||
|
||
[[nodiscard]]
|
||
auto rssi_pkt_dbm() const -> float {
|
||
return -static_cast<float>(rssi_pkt) / 2.0F;
|
||
}
|
||
|
||
[[nodiscard]]
|
||
auto snr_pkt_db() const -> float {
|
||
return static_cast<float>(snr_pkt) / 4.0F;
|
||
}
|
||
|
||
[[nodiscard]]
|
||
auto signal_rssi_pkt_dbm() const -> float {
|
||
return -static_cast<float>(signal_rssi_pkt) / 2.0F;
|
||
}
|
||
};
|
||
|
||
struct __attribute__((packed)) fsk_packet_status_t {
|
||
// bit 7: preamble err
|
||
// bit 6: sync err
|
||
// bit 5: adrs err
|
||
// bit 4: crc err
|
||
// bit 3: length err
|
||
// bit 2: abort err
|
||
// bit 1: pkt received
|
||
// bit 0: pkt sent
|
||
uint8_t rx_status;
|
||
// RSSI value latched upon the detection of the sync address.
|
||
// [negated, dBm, fixdt(0,8,1)]
|
||
// Actual signal power is –RssiSync/2 (dBm)
|
||
uint8_t rssi_sync;
|
||
// RSSI average value over the payload of the received packet. Latched upon
|
||
// the pkt_done IRQ. [negated, dBm, fixdt(0,8,1)] Actual signal power is
|
||
// –RssiAvg/2 (dBm)
|
||
uint8_t rssi_avg;
|
||
|
||
[[nodiscard]]
|
||
bool has_error() const {
|
||
return (rx_status & 0b11111100U) != 0;
|
||
}
|
||
|
||
[[nodiscard]]
|
||
bool packet_received() const {
|
||
return (rx_status & 0b00000010U) != 0;
|
||
}
|
||
|
||
[[nodiscard]]
|
||
auto rssi_sync_dbm() const -> float {
|
||
return -static_cast<float>(rssi_sync) / 2.0F;
|
||
}
|
||
|
||
[[nodiscard]]
|
||
auto rssi_avg_dbm() const -> float {
|
||
return -static_cast<float>(rssi_avg) / 2.0F;
|
||
}
|
||
};
|
||
|
||
union packet_status_t {
|
||
uint8_t raw[3];
|
||
lora_packet_status_t lora;
|
||
fsk_packet_status_t fsk;
|
||
};
|
||
static_assert(sizeof(packet_status_t) == 3);
|
||
|
||
struct __attribute__((packed)) DeviceErrors {
|
||
// LSB
|
||
|
||
bool RC64K_CALIB_ERR : 1; // [0] RC64k calibration failed
|
||
bool RC13M_CALIB_ERR : 1; // [1] RkC13M calibration failed
|
||
bool PLL_CALIB_ERR : 1; // [2] PLL calibration failed
|
||
bool ADC_CALIB_ERR : 1; // [3] ADC calibration failed
|
||
bool IMG_CALIB_ERR : 1; // [4] IMG calibration failed
|
||
bool XOSC_START_ERR : 1; // [5] XOSC failed to start
|
||
bool PLL_LOCK_ERR : 1; // [6] PLL failed to lock
|
||
uint8_t rfu_1 : 1; // [7] RFU
|
||
bool PA_RAMP_ERR : 1; // [8] PA ramping failed
|
||
uint8_t rfu_2 : 7; // [15:9] RFU
|
||
|
||
// MSB
|
||
|
||
[[nodiscard]]
|
||
std::string to_string() const {
|
||
auto ss = std::stringstream();
|
||
ss << "DeviceErrors{";
|
||
if (RC64K_CALIB_ERR) {
|
||
ss << "RC64K_CALIB_ERR(0) ";
|
||
}
|
||
if (RC13M_CALIB_ERR) {
|
||
ss << "RC13M_CALIB_ERR(1) ";
|
||
}
|
||
if (PLL_CALIB_ERR) {
|
||
ss << "PLL_CALIB_ERR(2) ";
|
||
}
|
||
if (ADC_CALIB_ERR) {
|
||
ss << "ADC_CALIB_ERR(3) ";
|
||
}
|
||
if (IMG_CALIB_ERR) {
|
||
ss << "IMG_CALIB_ERR(4) ";
|
||
}
|
||
if (XOSC_START_ERR) {
|
||
ss << "XOSC_START_ERR(5) ";
|
||
}
|
||
if (PLL_LOCK_ERR) {
|
||
ss << "PLL_LOCK_ERR(6) ";
|
||
}
|
||
if (PA_RAMP_ERR) {
|
||
ss << "PA_RAMP_ERR(8) ";
|
||
}
|
||
ss << "}";
|
||
return ss.str();
|
||
}
|
||
|
||
[[nodiscard]]
|
||
bool has_any_error() const {
|
||
return RC64K_CALIB_ERR || RC13M_CALIB_ERR || PLL_CALIB_ERR ||
|
||
ADC_CALIB_ERR || IMG_CALIB_ERR || XOSC_START_ERR || PLL_LOCK_ERR ||
|
||
PA_RAMP_ERR;
|
||
}
|
||
};
|
||
static_assert(sizeof(DeviceErrors) == 2);
|
||
|
||
constexpr bool in_range(const auto v, const auto min, const auto max) {
|
||
return v >= min && v <= max;
|
||
}
|
||
|
||
constexpr bool valid_cr(const uint8_t cr) {
|
||
using enum LoRaCodingRate;
|
||
switch (cr) {
|
||
case CR_4_5:
|
||
case CR_4_6:
|
||
case CR_4_7:
|
||
case CR_4_8:
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
constexpr std::tuple<uint8_t, uint8_t> cr_to_ratio(const LoRaCodingRate cr) {
|
||
using enum LoRaCodingRate;
|
||
switch (cr) {
|
||
case CR_4_5:
|
||
return {4, 5};
|
||
case CR_4_6:
|
||
return {4, 6};
|
||
case CR_4_7:
|
||
return {4, 7};
|
||
case CR_4_8:
|
||
return {4, 8};
|
||
default:
|
||
return {0, 1};
|
||
}
|
||
}
|
||
|
||
constexpr bool valid_ldr_optimize(const uint8_t ldr_optimize) {
|
||
using enum LoRaLowDataRateType;
|
||
switch (ldr_optimize) {
|
||
case LDR_ON:
|
||
case LDR_OFF:
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
constexpr bool valid_bw(const uint8_t bw) {
|
||
using enum LoRaBandwidth;
|
||
switch (bw) {
|
||
case BW_7_8:
|
||
case BW_10_4:
|
||
case BW_15_6:
|
||
case BW_20_8:
|
||
case BW_31_25:
|
||
case BW_41_7:
|
||
case BW_62_5:
|
||
case BW_125_0:
|
||
case BW_250_0:
|
||
case BW_500_0:
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
using bw_t = float;
|
||
constexpr bw_t bw_khz(const uint8_t bw) {
|
||
using enum LoRaBandwidth;
|
||
switch (bw) {
|
||
case BW_7_8:
|
||
return bw_t{7.8F};
|
||
case BW_10_4:
|
||
return bw_t{10.4F};
|
||
case BW_15_6:
|
||
return bw_t{15.6F};
|
||
case BW_20_8:
|
||
return bw_t{20.8F};
|
||
case BW_31_25:
|
||
return bw_t{31.25F};
|
||
case BW_41_7:
|
||
return bw_t{41.7F};
|
||
case BW_62_5:
|
||
return bw_t{62.5F};
|
||
case BW_125_0:
|
||
return bw_t{125.0F};
|
||
case BW_250_0:
|
||
return bw_t{250.0F};
|
||
case BW_500_0:
|
||
return bw_t{500.0F};
|
||
default:
|
||
return bw_t{0};
|
||
}
|
||
}
|
||
|
||
constexpr bool valid_sf(const uint8_t bw, const uint8_t sf) {
|
||
if (const bool ok = valid_bw(bw); not ok) {
|
||
return false;
|
||
}
|
||
switch (bw) {
|
||
case BW_125_0:
|
||
return in_range(sf, 5, 9);
|
||
case BW_250_0:
|
||
return in_range(sf, 5, 10);
|
||
case BW_500_0:
|
||
return in_range(sf, 5, 11);
|
||
default:
|
||
return in_range(sf, 5, 12);
|
||
}
|
||
}
|
||
|
||
constexpr bool valid_tx_power(int8_t power) {
|
||
if (power >= -9 && power <= 22) {
|
||
return true;
|
||
}
|
||
|
||
if (power >= -17 && power <= 14) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
constexpr bool valid_ramp_time(const uint8_t ramp_time) {
|
||
switch (ramp_time) {
|
||
case llcc68::SET_RAMP_10U:
|
||
case llcc68::SET_RAMP_20U:
|
||
case llcc68::SET_RAMP_40U:
|
||
case llcc68::SET_RAMP_80U:
|
||
case llcc68::SET_RAMP_200U:
|
||
case llcc68::SET_RAMP_800U:
|
||
case llcc68::SET_RAMP_1700U:
|
||
case llcc68::SET_RAMP_3400U:
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
constexpr bool valid_freq(const freq_t freq) {
|
||
return in_range(freq, freq_t{150.0}, freq_t{960.0});
|
||
}
|
||
|
||
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
|
||
};
|
||
|
||
enum class RfSwitchState : uint8_t {
|
||
Idle,
|
||
TX,
|
||
RX,
|
||
};
|
||
|
||
struct irq_status_t {
|
||
union {
|
||
uint16_t raw;
|
||
irq_status_bits_t bits;
|
||
};
|
||
|
||
operator uint16_t() const { return raw; }
|
||
|
||
[[nodiscard]]
|
||
uint8_t msb() const {
|
||
return static_cast<uint8_t>((raw >> 8) & 0xFF);
|
||
}
|
||
|
||
[[nodiscard]]
|
||
uint8_t lsb() const {
|
||
return static_cast<uint8_t>(raw & 0xFF);
|
||
}
|
||
|
||
static constexpr irq_status_t all() {
|
||
return irq_status_t{.raw = 0b0100001111111111};
|
||
}
|
||
[[nodiscard]]
|
||
std::string to_string() const;
|
||
};
|
||
static_assert(sizeof(irq_status_t) == 2, "irq_status_t must be 2 bytes");
|
||
|
||
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,
|
||
};
|
||
|
||
// 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 symbol 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.
|
||
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;
|
||
};
|
||
|
||
enum StartMode : uint8_t {
|
||
COLD_START = 0x00,
|
||
/**
|
||
* @brief device configuration is in retention
|
||
*/
|
||
WARM_START = 0x01,
|
||
};
|
||
|
||
struct sleep_config_t {
|
||
// LSB
|
||
|
||
/**
|
||
* @brief
|
||
* 0: RTC timeout disable
|
||
* 1: wake-up on RTC timeout
|
||
*/
|
||
bool rtc_timeout_en : 1 {false};
|
||
uint8_t _rfu_0 : 1 {};
|
||
StartMode start_mode : 1 {StartMode::COLD_START};
|
||
uint8_t _rfu_1 : 5 {};
|
||
};
|
||
static_assert(sizeof(sleep_config_t) == 1, "sleep_config_t must be 1 byte");
|
||
|
||
struct irq_params_t {
|
||
static constexpr uint16_t IRQ_MASK_NONE = 0;
|
||
// 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’.
|
||
irq_status_t irqMask{IRQ_MASK_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.
|
||
irq_status_t dio1Mask{IRQ_MASK_NONE};
|
||
irq_status_t dio2Mask{IRQ_MASK_NONE};
|
||
irq_status_t dio3Mask{IRQ_MASK_NONE};
|
||
};
|
||
|
||
struct transmit_result {
|
||
error_code post_action();
|
||
|
||
LLCC68 *self{};
|
||
airtime_t airtime_estimated;
|
||
};
|
||
} // namespace app::driver::llcc68
|
||
|
||
namespace app::radio {
|
||
namespace llcc68 = app::driver::llcc68;
|
||
}
|
||
|
||
namespace std {
|
||
template <> struct is_error_code_enum<app::driver::llcc68::Errc> : true_type {};
|
||
} // namespace std
|
||
|
||
#endif /* ECC594CF_EDF0_42B5_8518_0EB3B3583727 */
|