Microcontroller projects

LoRa sender on a breadboard (running on batteries)

last updated: 2020-10-25

Introduction

A first step was to dive a little deeper into sleep modes, so that a sensor could function on battery for al long time (min. 1 year). This information can be found in the tips and tricks section, and is necessary to understand this page.

http://weigu.lu/microcontroller/tips_tricks/sleep_tips_tricks/index.html

ATmega328p with DS18B20 temperature sensor and a LoRa chip

As seen we will run the mega328 chip on internal RC on 8 MHz and then reduce the frequency in software down to 4 MHz and 1 MHz (the DS18B20 needs temporarly 4 MHz). To do so we have to burn an alternative bootloader (minicore).

Now we can add the LoRa chip (SX1276). For this we use a breakout board RFM95W from hoperf. The board needs the SPI interface (MOSI, MISO, SCK, SS), a Reset (input) and an Interrupt DIO0, to signalize if data is received.

arduino on a breadboard circuit

board

We want to sent the temperature and the voltage over LoRa. To measure the voltage, it is necessary to connect AVCC zo 3 V and and AREF through a capacitor (100 nF) to ground.

The sketch awaits a return from a gateway, and uses an InvertIQ function to create a simple Gateway/Node logic.

Energy consumption

To measure the energy consumption is not so easy. I use again a CurrentRanger from lowpowerlab.com to do so.

The first screen shows the mA range, the second screen the µA range and the third screen the auto mode (switches between two ranges).

Osci mA    Osci mA_2
Click on the pictures for a sharp version!

If we measure the temperature every 2 minutes we need about:

If we don't wait for a returning message, we get about 0.07 mAh per hour or 610 mAh per year. As the voltage is dropping under 3 V an less energy is needed we can presume:

With a 1200mAh battery (2xAAA) we get 2 years! for our battery life.

The battery live changes with the length the sketch has to wait for a response.

    /*  lora_sender_uno_duplex_DS1820_868_deep_sleep.ino by weigu.lu
     *
     *  This code uses InvertIQ function to create a simple Gateway/Node logic.
     *  Gateway - Sends messages w. enableInvertIQ(), receives  with disableInvertIQ()
     *  Node    - Sends messages w. disableInvertIQ(), r
     *  InvertIQ function basically invert the LoRa I and Q signals.
     *
     *   We use an mega328p (Arduino) on a breadboard with a minicore bootloader with
     *   8MHz internal clock (https://github.com/MCUdude/MiniCore).
     *   Most of the time we sleep and go down to 1MHz by dividing the clock.
     *   An DS18B20 is connected with VCC to pin D3 and with the data input to pin D4.
     *   The DS18B20 library needs 8MHz! The temperature is send by serial to the monitor.
     *
     *   RESET  PC6  1  |  |  28 PC5 A5
     *   D0 RX  PD0  2  |  |  27 PC4 A4
     *   D1 TX  PD1  3  |  |  26 PC3 A3
     *   D2     PD2  4  |  |  25 PC2 A2
     *   D3 PWM PD3  5  |  |  24 PC1 A1
     *   D4     PD4  6  |  |  23 PC0 A0
     *   VCC         7  |  |  22 GND
     *   GND         8  |  |  21 AREF
     *   ctal   PB6  9  |  |  20 AVCC
     *   ctal   PB7 10  |  |  19 PB5 D13 SCK
     *   D5 PWM PD5 11  |  |  18 PB4 D12 MISO
     *   D6 PWM PD6 12  |  |  17 PB3 D11 MOSI PWM
     *   D7     PD7 13  |  |  16 PB2 D10 PWM
     *   D8     PB0 14  |  |  15 PB1 D9 PWM
     *
     *   RFM95W: CS(NSS)=10, RST=9, IRQ(0)=2, freq = 868MHz
     *
     *   If we get the temperature every 2 minutes we need about
     *   0.009mA·3600s+7mA·0.05s·30+0.68mA·0.5s·30+65mA·0.05s·30+22mA+0.08s·30+10mA·0.02s·30
     *   = 32.4mAs + 10.5mAs + 10.2mAs + 97.5mAs + 52.8mAs + 6mAs = 209.4mAs = 0.0582mAh
    */

    #include "LowPower.h"
    #include <SPI.h>
    #include <LoRa.h>
    #include <OneWire.h>
    #include <DallasTemperature.h>

    #define DEBUG

    const byte PIN_DS18B20_POW = 3;
    const byte PIN_DS18B20_DATA = 4;

    float temperature;
    unsigned int voltage;
    long send_interval = 15;        // interval between sends, multiple of 8s (15 = 2min)

    OneWire oneWire(PIN_DS18B20_DATA);
    DallasTemperature DS18B20(&oneWire);

    byte msg_out_id = 0;            // counter of outgoing messages = message id
    byte node_addr = 0x33;          // address of this device
    byte gateway_addr = 0xFE;       // destination to send to 0xFE = gateway 0xFF = broadcast
    byte packet_size = 0;
    byte addr_in_rec, addr_in_sender, msg_in_id, msg_in_length;
    String msg_out, msg_in, lora_rssi, lora_snr;
    volatile bool flag_message_received = false; // flag set by callback
    long millis_max_listen = 10000;       // max listening time
    long millis_now, millis_prev=0;


    void setup() {
      pinMode(PIN_DS18B20_POW,OUTPUT);
      digitalWrite(PIN_DS18B20_POW,LOW);
      #ifdef DEBUG
        Serial.begin(38400);
        Serial.println("LoRa duplex temperature sender (Arduino Uno)\n");
      #endif // DEBUG
      DS18B20.begin();
      if (!LoRa.begin(868E6)) {
        #ifdef DEBUG
          Serial.println("Error starting LoRa!");
        #endif // DEBUG
        while (true); // endless loop
      }
      LoRa.onReceive(onReceive); // callback
      void set_clock_to_1MHz();
      delayMicroseconds(100);
      millis_now = millis();
      #ifdef DEBUG
        Serial.println("Setup done");
        Serial.end();
      #endif // DEBUG
    }

    void loop() {
      void set_clock_to_4MHz(); // DS18B20 lib needs min. 4MHz
      delayMicroseconds(100);
      temperature = get_temperature_DS18B20();
      voltage = get_voltage();
      void set_clock_to_1MHz(); // back to 1MHz
      delayMicroseconds(100);
      msg_out = String(temperature) + "C" + String(voltage) + "mV";
      delay(random(100));     // randomize delay time to sent
      lora_send_message(msg_out);
      msg_out_id++;                         // increment message counter (ID)
      #ifdef DEBUG
        Serial.begin(38400);
        Serial.print("message sent: ");
        Serial.println(msg_out);
      #endif // DEBUG
      millis_prev = millis();              // wait for return message (til max_listen)
      while ((millis_now - millis_prev) < millis_max_listen) {
        if (flag_message_received) {
          lora_read_message();
          flag_message_received = false; // Set flag back to false
          break;
        }
        millis_now = millis();
      }
      delayMicroseconds(10000); //! needed
      #ifdef DEBUG
        Serial.end();
      #endif // DEBUG
      // go to sleep
      LoRa.sleep();
      for (int i=0; i<send_interval; i++) {
        LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
      }
    }

    float get_temperature_DS18B20() {
      digitalWrite(PIN_DS18B20_POW,HIGH);
      delayMicroseconds(100);
      DS18B20.requestTemperatures();
      LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF); // DS18B20 needs 750ms
      delayMicroseconds(100);
      return DS18B20.getTempCByIndex(0);
    }

    unsigned int get_voltage() { // in mV
      ADCSRA = 0x85;                   // ADEN, div = 32 = 125kHz
      ADCSRB &= 0xF8;                  // free running mode
      ADMUX = 0x6E;                    // 01 Ref = Vcc 1 ADLAR (left 8 Bit)
      delayMicroseconds(20000);        // wait for Vref to settle
      ADCSRA |= (1 << ADSC);           // start 1 conversion
      while (bit_is_set(ADCSRA,ADSC)); // measure and forget 1 conversion
      delayMicroseconds(10000);
      ADCSRA |= (1 << ADSC);           // start 2 conversion
      while (bit_is_set(ADCSRA,ADSC)); // measuring  Vcc (in mV); 281600=1.1*256*1000
      return 275000L / ADCH;    // adjusted to real multimeters!
    }

    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
    }

    void lora_send_message(String message_out) {
      LoRa_txMode();                        // set tx mode
      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
      LoRa_rxMode();                        // go back into receive mode
    }

    void lora_read_message() {
      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();
      }
      if (msg_in_length != msg_in.length()) {   // check length for error
        #ifdef DEBUG
         Serial.println("error: message length does not match length");
        #endif // DEBUG
        return;
      }
      if (addr_in_rec != node_addr && addr_in_rec != 0xFF) {
        #ifdef DEBUG
          Serial.println("This message is not for me.");
        #endif // DEBUG
        return;
      }
      lora_rssi = LoRa.packetRssi();
      lora_snr = LoRa.packetSnr();
      #ifdef DEBUG
        Serial.print("message received: " + msg_in);
        Serial.print(" (Length: " + String(msg_in_length));
        Serial.print(", ID: " + String(msg_in_id));
        Serial.print(")\nfrom: 0x" + String(addr_in_sender, HEX));
        Serial.print(" to: 0x" + String(addr_in_rec, HEX));
        Serial.print(" (tRSSI: " + lora_rssi);
        Serial.print(", Snr: " + lora_snr);
        Serial.println(")\n");
      #endif // DEBUG
      String acknack = msg_in.substring(0,3);
      msg_in.remove(0,3);
      if ((acknack == "NAK") && (msg_in.toInt() == (msg_out_id-1))) {
        lora_send_message(msg_out);
        #ifdef DEBUG
          Serial.print("message ");
          Serial.print(msg_out_id-1);
          Serial.println(" sent for a second time!");
        #endif // DEBUG
      }
      else if ((acknack == "ACK") && (msg_in.toInt() == (msg_out_id-1))) {
        #ifdef DEBUG
          Serial.print("message ");
          Serial.print(msg_out_id-1);
          Serial.println(" OK!");
        #endif // DEBUG
      }
      LoRa.sleep();
    }

    void LoRa_rxMode(){
      LoRa.enableInvertIQ();                // active invert I and Q signals
      LoRa.receive();                       // set receive mode
    }

    void LoRa_txMode(){
      LoRa.idle();                          // set standby mode
      LoRa.disableInvertIQ();               // normal mode
    }

Downloads