Learning Goals 5 min
Two LoRa modules, no internet, no gateway — just radios talking to each other across a field, a building, or a town. By the end of this lesson you will:
- Wire an SX1276 / RFM95 LoRa module to an Arduino over SPI.
- Use the LoRa library (by Sandeep Mistry) to send and receive plain text packets.
- Measure range: walk away from the transmitter and find where packets start dropping.
Warm-Up 10 min
Hardware:
- 2 × LoRa modules (RFM95, RA-02, or Heltec WiFi-LoRa boards).
- 2 × antennas attached.
- 2 × Arduino-class boards (UNO, ESP, Nano).
Install the LoRa library
Library Manager → search "LoRa" (by Sandeep Mistry). Install. This library handles the SX1276's SPI commands and exposes a simple LoRa.beginPacket() / LoRa.write() / LoRa.endPacket() API.
New Concept · LoRa over SPI 25 min
Wiring (RFM95 to UNO / ESP)
| RFM95 pin | UNO pin | ESP8266 pin |
|---|---|---|
| VCC | 3.3 V (not 5 V!) | 3.3 V |
| GND | GND | GND |
| MISO | D12 | D6 (GPIO12) |
| MOSI | D11 | D7 (GPIO13) |
| SCK | D13 | D5 (GPIO14) |
| NSS / CS | D10 | D8 (GPIO15) |
| RST | D9 | D0 (GPIO16) or any digital |
| DIO0 | D2 (interrupt-capable) | D1 (GPIO5) |
UNO note: the RFM95 is 3.3 V logic. Inputs (MOSI, NSS, SCK, RST) need level-shifters from 5 V UNO outputs. Use 1k+2k voltage dividers, a TXS0108E, or just use a 3.3 V Arduino (Pro Mini 3.3 V, ESP8266, Nano 33 BLE) to avoid the hassle.
Library API
#include <SPI.h>
#include <LoRa.h>
const long FREQ = 433E6; // 433.0 MHz — match to your region
void setup() {
Serial.begin(9600);
while (!Serial);
LoRa.setPins(10, 9, 2); // NSS, RESET, DIO0
if (!LoRa.begin(FREQ)) {
Serial.println("# LoRa init failed");
while (true);
}
LoRa.setSpreadingFactor(7);
LoRa.setSignalBandwidth(125E3);
LoRa.setCodingRate4(5);
LoRa.setTxPower(14); // 14 dBm = ~25 mW
Serial.println("# LoRa ready");
}Five settings:
- Frequency: 433 / 868 / 915 MHz for your region.
- Spreading factor (7..12): speed vs range.
- Bandwidth: 125 kHz default; lower = more range, slower.
- Coding rate: 4/5 default; 4/8 = more error correction, slower.
- Tx power: 2..20 dBm. 14 dBm safe and legal.
Both ends must match exactly on all five settings or they won't hear each other.
Transmit
LoRa.beginPacket();
LoRa.print("hello ");
LoRa.print(millis());
LoRa.endPacket();Receive (polling)
void loop() {
int packetSize = LoRa.parsePacket();
if (packetSize) {
String s;
while (LoRa.available()) s += (char)LoRa.read();
Serial.print("# RX: \""); Serial.print(s); Serial.print("\" RSSI ");
Serial.println(LoRa.packetRssi());
}
}LoRa.packetRssi() returns the received signal strength in dBm. Closer to 0 = stronger. Below −110 dBm = packets often dropped.
Receive (callback)
void onPacket(int packetSize) {
String s;
while (LoRa.available()) s += (char)LoRa.read();
Serial.print("# RX: "); Serial.println(s);
}
void setup() {
// ... LoRa.begin ...
LoRa.onReceive(onPacket);
LoRa.receive(); // enter continuous receive mode
}Better for low-power: the radio handles waking on packet arrival, your loop stays free.
Worked Example · Talk and listen 25 min
Step 1 — wire both modules
Two identical setups. Each Arduino + LoRa module + antenna.
Step 2 — TX sketch on board A
// L04-18 · LoRa TX
#include <SPI.h>
#include <LoRa.h>
const long FREQ = 433E6;
int counter = 0;
void setup() {
Serial.begin(9600);
LoRa.setPins(10, 9, 2);
if (!LoRa.begin(FREQ)) { Serial.println("# fail"); while (true); }
LoRa.setSpreadingFactor(7);
LoRa.setTxPower(14);
Serial.println("# TX ready");
}
void loop() {
counter++;
Serial.print("# send "); Serial.println(counter);
LoRa.beginPacket();
LoRa.print("hello ");
LoRa.print(counter);
LoRa.endPacket();
delay(3000); // 1% duty cycle margin
}Step 3 — RX sketch on board B
// L04-18 · LoRa RX
#include <SPI.h>
#include <LoRa.h>
const long FREQ = 433E6;
void setup() {
Serial.begin(9600);
LoRa.setPins(10, 9, 2);
if (!LoRa.begin(FREQ)) { Serial.println("# fail"); while (true); }
LoRa.setSpreadingFactor(7);
Serial.println("# RX ready");
}
void loop() {
int packetSize = LoRa.parsePacket();
if (packetSize) {
String s;
while (LoRa.available()) s += (char)LoRa.read();
Serial.print("# RX \""); Serial.print(s);
Serial.print("\" RSSI "); Serial.print(LoRa.packetRssi());
Serial.print(" dBm SNR "); Serial.println(LoRa.packetSnr());
}
}Step 4 — both running, side by side
Both boards on USB to your laptop (two ports, two Serial Monitors). Should see TX printing "send 1, send 2, ..." and RX printing "hello 1, hello 2, ..." with RSSI numbers around −30 to −50 dBm (close range, strong signal).
Step 5 — range test
Leave the TX board on a desk. Carry the RX board outside; walk away in a straight line. Note where RSSI starts dropping (~−80 dBm), where packets start being lost (~−110 dBm), and where you stop hearing anything.
Open ground at SF7 / 14 dBm / 433 MHz: typically 1–2 km. In a city with concrete: 200–500 m. Through walls: 50–100 m.
Step 6 — push to SF12 and re-test
Change both boards to LoRa.setSpreadingFactor(12). Re-test range. You should see significant improvement (perhaps 3–5 km open ground) at the cost of much longer per-packet time.
Try It Yourself 15 min
Goal: Send the current value of a pot (on the TX board's A0) every 5 s; on the RX board, drive an LED's brightness from the received value.
Goal: Bidirectional with ACK. After sending, the TX board switches to receive mode and waits up to 1 s for an "ACK" reply from the RX board. Count how many of the last 10 transmissions were acked. Print the success rate.
Goal: Add a simple addressing layer: each packet starts with a 1-byte destination ID. RX checks if the byte matches its own ID and ignores otherwise. Now multiple TX devices can share the same frequency without confusion.
Mini-Challenge · Range test in your neighbourhood 10 min
- Power the TX board with a power bank, set on a windowsill or roof.
- Carry the RX board (laptop + USB cable) outside.
- Walk to a corner, then another street, then a park.
- Record RSSI at each location. Map the boundary of reliable reception on a sketch of your area.
This is the data engineers gather when planning real LoRa deployments — "does this gateway cover the field".
Recap 5 min
LoRa point-to-point = two modules + matched settings + the Sandeep Mistry LoRa library. beginPacket() / print() / endPacket() on TX; parsePacket() / read() on RX. RSSI tells you signal strength; SNR tells you signal-to-noise. Spreading factor trades speed for range. Tomorrow: graduate from peer-to-peer to a real LoRaWAN gateway with The Things Network.
- SPI wiring
- LoRa modules attach via SPI (MOSI / MISO / SCK + chip select). Plus reset and DIO0 (interrupt) lines.
- LoRa.begin(freq)
- Initialises the radio at the given frequency. Returns false on hardware failure (wiring, missing module).
- setSpreadingFactor(7..12)
- Speed vs range. 7 = fast / short. 12 = slow / long.
- Bandwidth / coding rate
- Two other settings affecting speed and robustness. Defaults (125 kHz, 4/5) are usually fine.
- Tx power
- 2–20 dBm. Each region has a legal max. 14 dBm (25 mW) is universally safe.
- RSSI
- Received signal strength, dBm. −30 = right next to tx. −110 = on the edge.
- SNR
- Signal-to-noise ratio, dB. Positive = signal louder than noise. LoRa works even at −15 dB SNR.
- parsePacket()
- Polls for a complete received packet. Returns its length, or 0 if nothing arrived.
Homework 5 min
- Get TX + RX talking. Record the range test results.
- Save both sketches.
- Read ahead to ARD-L04-19 (LoRaWAN and TTN). No new hardware tomorrow.