Lr1121 tcxo optional tries xtal first, and get all my yamls in a row (#11215)

* LR11x0: try XTAL before TCXO when oscillator type is uncertain

On boards with TCXO_OPTIONAL, a TCXO-first attempt either hangs RadioLib's
calibration wait forever on a bare/non-TCXO module (unpatched upstream), or
costs a slow failed attempt before falling back even once that's fixed with
a timeout. Measured on hardware: XTAL succeeds immediately on a bare module
(~350ms) and fails fast and cleanly on a genuine TCXO module (~300ms,
RADIOLIB_ERR_SPI_CMD_FAILED), so trying XTAL first is a strict improvement
for hang-avoidance regardless of which oscillator is actually present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* compacted

* fix review comment

* fix femtofox switches

* correct the correction

* 13

* 3s timeout

* Treat SPI_CMD_TIMEOUT as an LR11x0 init failure

The BUSY watchdog breaks RadioLib's wait, so the next bounded transfer
returns SPI_CMD_TIMEOUT rather than SPI_CMD_FAILED. Only the latter was
checked, so a watchdog-triggered failure fell through to getVersionInfo(),
setRfSwitchTable() and startReceive() against an unresponsive chip.

Also use Throttle::isWithinTimespanMs() for the watchdog's elapsed-time
check instead of raw millis() arithmetic.

* Drop the BUSY watchdog and probe XTAL before TCXO

The watchdog bounded RadioLib's unbounded BUSY wait in LR11x0::config() by
having LockingArduinoHal::digitalRead() report a stuck pin low exactly once.
That let a TCXO-first attempt fail cleanly rather than hang, but it meant
lying to RadioLib about a GPIO from a HAL shared by every radio driver.

Ordering the attempts XTAL-first avoids the hang outright instead: attempt 1
configures no DIO3 Vref, so there is no calibration wait to get stuck in, and
the TCXO fallback is only reached on a module that answered and refused XTAL.

Attempts are now XTAL, then TCXO, then a settling retry on whichever
oscillator was settled on - after a fallback that is a second TCXO attempt.
Only TCXO_OPTIONAL builds probe XTAL; a variant that declares a Vref
unconditionally still goes straight to it and never probes XTAL at all.

SPI_CMD_TIMEOUT stays a failure alongside SPI_CMD_FAILED: a bounded
per-command BUSY wait in Module::SPItransferStream() reports it in its own
right, independently of the removed watchdog.

* Drop a stray tab from the promicro TCXO readme

trunk fmt: prettier flags the whitespace-only line inside the <summary>
block, which was the only failing check on the PR.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
This commit is contained in:
Tom
2026-08-05 08:29:23 +00:00
committed by GitHub
co-authored by Claude Sonnet 5 Thomas Göttgens
parent 03e6b80989
commit e78b121d9f
7 changed files with 206 additions and 98 deletions
@@ -42,7 +42,7 @@ Making your own node based on this design is straightforward. There are various
| Waveshare | Core1262-HF | yes | Ext | |
| Waveshare | LoRa Node Module | yes | Int | |
| Seeed | Wio-SX1262 | yes | Ext | Cute! DIO2/TXEN are not exposed |
| Seeed | Wio-LR1121 | yes | Int | LR1121, needs alternate rfswitch.h |
| Seeed | Wio-LR1121 | yes | Int | LR1121, build -D LR1121_MODULE_WIO |
| AI-Thinker | RA-02 | No | Int | SX1278 **433mhz band only** |
| RF Solutions | RFM95 | No | Int | Untested |
| Ebyte | E80-900M2213S | Yes | Int | LR1121 radio |
@@ -55,7 +55,24 @@ The E80 from CDEbyte is the most obtainable module at present, and has been sele
Naturally, CDEbyte have chosen to ignore the generic Semtech implementation of the RF switching logic and have supplied confusing and contradictory documentation, which is explained below.
tl;dr: The E80 is chosen as the default. **If you wish to use another module, the table in `rfswitch.h` must be adjusted accordingly.**
tl;dr: The E80 is chosen as the default. **If you wish to use another module, select it with an `LR1121_MODULE_*` build flag, or adjust the table in `rfswitch.h` accordingly.**
`rfswitch.h` carries a matrix for each known LR1121 module, guarded by a build flag:
| Build flag | Module |
| ------------------- | ------------------- |
| _(none)_ | Ebyte E80-900M2213S |
| `LR1121_MODULE_E80` | Ebyte E80-900M2213S |
| `LR1121_MODULE_WIO` | Seeed Wio-LR1121 |
Add it to your environment in `platformio.ini`:
```ini
build_flags = ${nrf52840_base.build_flags}
-I variants/nrf52840/diy/nrf52_promicro_diy_tcxo
-D NRF52_PROMICRO_DIY
-D LR1121_MODULE_WIO
```
### E80 switching - the saga
@@ -82,7 +99,7 @@ The Semtech default, the values are (taken from [here](https://github.com/Lora-n
<details>
<summary> < Click to expand >
</summary>
```cpp
@@ -119,4 +136,19 @@ Tests were conducted in each of the three configurations between a known-good SX
The RF switching is based on the code example given. Logically, this shows the DIO5 and DIO6 are swapped compared to the reference design.
If future DIYers wish to use an alternative module, the table in `rfswitch.h` must be adjusted accordingly.
## Seeed Wio-LR1121
The Wio is the counter-example to the E80: it follows the Semtech reference exactly, so no swap is needed. Its internal switch is a Skyworks SKY13373-460LF, controlled by DIO5 (V1) and DIO6 (V2) only - there is no third control line, so DIO7 is left out of the pin list. Section 4.5 of the module datasheet, "True Table of the Internal RF Switch", gives:
| V1 (DIO5) | V2 (DIO6) | Status | RadioLib mode |
| --------- | --------- | ------------------- | ------------- |
| 0 | 0 | Shutdown | `MODE_STBY` |
| 1 | 0 | RFI_P_LF & RFI_N_LF | `MODE_RX` |
| 0 | 1 | RFO_HP_LF | `MODE_TX_HP` |
| 1 | 1 | RFO_LP_LF | `MODE_TX` |
Note that the LR1121 has no GNSS or WiFi scanning, so `MODE_GNSS` and `MODE_WIFI` are left in the shutdown state.
Select it with `-D LR1121_MODULE_WIO`.
If future DIYers wish to use a module not listed above, add a matrix for it to `rfswitch.h` behind a new `LR1121_MODULE_*` guard.
@@ -6,6 +6,22 @@
#define LR20x0 LR2021
#endif
// LR1121 modules differ in how their internal RF switch is wired, so the matrix must match the
// module actually fitted. The Ebyte E80-900M2213S is the default; define one of the alternatives
// below in your build_flags (or above the #include of this header) to select a different module.
//
// LR1121_MODULE_E80 Ebyte E80-900M2213S (default if nothing is defined)
// LR1121_MODULE_WIO Seeed Wio-LR1121
#ifdef USE_LR1121
#if !defined(LR1121_MODULE_E80) && !defined(LR1121_MODULE_WIO)
#define LR1121_MODULE_E80
#endif
#if defined(LR1121_MODULE_E80) && defined(LR1121_MODULE_WIO)
#error "Define only one LR1121_MODULE_* option"
#endif
#ifdef LR1121_MODULE_E80
// This is rewritten to match the requirements of the E80-900M2213S
// The E80 does not conform to the reference Semtech switches(!) and therefore needs a custom matrix.
// See footnote #3 in "https://www.cdebyte.com/products/E80-900M2213S/2#Pin"
@@ -13,7 +29,6 @@
// DIO5 -> RFSW0_V1
// DIO6 -> RFSW1_V2
// DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible.
#ifdef USE_LR1121
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC,
RADIOLIB_NC};
@@ -30,7 +45,39 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
END_OF_MODE_TABLE,
// clang-format on
};
#endif
#endif // LR1121_MODULE_E80
#ifdef LR1121_MODULE_WIO
// Seeed Wio-LR1121. Unlike the E80 this module DOES follow the Semtech reference topology
// (SWSD006: rx = RFSW0, tx = RFSW0|RFSW1, tx_hp = RFSW1).
// The internal switch is a Skyworks SKY13373-460LF driven by two lines only:
// DIO5 -> V1
// DIO6 -> V2
// Datasheet section 4.5, "True Table of the Internal RF Switch":
// V1 V2 Status
// 0 0 Shutdown
// 1 0 RFI_P_LF & RFI_N_LF (receive)
// 0 1 RFO_HP_LF (sub-GHz high power)
// 1 1 RFO_LP_LF (sub-GHz low power)
// The LR1121 has no GNSS or WiFi scanning, so those modes are left in the shutdown state.
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[] = {
// clang-format off
// mode DIO5 DIO6
{LR11x0::MODE_STBY, {LOW, LOW}},
{LR11x0::MODE_RX, {HIGH, LOW}},
{LR11x0::MODE_TX, {HIGH, HIGH}},
{LR11x0::MODE_TX_HP, {LOW, HIGH}},
{LR11x0::MODE_TX_HF, {LOW, LOW}},
{LR11x0::MODE_GNSS, {LOW, LOW}},
{LR11x0::MODE_WIFI, {LOW, LOW}},
END_OF_MODE_TABLE,
// clang-format on
};
#endif // LR1121_MODULE_WIO
#endif // USE_LR1121
// LR2021 RF switch matrix following the standard Semtech / Seeed T1000-E reference topology.
// DIO5 -> antenna path select (HIGH = sub-GHz LF)
@@ -262,7 +262,7 @@ settings.
| Waveshare | Core1262-HF | yes | Ext | |
| Waveshare | LoRa Node Module | yes | Int | |
| Seeed | Wio-SX1262 | yes | Ext | Cute! DIO2/TXEN are not exposed |
| Seeed | Wio-LR1121 | yes | Int | LR1121, needs alternate rfswitch.h |
| Seeed | Wio-LR1121 | yes | Int | LR1121, build -D LR1121_MODULE_WIO |
| AI-Thinker | RA-02 | No | Int | SX1278 **433mhz band only** |
| RF Solutions | RFM95 | No | Int | Untested |
| Ebyte | E80-900M2213S | Yes | Int | LR1121 radio |