Skip to content

The LR2021

SIDE DETECTORS · IRQ TIMEOUT · RX BOOST · TWO BOARDS

The LR2021 is the seventh radio chip in the firmware and the first to break the shared pattern from The LoRa Transceiver on a few points. It can listen on more than one spreading factor at a time, it has its own handling of interrupt flags that get stuck, and its presence is noticeable outside src/helpers/radiolib/. This chapter describes what is different.

Note

Source. This page was verified against the firmware itself: MeshCore v1.17.1, commit d929643, 14 August 2026 — files src/helpers/radiolib/CustomLR2021.h, src/helpers/radiolib/CustomLR2021Wrapper.h, src/helpers/radiolib/RadioLibWrappers.cpp, src/helpers/CommonCLI.cpp and the build flags in variants/meshtracker_x1/ and variants/meshnology_w12/.

Two boards

The chip sits in two variants, both new in v1.17.1:

Board Variant directory Transmit power TCXO
SenseCAP MeshTracker X1 variants/meshtracker_x1/ LORA_TX_POWER=22 1.6f
Meshnology W12 variants/meshnology_w12/ LORA_TX_POWER=4 0.0f

The difference in transmit power is not a mistake. The W12 additionally sets MAX_LORA_TX_POWER=4, with a note in its platformio.ini that the GC1109 amplifier already saturates at around four dBm of input power. The power you set on that board is therefore the power before the amplifier, not what leaves the antenna. The W12 also has no TCXO, hence LR2021_TCXO_VOLTAGE=0.0f.

The radio flags otherwise follow the familiar pattern:

variants/meshtracker_x1/platformio.ini

  -D RADIO_CLASS=CustomLR2021
  -D USE_LR2021
  -D WRAPPER_CLASS=CustomLR2021Wrapper

One flag is new. LR2021_IRQ_DIO names which DIO pin of the chip carries the interrupt; on both boards that is DIO8. The constructor puts that value in irqDioNum, a field of RadioLib's LR2021 (src/helpers/radiolib/CustomLR2021.h r.14). The other chips do not have that choice: in the SX126x family DIO1 is fixed.

Side detectors: listening on more than one SF

This is what the chip was taken into the firmware for. A LoRa receiver normally listens on one spreading factor; whatever passes by on another SF it does not hear. Alongside the main SF, the LR2021 can switch on up to three side detectors, each on an SF of its own.

The wrapper sets them through setSideDetector() and keeps the setting, so that it can apply it again after every parameter change:

src/helpers/radiolib/CustomLR2021Wrapper.h r.65-69

  int16_t applySideDetectorConfig() {
    int16_t status = ((CustomLR2021 *)_radio)->setSideDetector(_sideDet, _numSideDet);
    RadioLibWrapper::idle(); // trigger startReceive()
    return status;
  }

Four requirements apply, all four in configSideDetectors():

Requirement What happens if you break it
An SF lies between 5 and 12 the whole setting is refused
A side SF is greater than the main SF idem
The gap with the main SF is at most 4 idem
With main SF 10 or higher: at most one side detector idem

The third and the fourth requirement together mean the gain is largest on a low main SF. A node running on SF7 may add SF8 through SF11. A node running on SF10 is left with one side detector.

Note

The list is accepted or refused as a whole. There is no partial result: configSideDetectors() first checks every SF given and only then calls setSideDetector(). If that call does not succeed, the previous setting stays in place.

set extra.sf: the setting from outside

The side detectors are the only radio feature in the firmware that can be operated through the CLI and exists on one chip family only. The command sits behind #if defined(USE_LR2021) and is unknown on every other board:

src/helpers/CommonCLI.cpp r.779-785

  #if defined(USE_LR2021)
  } else if (memcmp(config, "extra.sf ", 9) == 0) {
    strcpy(tmp, &config[9]);
    const char *parts[4];
    uint8_t sideDetSFs[4];
    int num = mesh::Utils::parseTextParts(tmp, parts, 4);
    if (num > 3) {

Usage: set extra.sf 8 9 10, with at most three numbers. The reply is OK - extra SFs set or Invalid extra SF config. The chosen SFs are kept in extra_sf[4] in the settings and survive a restart (src/helpers/CommonCLI.h r.72).

The route from command to chip runs through the contract: CommonCLI calls configSideDetectors() on CommonCLICallbacks, the application passes that on to the radio wrapper. RadioLibWrapper has a default answer that returns false, so a board without an LR2021 need do nothing (src/helpers/radiolib/RadioLibWrappers.h r.81). See The class model.

Note

Only the repeater passes the command on. examples/simple_repeater/ implements configSideDetectors(); the companion and room server applications do not and fall back on the default answer. On a MeshTracker X1 with companion firmware set extra.sf therefore exists, but it always answers Invalid extra SF config.

Interrupt flags that get stuck

The LR2021 reports a preamble and a valid header as separate interrupt flags. Those flags stay up until something clears them, and if the packet they belonged to never arrives, the chip stays "receiving" as far as the firmware is concerned. That is not a theoretical case: isReceivingPacket() decides whether noise floor samples are taken and whether the channel counts as busy.

CustomLR2021::isReceiving() solves that with a time limit per flag. A preamble may stay up for _preambleMillis, a header for _maxPayloadMillis; after that the flags are cleared and the state is reset. The two limits are recomputed at every parameter change from SF, bandwidth and coding rate, by calcMaxPacketMillis(). The default values are 66 and 3934 milliseconds (src/helpers/radiolib/CustomLR2021.h r.7-8).

A header CRC error clears all three flags at once and resets the state. The same happens when the header flag disappears while the firmware had seen it — that means something else cleared it.

There is a second workaround. RadioLibWrapper::startRecv() first puts the chip in standby, but only for this chip:

src/helpers/radiolib/RadioLibWrappers.cpp r.109-112

void RadioLibWrapper::startRecv() {
  #if defined(USE_LR2021)
  _radio->standby(); // without this LR2021 can throw -706 when calling startReceive after hardware CAD when side detectors are enabled
  #endif

The comment in the source names the reason: without that standby, startReceive() can return error −706 after a hardware channel activity detection once side detectors are on.

RX boost and the remaining differences

setRxBoostedGainMode() exists on the SX126x family as well, but the LR2021 requires standby before it accepts the setting, and the level is a number rather than on or off. LR2021_RX_BOOST_LEVEL defaults to 7 and is set to 0 when switching off (src/helpers/radiolib/CustomLR2021Wrapper.h r.10-12).

Three smaller differences from CustomSX1262:

  • setCRC(2) instead of setCRC(1), and explicitHeader() alongside it.
  • startReceive() is overridden to include the preamble flag in the reported interrupt flags; without that override isReceiving() would never see the preamble.
  • onSendFinished() sets the preamble length again, with a comment in the source that this removes odd behaviour with alternating small and large packets.

The TCXO fallback works the same as on the SX1262: if begin() fails with −707 or −706, a second attempt follows with the TCXO voltage at zero. The difference is that the LR2021 version prints no error line to the console where the SX1262 version does since v1.17.1.

Recomputing

The two boards with an LR2021 can be found with:

grep -rl "RADIO_CLASS=CustomLR2021" variants/

The classes and their location are in The class model; tools/class-groups.py shows them with:

python3 tools/class-groups.py /path/to/MeshCore --groep 3 | grep LR2021
python3 tools/class-groups.py /path/to/MeshCore --groep 2 | grep LR2021

CustomLR2021 sits in group 3 and CustomLR2021Wrapper in group 2 — the same pair as with every other chip, and for the same reason: the first adapts the chip driver, the second pours the result into the radio contract.

Sources

Firmware, commit d929643 (v1.17.1, 14 August 2026):

Related chapters: