ce56757dac
Add CONFIG_LLCC68_MAX_PAYLOAD_LENGTH with a default of 128 bytes and a hardware-bounded range of 1..255. Wire both the C++ and raw C LLCC68 payload buffer constants to the Kconfig value so application builds can tune radio buffer RAM without editing headers. Rename the fixed-length 100k preset from GMSK to GFSK and expose rx_bandwidth_hz() next to the GfskRxBandwidth enum so applications can report configured bandwidth without carrying driver-specific lookup tables.
1113 lines
26 KiB
C++
1113 lines
26 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::duration<uint32_t, std::micro>;
|
||
using error_code = std::error_code;
|
||
|
||
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 TxRxPinState : uint8_t {
|
||
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{};
|
||
std::chrono::milliseconds 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 */
|