diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index eb92c3e..40528bd 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -940,32 +940,40 @@ uint32_t LoRaRadioBase::getEstAirtimeFor(int len_bytes) * drifts with it — reported RX airtime, the TX airtime and duty-cycle * budget that now derive from it, and outbound_expiry. Upstream cannot * drift this way because it asks the radio (getTimeOnAir()); these - * accessors are our equivalent. bw is read directly rather than via - * getActiveBandwidthKHzX10(), whose fixed-point rounding would cost - * precision at 31.25 kHz. */ + * accessors are our equivalent. Resolve bandwidth through the same + * enum as buildModemConfig(): a saved 62 or 62.5 both program 62.5 kHz, + * and a rounded setting must not change the airtime calculation. */ uint8_t sf = getActiveSpreadingFactor(); float bw = _has_radio_override ? _override_bw : (_prefs ? _prefs->bw : (float)LoRaConfig::BANDWIDTH); uint8_t cr_val = getActiveCodingRate(); - if (sf < 6) sf = 6; + uint8_t min_sf = _loramac_node ? 6 : 5; + if (sf < min_sf) sf = min_sf; if (sf > 12) sf = 12; if (bw < 7.0f) bw = 125.0f; + bw = bandwidth_to_hz(bw_khz_to_enum((uint16_t)bw)) / 1000.0f; if (cr_val < 5) cr_val = 5; if (cr_val > 8) cr_val = 8; float t_sym = (float)(1 << sf) / (bw * 1000.0f); - float t_preamble = (preambleLengthForSF(sf) + 4.25f) * t_sym; + /* SX126x/LR11xx/LR20xx add two synchronization symbols at SF5/6 + * and carry eight more header bits there. SX127x uses the older + * format. See Semtech's lr11xx_radio_get_lora_time_on_air_numerator. + * Treating SF5 as SF6 charged 397 ms for a 126-byte SF5/BW62.5 CR4/8 + * packet whose Semtech airtime is 237 ms (confirmed by TX timing). */ + float fine_sync = (!_loramac_node && sf <= 6) ? 1.0f : 0.0f; + float t_preamble = (preambleLengthForSF(sf) + 4.25f + 2.0f * fine_sync) * t_sym; /* LDRO threshold must track the SX126x driver's should_enable_ldro() * exactly (symbol time > 16.38 ms) so this estimate's DE matches the * hardware's DE on every SF/BW pair. The old `sf >= 11` was only * correct at BW 125 kHz and diverged on every other bandwidth. */ float de = (t_sym > 0.01638f) ? 1.0f : 0.0f; - float num = 8.0f * len_bytes - 4.0f * sf + 28.0f + 16.0f; + float num = 8.0f * len_bytes - 4.0f * sf + 28.0f + 16.0f - 8.0f * fine_sync; float den = 4.0f * (sf - 2.0f * de); if (den < 1.0f) den = 4.0f; - float n_payload = 8.0f + fmaxf(ceilf(num / den) * (cr_val - 4 + 4), 0.0f); + float n_payload = 8.0f + fmaxf(ceilf(num / den) * cr_val, 0.0f); float t_payload = n_payload * t_sym; return (uint32_t)((t_preamble + t_payload) * 1000.0f); diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c index cb190cf..fb43d10 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -283,19 +283,7 @@ static lr11xx_system_tcxo_supply_voltage_t get_tcxo_voltage(uint16_t mv) /* Get kHz value from Zephyr BW enum — needed for duty cycle timing */ static float bw_enum_to_khz(enum lora_signal_bandwidth bw) { - switch (bw) { - case BW_7_KHZ: return 7.81f; - case BW_10_KHZ: return 10.42f; - case BW_15_KHZ: return 15.63f; - case BW_20_KHZ: return 20.83f; - case BW_31_KHZ: return 31.25f; - case BW_41_KHZ: return 41.67f; - case BW_62_KHZ: return 62.5f; - case BW_125_KHZ: return 125.0f; - case BW_250_KHZ: return 250.0f; - case BW_500_KHZ: return 500.0f; - default: return 125.0f; - } + return lr11xx_radio_get_lora_bw_in_hz(bw_enum_to_lr11xx(bw)) / 1000.0f; } /* ── Hardware reset (BUSY stuck recovery) ───────────────────────────── */ @@ -352,6 +340,14 @@ static void lr11xx_hardware_reset(struct lr11xx_data *data, /* ── Apply modem configuration ──────────────────────────────────────── */ +/* Share the LDRO decision between the hardware and its airtime estimate. */ +static uint8_t lr11xx_ldro(const struct lora_modem_config *mc) +{ + uint32_t bw_hz = lr11xx_radio_get_lora_bw_in_hz(bw_enum_to_lr11xx(mc->bandwidth)); + uint32_t symbol_time_us = ((1U << (uint8_t)mc->datarate) * 1000000U) / bw_hz; + return symbol_time_us > 16380; +} + static void lr11xx_apply_modem_config(struct lr11xx_data *data, const struct lr11xx_config *cfg, bool tx_mode) @@ -361,16 +357,11 @@ static void lr11xx_apply_modem_config(struct lr11xx_data *data, lr11xx_radio_set_rf_freq(ctx, mc->frequency); - /* LDRO must be enabled when symbol time > 16.38ms (SF11+/BW125 etc) */ - uint32_t bw_hz = (uint32_t)(bw_enum_to_khz(mc->bandwidth) * 1000.0f); - uint32_t symbol_time_us = ((1U << (uint8_t)mc->datarate) * 1000000U) / bw_hz; - uint8_t ldro = (symbol_time_us > 16380) ? 1 : 0; - lr11xx_radio_mod_params_lora_t mod = { .sf = (lr11xx_radio_lora_sf_t)mc->datarate, .bw = bw_enum_to_lr11xx(mc->bandwidth), .cr = cr_enum_to_lr11xx(mc->coding_rate), - .ldro = ldro, + .ldro = lr11xx_ldro(mc), }; lr11xx_radio_set_lora_mod_params(ctx, &mod); @@ -1088,20 +1079,24 @@ static uint32_t lr11xx_lora_airtime(const struct device *dev, struct lr11xx_data *data = dev->data; struct lora_modem_config *mc = &data->modem_cfg; - uint8_t sf = (uint8_t)mc->datarate; - float bw = bw_enum_to_khz(mc->bandwidth) * 1000.0f; - uint8_t cr = (uint8_t)mc->coding_rate + 4; - - float ts = (float)(1 << sf) / bw; - int de = (sf >= 11 && bw <= 125000.0f) ? 1 : 0; - float n_payload = 8.0f + fmaxf( - ceilf((8.0f * data_len - 4.0f * sf + 28.0f + 16.0f) / - (4.0f * (sf - 2.0f * de))) * cr, - 0.0f); - float t_preamble = (mc->preamble_len + 4.25f) * ts; - float t_payload = n_payload * ts; - - return (uint32_t)((t_preamble + t_payload) * 1000.0f); + /* Use Semtech's calculation, including SF5/6 synchronization and the + * actual LDRO/CRC configuration. The old SF>=11 && BW<=125 shortcut + * underestimated SF10/BW62.5 and SF12/BW250, including the TX watchdog + * budgets derived from this API. */ + lr11xx_radio_mod_params_lora_t mod = { + .sf = (lr11xx_radio_lora_sf_t)mc->datarate, + .bw = bw_enum_to_lr11xx(mc->bandwidth), + .cr = cr_enum_to_lr11xx(mc->coding_rate), + .ldro = lr11xx_ldro(mc), + }; + lr11xx_radio_pkt_params_lora_t pkt = { + .preamble_len_in_symb = mc->preamble_len, + .header_type = LR11XX_RADIO_LORA_PKT_EXPLICIT, + .pld_len_in_bytes = (uint8_t)data_len, + .crc = mc->packet_crc_disable ? LR11XX_RADIO_LORA_CRC_OFF + : LR11XX_RADIO_LORA_CRC_ON, + }; + return lr11xx_radio_get_lora_time_on_air_in_ms(&pkt, &mod); } /* Forward declaration — needed by LBT in send_async */