What You'll Build
A long-range wireless link between two boards using cheap LoRa modules. An Arduino Uno node reads a temperature sensor and sends a tiny 7-byte packet every 10 seconds. An ESP32 gateway receives it, decodes it, prints the temperature, and tells you the signal strength (RSSI), signal-to-noise ratio (SNR) and how many packets were lost.
You will learn:
- What LoRa is, how it differs from WiFi and Bluetooth, and what spreading factor does
- How to wire an SX1278/SX1276 module to an ESP32 and to a 5 V Arduino Uno without damaging it
- How to send and receive text packets, then compact binary packets
- How to calculate airtime, trade range against speed, and stay within duty-cycle limits
- How to choose an antenna and what range to realistically expect
| Difficulty | Intermediate |
| Build time | About 45 minutes |
| Boards | Arduino Uno (node) and ESP32 (gateway), or two of either |
| Module | SX1278 (433 MHz) or SX1276 (868/915 MHz), such as Ra-02 or RFM95 |
What Is LoRa?
LoRa (Long Range) is a radio modulation from Semtech that trades data rate for range and battery life. It works in the licence-free ISM bands (433, 868 and 915 MHz depending on region) and can reach several kilometres in open country with a modest antenna, while a node sipping a few milliamps can run for months on batteries.
It does this with chirp spread spectrum: instead of switching a carrier on and off, each symbol is a frequency sweep (a "chirp") across the whole channel bandwidth. Receivers can pull these sweeps out of noise that is stronger than the signal itself, which is where the remarkable sensitivity comes from.
| LoRa | WiFi | Bluetooth LE | |
|---|---|---|---|
| Typical range | Kilometres | 20 to 50 m indoors | 10 to 50 m |
| Data rate | 0.3 to about 5 kbps (our settings) | Tens of Mbps | Up to 1 to 2 Mbps |
| Power | Very low | High | Very low |
| Infrastructure | None (point to point) | Router | Phone or hub |
| Good for | Sensors, tracking, remote nodes | Streaming, web | Wearables, short links |
LoRa is not LoRaWAN. LoRa is the radio layer, which is what this article uses. LoRaWAN is a network protocol on top of it, with gateways, a server and encryption. We use raw LoRa for direct point-to-point links.
The Key Idea: Spreading Factor
The most important setting is the spreading factor (SF), from 7 to 12. A higher SF stretches each symbol out over a longer chirp.
- Higher SF: longer range and better sensitivity, but each packet takes much longer on the air and the data rate drops sharply.
- Lower SF: faster, shorter airtime, less range.
Each step up in SF roughly doubles the airtime. Bandwidth (125 kHz by default) and coding rate (4/5 by default) also play a part, but SF is the lever you will use most.
Airtime for a 20-byte payload at 125 kHz bandwidth and coding rate 4/5:
| Spreading factor | Airtime | Effective bit rate | Approx. sensitivity |
|---|---|---|---|
| SF7 | 56.6 ms | 5469 bps | -123 dBm |
| SF8 | 102.9 ms | 3125 bps | -126 dBm |
| SF9 | 185.3 ms | 1758 bps | -129 dBm |
| SF10 | 370.7 ms | 977 bps | -132 dBm |
| SF11 | 741.4 ms | 537 bps | -134.5 dBm |
| SF12 | 1318.9 ms | 293 bps | -137 dBm |
Sensitivity values are the typical figures from Semtech's documentation for a 125 kHz channel. The ranking matters more than the exact number: every SF step buys roughly 2.5 to 3 dB of extra link budget at the cost of double the airtime.
Duty Cycle and the Law
Because LoRa lives in shared licence-free bands, regulators limit how much you may transmit. In the EU 868 MHz band, many sub-bands are limited to a 1 percent duty cycle: you may be on air at most 1 percent of the time. That is where airtime maths becomes practical:
minimum interval = airtime / duty cycle
| Spreading factor | Airtime (20 B) | Minimum interval at 1% duty |
|---|---|---|
| SF7 | 56.6 ms | About 5.7 s |
| SF9 | 185.3 ms | About 18.5 s |
| SF12 | 1318.9 ms | About 132 s |
Rules differ between countries and sub-bands, and some allow more power or other limits (for example frequency-hopping or listen-before-talk in other regions). Always check your local regulations for the frequency, maximum power and duty cycle that apply to you. Choose the module and frequency that are legal where you live: 433 MHz boards are common, but not every region permits every band.
Components
9 itemsModule Pinout
| Pin | Function |
|---|---|
| 3.3V | Power: 3.3 V only |
| GND | Ground |
| SCK | SPI clock |
| MISO | SPI data out of the module |
| MOSI | SPI data into the module |
| NSS (CS) | SPI chip select |
| RST | Reset |
| DIO0 | Interrupt: signals "packet received" or "packet sent" |
| ANT | Antenna connection |
The Ra-02 has a 2 mm pin pitch that does not fit a breadboard, so it needs a small adapter board. The RFM95 has the same 2 mm pitch. Ready-made modules with 2.54 mm pins exist and are easier for prototyping.
Wiring the ESP32
The ESP32 is a 3.3 V device, so it connects directly using the default VSPI pins.
| LoRa module | ESP32 |
|---|---|
| 3.3V | 3V3 |
| GND | GND |
| SCK | GPIO 18 |
| MISO | GPIO 19 |
| MOSI | GPIO 23 |
| NSS | GPIO 5 |
| RST | GPIO 27 |
| DIO0 | GPIO 26 |
Wiring the Arduino Uno
The Uno's logic is 5 V, but the SX127x is 3.3 V only. Signals from the Uno into the module must be reduced to 3.3 V. Signals from the module into the Uno are fine.
| LoRa module | Uno | Level handling |
|---|---|---|
| 3.3V | Separate 3.3 V regulator | See warning below |
| GND | GND | Common ground |
| SCK | D13 | Divider (5 V to 3.3 V) |
| MISO | D12 | Direct |
| MOSI | D11 | Divider (5 V to 3.3 V) |
| NSS | D10 | Divider (5 V to 3.3 V) |
| RST | D9 | Divider (5 V to 3.3 V) |
| DIO0 | D2 | Direct |
A simple divider is 1 k from the Uno pin to the module pin, with 2 k from the module pin to GND, giving about 3.3 V. A 4-channel bidirectional level shifter does the same job more neatly.
🚨 Danger
**Never power the module from 5 V**, and never drive its inputs with 5 V logic. Either will destroy it. Also, the Uno's on-board 3.3 V pin is weak (about 50 mA), and a LoRa transmitter can draw well over 100 mA in bursts at higher power. Power the module from a dedicated 3.3 V regulator with a good capacitor close to the module, and join its ground to the Uno's. **Attach the antenna before you transmit.** Sending without one can damage the radio's output stage.
Steps
- 1Check your local rules and pick a legal frequency for your module (433, 868 or 915 MHz).
- 2Attach an antenna of the matching band to each module before powering anything.
- 3Wire the ESP32 and Uno as shown, powering each module from 3.3 V only.
- 4In Library Manager install **LoRa by Sandeep Mistry**.
- 5Upload the sender sketch to the Uno and the receiver sketch to the ESP32, then open the receiver Serial Monitor at 115200 baud.
Step 1: The Sender
This sketch works on both the Uno and the ESP32; the pins switch automatically. Three settings matter for a clean, private link:
LoRa.begin(FREQUENCY)must use the same frequency on both ends and one that is legal in your region.setSyncWord(0xA5)is a private network identifier. Radios with a different sync word ignore your packets. Do not use0x34, which is reserved for the public LoRaWAN networks.enableCrc()adds a checksum so corrupted packets are dropped instead of delivered as garbage.
LoRa.endPacket() blocks until the packet has been sent, so the loop takes at least one airtime longer than the delay.
Step 2: The Receiver
The receiver polls parsePacket(). When a packet arrives it prints the payload and two quality metrics:
- RSSI (received signal strength) in dBm: more negative is weaker. Around -30 dBm is extremely strong, and near -120 dBm is the edge of what SF7 can hear.
- SNR (signal-to-noise ratio) in dB: LoRa can decode with a negative SNR, down to about -7.5 dB at SF7 and about -20 dB at SF12. A positive SNR means a comfortable link.
Step 3: Send Real Data in Compact Binary
Text like "hello 12" is easy to read but wasteful. Airtime depends directly on payload length, so real sensor nodes send packed binary structs. The example uses a 7-byte packet:
| Field | Type | Bytes | Meaning |
|---|---|---|---|
| nodeId | uint8_t | 1 | Which node sent it |
| seq | uint16_t | 2 | Packet counter |
| tempX10 | int16_t | 2 | Temperature in tenths of a degree (253 = 25.3 C) |
| battMv | uint16_t | 2 | Supply voltage in millivolts |
The sequence number is the clever part: the receiver compares it with the previous one, and any gap is a lost packet. That gives you a free measure of link quality.
Both ends must use the exact same struct layout, including __attribute__((packed)) so the compiler does not insert padding bytes. The Uno and the ESP32 agree on byte order (both are little-endian), so a struct sent from one can be read directly on the other.
Node (Arduino Uno, TMP36 on A0):
Gateway (ESP32): it ignores any packet whose size is not exactly 7 bytes, decodes the rest, and counts losses using the sequence number. The subtraction is done in uint16_t, so the counter wraps around correctly at 65535.
Tuning the Radio
The library's defaults (SF7, 125 kHz, coding rate 4/5) suit short links and fast updates. To go further, change these identically on both ends:
| Setting | Call | Effect |
|---|---|---|
| Spreading factor | LoRa.setSpreadingFactor(10) | Range up, speed down (7 to 12) |
| Bandwidth | LoRa.setSignalBandwidth(62.5E3) | Narrower is more sensitive but slower |
| Coding rate | LoRa.setCodingRate4(8) | More error correction, longer airtime |
| TX power | LoRa.setTxPower(17) | 2 to 20 dBm on PA_BOOST |
A good workflow is to start at SF7 with the two boards on the same desk, confirm packets flow, then move apart and raise the SF only if you need more range. If you change a setting on one end and not the other, the link silently fails, because the receiver is listening for a different kind of chirp.
Antennas and Realistic Range
The antenna is the biggest range lever you have, and it costs a few cents. A quarter-wave wire is a good start:
| Frequency | Quarter-wave length |
|---|---|
| 433 MHz | 17.3 cm |
| 865 MHz | 8.7 cm |
| 868 MHz | 8.6 cm |
| 915 MHz | 8.2 cm |
Tips: mount the antenna vertically and as high as you can, away from metal and the board itself, and use an antenna cut for the band you actually run.
Range depends on the environment more than the module. Open line of sight can reach kilometres at a high SF, while buildings, trees and walls cut that dramatically, and inside a city a few hundred metres to a kilometre or two is more typical. Rather than trust a headline figure, measure it: walk away with the sender while watching RSSI and SNR on the receiver, and note where packets begin to drop. That is your real range for your setup.
Reading RSSI as Distance (Roughly)
RSSI falls with distance, and you can turn it into a rough estimate with the log-distance model: d = 10 ^ ((RSSI@1m - RSSI) / (10 x n)), where n is the path-loss exponent (about 2 in free space, 2.5 to 3 with obstacles). Using -59 dBm at 1 metre:
| RSSI | n = 2.0 | n = 2.5 | n = 3.0 |
|---|---|---|---|
| -50 dBm | 0.4 m | 0.4 m | 0.5 m |
| -60 dBm | 1.1 m | 1.1 m | 1.1 m |
| -70 dBm | 3.5 m | 2.8 m | 2.3 m |
| -80 dBm | 11.2 m | 6.9 m | 5.0 m |
| -90 dBm | 35.5 m | 17.4 m | 10.8 m |
The spread between columns is the point: indoors and around obstacles, RSSI is a very rough distance guide. Use it to compare positions, not to measure metres. The table is a simple model, and LoRa at long range depends on a calibration you must do yourself.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| "LoRa init failed" | Wiring, no 3.3 V power, wrong NSS/RST | Recheck SPI wiring, power from a stable 3.3 V, confirm pins in the sketch. |
| Init works but nothing is received | Frequency, sync word or SF mismatch | Make every radio setting identical on both ends. |
| Very short range | No antenna, wrong band, poor placement | Attach the right antenna, raise it, move away from metal. |
| Works at 5 cm, fails at 5 m | Antenna problem or low supply | Check the antenna and the 3.3 V rail during transmit. |
| Uno resets when transmitting | Supply sag from the 3.3 V pin | Use a dedicated regulator with a capacitor. |
| Module gets hot or dies | 5 V applied, or transmitting with no antenna | Use 3.3 V only; always connect the antenna. |
| Garbage characters | CRC off, or a nearby transmitter using another protocol | Enable CRC and use a private sync word. |
| Lost packets reported | Weak link or collisions | Check RSSI and SNR; raise SF or improve the antenna. |
| ESP32 SPI conflict | Another device on the same pins | Use a separate chip select, or move the module to the HSPI bus. |
Ideas to Extend
- Multiple nodes. Give each node an ID and let the gateway track them separately using the node ID and sequence number.
- Acknowledgements. Add a reply from the gateway so nodes can retry if a packet is lost.
- Bridge to the internet. Forward gateway data to your dashboard with the ESP32 MQTT tutorial.
- Battery operation. Put the node to sleep between transmissions. See the ESP32 deep sleep guide for the techniques.
- Compare with the alternatives. For a short-range link without a router, try ESP-NOW; for a wired field bus, see RS485 with MAX485.
FAQ
Do I need a licence to use LoRa? Not for the licence-free ISM bands, provided you stay within your region's limits for frequency, power and duty cycle. Check your local rules.
Which frequency should I buy? The one that is allowed where you live. 433 MHz modules are widely sold; 868 MHz and 915 MHz are common regional choices. The module, the antenna and the code must all match.
Is LoRa secure? Raw LoRa as used here is not encrypted, and the sync word is a filter, not security. Add your own encryption of the payload if the data is sensitive. LoRaWAN includes encryption as part of the protocol.
Can I send images or audio? No. Practical LoRa rates are a few kilobits per second at best. It is meant for small packets of sensor data.
How many bytes can a packet hold? The library allows up to 255 bytes, but shorter is better for airtime and reliability.
Why use a spreading factor of 12 if it is so slow? For the longest range from a very low-power node that sends rarely. At SF12 a 20-byte packet takes over a second, so use it only when you must.
Related on SolderHub
- RS485 with MAX485 and Arduino
- ESP32 Pinout: The Complete Guide
- Arduino Uno Pinout: The Complete Guide
- ESP32 Deep Sleep and Battery Life
Published on SolderHub