From 32a0c80ab042658629648a56076ab0ba317c5571 Mon Sep 17 00:00:00 2001 From: crosstyan Date: Mon, 25 May 2026 12:54:15 +0800 Subject: [PATCH] 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. --- include/llcc68.hpp | 9 +-- include/llcc68_definitions.hpp | 4 +- src/llcc68.cpp | 106 ++++++++++++++++++++++++--------- 3 files changed, 86 insertions(+), 33 deletions(-) diff --git a/include/llcc68.hpp b/include/llcc68.hpp index 558f81f..b3cef7c 100644 --- a/include/llcc68.hpp +++ b/include/llcc68.hpp @@ -41,12 +41,13 @@ constexpr auto DEFAULT_BUSY_TIMEOUT_MS = 100; * @param preamble_length Preamble length in symbols * @param header_type Header type (implicit or explicit). * @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, - LoRaCodingRate cr, - uint16_t preamble_length, - LoRaHeaderType header_type, - LoRaCrcType crc_type); + LoRaCodingRate cr, + uint16_t preamble_length, + LoRaHeaderType header_type, + LoRaCrcType crc_type, bool ldro_on); struct LLCC68 { /** trivial getter */ diff --git a/include/llcc68_definitions.hpp b/include/llcc68_definitions.hpp index 81e4a98..2267c49 100644 --- a/include/llcc68_definitions.hpp +++ b/include/llcc68_definitions.hpp @@ -14,7 +14,7 @@ namespace app::driver::llcc68 { struct LLCC68; -using airtime_t = std::chrono::duration; +using airtime_t = std::chrono::microseconds; using error_code = std::error_code; enum class Errc : uint8_t { @@ -1097,7 +1097,7 @@ struct transmit_result { error_code post_action(); LLCC68 *self{}; - std::chrono::milliseconds airtime_estimated; + airtime_t airtime_estimated; }; } // namespace app::driver::llcc68 diff --git a/src/llcc68.cpp b/src/llcc68.cpp index 624f8c8..5ebbf9b 100644 --- a/src/llcc68.cpp +++ b/src/llcc68.cpp @@ -111,6 +111,39 @@ namespace { // Max payload the radio supports and buffer sizing helpers 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) { switch (st.command_status) { 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, uint16_t preamble_length, LoRaHeaderType header_type, - LoRaCrcType crc_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_ = bw_khz(bw); - const auto ubw = static_cast(bw_ * 10); - const uint32_t symbolLength_us = - (static_cast(1000 * 10) << sf) / ubw; + LoRaCrcType crc_type, bool ldro_on) { + // Everything is in microseconds to allow integer arithmetic. Some constants + // have .25, these are multiplied by 4 and have an _x4 postfix. + const uint32_t bw_hz = lora_bw_hz(bw); + if (bw_hz == 0) { + return airtime_t{0}; + } + 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); + const uint8_t ldro_sf_offset = ldro_on ? 2 : 0; + if (sf <= ldro_sf_offset) { + return airtime_t{0}; } - constexpr int8_t bitsPerCrc = 16; - int8_t N_symbol_header = - header_type == RADIOLIB_SX126X_LORA_HEADER_EXPLICIT ? 20 : 0; + const uint8_t sfDivisor = 4 * (sf - ldro_sf_offset); + const int16_t header_bits = + header_type == LoRaHeaderType::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) auto bit_count = static_cast( static_cast(8) * static_cast(len) + - static_cast(crc_type * bitsPerCrc) - + static_cast(crc_type == LoRaCrcType::CRC_ON ? 16 : 0) - static_cast(4 * sf) + static_cast(sfCoeff2) + - static_cast(N_symbol_header)); + header_bits); bit_count = std::max(bit_count, 0); // add (sfDivisor) - 1 to the numerator to give integer CEIL(...) 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 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(1'000'000) << sf) * nSymbol_x4, + static_cast(bw_hz) * 4U); + return airtime_t{static_cast(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 expected LLCC68::read_stream(uint8_t cmd, std::span 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) { const auto address_bits = 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(payload_len) + gfsk_crc_len_bytes(params.packet_params.crc_type)) * 8U; - const uint32_t ms = (bits * 1000U + params.mod_params.bitrate_bps - 1U) / - params.mod_params.bitrate_bps; - return std::chrono::milliseconds{ms}; + if (params.mod_params.bitrate_bps == 0) { + return airtime_t{0}; + } + const uint64_t us = ceil_div_u64(static_cast(bits) * 1'000'000U, + params.mod_params.bitrate_bps); + return airtime_t{static_cast(us)}; } static expected @@ -1250,6 +1296,11 @@ LLCC68::hal_async_flush(lora_parameters_t params) { return ue(Errc::InvalidState); } 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, data().tx_xfer_size, params.packet_params.crc_type, @@ -1278,10 +1329,9 @@ LLCC68::hal_async_flush(lora_parameters_t params) { auto air = calc_time_on_air( data().tx_xfer_size, params.mod_params.sf, params.mod_params.bw, params.mod_params.cr, params.packet_params.preamble_len, - params.packet_params.hdr_type, params.packet_params.crc_type); - auto air_estimated = - std::chrono::duration_cast(air); - return transmit_result{this, air_estimated}; + params.packet_params.hdr_type, params.packet_params.crc_type, + params.mod_params.ldr_optimize == LoRaLowDataRateType::LDR_ON); + return transmit_result{this, air}; } expected @@ -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_gfsk_modulation_params(params.mod_params), + "gfsk_tx::set_modulation_params"); auto packet_params = params.packet_params; packet_params.payload_length = static_cast(data().tx_xfer_size); APP_RADIO_RETURN_ERR_CTX(set_gfsk_packet_params(packet_params),