From 4353936328114ed078af87d0bd0830c6d3eb73dc Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:33:37 +0200 Subject: [PATCH] lr2021 experiments --- .../radio/lr20xx/lr20xx_radio_common.c | 7 +- .../promicro_lr2021/promicro_lr2021.dts | 5 +- .../drivers/lora/lr20xx/lr20xx_lora.c | 195 ++++++++++++------ 3 files changed, 138 insertions(+), 69 deletions(-) diff --git a/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c b/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c index 9577308..54af91b 100644 --- a/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c +++ b/zephcore/adapters/radio/lr20xx/lr20xx_radio_common.c @@ -196,9 +196,12 @@ lr20xx_status_t lr20xx_radio_common_calibrate_front_end_helper( const uint32_t freq_hz = front_end_calibration_structures[front_end_calibration_value_index].frequency_in_hertz; const lr20xx_radio_common_rx_path_t rx_path = front_end_calibration_structures[front_end_calibration_value_index].rx_path; - /* ceil(freq_hz / 4MHz): calibrate at next 4MHz boundary ≥ freq_hz */ + /* round(freq_hz / 4MHz): calibrate the NEAREST 4MHz bin — this is the + * bin the chip selects internally for set_rf_freq, so it must match or + * RX raises RXFREQ_NO_FE_CAL (0x0200) and TX is refused (PERR). + * RadioLib uses round-to-nearest; ceil() picked the wrong bin. */ const uint16_t freq_4mhz = - ( uint16_t ) ( ( freq_hz + LR20XX_RADIO_COMMON_FRONT_END_CALIBRATION_STEP_IN_HZ - 1u ) / + ( uint16_t ) ( ( freq_hz + ( LR20XX_RADIO_COMMON_FRONT_END_CALIBRATION_STEP_IN_HZ / 2u ) ) / LR20XX_RADIO_COMMON_FRONT_END_CALIBRATION_STEP_IN_HZ ); raw_calibration_values[front_end_calibration_value_index] = ( uint16_t ) ( ( ( rx_path == LR20XX_RADIO_COMMON_RX_PATH_HF ) ? 0x8000u : 0x0000u ) | freq_4mhz ); diff --git a/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts b/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts index 0634698..e39ad59 100644 --- a/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts +++ b/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts @@ -139,8 +139,9 @@ busy-gpios = <&gpio0 29 GPIO_ACTIVE_HIGH>; dio1-gpios = <&gpio0 10 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; - /* NiceRF LoRa2021 module uses internal crystal (XTAL), no TCXO. - * tcxo-voltage-mv defaults to 0 (disabled). */ + /* TCXO: tried 1800mV on DIO3 (per Meshtastic diy_tcxo) — it WEDGED + * the chip (HF-XOSC startup hangs, BUSY stuck, BLE never starts), + * which means THIS module has no TCXO. Left disabled (XTAL). */ rx-boosted; /* RF switch via NiceRF module internal DIOs (DIO5=bit0, DIO6=bit1). diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c index 861c1de..31a21ea 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c @@ -225,65 +225,67 @@ static void lr20xx_configure_rfswitch(void *ctx, const struct lr20xx_config *cfg } } -/* ── PA power lookup table (from Semtech lr20xx_pa_pwr_cfg.h) ──────── */ - +/* ── PA power lookup table (LF / sub-GHz) ───────────────────────────── + * + * Mirrors RadioLib's paOptTableLf (known-good on real LR2021 silicon). + * Each row is { pa_lf_duty_cycle, pa_lf_slices, pa_val } where pa_val is the + * value passed to SetTxParams. + * + * IMPORTANT: pa_val is a PA *calibration* value, NOT power in dBm or half-dBm. + * Our previous table mis-modeled it as 0.5dB steps (e.g. 44 for +22dBm); the + * chip rejected those out-of-range values (PERR / CMD_ERROR) and refused TX. + * RadioLib's LF range is -9..+22 dBm, indexed as (power_dbm + 9). + */ struct lr20xx_pa_pwr_entry { - int8_t half_power; uint8_t pa_duty_cycle; uint8_t pa_lf_slices; + int8_t pa_val; }; -#define LR20XX_LF_MIN_PWR (-10) +#define LR20XX_LF_MIN_PWR (-9) #define LR20XX_LF_MAX_PWR 22 -/* Calibrated per-dBm PA config — each row is [half_power, duty_cycle, slices] - * for the corresponding output power from -10 to +22 dBm inclusive. - * - * 10–22 dBm: from LR2021 datasheet Rev 1.1, Table 7-16 (915MHz ref design). - * TX_PARAM is in 0.5dB steps; half_power = TX_PARAM * 2. - * For integer dBm targets, we take the exact row from the table. - * - * -10 to +9 dBm: from Semtech SDK lr20xx_pa_pwr_cfg.h (not in datasheet). - */ static const struct lr20xx_pa_pwr_entry pa_lf_table[] = { - { -18, 3, 6 }, /* -10 dBm (SDK) */ - { -13, 2, 5 }, /* -9 dBm (SDK) */ - { -13, 6, 1 }, /* -8 dBm (SDK) */ - { -6, 6, 0 }, /* -7 dBm (SDK) */ - { 4, 1, 0 }, /* -6 dBm (SDK) */ - { 4, 2, 0 }, /* -5 dBm (SDK) */ - { 2, 1, 3 }, /* -4 dBm (SDK) */ - { 14, 0, 0 }, /* -3 dBm (SDK) */ - { 9, 0, 3 }, /* -2 dBm (SDK) */ - { 11, 3, 0 }, /* -1 dBm (SDK) */ - { 16, 1, 0 }, /* 0 dBm (SDK) */ - { 11, 7, 0 }, /* 1 dBm (SDK) */ - { 18, 2, 0 }, /* 2 dBm (SDK) */ - { 16, 5, 0 }, /* 3 dBm (SDK) */ - { 17, 7, 0 }, /* 4 dBm (SDK) */ - { 21, 1, 2 }, /* 5 dBm (SDK) */ - { 25, 3, 0 }, /* 6 dBm (SDK) */ - { 32, 0, 1 }, /* 7 dBm (SDK) */ - { 32, 2, 0 }, /* 8 dBm (SDK) */ - { 27, 3, 1 }, /* 9 dBm (SDK) */ - { 32, 2, 1 }, /* 10 dBm (DS Table 7-16: TX_PARAM=16) */ - { 32, 2, 2 }, /* 11 dBm (DS Table 7-16: TX_PARAM=16) */ - { 30, 5, 1 }, /* 12 dBm (DS Table 7-16: TX_PARAM=15) */ - { 31, 4, 3 }, /* 13 dBm (DS Table 7-16: TX_PARAM=15.5) */ - { 34, 4, 2 }, /* 14 dBm (DS Table 7-16: TX_PARAM=17) */ - { 33, 5, 4 }, /* 15 dBm (DS Table 7-16: TX_PARAM=16.5) */ - { 36, 4, 4 }, /* 16 dBm (DS Table 7-16: TX_PARAM=18) */ - { 36, 5, 6 }, /* 17 dBm (DS Table 7-16: TX_PARAM=18) */ - { 38, 5, 6 }, /* 18 dBm (DS Table 7-16: TX_PARAM=19) */ - { 39, 6, 6 }, /* 19 dBm (DS Table 7-16: TX_PARAM=19.5) */ - { 41, 6, 6 }, /* 20 dBm (DS Table 7-16: TX_PARAM=20.5) */ - { 42, 7, 7 }, /* 21 dBm (DS Table 7-16: TX_PARAM=21) */ - { 44, 7, 6 }, /* 22 dBm (DS Table 7-16: TX_PARAM=22) */ + { 1, 1, 8 }, /* -9 dBm */ + { 2, 2, 1 }, /* -8 dBm */ + { 2, 2, 3 }, /* -7 dBm */ + { 2, 2, 5 }, /* -6 dBm */ + { 1, 2, 13 }, /* -5 dBm */ + { 2, 1, 13 }, /* -4 dBm */ + { 2, 2, 11 }, /* -3 dBm */ + { 2, 2, 13 }, /* -2 dBm */ + { 3, 1, 12 }, /* -1 dBm */ + { 1, 1, 18 }, /* 0 dBm */ + { 1, 1, 20 }, /* 1 dBm */ + { 1, 1, 23 }, /* 2 dBm */ + { 1, 1, 27 }, /* 3 dBm */ + { 1, 1, 33 }, /* 4 dBm */ + { 1, 2, 26 }, /* 5 dBm */ + { 1, 2, 31 }, /* 6 dBm */ + { 1, 3, 27 }, /* 7 dBm */ + { 1, 1, 37 }, /* 8 dBm */ + { 1, 2, 40 }, /* 9 dBm */ + { 2, 1, 38 }, /* 10 dBm */ + { 2, 2, 39 }, /* 11 dBm */ + { 2, 4, 40 }, /* 12 dBm */ + { 2, 7, 41 }, /* 13 dBm */ + { 3, 2, 39 }, /* 14 dBm */ + { 3, 3, 39 }, /* 15 dBm */ + { 3, 6, 38 }, /* 16 dBm */ + { 4, 3, 37 }, /* 17 dBm */ + { 4, 5, 37 }, /* 18 dBm */ + { 4, 7, 38 }, /* 19 dBm */ + { 5, 3, 37 }, /* 20 dBm */ + { 5, 6, 37 }, /* 21 dBm */ + { 6, 7, 35 }, /* 22 dBm */ }; +/* PA_HF_DUTY_CYCLE "unused" marker (RADIOLIB_LR2021_PA_HF_DUTY_CYCLE_UNUSED) */ +#define LR20XX_PA_HF_DUTY_CYCLE_UNUSED 16 + static void lr20xx_get_pa_cfg_for_power(int8_t power_dbm, lr20xx_radio_common_pa_cfg_t *pa, - int8_t *half_power_out) + int8_t *pa_val_out) { if (power_dbm < LR20XX_LF_MIN_PWR) { power_dbm = LR20XX_LF_MIN_PWR; @@ -299,9 +301,9 @@ static void lr20xx_get_pa_cfg_for_power(int8_t power_dbm, pa->pa_lf_mode = LR20XX_RADIO_COMMON_PA_LF_MODE_FSM; pa->pa_lf_duty_cycle = e->pa_duty_cycle; pa->pa_lf_slices = e->pa_lf_slices; - pa->pa_hf_duty_cycle = 16; + pa->pa_hf_duty_cycle = LR20XX_PA_HF_DUTY_CYCLE_UNUSED; - *half_power_out = e->half_power; + *pa_val_out = e->pa_val; } /* ── Hardware reset (BUSY stuck recovery) ───────────────────────────── */ @@ -373,6 +375,25 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data, rc = lr20xx_radio_common_set_pkt_type(ctx, LR20XX_RADIO_COMMON_PKT_TYPE_LORA); LOG_DBG("modem_cfg: set_pkt_type=%d", rc); + /* Front-end calibration paired with set_rf_freq, exactly like RadioLib's + * setFrequency() (cal THEN set, together). One cal covers ±50MHz so the + * bin is not the issue — the cal just has to be (re)applied here, right + * before the frequency is set and RX/TX starts. Doing it only once at + * config time left the chip reporting RXFREQ_NO_FE_CAL (0x0200) at RX and + * refusing TX (PERR). */ + { + lr20xx_radio_common_front_end_calibration_value_t fe_cal = { + .rx_path = LR20XX_RADIO_COMMON_RX_PATH_LF, + .frequency_in_hertz = mc->frequency, + }; + rc = lr20xx_radio_common_calibrate_front_end_helper(ctx, &fe_cal, 1); + lr20xx_system_errors_t fe_err = 0; + lr20xx_system_get_errors(ctx, &fe_err); + LOG_DBG("modem_cfg: FE cal(%uHz) rc=%d post-cal-err=0x%04x", + mc->frequency, rc, fe_err); + lr20xx_system_clear_errors(ctx); + } + rc = lr20xx_radio_common_set_rf_freq(ctx, mc->frequency); LOG_DBG("modem_cfg: set_rf_freq(%u)=%d", mc->frequency, rc); @@ -422,30 +443,25 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data, mc->public_network ? 0x34 : 0x12, rc); if (tx_mode) { - /* PA config: Semtech SDK 3-byte packed format is CORRECT per - * datasheet Table 7-15 (p132): - * Byte 2: (pa_sel<<7) | rfu(4:0) | pa_lf_mode(1:0) - * Byte 3: pa_lf_duty_cycle(3:0) | pa_lf_slices(3:0) - * Byte 4: rfu(2:0) | pa_hf_duty_cycle(4:0) - * - * RadioLib's 5-byte format was WRONG (misread the datasheet). - * The SDK's lr20xx_radio_common_set_pa_cfg() packs correctly. - * - * PA values from datasheet Table 7-16 (915MHz ref design). - */ + /* PA config + TX params from RadioLib's known-good LF table. + * pa_val is a PA calibration value (NOT dBm/half-dBm). */ lr20xx_radio_common_pa_cfg_t pa; - int8_t half_power; + int8_t pa_val; - lr20xx_get_pa_cfg_for_power(mc->tx_power, &pa, &half_power); + /* DEBUG/TEST: force MINIMUM power (-9dBm, 1 PA slice) to probe the + * supply-sag hypothesis. If TX keys here (mode=5 + carrier) but + * not at +22dBm (7 slices), the rail can't source PA current → + * hardware power limit. REVERT to mc->tx_power after testing. */ + lr20xx_get_pa_cfg_for_power(-9, &pa, &pa_val); rc = lr20xx_radio_common_set_pa_cfg(ctx, &pa); LOG_DBG("modem_cfg: set_pa_cfg(sel=%d mode=%d duty=%d slices=%d hf_duty=%d)=%d", pa.pa_sel, pa.pa_lf_mode, pa.pa_lf_duty_cycle, pa.pa_lf_slices, pa.pa_hf_duty_cycle, rc); - rc = lr20xx_radio_common_set_tx_params(ctx, half_power, + rc = lr20xx_radio_common_set_tx_params(ctx, pa_val, LR20XX_RADIO_COMMON_RAMP_48_US); - LOG_DBG("modem_cfg: set_tx_params(half_pwr=%d ramp=0x05)=%d", - half_power, rc); + LOG_DBG("modem_cfg: set_tx_params(pa_val=%d ramp=0x05)=%d", + pa_val, rc); } rc = lr20xx_system_set_dio_irq_cfg(ctx, LR20XX_SYSTEM_DIO_9, @@ -572,6 +588,10 @@ static void lr20xx_start_rx(struct lr20xx_data *data, data->in_rx_mode = true; data->tx_active = false; + + /* DEBUG: dump state AFTER SET_RX — should show mode=4 (RX). The + * "modem-RX" dump inside apply_modem_config is taken before SET_RX. */ + DUMP_CHIP_STATE(ctx, &data->hal_ctx, "post-SET_RX"); } /* ── Lightweight RX restart (no modem reconfig) ─────────────────────── */ @@ -927,6 +947,29 @@ static int lr20xx_lora_send_async(const struct device *dev, lr20xx_radio_common_set_tx(ctx, 5000); +#if IS_ENABLED(CONFIG_LOG) + /* DEBUG: poll chip state right after SET_TX. Non-destructive + * (get_status does NOT clear IRQs). We want to see: + * - cmd= on the FIRST poll == SET_TX's own command status + * (2=OK accepted, 1=PERR rejected, 0=FAIL not executed) + * - mode transitions 1(STBY)->5(TX)->1(fallback) if it really TXes + * - irq gaining TX_DONE (bit19, 0x00080000) at the chip level + * - whether DIO9 physically asserts (the MCU IRQ line) + * Diagnostic only — the spi_mutex is held, so the DIO9 work handler + * blocks until we unlock, then processes TX_DONE normally. */ + for (int i = 0; i < 20; i++) { + lr20xx_system_stat1_t s1 = {0}; + lr20xx_system_stat2_t s2 = {0}; + lr20xx_system_irq_mask_t dbgirq = 0; + lr20xx_system_get_status(ctx, &s1, &s2, &dbgirq); + LOG_INF("TXpoll[%2d] cmd=%d mode=%d irq=0x%08x BUSY=%d DIO9=%d", + i, s1.command_status, s2.chip_mode, dbgirq, + gpio_pin_get_dt(&data->hal_ctx.busy), + gpio_pin_get_dt(&data->hal_ctx.dio1)); + k_msleep(20); + } +#endif + k_mutex_unlock(&data->spi_mutex); return 0; @@ -1392,6 +1435,15 @@ static int lr20xx_hw_init(struct lr20xx_data *data, st = lr20xx_radio_common_set_rx_tx_fallback_mode(ctx, LR20XX_RADIO_FALLBACK_STDBY_RC); + /* DEBUG/TEST: disable the low-battery / EoL detector. The chip was + * raising LOW_BATTERY (IRQ bit10, 0x400) during FE cal and TX and + * refusing to enter TX. Disabling it disambiguates: + * - TX now keys → it was a FALSE VBAT reading (sense pin), done. + * - TX still dead → genuine supply brownout under RF (hardware). + * Default trim is 1.88V; we both disable AND set the lowest (1.60V). */ + st = lr20xx_system_set_lbd_cfg(ctx, false, LR20XX_SYSTEM_LBD_TRIM_1_60_V); + LOG_DBG("init: disable LBD (low-battery detect)=%d", st); + DUMP_CHIP_STATE(ctx, &data->hal_ctx, "pre-cal"); lr20xx_system_clear_errors(ctx); @@ -1437,6 +1489,19 @@ static int lr20xx_hw_init(struct lr20xx_data *data, DUMP_CHIP_STATE(ctx, &data->hal_ctx, "init-done"); + /* DEBUG: what voltage does the chip see on its OWN supply pin? (mV, + * after MU calibration). If this reads low (≪3000mV) while the board + * is powered, the LR2021 VBAT/supply pin is floating or miswired — that + * is why it flags LOW_BATTERY and aborts TX no matter the system rail + * or external battery. ~3000-3300mV ⇒ supply is fine, sag is transient. */ + { + uint16_t vbat_mv = 0; + lr20xx_status_t vrc = lr20xx_system_get_vbat(ctx, + LR20XX_SYSTEM_VALUE_FORMAT_UNIT, + LR20XX_SYSTEM_MEAS_RES_12_BITS, &vbat_mv); + LOG_INF("init: chip VBAT reads %u mV (rc=%d)", vbat_mv, vrc); + } + lr20xx_system_clear_errors(ctx); lr20xx_hal_enable_dio1_irq(&data->hal_ctx);