Microcontroller projects

LoRa P2P with the SX1276 from SEMTECH

last updated: 2021-01-21

Breakout board RFM95W

LoRa was developped by SEMTECH. We will use here their simple LoRa chip SX1276, that can be bought on a breakout board RFM95W-V2.0 from hoperf (specification document). Unfortunately the board uses a 2 mm pin header pitch instead of the usually used 2.54 mm (0.1 in). So we need an additional breakout board to be able to use the chip on a breadboard.

lora breakout

lora breakout
Source: RFM95W-V2.0 specification document from hoperf

Connections:

Don't forget to connect the antenna before powering the board! Without antenna the board can be damaged!

If you don't have an antenna, use a wire with 8.2 cm in length.

The board uses the SPI interface (MISO, MOSI, SCK, SS (NSS)) to communicate with the microcontroller, and it is powered with 3.3 V (GND, +3.3 V). For SPI we usually use the pins of the default SPI interface for MISO, MOSI and SCK. With the function SPI.begin() it is possible to change if a second interface is available. The chip select pin is freely selectable. The library uses by default pin 5 for the Uno. The DI00 pin (RFM95W) must be connected to a pin supporting interrupts, because the LoRa chip signals an incoming message with this output to the microcontroller (default Uno pin 2). Same for DI01, needed only for LoRaWAN (pin 6 for UNO). An output pin of the microcontroller can also be used to reset (RESET pin) the LoRa chip if needed.

Even if we find schematics connecting the Arduino Uno directly to the RFM95W board, this is not a valabel solution! The output pins (MOSI, CLK etc.) of the Uno have 5 V! One solution can be to use level shifters like on the Dragino shield, or to use voltage dividers for all outputs. A simpler solution is an Arduino Uno with 3V by changing the voltage regulator. We will use ESP boards for our exercises.

LoRa board Arduino Uno (3V!) ESP8266 (Lolin/Wemos) ESP32
3.3V (Vin) 3.3V 3.3V 3.3V
GND GND GND GND
MOSI 11 14 23
MISO 12 12 19
SCK 13 13 18
NSS (SS, CS) 5 16 5
RST (Reset) 9 not connected 33
DIO0 (G0, IRQ) 2 15 26
DIO1 (G1, IRQ) 6 (only needed for LoRaWAN!) (only needed for LoRaWAN!) (only needed for LoRaWAN!)
DIO2-5 not connected not connected not connected
ANT antenna antenna antenna

circuit esp rfm95W


lora breakout esp32

Software

We use the LoRa library from Sandeep Mistry. Information about the library can be found in the corresponding LoRa Application Programming Interface (API).

Here are two basic sketches for the sender (transmitter):

    // LoRa simple sender lora_sender_esp32_868.ino
    // ESP32 MH ET Live MiniKit with RFM95W (SX1276)
    // CS(SS)=5, RST=33, DIO0(IRQ)=26, freq = 868MHz
    // SPI (ESP32 default): MOSI=23, MISO=19, SCK=18

    #include <SPI.h>
    #include <LoRa.h>

    const byte PIN_SS = 5;                      // LoRa radio chip select
    const byte PIN_RST = 33;                    // LoRa radio reset
    const byte PIN_IRQ = 26;                    // hardware interrupt pin!
    //const byte PIN_SCK = 18;
    //const byte PIN_MISO = 19;
    //const byte PIN_MOSI = 23;

    int counter = 0;

    void setup() {
      Serial.begin(115200);
      Serial.println("LoRa sender\n");
      //SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_SS);   //SPI LoRa pins
      LoRa.setPins(PIN_SS, PIN_RST, PIN_IRQ);  // setup LoRa transceiver module
      if (!LoRa.begin(868E6)) {
        Serial.println("Error starting LoRa!");
        while (true);      //endless loop
      }
    }

    void loop() {
      Serial.print("Sending packet number: ");
      Serial.println(counter);
      LoRa.beginPacket();
      LoRa.print("This is packet number ");
      LoRa.print(counter);
      LoRa.endPacket();
      counter++;
      delay(60000);
    }

And the receiver:

    // Lora simple receiver lora_receiver_esp32_868.ino
    // ESP32 MH ET Live MiniKit with RFM95W (SX1276)
    // CS(SS)=5, RST=33, DIO0(IRQ)=26, freq = 868MHz
    // SPI (ESP32 default): MOSI=23, MISO=19, SCK=18  

    #include <SPI.h>
    #include <LoRa.h>

    const byte PIN_SS = 5;                      // LoRa radio chip select
    const byte PIN_RST = 33;                    // LoRa radio reset
    const byte PIN_IRQ = 26;                    //  hardware interrupt pin!
    //const byte PIN_SCK = 18;
    //const byte PIN_MISO = 19;
    //const byte PIN_MOSI = 23;

    byte packet_size = 0;

    void setup() {
      Serial.begin(115200);  
      Serial.println("LoRa receiver\n");
      //SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_SS);   //SPI LoRa pins 
      LoRa.setPins(PIN_SS, PIN_RST, PIN_IRQ);  // setup LoRa transceiver module
      if (!LoRa.begin(868E6)) {
        Serial.println("Error starting LoRa!");
        while (true);                           // endless loop
      }
    }

    void loop() {  
      packet_size = LoRa.parsePacket();         // try to parse packet
      if (packet_size) {  // received a packet
        Serial.print("Received packet with ");
        Serial.print(packet_size);
        Serial.print(" byte: '");
        while (LoRa.available()) {              // read packet
          Serial.print((char)LoRa.read());
        }    
        Serial.print("' with RSSI = ");
        Serial.print(LoRa.packetRssi());
        Serial.print("dBm");
      }
    }

Note: If we use other pins than the library defaults to connect to our chip, we must declare this in the software:

    const byte PIN_SS = 5;                      // LoRa radio chip select
    const byte PIN_RST = 33;                    // LoRa radio reset
    const byte PIN_IRQ = 26;                    //  hardware interrupt pin!

    void setup() {
      ...
      LoRa.setPins(PIN_SS, PIN_RST, PIN_IRQ);  // setup LoRa transceiver module
      ...
    }

Transceiver

The following sketch shows how a transceiver can be build with duplex communication and callbacks. Transmitter and receiver use a unique address (only 1 byte = 256 addresses, 0xFF for broadcast messages). The message gets an identifier (ID), realised in the sketch with a simple message counter. The delay time between sends is not static (randomized) to avoid collisions.

    // LoRa duplex communication with callback lora_sender_esp32_868_duplex.ino
    // ESP32 MH ET Live MiniKit with RFM95W (SX1276)
    // CS(SS)=5, RST=33, DIO0(IRQ)=26, freq = 868MHz
    // SPI (ESP32 default): MOSI=23, MISO=19, SCK=18

    #include <SPI.h>
    #include <LoRa.h>

    const byte PIN_SS = 5;                      // LoRa radio chip select
    const byte PIN_RST = 33;                    // LoRa radio reset
    const byte PIN_IRQ = 26;                    //  hardware interrupt pin!
    //const byte PIN_SCK = 18;
    //const byte PIN_MISO = 19;
    //const byte PIN_MOSI = 23;

    const byte NODE_ADDR = 0x01;                 // address of this device
    const byte GATEWAY_ADDR = 0xFE;              // destination 0xFE = gateway 0xFF = broadcast
    unsigned long send_delay = 6000;             // delay in ms between sends
    byte msg_out_id = 0;                         // counter of outgoing messages = message id

    byte addr_in_rec, addr_in_sender, msg_in_id, msg_in_length;
    String message, msg_out, msg_in, lora_rssi, lora_snr;
    volatile bool flag_message_received = false; // flag set by callback

    void setup() {
      Serial.begin(115200);
      Serial.println("LoRa duplex with callback\n");
      //SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_SS);   //SPI LoRa pins
      LoRa.setPins(PIN_SS, PIN_RST, PIN_IRQ);  // setup LoRa transceiver module

      if (!LoRa.begin(868E6)) {
        Serial.println("Error starting LoRa!");
        while (true); // endless loop
      }
      LoRa.onReceive(onReceive);                 // init the callback function
      LoRa.receive();                            // start receive mode
    }

    void loop() {
      LoRa.receive();                            // go back into receive mode
      if (flag_message_received) {               // if receive flag is set by callback
        readMessage();
        flag_message_received = false;           // set flag back to false
      }
      if (non_blocking_delay(send_delay)) {      // send a message all delay ms
        message = "HeLoRa World!";
        send_message(message);
        Serial.println("Sending:");
        Serial.print("GW addr.: ");
        Serial.print(GATEWAY_ADDR);
        Serial.print(" (1 byte) + node addr.: ");
        Serial.print(NODE_ADDR);
        Serial.print(" (1 byte) + msg ID: ");
        Serial.print(msg_out_id);
        Serial.println(" (1 byte) +");
        Serial.print("msg length: ");
        Serial.print(message.length());
        Serial.println(" (1 byte) + message: \"" + message + "\"");
        Serial.println("---------------------------------------------------------------------");
        send_delay = send_delay + random(1000);  // randomize delay time (avoid collisions)
      }
      delay(1);
    }

    // callback function
    void onReceive(int packetSize) {
      if (packetSize == 0) {                // if there's no packet, return
        return;
      }
      flag_message_received = true;         //Set flag to perform read in main loop
    }

    // send the message and increment ID
    void send_message(String message_out) {
      LoRa.beginPacket();                   // start packet
      LoRa.write(GATEWAY_ADDR);             // add destination address
      LoRa.write(NODE_ADDR);                // add sender address
      LoRa.write(msg_out_id);               // add message ID (counter)
      LoRa.write(message_out.length());     // add payload length
      LoRa.print(message_out);              // add payload
      LoRa.endPacket();                     // finish packet and send it
      msg_out_id++;                         // increment message counter (ID)
    }

    // read a message and check if valid
    void readMessage() {
      addr_in_rec = LoRa.read();            // recipient address
      addr_in_sender = LoRa.read();         // sender address
      msg_in_id = LoRa.read();              // incoming msg ID
      msg_in_length = LoRa.read();          // incoming msg length
      while (LoRa.available()) {
        msg_in = LoRa.readString();
        yield();
      }
      if (msg_in_length != msg_in.length()) {// check length for error
        Serial.println("error: message length does not match length");
        return;
      }
      if (addr_in_rec != GATEWAY_ADDR && addr_in_rec != 0xFF) {
        Serial.println("This message is not for me.");
        return;
      }
      lora_rssi = LoRa.packetRssi();
      lora_snr = LoRa.packetSnr();
      Serial.print("From: 0x" + String(addr_in_sender, HEX));
      Serial.print("\tto: 0x" + String(addr_in_rec, HEX));
      Serial.print("\tRSSI: " + lora_rssi);
      Serial.println("\tSnr: " + lora_snr);
      Serial.print("Message: " + msg_in);
      Serial.print("\tLength: " + String(msg_in_length));
      Serial.println("\tID: " + String(msg_in_id));
      Serial.println("-----------------------------------------------------------");
    }

    // non blocking delay using millis(), returns true if time is up
    bool non_blocking_delay(unsigned long milliseconds) {
      static unsigned long nb_delay_prev_time = 0;
      if(millis() >= nb_delay_prev_time + milliseconds) {
        nb_delay_prev_time += milliseconds;
        return true;
      }
      return false;
    }