RF physical foundations and practical implementation of LoRa 433 MHz NLOS link

What's inside this article βŒ„
  • Creation of an electromagnetic wave by alternating current and electrons
  • Why 433 MHz penetrates concrete better than traditional 2.4 GHz Wi-Fi
  • Chirp Spread Spectrum (CSS) modulation and how chirps work
  • The impact of Spreading Factor (SF), BW, and CR parameters on range and speed
  • Amplifier burnout without an antenna and Voltage Standing Wave Ratio (VSWR)
  • Voltage drops (brownouts) and a hardware method for isolating burned silicon
  • Connecting the SX1278 chip via SPI to ESP32 and Raspberry Pi Pico 2W boards
  • Practical RSSI analysis and penetrating 140 meters of urban environment (NLOS)

Introduction

In this article, we will solve a practical problem – transmitting a signal over a distance of 140 meters in Non-Line-of-Sight (NLOS) conditions through dense urban environments and reinforced concrete floors.

To solve this problem, LoRa (Long Range) technology is perfectly suited – a radio modulation method developed to transmit small amounts of data at very low power consumption and high noise immunity.

In this article, we will analyze the theory of radio physics, the principles of LoRa modulation, and also assemble transmitting and receiving devices based on ESP32, Raspberry Pi Pico 2W boards, and SX1278 chips.


Radio Physics. From the Electron to the Radio Wave

Any radio transmission starts with alternating current. The SX1278 chip generates a high-frequency alternating current with a frequency of 433 MHz and feeds it to the antenna.

An antenna is simply a metal conductor. Any metal contains free electrons. Under the influence of an alternating voltage, they begin to oscillate back and forth at the same frequency of 433 MHz.

Here begins fundamental physics.

From the point of view of classical physics

Any moving electrical charge creates a magnetic field around itself. Since the electrons in the antenna are constantly changing direction, this magnetic field is also constantly changing (pulsating).

According to the laws of electromagnetism (Maxwell’s equations):

  • A changing magnetic field generates an electric field (Faraday’s law).
  • A changing electric field generates a magnetic field (Maxwell’s addition).

These two pulsating fields latch onto each other, mutually support each other, and at some point detach from the antenna. They fly into space at the speed of light. This is an electromagnetic wave.

The reception process works exactly in reverse. When this electromagnetic wave reaches the receiving antenna, its changing electric field begins to “push” the free electrons in the metal. The electrons start moving at the same frequency – 433 MHz.

A microscopic alternating current (induced current) arises in the receiving antenna. The transceiver reads this tiny current, passes it through an amplifier (LNA, Low Noise Amplifier), and demodulates it back into digital zeros and ones.

From the point of view of quantum physics

In the quantum world, any change in the speed or direction of a charge’s movement is accompanied by radiation. An accelerating electron emits a photon – an elementary particle, a quantum of the electromagnetic field. It is photons that carry energy through space.

So, no magic: every time an electron in an antenna changes its direction of movement, it emits a photon; photons fly from the transmitter to the receiver at the speed of light.

At the same time, radio waves, infrared radiation, visible light, and X-rays are physically the same thing. It is all a stream of photons flying at the speed of light.

A photon is a quantum object (wave-particle duality). It behaves as a particle at the moment of emission and absorption, and as a wave during its flight. In a radio signal, there are so many photons (quintillions of pieces per second) that at the macro level they merge into a continuous wave. Just as individual Hβ‚‚O molecules merge into the single smooth surface of a lake.

The only difference between radio and light is the energy of the photons. It is strictly linked to the frequency by Planck’s formula: $$ E = hf $$

where $h$ is Planck’s constant, and $f$ is the frequency.

Where does the photon get this frequency? From the source that generated it. The electron in the antenna oscillated at a frequency of 433 MHz β†’ it generated a photon with an energy exactly corresponding to 433 MHz.

We can distinguish them: a 433 MHz photon is a radio wave, a ~500 THz photon is visible light, even higher is an X-ray. It is the exact same object, just with different energies.

The frequency of a photon is not a abstract mathematical substitution. The electric and magnetic fields carried by this particle actually oscillate 433 million times per second.

Photons reach the metal of the receiving antenna and are absorbed by its free electrons, transferring their energy to them. The energy of a single radio photon is infinitesimally small, but since a giant coherent wave of such particles flies from the transmitter, their combined impact forces billions of electrons in the receiver to swing synchronously. A current arises, which the chip then registers.


Wavelength and Physics of Obstacles

A key characteristic of any electromagnetic wave is its frequency ($f$). It is strictly linked to the wavelength ($\lambda$) through the speed of light ($c$).

The formula is simple: $$ \lambda = \frac{c}{f} $$

Let’s calculate the wavelength for two popular unlicensed (ISM) bands – Wi-Fi (2.4 GHz) and LoRa (433 MHz).

For convenience, we use the speed of light in megameters per second (~300) so that the frequency in megahertz can be substituted directly:

  • 2.4 GHz (Wi-Fi, Bluetooth): $300 / 2400 = 0.125$ m (12.5 centimeters).
  • 433 MHz (LoRa, key fobs): $300 / 433 \approx 0.69$ m (69 centimeters).

It is the wavelength that determines how the signal will behave when encountering obstacles. A rule works in radio physics: a radio wave is capable of effectively bending around obstacles (diffraction) whose size is comparable to or less than half of its wavelength.

  • A 2.4 GHz wave with a length of 12.5 cm fades out because of a pipe or a brick. It reflects off them or is absorbed by the material (especially concrete containing moisture), quickly losing energy. This is exactly why home Wi-Fi often dies behind two load-bearing walls.

  • A 433 MHz wave with a length of almost 70 cm behaves completely differently. It is capable of bending around pipes, metal profiles, and large rebar meshes inside reinforced concrete structures. For such a wave, a building wall represents a translucent medium, not a solid mirror.

But even with the good penetrating power at 433 MHz, we face another problem. In factories, electrical substations, and amidst dense urban development (our target 70 meters NLOS), the signal rarely travels in a straight line.

Due to the abundance of metal structures and automation cabinets, the radio wave repeatedly reflects off surfaces. The effect of Multipath Propagation occurs.

The receiving antenna does not receive a single clean signal, but several of its copies at once, reflected from different walls and pillars. These copies arrive with microscopic time delays. In traditional communication systems (for example, in cheap 433 MHz gate remotes with ASK/FSK modulation), these reflections superimpose on each other in anti-phase and cancel out the useful signal.

Multipath Fading arises. In order for the signal not only to pass through floors but also to be correctly decoded by the receiver under conditions of severe reflections and radio noise, a special approach to coding information in the wave is required.

And here is the solution – LoRa modulation.


Linear Frequency Modulation LoRa

Most classic digital radio channels (for example, the same gate remotes or simple telemetry sensors) use frequency shift keying (FSK).

Roughly speaking: the transmitter radiates at a frequency of 433.05 MHz – this is a logical 0, it switches to 433.15 MHz – this is a logical 1. This scheme is simple, but extremely vulnerable to noise and reflections (that very multipath fading).

LoRa technology took a different path. It is based on CSS modulation (Chirp Spread Spectrum – linear frequency modulation).

Chirp Spread Spectrum – linear frequency modulation

Instead of jumping between two fixed frequencies, LoRa generates chirps. A chirp is a radio signal whose frequency continuously increases (Up-chirp) or decreases (Down-chirp) over time, “smearing” the energy across the entire allocated channel.

LoRa encodes data at the starting point of the chirp. The chirp always sweeps through the entire channel, but it does not always start from the exact same place.

If we could hear LoRa with our ears, it would sound like a wailing police siren – a sound that smoothly changes its pitch from low to high.

Let’s say LoRa uses the band from 430 to 431 MHz. This is a 1 MHz wide ruler. The bit is embedded at the starting point. For example:

  • Start from 430 MHz β†’ this is “0”
  • Start from 430.5 MHz β†’ this is “1”

If the chirp starts from the middle of the ruler and runs to the right – it reaches the right edge (431 MHz), jumps to the left edge (430 MHz), and runs the remaining distance to the starting point.

But there are many starting positions! Therefore, one symbol (chirp) encodes more than one bit. This is determined by the SF – Spreading Factor:

  • If we divide the band into 2 positions β†’ then one symbol (chirp) will transmit 1 bit of information
  • 4 positions β†’ 2 bits of information
  • 8 positions β†’ 3 bits
  • 128 positions β†’ 7 bits (this is SF7)
  • 4096 positions β†’ 12 bits (this is SF12)

That is, the number of positions (chips – from the English chip, a signal element in communication theory, not to be confused with a microchip or chirps) into which the band is divided: $N = 2^n$.

The human ear is able to isolate the sound of a siren even in a very noisy crowd precisely because of its continuous change. Similarly, a LoRa receiver is capable of recognizing a mathematically correct chirp even when it is below the background radio noise level (negative SNR – Signal-to-Noise Ratio).

Reflected signals arriving with a delay do not break the picture either: the receiver simply synchronizes to the strongest chirp, and treats the rest as white noise.

The higher the SF, the slower the data is transmitted, because the receiver must be able to calculate the starting position of the chirp by its shift among others (they all have the same “tilt” angle). Therefore, the more starting positions there are, the longer the receiver needs to observe each chirp – to accumulate more energy for the mathematical filter and avoid confusing adjacent segments under noisy conditions.

But this has a huge advantage: as SF increases, the penetrating power of the signal and the transmission range increase. A long chirp allows the receiver to accumulate more signal energy and “pull” the data out of dense radio noise.


LoRa Frame (PHY Frame) and Overhead

For two devices to communicate, chirps alone are not enough. They need to be packaged into a standardized packet – a physical frame (PHY Frame). Whether we are sending a massive JSON or just one byte (for example, the command 0x01 to close a relay), the frame structure remains the same:

  1. Preamble: a series of basic Up-chirps. This is a loud shout into the ether – a signal for the receiver to wake up. This is necessary to synchronize the receiver and transmitter clocks.
  2. Header and Header CRC: contains metadata – the length of the payload and coding information.
  3. Payload: the data itself.
  4. CRC: a checksum for integrity verification. If reflections in the ether still damaged a bit, the frame will be discarded.

Exactly how much time it will take to transmit a frame is determined by the LoRa “triangle of compromises”.


The Triangle of Compromises: SF, BW, and CR

When setting up a radio channel, we operate with three key parameters that directly affect stealth and range:

  • Bandwidth (BW): the frequency range over which the chirp is “smeared”. Usually, this is 125 kHz, 250 kHz, or 500 kHz. The wider the channel, the faster the data is transmitted, but the lower the receiver’s sensitivity.
  • Coding Rate (CR): the level of redundancy (FEC - Forward Error Correction). At CR 4/8, for every 4 bits of data, 4 recovery bits are transmitted. This saves the frame from interference but increases packet length.
  • Spreading Factor (SF): The most important parameter for OPSEC. It takes values from SF7 to SF12 and determines the steepness (speed) of our chirp.

When increasing the SF by one step (for example, from SF7 to SF8), the transmission time of each symbol doubles:

SF7: Fast chirps. The signal is short, saves battery, harder to detect with a radio scanner, but the communication range is minimal (poorly penetrates concrete).

SF12: Slow, drawling chirps. Much greater penetrating power, the signal is demodulated even from under dense noise. However, transmitting 10 bytes at SF12 can take a whole second – in the world of radio intelligence, this glow is akin to a signal bonfire.


Specifics of Working with RF Equipment

Working with radio transmitting equipment, including modules based on SX1278, requires compliance with several hardware limitations, mounting, and operating requirements, as errors can lead to performance degradation or equipment damage.

Electrodynamics of Antennas and VSWR

The main rule: never apply power and initiate transmission (TX) on a module to which no antenna is connected.

If you remove the antenna from a LoRa transmitter and run a packet sending script, there is a high probability of damaging the silicon chip.

Why does this happen? The answer lies in a parameter called VSWR (Voltage Standing Wave Ratio).

When the SX1278 chip generates a packet, its built-in Power Amplifier (PA) pumps high-frequency alternating current and sends it along the board trace to the antenna output.

The antenna in this system acts as a “matching transformer” between the conductor and free space. If the antenna is connected and its length perfectly matches the wavelength (433 MHz), almost all the energy goes into the ether in the form of photons. VSWR is ideally 1.0.

But if there is no antenna, the resistance at the end of the circuit becomes infinite (open circuit). The electromagnetic energy has nowhere to go. Physical reflection occurs: all the power that the amplifier pushed forward bounces off the empty connector and returns back into the chip’s crystal in the form of a reflected (standing) wave.

But if a phase wire sticks out of a house wall, why doesn’t anything burn out at the substation? There is voltage there, but no current – because there is no closed circuit. The generator at the substation simply pushes nothing into this wire. It pushes only where there is a load. The generator does not push energy into the void.

But in the case of a radio transmitter, everything is different. A transmitter is not a passive voltage source. It is an amplifier that actively pumps energy, regardless of what is at the end. It has already accelerated the electrons, already created a wave in the cable – and this wave flies forward.

Then it reaches the empty connector, reflects, and flies back. It arrives at the chip’s output transistor – a small one, designed exactly for its load – and adds to what it is already generating. The voltage on the transistor jumps above the maximum. The transistor burns out.

By the way, the wavelength at 50 Hz is 6000 kilometers. A two-meter wire simply does not exist for it as an antenna or a transmission line. No reflections, no standing waves.

Power Issues (Brownouts)

The second classic problem is unstable power. In receive mode (RX) or deep sleep mode, the LoRa chip consumes microamperes. But at the moment of transmitting a frame (TX) at maximum power, consumption can sharply jump to 120-150 mA.

If the SX1278 module is powered by long, thin DuPont wires from the 3.3V pin of the ESP32 or Raspberry Pi board, this sudden current spike can cause a brief voltage drop.

If the voltage drops below the Brownout Detector threshold, the ESP32 performs a hardware reset. Externally, this may look like the program hanging on packet transmission, although a microcontroller reboot is actually occurring.

If the drops are particularly deep, the problem manifests itself even earlier: the chip doesn’t reach transmission at all – the program hangs on the initialization of the SX1278 chip.

Solutions:

  1. Use a power supply with a sufficient current margin, as well as short and low-resistance 3.3V and GND lines.
  2. Install a 100-470 Β΅F capacitor directly next to the LoRa module. During short-term consumption peaks, the capacitor acts as a local energy storage and reduces the voltage drop on the power line.
Hardware Debugging: Switching Roles

How do you know that the VSWR rule was violated and the chip burned out? The most unpleasant thing about the burnout of the power amplifier (PA) is that externally the module looks absolutely working. It initializes successfully via the SPI bus, the microcontroller writes “LoRa OK”, but packets do not reach the receiver even from a short distance.

This happens because the receive and transmit paths are separated inside the SX1278 chip. Only the power amplifier for transmission (TX) burns out. The low noise amplifier for reception (LNA) remains alive.

Fault Localization Method

If you have two modules, the operability of the transmitting and receiving paths can be tested separately. To do this, configure a known-good module for transmission, and the module under test for reception. If the tested module stably receives packets, but is unable to transmit them with the correct configuration and a working antenna, this indicates a possible malfunction of the transmit path.

In this case, the module can only be used as a receiver or must be replaced.


Building an RF Bridge on ESP32 and RPI Pico 2W

Let’s move on to the practical implementation. The system consists of two nodes: a transmitter and a receiver 70 meters away behind concrete walls. ESP32 and Raspberry Pi boards are used as computational cores, while the radio channel is managed by LoRa Ra-02 modules based on the SX1278 chip.

Image

Architecture and Wiring (SPI)

The SX1278 transceiver communicates with the microcontroller over the classic high-speed SPI (Serial Peripheral Interface) bus.

We discussed the SPI bus in detail in one of the previous articles. In its classic form, it consists of 4 wires: SCK, MOSI, MISO, and CS.

More details about each of the pins
  1. SCK (Serial Clock): clock signal. This is a metronome. It is controlled only by the Master. It sets the rhythm of the whole system. One clock tick – one transmitted bit.

  2. MOSI (Master Out, Slave In): transmission line from the Master. Data flies along this wire from the main processor to the peripheral (for example, we send a picture to a display). In modern chips, it is often labeled as SDO (Serial Data Out) on the master side and SDI (Serial Data In) on the peripheral side.

  3. MISO (Master In, Slave Out): response line from the peripheral. Data flies along this wire from the sensor back to the main processor.

  4. CS (Chip Select) or SS (Slave Select): chip select. This is an individual control wire (or selector wire). It is needed to show a specific chip that we are going to talk to it now. Usually, it is inverted (active low). As long as the CS line is a logical 1 (high voltage), the chip sleeps and ignores the bus. The Master pulls the line down to 0 – the chip wakes up.

In addition to the four standard SPI pins, we also need:

  • DIO0 (Digital I/O 0): hardware interrupt line. Instead of constantly polling the module in a loop, the microcontroller can sleep or do its own things. As soon as the SX1278 successfully demodulates the frame and checks the CRC, it will trigger the DIO0 pin, causing an interrupt in the ESP32/RPI.

  • 3.3V: power supply. It should be noted that devices with different logic levels (3.3 V, 5.5 V) require verification of input and output voltage compatibility, and in some cases – the use of level shifters.

  • GND: ground. Necessary for the correct operation of logic.

This results in 7 wires.

Wiring to ESP32

Let’s look at the ESP32 pinout (may vary depending on the board revision):

Wiring diagram to ESP32:

  • CS β†’ GPIO 5
  • SCK β†’ GPIO 18
  • MISO β†’ GPIO 19
  • MOSI β†’ GPIO 23
  • DIO0 β†’ GPIO 36

Wiring to RPI Pico 2W

Let’s look at the RPI pinout (may vary depending on the board revision):

Wiring diagram to RPI Pico 2W:

  • CS β†’ GPIO 9
  • SCK β†’ GPIO 10
  • MISO β†’ GPIO 8
  • MOSI β†’ GPIO 11
  • DIO0 β†’ GPIO 27

Wiring Result

If the LoRa Ra-02 module does not fit on the breadboard due to its width, the module can be connected to the controller using M-F jumper wires and placed on top.

Connect the wires according to the diagram and we get the assembled receiver and transmitter:

Image

Software Implementation and Setup

Flashing Boards with MicroPython

  1. MicroPython must be installed on each of the boards. To do this, download thefirmware for ESP32, as well as the firmware for RPI Pico 2W.

  2. Install the CP210x Universal Windows Driver from Silicon Labs.

  3. After that, install the Thonny IDE, and flash each of the boards:

    • Run β†’ Configure Interpreter
    • “Install or update MicroPython”
    • MicroPython (ESP32) / MicroPython (Raspberry PI Pico)
    • Select the COM port
    • Click “Install or update MicroPython”
    • Select the downloaded .bin
    • Install

Important: to switch between boards, you need to change the interpreter in the settings: Run β†’ Configure interpreter, and then select MicroPython (ESP32) or MicroPython (Raspberry PI Pico) and click OK.

Besides this - in Thonny, you can disable “single instance mode” and open a separate window for each board.

Driver for the SX1278 Chip

To work with the SX1278 chip installed on the LoRa Ra-02 module, we will use the modified driver wybiral/micropython-lora.

Connect the RPI, click on the Stop/Restart backend button, then open the driver file lora.py in Thonny, and save it directly to the board (not to the computer): File β†’ Save as β†’ Raspberry Pi Pico.

Image

We do the exact same thing with the ESP32.

Receiver Logic (RPI)

The incoming packet processing logic works in asynchronous mode.

In the previous article, we read the raw signal from the SPI bus and decoded the data. In the code, the SPI bus speed is set to 1 million baud so that the logic analyzer can correctly read the signal, but it can be increased if necessary.

The driver is configured for a frequency of 433.0 MHz and an SF (Spreading Factor) equal to 7.

from machine import Pin, SPI
import time
from lora import LoRa

SPI_ID = 1
PIN_SCK = 10
PIN_MOSI = 11
PIN_MISO = 8
PIN_CS = 9
PIN_RX = 27  

print("Initialization ...")

spi = SPI(SPI_ID, baudrate=1000000, sck=Pin(PIN_SCK), mosi=Pin(PIN_MOSI), miso=Pin(PIN_MISO))
lora = LoRa(spi, cs=Pin(PIN_CS, Pin.OUT), rx=Pin(PIN_RX, Pin.IN))

packet_flag = False
packet_data = b""

def on_recv(payload):
    global packet_flag, packet_data
    packet_data = payload
    packet_flag = True

lora.on_recv(on_recv)
lora.recv()

print("Initialization complete.")
print("Listening RF 433.0 MHz ...")


while True:
    if packet_flag:
        packet_flag = False
        try:
            msg = packet_data.decode()
            rssi = lora.get_rssi()
            print(f"Received packet[{rssi} dBm - 433.0 MHz] #{msg}")
        except Exception as e:
            print(f"Err: {e}")
            
    time.sleep_ms(10)

Transmitter Logic (ESP32)

To exert minimal impact on the radio airwaves, we set the minimum transmitter power: set_tx_power(2). The SPI bus speed matches the receiver on the RPI (1 million baud).

The transmitter synchronously sends packets 0, 1, 2, and so on.

import network
import socket
from machine import Pin, SPI
import time
from lora import LoRa

SPI_ID = 2
PIN_SCK = 18
PIN_MOSI = 23
PIN_MISO = 19
PIN_CS = 5
PIN_RX = 36  

print("Initialization ...")

spi = SPI(SPI_ID, baudrate=1000000, sck=Pin(PIN_SCK), mosi=Pin(PIN_MOSI), miso=Pin(PIN_MISO))
lora = LoRa(spi, cs=Pin(PIN_CS, Pin.OUT), rx=Pin(PIN_RX, Pin.IN))

tx_power = 2
lora.set_tx_power(tx_power)

print("Initialization complete.")

counter = 0

while True:
    print(f"Sending packet #{counter} - 433 MHz")
    lora.send(str(counter))
    counter = counter + 1
    time.sleep_ms(500)

Analyzing Signal Quality, the RSSI Indicator

RSSI (Received Signal Strength Indicator) – the power level of the received signal.

Measured in dBm (decibel-milliwatts). It is always a negative number – the closer to zero, the stronger the signal:

For example:

  • RSSI = -30 dBm β†’ very strong signal
  • RSSI = -60 dBm β†’ good
  • RSSI = -80 dBm β†’ weak
  • RSSI = -120 dBm β†’ on the edge, packets will start dropping

There is also SNR (Signal-to-Noise Ratio). If RSSI says how loud the signal is, then SNR says how clean the signal is relative to the noise. You can have a high RSSI but a bad SNR (the signal is strong, but there is just as much noise).

The receiver logs the RSSI via lora.get_rssi().

Test 1: RF=433.0 MHz, tx_power=2, BW=125 kHz, SF=7, distance 10 cm

Place both devices at a distance of 10 cm from each other, set the minimum transmission power set_tx_power(2) and look at the result:

Image

It can be seen that the RSSI stays at the level of -65 dBm, that is, on the verge of a weak signal.

Test 2: RF=433.0 MHz, tx_power=17, BW=125 kHz, SF=7, distance 10 cm

Set the maximum transmission power set_tx_power(17) and test the RSSI at the same distance (10 cm):

Image

A completely different matter! We see an RSSI of -14 dBm, which is an exceptionally strong signal.

Test 3: RF=433.0 MHz, tx_power=17, BW=125 kHz, SF=12, maximum range (NLOS)

Transmission power: set_tx_power(17), in addition, we switch SF to 12: lora.set_spreading_factor(12). Connect the ESP32 to an external power source:

Image

We take the device to the maximum distance from the receiver in an urban environment without direct line of sight (NLOS urban environment) and take measurements:

Image

Between packets #18 and #24, losses and a checksum failure are visible.

The maximum distance was over 140 meters, provided that the receiver is located deep inside the building, there is no line of sight, and the signal penetrates several reinforced concrete buildings.


Authorship and Disclaimer

This engineering and research article is an independent work by Mark Chesnavskii (2026). The structure of the material, analytical reviews, source code, and practical implementations (PoC) represent original authorial work. Any AI-generated content based on this material, including reproduction, excerpt extraction, and summarization, must be accompanied by appropriate attribution to the original author and a link to the original source.

The author is not responsible for any potential physical or material damage, equipment failure, or any legal consequences arising from attempts to replicate the described actions. All actions are performed entirely at your own risk.