Decoding LoRa with an RTL-SDR: From Chirps to Frames

I keep mentioning LoRa in passing. Back in my writeup on decoding 433 MHz IoT protocols I said I had been tuning around looking for LoRa packets when I got distracted by a neighbor's weather station. Several people asked the obvious follow-up: how do you actually decode the LoRa itself? So this weekend I sat down with the spectrum analyzer running and worked through the whole chain, from raw chirps to readable frames. Here is what I found.

Why rtl_433 Cannot Help You Here

The reason LoRa needs its own guide is that it does not look anything like the on-off keyed signals that fill the 433 MHz band. LoRa uses CSS, chirp spread spectrum. Instead of switching a carrier on and off, a LoRa transmitter sweeps its frequency linearly across the channel bandwidth. Each of those sweeps is a chirp, and the symbol data is encoded in where each chirp starts before it wraps around.

[Photo: waterfall display of a single LoRa transmission, showing the diagonal up-chirps of the preamble followed by the data symbols]

You can spot LoRa on a waterfall instantly once you know the shape. The preamble is a run of clean diagonal lines, all sloping the same way. That visual signature is why the Signal Identification Wiki lists it the way it does. But rtl_433, which is built around amplitude and frequency-shift demodulation, has no idea what to do with a chirp. Decoding it means reversing the spread spectrum, and that needs software written specifically for LoRa.

The Parameters You Have to Match

CSS is a joy to receive and a nuisance to blindly decode, because a LoRa frame is only interpretable if you know the parameters it was sent with. There are four that matter:

  • Spreading factor (SF), from SF7 to SF12. This sets how many chirps encode one symbol. Higher SF means slower data but longer range. It is the single most important value to get right.
  • Bandwidth (BW), usually 125, 250, or 500 kHz. This is how wide each chirp sweeps.
  • Coding rate (CR), from 4/5 to 4/8, the forward error correction overhead.
  • Sync word, a single byte that separates networks. 0x34 is reserved for public LoRaWAN, 0x12 is the common default for private links.

If you are decoding your own sensors you already know these because you set them. If you are exploring an unknown signal, you measure the bandwidth off the waterfall and then sweep the spreading factor until frames start falling out. It is trial and error, and it is part of the fun.

The Bench Setup

Nothing exotic is required to receive one LoRa channel. My setup:

  • RTL-SDR Blog V4 dongle. A single LoRa channel at 125 or 250 kHz fits comfortably inside the V4's usable 2.4 MSPS, so you do not need a wideband radio for basic work.
  • A tuned antenna. I used a 868 MHz half-wave whip with an SMA connector. If you are in ITU Region 2, LoRa lives around 915 MHz instead, so tune your antenna to the band you are actually receiving.
  • A known transmitter to practice on. I used a LILYGO T3S3 development board running a plain LoRa transmit sketch at SF7, BW 125 kHz, on 868.1 MHz. Having a signal whose parameters you already know makes the first decode enormously less frustrating.

[Photo: RTL-SDR Blog V4 connected to a whip antenna, with the LILYGO board on the bench beside it acting as the test transmitter]

For wider surveys, where you want to watch several LoRaWAN channels at once, a HackRF One or a BladeRF gives you the sample rate to cover the whole 868 or 915 band. For a single channel, the RTL-SDR is enough, and I did all of the work below with just the dongle.

Software, the Direct Route: SDRangel

If you want frames on the screen with the least ceremony, SDRangel has a LoRa demodulator built in, and it is the fastest path I found. Add the RTL-SDR as a source, set the center frequency to your channel, then drop a "ChirpChat" demod onto the channel. ChirpChat is SDRangel's name for the LoRa/CSS family.

In the demod settings you dial in the same four parameters: bandwidth, spreading factor, coding rate, and the number of preamble chirps. Set the deviation to match your channel bandwidth, pick SF7 to start, and point it at a channel you know is active. When the parameters line up, decoded messages appear in the demod's message log along with the payload bytes and the CRC status. Watching the first frame decode after you finally match the spreading factor is genuinely satisfying.

Software, the GNU Radio Route: gr-lora_sdr

When I want to build the decode into a larger flowgraph, or process a recorded IQ file offline, I reach for GNU Radio. The maintained block set is gr-lora_sdr from the Telecommunications Circuits Laboratory at EPFL. It targets modern GNU Radio (3.8 and later), which matters because several older LoRa decoders stopped building years ago.

Build it against your GNU Radio install following the repository's instructions, and it registers a set of LoRa blocks in GNU Radio Companion. A minimal receive flowgraph looks like this:

  • An RTL-SDR source (via gr-osmosdr) tuned to your channel, sampling at a rate that is an integer multiple of your bandwidth.
  • The frame synchronization block, configured with your spreading factor, bandwidth, and sample rate.
  • The FFT demod, Gray demap, deinterleaver, Hamming decode, and header decode blocks in sequence. That chain is the receive side of the LoRa PHY, undoing each transform the transmitter applied.
  • A message sink or file sink at the end to capture the decoded payloads.

The example flowgraphs shipped in the repository's examples directory are the right starting point. Load one, change the source from a file to your RTL-SDR, set your parameters, and run it. The GNU Radio project documentation is the reference for anything about the blocks themselves.

Reading What Comes Out

Once frames decode, you are looking at the LoRa physical layer payload. What that payload contains depends entirely on what is riding on top of the radio.

If the transmitter is a plain point-to-point LoRa link, like my test board, the payload is often exactly the bytes the developer sent, in the clear. There is no encryption in raw LoRa itself. The chip modulates whatever you hand it. This is the security point worth sitting with: a homebrew LoRa sensor network, or a cheap product that skipped LoRaWAN, may be broadcasting its readings with no confidentiality at all, exactly like those 433 MHz weather stations.

If the transmitter is part of a LoRaWAN network, the picture changes. LoRaWAN wraps the payload in a MAC layer that encrypts the application data with AES-128 using per-device session keys (the NwkSKey and AppSKey established at join). You will decode the PHY, see the device address and frame counter in the clear, and find the application payload as ciphertext you cannot read without the keys. The frame structure is documented in The Things Network's LoRaWAN reference, which is the clearest write-up of the layering I have found.

So the decode tells you two useful things at once: whether a given transmitter bothered with LoRaWAN at all, and if it did, whether the metadata it leaks in the clear (device address, counters, timing) says anything you care about. Plenty of "secure" deployments still expose their whole device inventory through unencrypted frame headers.

A Note on the Law

The usual reminder. Everything above is receive-only analysis. Receiving radio signals is legal in most places; transmitting on these bands without meeting your region's power and duty-cycle limits is not, and decoding communications you are not a party to can run into wiretapping law regardless of whether you retransmit. Practice on your own transmitters, as I did here, and know the rules where you are before you key up anything.

If you have never set up an RTL-SDR before, start with my RTL-SDR beginner's guide and come back once you have a waterfall running. LoRa is a good signal to learn on precisely because it is visually obvious and, once you match the parameters, cleanly decodable. Let me know what your neighbors are broadcasting.

strcpy