XLRS is timing-sensitive radio-link firmware for a paired TX/RX running on Raspberry Pi Pico (RP2040) boards with Semtech SX1280 2.4 GHz radios. It is ExpressLRS-like in shape — CRSF in, CRSF out, FHSS, link statistics — but it is not ExpressLRS OTA compatible. The over-the-air format, binding, and hopping are XLRS-native.
RC handset / CRSF handset ──▶ TX Pico ──▶ SX1280 LoRa + FHSS ──▶ RX Pico ──▶ CRSF flight controller
(2.4 GHz)
downlink telemetry ◀────────────────────────────── link stats / RSSI / LQ
The repository also includes an RC handset firmware target (RP2350) that reads sticks/switches and drives a TX module over CRSF.
Everything is driven by PlatformIO and a small set of wrapper scripts. From the repository root:
scripts/check-env.sh # verify the toolchain (pio, python3, ...)
scripts/test.sh # host-native Unity tests (pio test -e native)
scripts/build.sh # build the TX/RX firmware pairFlash one Pico at a time in BOOTSEL mode:
scripts/flash.sh tx # → .pio/build/tx_lora_pico/firmware.uf2
scripts/flash.sh rx # → .pio/build/rx_lora_pico/firmware.uf2
scripts/flash.sh rc # → .pio/build/rc-rp2350/firmware.uf2 (handset)Targeted direct flash helpers build and upload one role. Use these when a single matching board is connected:
scripts/flash-tx.sh
scripts/flash-rx.sh
scripts/flash-rc.shWhen multiple USB serial devices are connected, pass the target port explicitly:
scripts/flash-tx.sh /dev/cu.usbmodem1101
scripts/flash-rx.sh /dev/cu.usbmodem5
scripts/flash-rc.sh /dev/cu.usbmodem14101The same ports can be supplied through environment variables:
TX_PORT=/dev/cu.usbmodem1101 scripts/flash-tx.sh
RX_PORT=/dev/cu.usbmodem5 scripts/flash-rx.sh
RC_PORT=/dev/cu.usbmodem14101 scripts/flash-rc.shMonitor all connected modules in one color-tagged terminal view:
scripts/monitor-all.shThe monitor probes USB serial ports with status, identifies role=tx,
role=rx, and role=rc-rp2350, ignores non-module ports, and prints timestamped
TX/RX/RC logs. It sends status every 5 seconds by default. Useful variants:
scripts/monitor-all.sh /dev/cu.usbmodem1101 /dev/cu.usbmodem5 /dev/cu.usbmodem14101
scripts/monitor-all.sh --status-interval 1
scripts/monitor-all.sh --status-interval 0
scripts/monitor-all.sh --no-clearEquivalent manual PlatformIO commands:
pio test -e native
pio run -e tx_lora_pico
pio run -e rx_lora_pico
pio run -e rc-rp2350See docs/build-test-flash.md for the full workflow,
including FLASH_METHOD=uf2|picotool and serial monitoring.
| Environment | Board | Role |
|---|---|---|
tx_lora_pico |
RP2040 | CRSF handset input, LoRa uplink, telemetry back to handset |
rx_lora_pico |
RP2040 | LoRa receiver, CRSF RC + link-stats output to a flight controller |
rc-rp2350 |
RP2350 | RC handset firmware: stick/switch sampling, OLED, CRSF out to a TX module |
tx_usb_diag |
RP2040 | tx_lora_pico with USB diagnostic logging (LORA_USB_DIAG) |
tx_crsf_diag |
RP2040 | tx_lora_pico with CRSF diagnostic logging (LORA_CRSF_DIAG) |
native |
host | Off-device Unity tests for protocol/scheduler/config helpers |
tx_lora_pico and rx_lora_pico are the default build targets. The default
binding phrase, pins, and rates are build flags in
platformio.ini.
- Roles are fixed at build time. One firmware image is the time-master TX,
the other is the RX, selected by
-DLORA_TX_ROLE=1/-DLORA_RX_ROLE=1. - Binding is phrase-based. Both ends must share the same binding phrase. The
phrase derives an 8-byte Link UID, which seeds the SX1280 sync word, the
FHSS hop sequence, and a
uid_checkvalue carried in every OTA frame. There is no over-the-air pairing handshake — matching phrases bind the pair. - FHSS is on by default across 40 channels (2404–2482 MHz, 2 MHz spacing). The TX is the time master; the RX disciplines its own tick clock to the TX with a phase/frequency detector (PFD) so both ends hop together.
- Two rates are available:
L250(≈250 Hz, SF6) andL100(≈100 Hz, SF5 with periodic downlink telemetry slots). Both use 812.5 kHz bandwidth. - OTA frames are plaintext. A 33-byte frame carries a 22-byte payload plus a
CRC16-CCITT and the binding-derived
uid_check. These reject corruption and wrong-link traffic, but they are not authentication or encryption. - Failsafe is loss-driven. When the uplink goes stale (no valid frame within the failsafe window, default 250 ms) the RX stops emitting CRSF RC frames so the flight controller sees RXLOSS and runs its own failsafe policy.
Protocol constants live in include/lora_link/protocol.h and the per-tick scheduler/state machine in include/lora_link/rf/scheduler.h.
USB serial runs at 115200 baud on both TX and RX. Commands:
bind get show the current binding phrase, UID, and uid_check
bind set <1..32 chars> set and persist a new binding phrase (requires reboot)
bind clear clear the persisted phrase back to the build default
rate L250 switch to the 250 Hz rate
rate L100 switch to the 100 Hz rate (with downlink telemetry)
status print role, link health, and scheduler diagnostics
channels print the current RC channel values
reboot reboot the device
TX and RX also accept the rc.v1 binding commands over direct USB
(binding_get/binding_set/binding_clear/binding_verify with
target=tx|rx), used by the WebUI binding proxy.
The RP2350 handset firmware samples sticks and switches, drives an OLED, manages battery/charging, and emits CRSF to the TX module. It has its own USB console:
status handset state summary
channels live channel values
power battery / charger status
reboot reboot the handset
It also implements the rc.v1 WebUI subset for discovery, live state, and a
TX-binding proxy:
rc.v1 hello
rc.v1 tx_hello
rc.v1 state
rc.v1 binding_set target=tx phrase=<phrase>
Handset architecture and the WebUI protocol are documented under docs/rc-rp2350/. The browser config tool lives in tools/rc-webui/.
Radio and CRSF pins are build flags in platformio.ini.
| Signal | Pico pin |
|---|---|
| CRSF UART TX | GP8 |
| CRSF UART RX | GP9 |
| CRSF baud | 400000 |
| SX1280 SCK | GP18 |
| SX1280 MOSI | GP19 |
| SX1280 MISO | GP16 |
| SX1280 CS | GP17 |
| SX1280 BUSY | GP20 |
| SX1280 DIO1 | GP21 |
| SX1280 RST | GP22 |
| SX1280 RXEN | GP14 |
| SX1280 TXEN | GP15 |
| Status LED | GP25 |
Full pinout and SX1280 wiring: docs/hardware/pinout.md and docs/hardware/sx1280-wiring.md.
include/
lora_link/ public protocol + scheduler + control-frame headers
rc_handset/ RC handset module headers
src/
main.cpp TX/RX role firmware (role selected by build flag)
protocol.cpp OTA frames, CRC, UID/FHSS, CRSF, RC packing, config
lora_link/rf/ per-tick RF scheduler + RX timing/PFD
control_frame.cpp control-frame helpers
rc_handset/ RC handset firmware (input, display, power, USB rc.v1)
lib/
CrsfProtocol/ CRSF transmit helpers (SimpleTxCrsf)
crsfSerial/ CRSF serial parsing
UARTProtocol/ handset↔TX UART protocol
crc8/ CRSF CRC8
test/ host-native Unity tests (pio test -e native)
tools/rc-webui/ browser-based RC config tool
docs/ developer + hardware + interface + troubleshooting docs
scripts/ build/test/flash/lint/monitor helpers
datasheets/ vendor datasheets (fetch with scripts/fetch-datasheets.sh)
platformio.ini build environments and pin/rate flags
_legacy/ earlier XLRS core — reference only, not built (see below)
Note on
_legacy/._legacy/xlrs/(formerlylib/xlrs/) and_legacy/apps/(formerlyapps/) hold an earlier clean-slate "XLRS core" — a CMake/Pico-SDK layered design. Nothing in the active build depends on it and PlatformIO does not scan it; the firmware that builds and flashes is the PlatformIOsrc/tree above. See _legacy/README.md.
A fuller map is in docs/developer/code-map.md.
Start at the documentation index.
- Getting Started
- Build, Test, Flash
- Developer: code map · architecture · OTA protocol · timing & scheduler · terminology
- Hardware: pinout · SX1280 wiring · bench bring-up
- Interfaces: RX CRSF · TX controller CRSF
- RC handset: overview
- Troubleshooting: symptom matrix · serial logs
Contributor workflow and the docs-update contract are in CONTRIBUTING.md. Agent/automation instructions are in AGENTS.md.
This firmware drives RC links. Treat these areas as safety-sensitive and validate changes on the bench:
- Failsafe behavior and CRSF output gating.
- RF scheduler timing, slot selection, FHSS advance, and PFD updates.
- Binding identity, Link UID derivation, sync words, and
uid_check. - RF region/frequency tables and TX power.
- OTA frame formats and the flash-backed config schema.
FHSS is enabled by default (RF_FIXED_CHANNEL=-1). Set RF_FIXED_CHANNEL to a
non-negative channel index only for bench/debug fixed-channel operation, and
review regional RF regulations before transmitting above bench power levels.