Compare commits

...

3 Commits

Author SHA1 Message Date
crosstyan 32a0c80ab0 fix(llcc68): align airtime estimate with radio config
Calculate LoRa airtime from the exact LDRO value supplied to SetModulationParams instead of inferring it from symbol time.

Keep transmit_result airtime in microseconds for both LoRa and GFSK, using integer Hz math and 64-bit ceil division to avoid truncation errors.

Reapply modulation parameters during async TX flush so the estimator and the bytes sent to the radio stay in sync, and add a compile-time check for the intended SF7/BW125 implicit 6-byte airtime.
2026-05-25 12:54:15 +08:00
crosstyan d8db9e1eb0 fix(llcc68): move SPI CS delay to devicetree
Zephyr deprecates passing the chip-select delay as the variadic delay argument to SPI_DT_SPEC_INST_GET. Remove that deprecated macro argument from the raw LLCC68 device initializer.

Add spi-cs-setup-delay-ns and spi-cs-hold-delay-ns defaults to the custom LLCC68 devicetree binding. Both defaults are 100000 ns, preserving the previous 100 us delay behavior while using the current Zephyr SPI devicetree properties.

Verified with cmake -S . -B build and cmake --build build from the parent application.
2026-05-25 10:12:53 +08:00
crosstyan ce56757dac feat(llcc68): make payload limit configurable
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.
2026-05-20 11:57:37 +08:00
7 changed files with 156 additions and 37 deletions
+11
View File
@@ -14,6 +14,17 @@ config LLCC68_INIT_PRIORITY
The priority of the LLCC68 initialization. Lower numbers indicate The priority of the LLCC68 initialization. Lower numbers indicate
higher priority. higher priority.
config LLCC68_MAX_PAYLOAD_LENGTH
int "LLCC68 maximum payload length"
default 128
range 1 255
help
Maximum radio payload length accepted by the LLCC68 driver.
The LLCC68 packet length fields support up to 255 bytes. Keep this
lower when the application has a known payload ceiling to reduce RAM
used by the driver's SPI staging buffers.
config LLCC68_ALWAYS_USE_SX1262_HIGH_PA config LLCC68_ALWAYS_USE_SX1262_HIGH_PA
bool "LLCC68 Always Use SX1262 High Power Amplifier" bool "LLCC68 Always Use SX1262 High Power Amplifier"
default y default y
+1 -1
View File
@@ -46,7 +46,7 @@ int llcc68_init(const struct device *dev) {
static struct llcc68_data llcc68_data_##inst; \ static struct llcc68_data llcc68_data_##inst; \
static const struct llcc68_config llcc68_config_##inst = \ static const struct llcc68_config llcc68_config_##inst = \
{ \ { \
.spi = SPI_DT_SPEC_INST_GET(inst, SPI_WORD_SET(8) | SPI_TRANSFER_MSB, 100), \ .spi = SPI_DT_SPEC_INST_GET(inst, SPI_WORD_SET(8) | SPI_TRANSFER_MSB), \
.reset_gpio = GPIO_DT_SPEC_INST_GET(inst, reset_gpios), \ .reset_gpio = GPIO_DT_SPEC_INST_GET(inst, reset_gpios), \
.busy_gpio = GPIO_DT_SPEC_INST_GET(inst, busy_gpios), \ .busy_gpio = GPIO_DT_SPEC_INST_GET(inst, busy_gpios), \
.dio1_gpio = GPIO_DT_SPEC_INST_GET(inst, dio1_gpios), \ .dio1_gpio = GPIO_DT_SPEC_INST_GET(inst, dio1_gpios), \
+6
View File
@@ -47,3 +47,9 @@ properties:
description: | description: |
Antenna switch RX enable GPIO. If set, the driver tracks the Antenna switch RX enable GPIO. If set, the driver tracks the
state of the radio and controls the RF switch. state of the radio and controls the RF switch.
spi-cs-setup-delay-ns:
default: 100000
spi-cs-hold-delay-ns:
default: 100000
+3 -2
View File
@@ -11,7 +11,7 @@
#include <zephyr/drivers/gpio.h> #include <zephyr/drivers/gpio.h>
namespace app::driver::llcc68 { namespace app::driver::llcc68 {
constexpr size_t MAX_BUFFER_PAYLOAD = 128; constexpr size_t MAX_BUFFER_PAYLOAD = CONFIG_LLCC68_MAX_PAYLOAD_LENGTH;
constexpr uint32_t TIMEOUT_NONE = 0; constexpr uint32_t TIMEOUT_NONE = 0;
constexpr uint32_t TIMEOUT_INF = 0xffffffff; constexpr uint32_t TIMEOUT_INF = 0xffffffff;
@@ -41,12 +41,13 @@ constexpr auto DEFAULT_BUSY_TIMEOUT_MS = 100;
* @param preamble_length Preamble length in symbols * @param preamble_length Preamble length in symbols
* @param header_type Header type (implicit or explicit). * @param header_type Header type (implicit or explicit).
* @param crc_type CRC type (none or 16-bit) * @param crc_type CRC type (none or 16-bit)
* @param ldro_on Low data rate optimization setting sent to the radio
*/ */
constexpr airtime_t calc_time_on_air(uint8_t len, uint8_t sf, LoRaBandwidth bw, constexpr airtime_t calc_time_on_air(uint8_t len, uint8_t sf, LoRaBandwidth bw,
LoRaCodingRate cr, LoRaCodingRate cr,
uint16_t preamble_length, uint16_t preamble_length,
LoRaHeaderType header_type, LoRaHeaderType header_type,
LoRaCrcType crc_type); LoRaCrcType crc_type, bool ldro_on);
struct LLCC68 { struct LLCC68 {
/** trivial getter */ /** trivial getter */
+52 -3
View File
@@ -14,7 +14,7 @@
namespace app::driver::llcc68 { namespace app::driver::llcc68 {
struct LLCC68; struct LLCC68;
using airtime_t = std::chrono::duration<uint32_t, std::micro>; using airtime_t = std::chrono::microseconds;
using error_code = std::error_code; using error_code = std::error_code;
enum class Errc : uint8_t { enum class Errc : uint8_t {
@@ -376,6 +376,55 @@ enum class GfskRxBandwidth : uint8_t {
Bw467000 = 0x09, 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 { enum class GfskPreambleDetector : uint8_t {
Off = 0x00, Off = 0x00,
Bits8 = 0x04, Bits8 = 0x04,
@@ -478,7 +527,7 @@ struct gfsk_parameters_t {
} }
static constexpr gfsk_parameters_t static constexpr gfsk_parameters_t
Gmsk100kFixed6(freq_t frequency_mhz = 434.18, Gfsk100kFixed6(freq_t frequency_mhz = 434.18,
tx_params_t tx_params = tx_params_t::Default()) { tx_params_t tx_params = tx_params_t::Default()) {
return { return {
.mod_params = .mod_params =
@@ -1048,7 +1097,7 @@ struct transmit_result {
error_code post_action(); error_code post_action();
LLCC68 *self{}; LLCC68 *self{};
std::chrono::milliseconds airtime_estimated; airtime_t airtime_estimated;
}; };
} // namespace app::driver::llcc68 } // namespace app::driver::llcc68
+1 -1
View File
@@ -6,7 +6,7 @@
#include <zephyr/types.h> #include <zephyr/types.h>
#include <stddef.h> #include <stddef.h>
#define LLCC68_MAX_BUFFER_PAYLOAD 128 #define LLCC68_MAX_BUFFER_PAYLOAD CONFIG_LLCC68_MAX_PAYLOAD_LENGTH
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
+79 -27
View File
@@ -111,6 +111,39 @@ namespace {
// Max payload the radio supports and buffer sizing helpers // Max payload the radio supports and buffer sizing helpers
constexpr size_t kMaxPayload = 32; constexpr size_t kMaxPayload = 32;
constexpr uint64_t ceil_div_u64(uint64_t numerator, uint64_t denominator) {
if (denominator == 0) {
return 0;
}
return numerator / denominator + (numerator % denominator == 0 ? 0 : 1);
}
constexpr uint32_t lora_bw_hz(LoRaBandwidth bw) {
switch (bw) {
case LoRaBandwidth::BW_7_8:
return 7'810;
case LoRaBandwidth::BW_10_4:
return 10'420;
case LoRaBandwidth::BW_15_6:
return 15'630;
case LoRaBandwidth::BW_20_8:
return 20'830;
case LoRaBandwidth::BW_31_25:
return 31'250;
case LoRaBandwidth::BW_41_7:
return 41'670;
case LoRaBandwidth::BW_62_5:
return 62'500;
case LoRaBandwidth::BW_125_0:
return 125'000;
case LoRaBandwidth::BW_250_0:
return 250'000;
case LoRaBandwidth::BW_500_0:
return 500'000;
}
return 0;
}
error_code command_status_to_error(status_t st) { error_code command_status_to_error(status_t st) {
switch (st.command_status) { switch (st.command_status) {
case CommandStatus::FAILURE_TO_EXECUTE_COMMAND: case CommandStatus::FAILURE_TO_EXECUTE_COMMAND:
@@ -163,47 +196,57 @@ constexpr airtime_t calc_time_on_air(uint8_t len, uint8_t sf, LoRaBandwidth bw,
LoRaCodingRate cr, LoRaCodingRate cr,
uint16_t preamble_length, uint16_t preamble_length,
LoRaHeaderType header_type, LoRaHeaderType header_type,
LoRaCrcType crc_type) { LoRaCrcType crc_type, bool ldro_on) {
// everything is in microseconds to allow integer arithmetic // Everything is in microseconds to allow integer arithmetic. Some constants
// some constants have .25, these are multiplied by 4, and have _x4 postfix to // have .25, these are multiplied by 4 and have an _x4 postfix.
// indicate that fact const uint32_t bw_hz = lora_bw_hz(bw);
const auto bw_ = bw_khz(bw); if (bw_hz == 0) {
const auto ubw = static_cast<uint32_t>(bw_ * 10); return airtime_t{0};
const uint32_t symbolLength_us = }
(static_cast<uint32_t>(1000 * 10) << sf) / ubw;
uint8_t sfCoeff1_x4 = 17; // (4.25 * 4) uint8_t sfCoeff1_x4 = 17; // (4.25 * 4)
uint8_t sfCoeff2 = 8; uint8_t sfCoeff2 = 8;
if (sf == 5 || sf == 6) { if (sf == 5 || sf == 6) {
sfCoeff1_x4 = 25; // 6.25 * 4 sfCoeff1_x4 = 25; // 6.25 * 4
sfCoeff2 = 0; sfCoeff2 = 0;
} }
uint8_t sfDivisor = 4 * sf; const uint8_t ldro_sf_offset = ldro_on ? 2 : 0;
if (symbolLength_us >= 16000) { if (sf <= ldro_sf_offset) {
sfDivisor = 4 * (sf - 2); return airtime_t{0};
} }
constexpr int8_t bitsPerCrc = 16; const uint8_t sfDivisor = 4 * (sf - ldro_sf_offset);
int8_t N_symbol_header = const int16_t header_bits =
header_type == RADIOLIB_SX126X_LORA_HEADER_EXPLICIT ? 20 : 0; header_type == LoRaHeaderType::HEADER_EXPLICIT ? 20 : 0;
// numerator of equation in section 6.1.4 of SX1268 datasheet v1.1 (might // numerator of equation in section 6.1.4 of SX1268 datasheet v1.1 (might
// not actually be bitcount, but it has len * 8) // not actually be bitcount, but it has len * 8)
auto bit_count = static_cast<int16_t>( auto bit_count = static_cast<int16_t>(
static_cast<int16_t>(8) * static_cast<int16_t>(len) + static_cast<int16_t>(8) * static_cast<int16_t>(len) +
static_cast<int16_t>(crc_type * bitsPerCrc) - static_cast<int16_t>(crc_type == LoRaCrcType::CRC_ON ? 16 : 0) -
static_cast<int16_t>(4 * sf) + static_cast<int16_t>(sfCoeff2) + static_cast<int16_t>(4 * sf) + static_cast<int16_t>(sfCoeff2) +
static_cast<int16_t>(N_symbol_header)); header_bits);
bit_count = std::max<int16_t>(bit_count, 0); bit_count = std::max<int16_t>(bit_count, 0);
// add (sfDivisor) - 1 to the numerator to give integer CEIL(...) // add (sfDivisor) - 1 to the numerator to give integer CEIL(...)
const uint16_t nPreCodedSymbols = (bit_count + (sfDivisor - 1)) / (sfDivisor); const uint16_t nPreCodedSymbols = (bit_count + (sfDivisor - 1)) / (sfDivisor);
const auto de = std::get<1>(cr_to_ratio(cr)); const auto cr_denominator = std::get<1>(cr_to_ratio(cr));
// preamble can be 65k, therefore nSymbol_x4 needs to be 32 bit // preamble can be 65k, therefore nSymbol_x4 needs to be 32 bit
const uint32_t nSymbol_x4 = const uint32_t nSymbol_x4 =
(preamble_length + 8) * 4 + sfCoeff1_x4 + nPreCodedSymbols * de * 4; (preamble_length + 8) * 4 + sfCoeff1_x4 +
nPreCodedSymbols * cr_denominator * 4;
return airtime_t{symbolLength_us * nSymbol_x4 / 4}; const uint64_t time_us = ceil_div_u64(
(static_cast<uint64_t>(1'000'000) << sf) * nSymbol_x4,
static_cast<uint64_t>(bw_hz) * 4U);
return airtime_t{static_cast<airtime_t::rep>(time_us)};
} }
static_assert(calc_time_on_air(6, 7, LoRaBandwidth::BW_125_0,
LoRaCodingRate::CR_4_5, 8,
LoRaHeaderType::HEADER_IMPLICIT,
LoRaCrcType::CRC_OFF, false) ==
airtime_t{25'856});
// SPI helpers implementing the LLCC68 wire protocol // SPI helpers implementing the LLCC68 wire protocol
expected<unit, error_code> LLCC68::read_stream(uint8_t cmd, expected<unit, error_code> LLCC68::read_stream(uint8_t cmd,
std::span<uint8_t> data, std::span<uint8_t> data,
@@ -672,7 +715,7 @@ static constexpr uint8_t gfsk_crc_len_bytes(GfskCrcType crc_type) {
} }
} }
static constexpr std::chrono::milliseconds static constexpr airtime_t
calc_gfsk_time_on_air(gfsk_parameters_t params, uint8_t payload_len) { calc_gfsk_time_on_air(gfsk_parameters_t params, uint8_t payload_len) {
const auto address_bits = const auto address_bits =
params.packet_params.address_filtering == GfskAddressFiltering::Disabled params.packet_params.address_filtering == GfskAddressFiltering::Disabled
@@ -688,9 +731,12 @@ calc_gfsk_time_on_air(gfsk_parameters_t params, uint8_t payload_len) {
(static_cast<uint32_t>(payload_len) + (static_cast<uint32_t>(payload_len) +
gfsk_crc_len_bytes(params.packet_params.crc_type)) * gfsk_crc_len_bytes(params.packet_params.crc_type)) *
8U; 8U;
const uint32_t ms = (bits * 1000U + params.mod_params.bitrate_bps - 1U) / if (params.mod_params.bitrate_bps == 0) {
params.mod_params.bitrate_bps; return airtime_t{0};
return std::chrono::milliseconds{ms}; }
const uint64_t us = ceil_div_u64(static_cast<uint64_t>(bits) * 1'000'000U,
params.mod_params.bitrate_bps);
return airtime_t{static_cast<airtime_t::rep>(us)};
} }
static expected<unit, error_code> static expected<unit, error_code>
@@ -1250,6 +1296,11 @@ LLCC68::hal_async_flush(lora_parameters_t params) {
return ue(Errc::InvalidState); return ue(Errc::InvalidState);
} }
APP_RADIO_RETURN_ERR_CTX(set_standby(), "tx::standby"); APP_RADIO_RETURN_ERR_CTX(set_standby(), "tx::standby");
APP_RADIO_RETURN_ERR_CTX(
set_modulation_params(params.mod_params.sf, params.mod_params.bw,
params.mod_params.cr,
params.mod_params.ldr_optimize),
"tx::set_modulation_params");
APP_RADIO_RETURN_ERR_CTX(set_packet_params(params.packet_params.preamble_len, APP_RADIO_RETURN_ERR_CTX(set_packet_params(params.packet_params.preamble_len,
data().tx_xfer_size, data().tx_xfer_size,
params.packet_params.crc_type, params.packet_params.crc_type,
@@ -1278,10 +1329,9 @@ LLCC68::hal_async_flush(lora_parameters_t params) {
auto air = calc_time_on_air( auto air = calc_time_on_air(
data().tx_xfer_size, params.mod_params.sf, params.mod_params.bw, data().tx_xfer_size, params.mod_params.sf, params.mod_params.bw,
params.mod_params.cr, params.packet_params.preamble_len, params.mod_params.cr, params.packet_params.preamble_len,
params.packet_params.hdr_type, params.packet_params.crc_type); params.packet_params.hdr_type, params.packet_params.crc_type,
auto air_estimated = params.mod_params.ldr_optimize == LoRaLowDataRateType::LDR_ON);
std::chrono::duration_cast<std::chrono::milliseconds>(air); return transmit_result{this, air};
return transmit_result{this, air_estimated};
} }
expected<transmit_result, error_code> expected<transmit_result, error_code>
@@ -1304,6 +1354,8 @@ LLCC68::hal_gfsk_async_flush(gfsk_parameters_t params) {
} }
APP_RADIO_RETURN_ERR_CTX(set_standby(), "gfsk_tx::standby"); APP_RADIO_RETURN_ERR_CTX(set_standby(), "gfsk_tx::standby");
APP_RADIO_RETURN_ERR_CTX(set_gfsk_modulation_params(params.mod_params),
"gfsk_tx::set_modulation_params");
auto packet_params = params.packet_params; auto packet_params = params.packet_params;
packet_params.payload_length = static_cast<uint8_t>(data().tx_xfer_size); packet_params.payload_length = static_cast<uint8_t>(data().tx_xfer_size);
APP_RADIO_RETURN_ERR_CTX(set_gfsk_packet_params(packet_params), APP_RADIO_RETURN_ERR_CTX(set_gfsk_packet_params(packet_params),