Arduino-LMIC library with low power mode

In this post I try to make my Arduino based TTN Nodes more power efficient.

Try to save energy

An Arduino Pro Mini with 3.3V consumes power. However, many do not know that you can already save a lot of electricity here!

Today I try to drastically reduce power consumption.

How to basically reduce power consumption, can be found under “Arduino Pro Mini 3.3V Low Power” at Google. I have already done everything. So unsolder LED and Regulator.

From 5mA normal you can reduce a lot:

  • 1mA through the LED
  • 0.5mA through the power regulator

Now we still have 3.5mA. This is too much.
Through the LowPower library we can put the Arduino into deep sleep, which is really impressive!
Theoretically we come down to 5uA, so 0.005mA!

Measurements

Today my new multimeter arrived 🙂
UNI-T UT61E, really nice!

So let me take the first measurement.
First I tried to use the built-in measurement with uAmA. However, it only show me errors and I quickly dodged on the already more accurate shunt method. For this one, take a resistance between battery and consumer.
So that the consumer still gets enough electricity, take as small as possible. If one takes too small, however, the voltage drop is too low and the multimeter gets only fractions of mV, which are then also inaccurate.

I first tried 10 ohms. However, the node was then in a permanent reset.
I then turned four 10 ohm resistors together so that I get to 2.5 ohms, in my case 2.6 ohms. Now it works fine!

If we measure 100mV, we divide it by 2.6 ohms. So 38mA.

Without LowPower-Mode

Without the LowPower library, the result is not so good:

9.88mV / 2.6 Ohm = 3.8mA

    case EV_TXCOMPLETE:
      Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
      if (LMIC.txrxFlags & TXRX_ACK)
        Serial.println(F("Received ack"));
      if (LMIC.dataLen) {
        Serial.println(F("Received "));
        Serial.print(LMIC.dataLen);
        Serial.println(F(" bytes of payload"));
      }
      // Schedule next transmission
      os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
      break;

With LowPower-Mode

0.1mV / 2.6 Ohm = 0.038mA = 38uA 

We saved 100 times alone with this step:

    case EV_TXCOMPLETE:
      Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
      if (LMIC.txrxFlags & TXRX_ACK)
        Serial.println(F("Received ack"));
      if (LMIC.dataLen) {
        Serial.println(F("Received "));
        Serial.print(LMIC.dataLen);
        Serial.println(F(" bytes of payload"));
      }
      // Schedule next transmission
      //os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
      do_send(&sendjob);
      for (int i=0; i<int(TX_INTERVAL/8); i++) {
        // Use library from https://github.com/rocketscream/Low-Power
        LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
      }
      break;
As you can see in the code, the LowPower function was used with 8s deep sleep. Very easy, very successful!
But are 38uA good? As you can read here, I expected 10uA and thought that would be generous. 

So, what’s the problem? Or do we have a problem at all?

 

3200mA / 0.01mAh = 320.000h = 36 years!!
That was the original calculation for deep sleep without sending and without self-discharge. Let’s adjust the value:
 
3200mA / 0.038mAh = 84.210h = 9.6 years!
 
Before using the LowPower library:
3200mA / 3.8mAh = 35 days!
 

Conclusion:

I would say we have no problem. We learned in the already linked article that 5% self-discharge is a bigger problem.
So all right!
Very satisfied 🙂
 

LowPower Test Node Code:

/*******************************************************************************
   Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman

   Permission is hereby granted, free of charge, to anyone
   obtaining a copy of this document and accompanying files,
   to do whatever they want with them without any restriction,
   including, but not limited to, copying, modification and redistribution.
   NO WARRANTY OF ANY KIND IS PROVIDED.

   This example sends a valid LoRaWAN packet with payload "Hello,
   world!", using frequency and encryption settings matching those of
   the The Things Network.

   This uses ABP (Activation-by-personalisation), where a DevAddr and
   Session keys are preconfigured (unlike OTAA, where a DevEUI and
   application key is configured, while the DevAddr and session keys are
   assigned/generated in the over-the-air-activation procedure).

   Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
   g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
   violated by this sketch when left running for longer)!

   To use this sketch, first register your application and device with
   the things network, to set or generate a DevAddr, NwkSKey and
   AppSKey. Each device should have their own unique values for these
   fields.

   Do not forget to define the radio type correctly in config.h.

 *******************************************************************************/

#include <Arduino.h>
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
#include <Wire.h>
#include <LowPower.h>


// Enable debug prints to serial monitor
#define MY_DEBUG

int BATTERY_SENSE_PIN = A0;
int BATTERY_FULL = 4.2;

// LoRaWAN NwkSKey, network session key
// This is the default Semtech key, which is used by the early prototype TTN
// network.
// DEVEUI und AppEUI LSB
static const PROGMEM u1_t NWKSKEY[16] = { };


// LoRaWAN AppSKey, application session key
// This is the default Semtech key, which is used by the early prototype TTN
// network.
// MSB
static const u1_t PROGMEM APPSKEY[16] = { };

// LoRaWAN end-device address (DevAddr)
static const u4_t DEVADDR = 0x00

// These callbacks are only used in over-the-air activation, so they are
// left empty here (we cannot leave them out completely unless
// DISABLE_JOIN is set in config.h, otherwise the linker will complain).
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }

static uint8_t mydata[] = "Hello, world!";
static osjob_t sendjob;

// Schedule TX every this many seconds (might become longer due to duty
// cycle limitations).
const unsigned TX_INTERVAL_MINUTES = 1;
const unsigned TX_INTERVAL = 60 * TX_INTERVAL_MINUTES;

// Pin mapping
const lmic_pinmap lmic_pins = {
  .nss = 10,
  .rxtx = LMIC_UNUSED_PIN,
  .rst = 9,
  .dio = {4, 5, 7}, //DIO0, DIO1 and DIO2 connected
};

void onEvent (ev_t ev) {
  Serial.print(os_getTime());
  Serial.print(": ");
  switch (ev) {
    case EV_SCAN_TIMEOUT:
      Serial.println(F("EV_SCAN_TIMEOUT"));
      break;
    case EV_BEACON_FOUND:
      Serial.println(F("EV_BEACON_FOUND"));
      break;
    case EV_BEACON_MISSED:
      Serial.println(F("EV_BEACON_MISSED"));
      break;
    case EV_BEACON_TRACKED:
      Serial.println(F("EV_BEACON_TRACKED"));
      break;
    case EV_JOINING:
      Serial.println(F("EV_JOINING"));
      break;
    case EV_JOINED:
      Serial.println(F("EV_JOINED"));
      break;
    case EV_RFU1:
      Serial.println(F("EV_RFU1"));
      break;
    case EV_JOIN_FAILED:
      Serial.println(F("EV_JOIN_FAILED"));
      break;
    case EV_REJOIN_FAILED:
      Serial.println(F("EV_REJOIN_FAILED"));
      break;
    case EV_TXCOMPLETE:
      Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
      if (LMIC.txrxFlags & TXRX_ACK)
        Serial.println(F("Received ack"));
      if (LMIC.dataLen) {
        Serial.println(F("Received "));
        Serial.print(LMIC.dataLen);
        Serial.println(F(" bytes of payload"));
      }
      // Schedule next transmission
      //os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
      do_send(&sendjob);
      for (int i=0; i<int(TX_INTERVAL/8); i++) {
        // Use library from https://github.com/rocketscream/Low-Power
        LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
      }
      break;
    case EV_LOST_TSYNC:
      Serial.println(F("EV_LOST_TSYNC"));
      break;
    case EV_RESET:
      Serial.println(F("EV_RESET"));
      break;
    case EV_RXCOMPLETE:
      // data received in ping slot
      Serial.println(F("EV_RXCOMPLETE"));
      break;
    case EV_LINK_DEAD:
      Serial.println(F("EV_LINK_DEAD"));
      break;
    case EV_LINK_ALIVE:
      Serial.println(F("EV_LINK_ALIVE"));
      break;
    default:
      Serial.println(F("Unknown event"));
      break;
  }
}

void do_send(osjob_t* j) {
  // Read sensor values and multiply by 100 to effectively keep 2 decimals
  // Signed 16 bits integer, -32767 up to +32767
  int16_t t = 0;
  // Unsigned 16 bits integer, 0 up to 65535
  uint16_t h = 0;
  // Unsigned 16 bits integer, 0 up to 65535
  uint16_t b = readBat() * 100;
  byte buffer[6];
  buffer[0] = t >> 8;
  buffer[1] = t;
  buffer[2] = h >> 8;
  buffer[3] = h;
  buffer[4] = b >> 8;
  buffer[5] = b;
  LMIC_setTxData2(1, buffer, sizeof(buffer), 0);

  Serial.println("");
  Serial.print("Sending - temperature: ");
  Serial.print(t);
  Serial.print(", humidity: ");
  Serial.print(h);
  Serial.print(", battery: ");
  Serial.print(b);
  Serial.println("");

  // Check if there is not a current TX/RX job running
  if (LMIC.opmode & OP_TXRXPEND) {
    Serial.println(F("OP_TXRXPEND, not sending"));
  } else {
    // Prepare upstream data transmission at the next possible time.
    LMIC_setTxData2(1, buffer, sizeof(buffer), 0);
    Serial.println(F("Packet queued"));
  }
  // Next TX is scheduled after TX_COMPLETE event.
}

void setup() {
  Serial.begin(115200);
  Serial.println(F("Starting"));

  // Fuer A0 Battery:
  analogReference(INTERNAL);

#ifdef VCC_ENABLE
  // For Pinoccio Scout boards
  pinMode(VCC_ENABLE, OUTPUT);
  digitalWrite(VCC_ENABLE, HIGH);
  delay(1000);
#endif

  // LMIC init
  os_init();
  // Reset the MAC state. Session and pending data transfers will be discarded.
  LMIC_reset();

  // Set static session parameters. Instead of dynamically establishing a session
  // by joining the network, precomputed session parameters are be provided.
#ifdef PROGMEM
  // On AVR, these values are stored in flash and only copied to RAM
  // once. Copy them to a temporary buffer here, LMIC_setSession will
  // copy them into a buffer of its own again.
  uint8_t appskey[sizeof(APPSKEY)];
  uint8_t nwkskey[sizeof(NWKSKEY)];
  memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
  memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
  LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
#else
  // If not running an AVR with PROGMEM, just use the arrays directly
  LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
#endif

#if defined(CFG_eu868)
  // Set up the channels used by the Things Network, which corresponds
  // to the defaults of most gateways. Without this, only three base
  // channels from the LoRaWAN specification are used, which certainly
  // works, so it is good for debugging, but can overload those
  // frequencies, so be sure to configure the full frequency range of
  // your network here (unless your network autoconfigures them).
  // Setting up channels should happen after LMIC_setSession, as that
  // configures the minimal channel set.
  // NA-US channels 0-71 are configured automatically
  //    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  //    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
  //    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  //    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  //    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  //    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  //    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  //    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  //    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
  LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  LMIC_setupChannel(1, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7B),  BAND_CENTI);      // g-band
  LMIC_setupChannel(2, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  LMIC_setupChannel(3, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  LMIC_setupChannel(4, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  LMIC_setupChannel(5, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  LMIC_setupChannel(6, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  LMIC_setupChannel(7, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
  LMIC_setupChannel(8, 868100000, DR_RANGE_MAP(DR_FSK, DR_FSK),  BAND_MILLI);      // g2-

  // TTN defines an additional channel at 869.525Mhz using SF9 for class B
  // devices' ping slots. LMIC does not have an easy way to define set this
  // frequency and support for class B is spotty and untested, so this
  // frequency is not configured here.
#elif defined(CFG_us915)
  // NA-US channels 0-71 are configured automatically
  // but only one group of 8 should (a subband) should be active
  // TTN recommends the second sub band, 1 in a zero based count.
  // https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json
  LMIC_selectSubBand(1);
#endif

  // Disable link check validation
  LMIC_setLinkCheckMode(0);

  // TTN uses SF9 for its RX2 window.
  LMIC.dn2Dr = DR_SF9;

  // Set data rate and transmit power for uplink (note: txpow seems to be ignored by the library)
  LMIC_setDrTxpow(DR_SF7, 14);

  // Start job
  do_send(&sendjob);
}

float readBat() {
  int sensorValue = analogRead(BATTERY_SENSE_PIN);
#ifdef MY_DEBUG
  Serial.println(sensorValue);
#endif
  // 1M, 470K divider across battery and using internal ADC ref of 1.1V
  // Sense point is bypassed with 0.1 uF cap to reduce noise at that point
  // ((1e6+470e3)/470e3)*1.1 = Vmax = 3.44 Volts
  // 3.44/1023 = Volts per bit = 0.003363075

  // 2M, 470K 1.1V ref => 5,78V Max!
  // 5,78/1023 = 0.00565
  float batteryV  = sensorValue * 0.00565;

#ifdef MY_DEBUG
  Serial.print("Battery Voltage: ");
  Serial.print(batteryV);
  Serial.println(" V");
#endif
  return batteryV;
}

void loop() {
  os_runloop_once();
}

 

25 thoughts on “Arduino-LMIC library with low power mode”

  1. Luciano Picchioni

    Hi Mario, great article!
    How did you connect the battery to the Arduino Pro mini 3.3v?
    The battery, full charge, has 4.6V, removing the regulator
    do you risk damaging the RFM95 module?
    Thanks

    1. I hava a LDO (MCP1700-330) between BAT out of Protection Circuit and Arduino, so i have perfect 3.3V. This LDO has only 1 uA Current instead of the Original Converter from the Arduino RAW Pin!
      Nice day!

  2. Luciano Picchioni

    Thanks for the reply
    but I still have a doubt …
    also in your article “My first own TTN node with self designed PCB”,
    in the components list there is “MCP1700-330 LDO (1.6uA, 250mAh max, 2.3-6V input)”, but in the “Affiliate links” it does not appear

    sorry, but I do not know the component well
    looking on the internet, I find the component in the TO-92-3 format
    use the component individually, have you bought a small card that contains it?

    do you have a reference link?

  3. Hello.
    Thank a lot for that great tuto. It’s exactely what I need as I work with Arduino-lmci and a Feather MO Adaloger
    Howerver,I sadly can not make it working.
    When I compile my code, I got the following error:
    ‘class LowPowerClass’ has no member named ‘powerDown’

    I double checked your example and I can not where is my error.
    No object need to be ceated for LowPower?

    I also read the following:
    ####Notes: External interrupt during standby on ATSAMD21G18A requires a patch to the Arduino SAMD Core in order for it to work.

    This is not really clear for me as you have not specify it. Did you imported via the Board Manager and did nothing else??

    Many thank

  4. Hi Mario,
    Thank’s for the great work!
    Hi have a doubt!
    I tried to run the code on an Arduino Uno but the consumption remains high: 20mA in PowerDown and 40mA in transmission.
    The only thing that I changed is the pin mapping:
    // Pin mapping
    const lmic_pinmap lmic_pins = {
    .nss = 4,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 1,
    .dio = {2, 3}, // DIO0, DIO1 and DIO2 connected
    };
    In your opinion, what can these high consumption depend on?
    Hello and thank you again.
    Silvano

    1. Hello,
      I think you did everything right.
      Unfortunately the Arduino Uno is not very energy saving.
      It consumes 50mA on average.
      Your 20mA are relatively good.

      Take an Arduino Pro Mini 3.3V

      Greetings

  5. Compared with the original code by 2015 Thomas Telkamp and Matthijs Kooijman, you seem to use different KEY names, why is that?
    You use:
    static const PROGMEM u1_t NWKSKEY[16], is this lsb static const u1_t DEVEUI[8] + static const u1_t APPEUI[8]?
    Doing so my ProMini + RFM95 did not seem to function. In the original code there was an indicator LED to see when the joining process was going on. That would seem usefull.

    1. Hello,
      is it possible that you’re using an old version of the library?
      I didn’t change anything.

  6. I don’t see any attempts to lower the power of the LoRa circuitry. Is it powered down enough anyhow between transmissions?

    Isn’t there a risk EV_TXCOMPLETE never occurs, and the Arduino therefore never enters power save?

    1. Hello,

      the radio chip doesn’t consume as much when it’s not transmitting or receiving.
      You can also put the chip into sleep mode.
      LMIC_shutdown();
      After that the RFM95 would have to consume only 0.7uA. (0.2uA for the radio chip, 0.5uA for the rest on the HopeRF).

      The case “EV_TXCOMPLETE” is completed.
      It is regularly triggered. They wait for a reception. If this is not the case, it should send (do_send) and then sleep. Then EV_TXCOMPLETE is ready and starts again.

      Greetings

      1. Dear Mario

        Your wrote:
        You can also put the chip into sleep mode.
        LMIC_shutdown();

        For the next do_send, do we need to reinitialize the RFM95 module? or we just need to lunch the do_send function as if we would not have ran LMIC_shutdown()

  7. Hi, whats the lowest power it consumes when in RX mode? I read that you can set a receiver in a Duty-Cycle Receive Mode so it doenst constantly needs a lot of power: sleep -> wake up when rx signal -> sleep again etc

    I need it for a battery powered receiver

    1. Power calculation
      From 4.2V to 3.5V mean about 85% capacity.
      At 900mAh that means 765mAh.
      At 6 weeks, about 42 days, it is about 1,000 hours.
      Li-ion has about 5% self-discharge per month.
      That leaves about 700mAh.
      So we come to about 0.7mA consumption.
      Calculated at that time were 0.04mA for Deep Sleep. So stay 0.66mA for pure transmission.
      Sent every 10 minutes, so 144 times a day.
      At 42 days, that’s 6,336 times.
      This required 660mAh.
      Means 0.1mAh per send.

      Read here:
      https://mariozwiers.de/2018/09/14/first-lora-node-the-real-power-consumption/

  8. Hi Mario,

    thanks for the great article! However I have one question: In your low-power example you commented out the rescheduling of your send job with (os_setTimedCallback(…)) and triggered it manually.

    Did you get errors with this in the long run or does it send stable?

    Richard

    1. With some nodes i had problems on long run time. But whether that has something to do with it, I hardly think so.
      If necessary, you build a software reset after x days…

    1. Sure,

      function Decoder(bytes, port) {
      var t = (bytes[0] & 0x80 ? 0xFFFF<<16 : 0) | bytes[0]<<8 | bytes[1]; var h = bytes[2]<<8 | bytes[3]; var b = bytes[4]<<8 | bytes[5]; return { temperature: t / 100, humidity: h / 100, battery: b / 100 } }

Comments are closed.