The LoRa Transceiver¶
SIX WRAPPERS · STD_INIT · TCXO FALLBACK · REGISTER PATCH · NOISE FLOOR
The radio is the only part that makes a node a node. In the firmware that is
not a chip but a class: RADIO_CLASS, filled in per board with one of six
wrappers around RadioLib. This chapter describes what that wrapper adds on
top of RadioLib — initialisation with a fallback, the optional per-board
flags, the register patch for three boards, and the fact that the radio
measures its own noise floor.
[!NOTE] Source. This page has been verified against the firmware itself:
MeshCorev1.16.0, commit03b6ef4, 28 July 2026 — filessrc/helpers/radiolib/CustomSX1262.h,src/helpers/radiolib/RadioLibWrappers.cpp, the other wrappers insrc/helpers/radiolib/and the radio flags invariants/.
Six chips, one pattern¶
src/helpers/radiolib/ holds six wrappers, each with a matching
…Wrapper.h:
| Wrapper | Chip | Notable |
|---|---|---|
CustomSX1262.h |
SX1262 | the most common one; 868 MHz |
CustomSX1268.h |
SX1268 | same family, 433 MHz variant |
CustomLLCC68.h |
LLCC68 | SX126x family, limited SF range |
CustomSX1276.h |
SX1276 | older SX127x family, different registers |
CustomLR1110.h |
LR1110 | with its own reset routine in LR11x0Reset.h |
CustomSTM32WLx.h |
STM32WL | radio sits inside the SoC, no SPI |
Which board has which chip is in Node Matrix and is not repeated here. The choice is made in the build flags:
variants/lilygo_tbeam_1w/platformio.ini
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
The rest of the firmware knows only RADIO_CLASS and WRAPPER_CLASS. That
is why six chip families coexist without a single #ifdef on chip type
anywhere outside this directory.
std_init(): initialisation with a fallback¶
Every wrapper has a std_init(). It first sets the SPI pins — differently
per platform, see The SPI Bus — and then calls
RadioLib's begin().
src/helpers/radiolib/CustomSX1262.h r.45-55
int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
// if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f
if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) {
tcxo = 0.0f;
status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
}
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
The fallback is the interesting part. The TCXO voltage defaults to 1.6f
and is overridden by SX126X_DIO3_TCXO_VOLTAGE. If initialisation fails
with -706 (RADIOLIB_ERR_SPI_CMD_INVALID) or -707
(RADIOLIB_ERR_SPI_CMD_FAILED), the firmware tries once more with the TCXO
voltage set to zero.
[!NOTE] That is exactly how a board with a plain crystal instead of a TCXO behaves: DIO3 drives nothing and the chip rejects the command. The firmware does not guess this from the build flags but infers it from the error code.
The three fixed numbers in that call come from the root platformio.ini and
are the same for every board that does not override them: LORA_FREQ=869.618,
LORA_BW=62.5, LORA_SF=8. The coding rate is LORA_CR or otherwise 5,
the preamble length is 16, and the sync word is the SX126x family's private
sync word.
[!NOTE] These are build values, not network settings.
LORA_SF=8is the default inplatformio.ini; the Dutch network runs on SF7, which the node configuration applies on top after flashing. See Getting Started.
Four optional flags¶
After a successful begin() CRC is always on, followed by four blocks that
are compiled only if the variant sets the matching flag.
| Flag | Does | Variant directories |
|---|---|---|
SX126X_CURRENT_LIMIT |
current limit on the power amplifier | 66 |
SX126X_DIO2_AS_RF_SWITCH |
DIO2 drives the RF switch | 60 |
SX126X_RX_BOOSTED_GAIN |
more sensitive reception, more current | 64 |
SX126X_RXEN · SX126X_TXEN |
separate switch pins | 24 |
The count is per variant directory and includes both -D flags in
platformio.ini and #define lines in a header inside that directory;
commented-out lines do not count. Of the 79 variant directories, 60 set the
DIO2 flag: 45 in platformio.ini, 22 in a header, 7 in both places. What
that switch does is in Antenna.
If one of the two separate switch pins is missing, the wrapper fills it in itself:
src/helpers/radiolib/CustomSX1262.h r.68-76
#if defined(SX126X_RXEN) || defined(SX126X_TXEN)
#ifndef SX126X_RXEN
#define SX126X_RXEN RADIOLIB_NC
#endif
#ifndef SX126X_TXEN
#define SX126X_TXEN RADIOLIB_NC
#endif
setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif
RADIOLIB_NC means not connected. A board that sets only SX126X_RXEN
therefore does get setRfSwitchPins(), with the TX pin as not connected —
that is not a fault but the normal pattern on a board where DIO2 switches
the TX path and a separate pin switches the RX path.
A register patch for three boards¶
The last step in std_init() is a patch compiled only behind
SX126X_REGISTER_PATCH:
src/helpers/radiolib/CustomSX1262.h r.78-84
// for improved RX with Heltec v4
#ifdef SX126X_REGISTER_PATCH
uint8_t r_data = 0;
readRegister(0x8B5, &r_data, 1);
r_data |= 0x01;
writeRegister(0x8B5, &r_data, 1);
#endif
Bit 0 of register 0x8B5 is set. The comment names only the Heltec V4, but
three variant directories set the flag: heltec_v4, heltec_tracker_v2 and
rak3401 — the last one with a note that it concerns the SKY66122 front-end
module. The comment in the wrapper is therefore narrower than the usage.
Patches like this belong to a board and not to a chip; they exist because
the manufacturer did something other than the reference design.
The radio measures its own noise floor¶
The layer above the wrapper does more than pass things through. While
receiving, RadioLibWrapper samples the RSSI to determine a noise floor.
src/helpers/radiolib/RadioLibWrappers.cpp r.87-99
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
int rssi = getCurrentRSSI();
if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD
_num_floor_samples++;
_floor_sample_sum += rssi;
}
} else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) {
_noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES;
if (_noise_floor < -120) {
_noise_floor = -120; // clamp to lower bound of -120dBi
}
Three things stand out. Sixty-four samples are averaged
(NUM_NOISE_FLOOR_SAMPLES). A sample counts only if it is below the current
floor plus 14 dB (SAMPLING_THRESHOLD), so a passing packet cannot drag the
measurement upwards. And the result is clamped at −120 dBm: the firmware
never reports anything lower, even if the environment is quieter.
That floor is not a statistic for the logs. The channel counts as busy as soon as the current RSSI rises above floor plus threshold. What the floor means for achievable distance is in Link Budget.
Sources¶
Firmware, commit 03b6ef4 (v1.16.0, 28 July 2026):
src/helpers/radiolib/CustomSX1262.h—std_init(), the TCXO fallback, the optional flags and the register patchsrc/helpers/radiolib/— the six wrapperssrc/helpers/radiolib/RadioLibWrappers.cpp— the measured noise floor and the busy thresholdvariants/lilygo_tbeam_1w/platformio.ini— the radio flags of one board
Related in this documentation:
- The SPI Bus — how the chip hangs off the SoC
- Antenna — what sits on the RF port
- Link Budget — what the transmit power is worth
- RadioLib — the library itself
- LoRa Modulation — what happens to those bits
Translated from Dutch by Anthropic Claude