A bench test for one question about the Semtech LR2021: does the coding-rate field of
GetLoraPacketStatus (0x022A, reply byte 2, bits 3:0, DS rev 2.2 §9.9.9 Table 9-13) report the
rate of the frame just received, or the bitwise OR of every header's rate since reset?
It reports the OR. After a single off-rate frame, the field keeps the ORed value for every following frame until the chip is reset. Two implementations show this on two LR2021 LoRa Plus EVKs (chip ROM 1.24): one built on RadioLib, and one with no driver code at all.
Background, and the firmware mitigation that shipped, are in meshtastic/firmware#12183.
| what it is | built with | results | |
|---|---|---|---|
| Prebuilt images | RX, TX and bit-3 TX .hex files of the RadioLib probe, at 915 MHz |
nothing: flash with pyocd | the same as the RadioLib probe |
| Repository root | the RadioLib probe: RadioLib on the Arduino core, with no Meshtastic stack, scheduler or concurrency | PlatformIO | logs/, 2026-11-03 |
bare/ |
the same test with no driver: every host command built from the datasheet in one portable C file behind a six-function HAL, plus a Zephyr port and a workstation harness | upstream Zephyr v4.4.0 | bare/logs/, 2026-11-05 |
Which one to use depends on what you want:
- To see the effect: flash the prebuilt images onto two kits and read the RX serial port.
- To change the experiment, for example to add a phase or try another rate: use the RadioLib probe, described below.
- To check every byte on the wire, or to compare against another LR2021 driver such as USP's:
use
bare/. Its README has the full build route from an empty machine.
Both implementations used the same five phases on the same two kits, with the receiver configured
at 4/8 (0x04):
| phases | RadioLib probe (logs/run1-rx.txt) |
bare/ on Zephyr (bare/logs/zephyr-run1-rx.txt) |
|---|---|---|
P1: 10 frames at 0x04 |
0x04 |
0x04 |
P2: 1 frame at 0x01 |
0x05 |
0x05 |
P3: 10 frames at 0x04 |
0x05 |
0x05 |
P4: 1 frame at 0x02 |
0x07 |
0x07 |
P5: 5 frames at 0x04 |
0x07 |
0x07 |
A chip that reported the received frame would read 0x04 throughout P3 and P5.
The RadioLib probe also has these results (details below):
- Bit 3: one
0x09frame gives0x0Dfrom a0x04base, and0x0Ffrom a receiver already at0x07. These are the out-of-table values seen in the field. - Register write: writing the configured code into the undocumented word
0x8003C4clears the field. - NRESET: the chip's reset pin clears it too.
The two hypotheses produce the same output if the transmitter keeps cycling through coding rates,
because both give a stream of changing values. To tell them apart, send off-rate frames and then
stop. The receiver stays at 4/8 (raw code 0x04) throughout, and the transmitter walks through
these phases:
| phase | transmit | correct chip | accumulating chip |
|---|---|---|---|
| P1 | 10 frames at 0x04 |
0x04 |
0x04 |
| P2 | 1 frame at 0x01 |
0x01 |
0x05 |
| P3 | 10 frames at 0x04 |
0x04 |
0x05 |
| P4 | 1 frame at 0x02 |
0x02 |
0x07 |
| P5 | 5 frames at 0x04 |
0x04 |
0x07 |
P3 is the test. By then every frame on air is at the configured rate again.
Each frame carries its own phase and coding rate in its payload, so the RX log alone is a complete record of the run.
| run | log | sent | reported cr |
|---|---|---|---|
| 1 | run1-rx.txt |
default sequence | 0x04 ×10 → 0x05 from P2 and through all of P3 → 0x07 from P4 through P5 |
| 2 | run2-rx-after-clear.txt |
c (register write), then default sequence |
write moves 0x8003C4 from …0117 to …0114, next frame reads 0x04, then the same latch pattern |
| 3 | run3-rx-bit3.txt |
tx_xiao_bit3: one 0x09 frame from a 0x04 base |
0x0D (= 0x04 | 0x09), held for the next 10 frames |
| 3, before the reset | same file, first lines | the same 0x09 frame at a receiver still latched at 0x07 |
0x0F (= 0x07 | 0x09) |
- Clean link: all 78 complete frames decoded without a CRC error, at about −30 dBm RSSI and
about 15 dB SNR. RadioLib's
readData()returned every payload, and none has a header-CRC error flagged in theirqcolumn. - Two readouts agree: on every line, the documented packet-status
crequals the low nibble of the undocumented word at0x8003C4. - Bit 3: an LR20xx transmitter can send
0x08or0x09, and a standard LoRa header cannot. The receiver ORs that bit in as well. This explains the values outside Table 9-4 (0x0D,0x0F) that were logged in the field. - NRESET clears it: resetting the host makes RadioLib's
begin()pulse the LR2021's NRESET pin, and that brings the field back to0x04. The tail of run 3's previous session (0x0F) is followed by the fresh boot (0x04).
Semtech LR2021 LoRa Plus EVK: a Seeed XIAO nRF54L15 on the LoRa Plus expansion board, with a Wio-LR2021 module. You need two kits, one for RX and one for TX. Use the LF SMA port for sub-GHz.
| signal | XIAO pin | note |
|---|---|---|
| NSS | D3 | |
| IRQ | D0 | driven by the LR2021's DIO8, not RadioLib's default DIO5 |
| NRESET | D2 | |
| BUSY | D1 | |
| SPI | D8 SCK / D9 MISO / D10 MOSI | |
| clock | 32 MHz crystal | no TCXO. Setting 1.8 V makes begin() fail with −707 |
| RF switch | none | switchless direct-tie front end |
The pins come from Semtech's
usp_zephyr semtech_wio_lr20xx shield devicetree, and
meshtastic/firmware's xiao_nrf54l15_lr2021 variant agrees with them. The devicetree's 1.8 V TCXO
setting does not match this hardware.
variants/xiao_nrf54l15_lr2021/ shadows the framework's variant for one reason. The stock variant
puts Serial on header pins D6/D7, but the XIAO's USB serial port is the on-board SAMD11 bridge.
There are also *_nucleo and *_esp32s3 envs for other hosts. They build, but their pin maps are
placeholders and have never run on hardware.
You need PlatformIO and pyocd. For bare/, see its README.
pyocd list # note each kit's probe unique ID
# set the --uid values in platformio.ini's rx_xiao / tx_xiao upload_command
pio run -e rx_xiao -t upload
pio run -e tx_xiao -t upload # or tx_xiao_bit3 for the 0x09 follow-on
pio device monitor -p /dev/serial/by-id/<RX probe>-if02 -b 115200The TX starts its sequence 5 s after boot. Press g on its console to run it again.
The RX console takes these keys:
| key | action |
|---|---|
s |
summary |
c |
write the configured code into 0x8003C4 |
h |
help |
Treat c as a mechanism experiment. The address is undocumented, and nothing should ship a write
to it.
The default radio settings are 915.0 MHz, SF7, BW500, 4/8, sync word 0x12 and 10 dBm. Frames go
out 400 ms apart, 27 in a sequence. Change PROBE_FREQ_MHZ in platformio.ini for your region.
The frequency stays off Meshtastic's default US slots. Damaged foreign headers also feed the accumulator, so stray mesh traffic would contaminate the run.
# n sent cr exp crc rssi_dbm snr_q reg irq
12 P3 F01 CR04 0x05 0x04 1 -30 60 0x9D4A0115 0x00060170 OFF-CONFIGURED
| column | meaning |
|---|---|
sent |
the transmitted payload: phase, frame number and coding rate |
cr |
the packet-status coding rate, the documented field |
exp |
the configured code |
crc |
the header's CRC-present flag (1 = the frame carries a payload CRC). USP decodes this bit as LR20XX_RADIO_LORA_CRC_ENABLED, not as a pass/fail result. |
rssi_dbm |
whole dBm |
snr_q |
quarter-dB |
reg |
the 0x8003C4 word, logged only |
irq |
the raw IRQ status |
Any line where cr differs from exp is tagged OFF-CONFIGURED. If an OFF-CONFIGURED line has
bit 9 (0x200) set in irq, it was a header-CRC error, not a decoded frame.
Most lines read irq = 0x00060170. The bits that matter there are 18 (RX_DONE) and 17
(CMD_ERROR); see open questions.
The logs are raw serial captures, kept exactly as received:
run1-rx.txt: starts after the first two banner lines, which the board printed before the capture opened.run3-rx-bit3.txt: opens mid-line, with the end of the receiver's previous session. Two lines are merged there because the capture opened mid-print. That merged line holds the0x0Fframe.run2-tx.txt: starts with a few junk bytes from the USB bridge at boot.rx-bringup*.txt: the bring-up attempts.- 868.1 MHz failed front-end calibration (−1300). This was not investigated.
- The 1.8 V TCXO attempt failed with −707.
- The 915 MHz boot was the first good one.
CMD_ERRORon every reception after the first. IRQ bit 17 is set alongsideRX_DONEon every frame except the first after boot. It appears in the RadioLib probe and inbare/, under both Arduino and Zephyr, so it is not a driver quirk. Both stacks re-arm withClearIrqand thenSetRxwhile the chip is already in continuous RX, so the likeliest explanation is that the chip rejects thatSetRx. This is not verified, and it does not affect the coding-rate result.- 868.1 MHz.
begin()failed front-end calibration there (−1300) on these kits. The bench moved to 915 MHz, and this was never investigated. - Not yet tested:
- channel-activity detection (CAD) interleaved with reception, which Meshtastic runs before every transmission;
- a 125 kHz bandwidth run, which Semtech used and we did not;
- whether
SetSleepwithout retention clears the field.
- RadioLib probe: RadioLib is pinned to
5878f990, the revision meshtastic/firmware pins. The XIAO uses Meshtastic'splatform-nordicnrf54v0.3.1 with the S145 SoftDevice Arduino core. bare/: upstream Zephyr v4.4.0 with Zephyr SDK 1.0.1.
GPL-3.0. The files in variants/xiao_nrf54l15_lr2021/ are derived from
meshtastic/firmware.