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 ofsetCRC(1), andexplicitHeader()alongside it.startReceive()is overridden to include the preamble flag in the reported interrupt flags; without that overrideisReceiving()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):
src/helpers/radiolib/CustomLR2021.h—std_init(),isReceiving()and the time limits per interrupt flagsrc/helpers/radiolib/CustomLR2021Wrapper.h— the side detectors and the RX boostsrc/helpers/radiolib/RadioLibWrappers.cpp— the standby beforestartReceive()src/helpers/CommonCLI.cpp—set extra.sfvariants/meshtracker_x1/platformio.ini— the radio flags of the MeshTracker X1variants/meshnology_w12/platformio.ini— the radio flags of the W12
Related chapters:
- The LoRa Transceiver — the shared pattern of all seven wrappers
- The class model — why there are two classes per chip
- Node Matrix — which board has which chip