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
@@ -1,30 +1,31 @@
---
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
## Ebyte E80-900M2213S
## This is a bit experimental
##
##
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
rfswitch_table:
pins: [DIO5, DIO6, DIO7]
MODE_STBY: [LOW, LOW, LOW]
MODE_RX: [LOW, HIGH, LOW]
MODE_TX: [HIGH, HIGH, LOW]
MODE_TX_HP: [HIGH, LOW, LOW]
MODE_TX_HF: [LOW, LOW, LOW]
MODE_GNSS: [LOW, LOW, HIGH]
MODE_WIFI: [LOW, LOW, LOW]
# CDEbyte do not follow the Semtech reference switching: DIO5/DIO6 are swapped.
# DIO7 is wired to the GNSS path. See variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md
rfswitch_table:
pins: [DIO5, DIO6, DIO7]
MODE_STBY: [LOW, LOW, LOW]
MODE_RX: [LOW, HIGH, LOW]
MODE_TX: [HIGH, HIGH, LOW]
MODE_TX_HP: [HIGH, LOW, LOW]
MODE_TX_HF: [LOW, LOW, LOW]
MODE_GNSS: [LOW, LOW, HIGH]
MODE_WIFI: [LOW, LOW, LOW]
General:
MACAddressSource: eth0
@@ -1,46 +1,28 @@
---
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
## Generic LR1121 module (Semtech reference RF switch topology)
## This is a bit experimental
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
rfswitch_table:
pins:
- DIO5
- DIO6
MODE_STBY:
- LOW
- LOW
MODE_RX:
- HIGH
- LOW
MODE_TX:
- HIGH
- HIGH
MODE_TX_HP:
- LOW
- HIGH
MODE_TX_HF:
- LOW
- LOW
MODE_GNSS:
- LOW
- LOW
MODE_WIFI:
- LOW
- LOW
# Semtech reference switching (SWSD006): rx=RFSW0, tx=RFSW0|RFSW1, tx_hp=RFSW1.
rfswitch_table:
pins: [DIO5, DIO6]
MODE_STBY: [LOW, LOW]
MODE_RX: [HIGH, LOW]
MODE_TX: [HIGH, HIGH]
MODE_TX_HP: [LOW, HIGH]
MODE_TX_HF: [LOW, LOW]
MODE_GNSS: [LOW, LOW]
MODE_WIFI: [LOW, LOW]
General:
MACAddressSource: eth0
+23 -16
View File
@@ -1,30 +1,37 @@
---
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
## Seeed Wio-LR1121
## This is a bit experimental
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
rfswitch_table:
pins: [DIO5, DIO6, DIO7]
MODE_STBY: [LOW, LOW, LOW]
MODE_RX: [LOW, LOW, LOW]
MODE_TX: [LOW, HIGH, LOW]
MODE_TX_HP: [HIGH, LOW, LOW]
# MODE_TX_HF: []
# MODE_GNSS: []
MODE_WIFI: [LOW, LOW, LOW]
# Internal RF switch is a Skyworks SKY13373-460LF driven by DIO5 (V1) and DIO6 (V2).
# Datasheet section 4.5 "True Table of the Internal RF Switch":
# V1 V2 Status
# 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
# This matches the Semtech reference topology (SWSD006):
# rx=RFSW0, tx=RFSW0|RFSW1, tx_hp=RFSW1.
# The LR1121 has no GNSS/WiFi scanning, so those modes are unused.
rfswitch_table:
pins: [DIO5, DIO6]
MODE_STBY: [LOW, LOW]
MODE_RX: [HIGH, LOW]
MODE_TX: [HIGH, HIGH]
MODE_TX_HP: [LOW, HIGH]
MODE_TX_HF: [LOW, LOW]
MODE_GNSS: [LOW, LOW]
MODE_WIFI: [LOW, LOW]
General:
MACAddressSource: eth0
+66 -27
View File
@@ -47,7 +47,6 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
#endif
// the 2.4G part maxes at 13dBm
#if ARCH_PORTDUINO
#define LR1120_MAX_POWER portduino_config.lr1120_max_power
#endif
@@ -55,6 +54,23 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
#define LR1120_MAX_POWER 13
#endif
// Vref to assume for a board that declares a TCXO may be fitted without saying at what voltage.
// "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
#if defined(TCXO_OPTIONAL)
#define LR11X0_TCXO_DEFAULT_VOLTAGE 1.6f
#else
#define LR11X0_TCXO_DEFAULT_VOLTAGE 0
#endif
// A chip that never answers can surface either way depending on where RadioLib gave up: a bounded
// per-command BUSY wait in Module::SPItransferStream() reports SPI_CMD_TIMEOUT rather than
// SPI_CMD_FAILED, so both have to count as "the chip did not talk to us"
static inline bool lr11x0SpiFailed(int res)
{
return res == RADIOLIB_ERR_SPI_CMD_FAILED || res == RADIOLIB_ERR_SPI_CMD_TIMEOUT;
}
template <typename T>
LR11x0Interface<T>::LR11x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy)
@@ -73,22 +89,26 @@ template <typename T> bool LR11x0Interface<T>::init()
digitalWrite(LR11X0_POWER_EN, HIGH);
#endif
// An explicit Vref always wins; TCXO_OPTIONAL only supplies a default for boards that declare a
// TCXO may be fitted without saying at what voltage. Both may appear in the same variant file.
#if ARCH_PORTDUINO
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE
// Portduino leaves dio3_tcxo_voltage at 0 whenever the YAML omits DIO3_TCXO_VOLTAGE, which is the
// "no explicit Vref" case, so the TCXO_OPTIONAL default still has to apply there
float tcxoVoltage =
portduino_config.dio3_tcxo_voltage > 0 ? (float)portduino_config.dio3_tcxo_voltage / 1000 : LR11X0_TCXO_DEFAULT_VOLTAGE;
#elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE;
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE);
// (DIO3 is not free to be used as an IRQ)
#elif defined(TCXO_OPTIONAL)
float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure
LOG_DEBUG("TCXO_OPTIONAL: no LR11X0_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first");
#else
float tcxoVoltage =
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
// (DIO3 is free to be used as an IRQ)
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
float tcxoVoltage = LR11X0_TCXO_DEFAULT_VOLTAGE;
#endif
// DIO3 is free to be used as an IRQ only while no TCXO Vref is driven on it
if (tcxoVoltage > 0)
LOG_DEBUG("LR11x0 TCXO Vref %f V on DIO3 (DIO3 unavailable as an IRQ)", tcxoVoltage);
else
LOG_DEBUG("LR11x0 no TCXO Vref, XTAL only (DIO3 free as an IRQ)");
#if defined(TCXO_OPTIONAL)
LOG_DEBUG("TCXO_OPTIONAL: oscillator type unknown, probing XTAL first and using any TCXO Vref only as fallback");
#endif
RadioLibInterface::init();
@@ -114,29 +134,48 @@ template <typename T> bool LR11x0Interface<T>::init()
// Allow extra time for TCXO to stabilize after power-on
delay(10);
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
// Retry if we get SPI command failed - some units need extra TCXO stabilization time
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
LOG_WARN("LR11x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res);
delay(100);
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
}
// Timestamped brackets so a hang inside RadioLib leaves a dangling "attempt" line in the boot log
auto tryBegin = [&](int attempt, float vref) {
uint32_t attemptStart = millis();
LOG_INFO("LR11x0 begin() attempt %d: tcxoVoltage=%.3fV at t=%ums", attempt, vref, attemptStart);
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, vref);
LOG_INFO("LR11x0 begin() attempt %d returned %d after %ums", attempt, res, millis() - attemptStart);
return res;
};
#if defined(TCXO_OPTIONAL)
// If init failed for any reason other than chip not found, retry without TCXO (XTAL mode)
// 1. XTAL, because a TCXO-first attempt hangs RadioLib's unbounded calibration wait on a module
// with no TCXO fitted, whereas XTAL fails fast and cleanly on a module that does have one
float attemptVoltage = 0;
#else
// 1. Whatever Vref the variant configured, which it declared unconditionally
float attemptVoltage = tcxoVoltage;
#endif
int res = tryBegin(1, attemptVoltage);
#if defined(TCXO_OPTIONAL)
// 2. XTAL failed with the chip present, so fall back to the TCXO if the variant configured one
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
LOG_WARN("LR11x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res);
tcxoVoltage = 0;
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
LOG_WARN("LR11x0 XTAL init failed (err %d), retrying with TCXO Vref %f V", res, tcxoVoltage);
attemptVoltage = tcxoVoltage;
res = tryBegin(2, attemptVoltage);
if (res == RADIOLIB_ERR_NONE)
LOG_INFO("LR11x0 init success without TCXO (XTAL mode)");
LOG_INFO("LR11x0 init success with TCXO Vref %f V", tcxoVoltage);
}
#endif
// 3. Some units need extra settling time, so give whichever oscillator we settled on one retry.
// After a step 2 fallback that is a second TCXO attempt, which is where settling actually matters.
if (lr11x0SpiFailed(res)) {
LOG_WARN("LR11x0 init failed with %d (SPI command failure), retrying after delay...", res);
delay(100);
res = tryBegin(3, attemptVoltage);
}
// \todo Display actual typename of the adapter, not just `LR11x0`
LOG_INFO("LR11x0 init result %d", res);
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) {
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || lr11x0SpiFailed(res)) {
#ifdef LR11X0_UPDATE_FIRMWARE_TO
// An interrupted update leaves the radio sitting in bootloader mode, where begin() fails. Retry the
// flash from here rather than giving up, otherwise the device could never recover on its own.
@@ -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 |