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.
Basic 5 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.
Challenge 1 5 min
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.
Challenge 2 5 min
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.
Challenge 3 · Why can't RX hear TX? 10 min
Two classmates built a LoRa pair. TX prints that it is sending, but RX never prints anything. There are four faults: two in the code, one in the wiring and one that breaks the law.
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(9);
LoRa.setTxPower(14);
}
void loop() {
counter = counter + 1;
Serial.print("# send ");
Serial.println(counter);
LoRa.beginPacket();
LoRa.print("hello ");
LoRa.print(counter);
LoRa.endPacket();
delay(100);
}RX
#include <SPI.h>
#include <LoRa.h>
const long FREQ = 433E6;
void onPacket(int packetSize) {
String s;
while (LoRa.available()) {
s += (char)LoRa.read();
}
Serial.print("# RX: ");
Serial.println(s);
}
void setup() {
Serial.begin(9600);
LoRa.setPins(10, 9, 2);
if (!LoRa.begin(FREQ)) {
Serial.println("# fail");
while (true) {
}
}
LoRa.onReceive(onPacket);
LoRa.receive();
}
void loop() {
}RX wiring: the module's DIO0 goes to D3. Its VCC goes to the UNO's 5V pin.
Fix all four on your own pair.
It works if RX prints every packet. TX must send no more often than the 1 % duty-cycle rule allows.
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.
Extra Mission 5 min
Part 1 — Design a two-board LoRa gadget
Use your long-range gadget from ARD-L04-17, or pick a new one. Design both boards on paper.
Your design must include:
- What the TX board senses and puts in each packet.
- What the RX board does when a packet arrives.
- All five radio settings, the same on both sides.
- How often TX sends, checked against the duty-cycle rule.
Part 2 — Make it
Build both boards and get them talking. Then do a range test. Leave TX in one place and record RSSI at three spots, each further away. Stay where you are allowed to be.
Bring back next class: both sketches, a photo of each board, and your three RSSI readings with rough distances.