Full Arduino Mini LoraWAN below 1uA Sleep Mode

Remove the bootloader entirely and get a cheap ISP AVR programmer from eBay, you’ll be able to use the full 32KB of progmem.

Time for a new antenne experiment. Now with the SMA chassis part and a single wire mounted in the SMA connector pin.

1 Like

@Gig
For sure, but don’t forget that ICSP connector is not present on the board, so you need to wire the ICSP once the Mini is soldered on the board PCB, so, big pain in perspective.

That’s why I’m flashing with Optiboot my Arduino Mini once received with this custom ICSP/FTDI programmer shield and PogoPins.

You can get it there on PCBs.io, tested and working :wink:
github repo of the adapter with build files, schematics and bootloaders

nice work.
could you please tell us what PogoPins you used (link?) and it would be nice seeing a picture of an assembled board
thanks

I’m using Pogo Pin P75-E2 or P75-LM2
Plenty of them on ebay :wink:

I’ll take a picture, I’m at the office right now :wink:

3 Likes

@ursm
Updated the github repo for the ICSP programmer with some pictures, but looks like this

In the meantime, I’ve just received V1.1 of MiniLora PCB, classic and groove, need some testing

4 Likes

Just to let you know I’ve just released the Mini-LoRa files (Schematics, boards, pictures,…) on github.
Check this out here
If you like, don’t hesitate to star :grinning:

5 Likes

with matthijskooijman/arduino-lmic, the BME280 (Glenn Tyler) and Voltage measurement published above (thanks @Charles) and low power sleep I’m at 29126 bytes (94%).
It’s sad not able to add the Multichannel Gas Sensor (MiCS-6814) which library needs at least 8kB

Next will be running it on battery (clips just arrived) and measuring current.

5 Likes

For information, adding BME280 code to my lib increased 700 bytes (including sending BME280 payload to TTN). Of course I already got I2C sharing reading/writing register functions for other I2C devices, so I’m cheating a little.

without BME280

Sketch uses 29822 bytes (97%) of program storage space. Maximum is 30720 bytes.
Global variables use 1339 bytes (65%) of dynamic memory, leaving 709 bytes for local variables. Maximum is 2048 bytes.

with BME280

Sketch uses 30522 bytes (99%) of program storage space. Maximum is 30720 bytes.
Global variables use 1339 bytes (65%) of dynamic memory, leaving 709 bytes for local variables. Maximum is 2048 bytes.

8Kb for a sensor library is just amazing, just took a look onto it, using float calculation and math pow function are consuming. Library fitted with also lot of print, I’m pretty sure you can tweak it by a 2 factor, I could try but does not have this sensor to test
A good sensor for Mini Lora grove version :wink:

8kB is with stripping all Serial.prints :frowning:
squeezing the multisensor gas library is something for experts - not for me

Your device sensor (very interesting) has an Arduino (ATMega168) on board with a firmware (updatable).
When I compile the firmware, 32% of sketch size

Sketch uses 4612 bytes (32%) of program storage space. Maximum is 14336 bytes.
Global variables use 439 bytes (42%) of dynamic memory, leaving 585 bytes for local variables. Maximum is 1024 bytes.
  • This mean a huge part of the library code should have been done on the sensor itself which makes sense from my point of view :wink:
  • Take care that firmware on the board is far from Low Power, no sleep mode used when sensor is doing nothing

it consumes 48mA forever :wink:
the command powerOff does not reduce current.

//-------------------------------------------------------------------------------------
// get some data from the sensor
*include “Wire.h”
*include “MutichannelGasSensor.h”
*define SENSOR_ADDR 0X04 // default to 0x04

void setup()
{
Serial.begin(115200);
gas.begin(SENSOR_ADDR);
gas.powerOn();
}

void loop()
{
delay(5000);
float c;
c = gas.measure_NH3();
Serial.print("NH3 is “);
if(c>=0) Serial.print(c);
c = gas.measure_CO();
Serial.print(” CO is “);
if(c>=0) Serial.print(c);
c = gas.measure_NO2();
Serial.print(” NO2 is ");
if(c>=0) Serial.println(c);
}
//-------------------------------------------------------------------------------
Sketch uses 8246 bytes (26%) of program storage space. Maximum is 30720 bytes. :frowning:

@ursm
this size if for total sketch not only the library, since it contains float management and arduino core, it’s not only library.

Look this basic sketch

void setup() {    
  Serial.begin(115200);
}
void loop()  {
  static float f = 1.0;
  Serial.print(f);
  delay(1000);
  f += 0.1;
}

and compilation

Sketch uses 3148 bytes (10%) of program storage space. Maximum is 30720 bytes.
Global variables use 200 bytes (9%) of dynamic memory, leaving 1848 bytes for local variables. Maximum is 2048 bytes.

3148 bytes used by Arduino core, so may be lib is more like 4K than 8K, but still too much for an I2C device

And for fun, adding I2C library (wire), 4642 bytes, so definitively Gas lib should fit with some tweaking

#include <Wire.h>

void setup() {    
  Serial.begin(115200);
  Wire.begin();
  Wire.beginTransmission(0xff); 
  Wire.write(0x00);    
  Wire.write(0x01);           
  Wire.endTransmission(); 
}

void loop() {
  static float f = 1.0;

  Serial.print(f);
  delay(1000);
  f += 0.1;
}
Sketch uses 4642 bytes (15%) of program storage space. Maximum is 30720 bytes.
Global variables use 378 bytes (18%) of dynamic memory, leaving 1670 bytes for local variables. Maximum is 2048 bytes.

Do you have some examples of how you are entering and resuming from sleep in the LMIC library between measurements?

Doesn’t necessarily need to be with your cut down code but I’d be interested to see an example even with the base library. Thanks for all the great boards!

@tkerby

All is done in loop, with LMIC events callback and a global flag (timetosleep) to indicate the sleep mode needed.

I’m sleeping x time of watchdog 8s in loop, once max time is triggered, I set timetosleep to false and I send LMIC data and refreshing LMIC in loop
on EV_TXCOMPLETE event I set timetosleep to true, then, loop() go back sleeping until x time of 8S watchdog occurs.

Of course setup need to start lmic and join, on joined I fire a LMIC timer to send first packed 10ms later. then on packet sent (EV_TXCOMPLETE ) loop will go to sleep

2 Likes

Hi Charles, I think we need to adjust the LMIC ticks so the duty cycle is not violated. Some solution posted in this forum includes adjusting the Timer0 overflow (which then adjust the micros()). But, I think that is too hardware dependent. I’m looking whether we can add a function to LMIC like adjustTime(SLEEP_INTERVAL) (I’m simply putting a name here). Call after the MCU wake up.

@rocketscream
I’m sending a packet every 5 min (approx), so I don’t think I’m violating the duty cyle. In fact, the Watchdog wake me 37 times on which I’m going back to sleep immediately. the 38 wake, I’m powering sensors, do measure, send packet and going to sleep again :wink:

For those asked here a skeleton of my code, I removed lot of debug and sensor management, but you’ve got the main concept. Note that push button help me to do different actions depending on how much time I press it :wink:

// Schedule TX every this many seconds (multiple of 8 due to watchdog).
// Takr care of to duty cycle limitations).
#define TX_INTERVAL 300

// Watchdog count between transmit
#define WDT_WAKE_COUNT (TX_INTERVAL/8)

// Some counter demo used in IRQ
volatile uint32_t iWakeCounter = 0;
volatile uint32_t iSwitchCounter = 0 ;
volatile uint32_t iWatchdogCounter = 0 ;
volatile uint8_t  iIrq=0;
bool timeToSleep = false;

// give ULPNode instance
ULPNode  ulpn;

/* ======================================================================
Function: wakeInterruptHandler
Purpose : IRQ Handler called when external device wake us
Input   : - 
Output  : - 
Comments: once fired this interrupt disable itself
====================================================================== */
void wakeInterruptHandler(void)
{
  // Inc counter and set flag for main loop
  iWakeCounter++;
  iIrq |= SLEEP_WAKE_EXT;
}

/* ======================================================================
Function: switchInterruptHandler
Purpose : IRQ Handler called when switch is pressed/released (for wake)
Input   : - 
Output  : - 
Comments: once fired this interrupt disable itself
====================================================================== */
void switchInterruptHandler(void)
{
  // Inc counter and set flag for main loop
  iSwitchCounter++;
  iIrq |= SLEEP_WAKE_SWITCH;
}

/* ======================================================================
Function: watchdogInterruptHandler
Purpose : IRQ Handler called when watchdog IRQ occurs
Input   : - 
Output  : - 
Comments: once fired this interrupt disable the watchdog
====================================================================== */
void watchdogInterruptHandler(void) 
{
  // Inc counter and set flag for main loop
  iWatchdogCounter++;
  iIrq |= SLEEP_WAKE_WATCHDOG ;
} 


/* ======================================================================
Function: onEvent
Purpose : called my LMIC stack on event received
Input   : event type
Output  : - 
Comments: -
====================================================================== */
void onEvent (ev_t ev) {
  static unsigned long last_time=0;
  unsigned long now = millis() / 1000;
  showTime(now);
  DebugF(" ("); showTime(now-last_time);  DebugF(") ");
  last_time = now;
  switch(ev) {
    case EV_SCAN_TIMEOUT:   DebuglnF("EV_SCAN_TIMEOUT");    break;
    case EV_BEACON_FOUND:   DebuglnF("EV_BEACON_FOUND");    break;
    case EV_BEACON_MISSED:  DebuglnF("EV_BEACON_MISSED");   break;
    case EV_BEACON_TRACKED: DebuglnF("EV_BEACON_TRACKED");  break;
    case EV_JOINING:        DebuglnF("EV_JOINING");         break;
    case EV_RFU1:           DebuglnF("EV_RFU1");            break;
    case EV_JOIN_FAILED:    DebuglnF("EV_JOIN_FAILED");     break;
    case EV_REJOIN_FAILED:  DebuglnF("EV_REJOIN_FAILED");   break;
    case EV_LOST_TSYNC:     DebuglnF("EV_LOST_TSYNC");      break;
    case EV_RESET:          DebuglnF("EV_RESET");           break;
    case EV_RXCOMPLETE:     DebuglnF("EV_RXCOMPLETE");      break;
    case EV_LINK_DEAD:      DebuglnF("EV_LINK_DEAD");       break;
    case EV_LINK_ALIVE:     DebuglnF("EV_LINK_ALIVE");      break;
    case EV_SCAN_FOUND:     DebuglnF("EV_SCAN_FOUND");      break;
    case EV_TXSTART:        DebuglnF("EV_TXSTART");         break;

    case EV_TXCOMPLETE:
      DebugF("EV_TXCOMPLETE ");

      // Remove timeout job;
      os_clearCallback(&timeoutjob);

      if (LMIC.txrxFlags & TXRX_ACK) {
        DebugF("with ACK");
        DebugFlush();
        ulpn.RGBBlink(2, RGB_GREEN, WDTO_120MS);
      } else {
        // Needed ACK didn't received it ?
        if ( send_packet_ack) {
          ulpn.RGBBlink(1, RGB_RED, WDTO_120MS);
        } 
      }
      Debugln();

      if (LMIC.dataLen) {
        DebugF("Received ");
        Debugln(LMIC.dataLen);
        DebuglnF(" bytes");
        DebugFlush();
        ulpn.RGBBlink(2, RGB_BLUE, WDTO_120MS);
      }

      ulpn.RGBShow(RGB_OFF);
      // we done
      timeToSleep = true;
    break;

    case EV_JOINED: {
      // Disable link check validation (automatically enabled
      // during join, but not supported by TTN at this time).
      LMIC_setLinkCheckMode(0);

      // Ok send our first data in 10 ms
      os_setTimedCallback(&sendjob, os_getTime() + ms2osticks(10), do_send);
    }
    break;

    default:
      DebugF("Unknown event #");
      Debugln(ev);
    break;
    }
  }

/* ======================================================================
Function: do_send
Purpose : send LoraWAN packet
Input   : 
Output  : - 
Comments: -
====================================================================== */
void do_send(osjob_t* j) 
{
  static uint16_t frameCounter=0;

  // Check if there is not a current TX/RX job running
  if (LMIC.opmode & OP_TXRXPEND) {
    #if DEBUG > 1
    showTime(millis() / 1000);
    DebuglnF(" OP_TXRXPEND, not sending");
    #endif

  } else if (LMIC.opmode & OP_JOINING) {
    #if DEBUG > 1
    showTime(millis() / 1000);
    DebuglnF(" OP_JOINING, not sending");
    #endif
  } else {
    uint8_t len = 0;
    uint8_t payload[32] ; // Max, not all will be used, len is calculated on each data added
    uint8_t *p=&payload[0];
    
    // sensors reading + payload creation
    // ...
    // ...

    ulpn.setDevice(DEVICE_SENSORS_OFF); 

    // calculate Len of packet we created
    len = p - &payload[0];

    // Send Data
    LMIC_setTxData2(1, payload, len,  send_packet_ack);

  }
}


/* ======================================================================
Function: setup
Purpose : setup initial config
Input   : 
Output  : - 
Comments: -
====================================================================== */
void setup() {
  uint8_t tmp;

  // Init ULPNode I/O, Radio; Vbat 
  ulpn.init();

  // Enable global watchdog to avoid lockups
  ulpn.setWatchdog(APP_WATCHDOG_TO);

  ulpn.RGBShow(RGB_OFF);

  // Define IRQ callbacks we need in "user space"
  // Here we want callbacks of 
  // watchdog, wake and switch push button
  ulpn.attachWakeInterrupt( wakeInterruptHandler ); 
  ulpn.attachSwitchInterrupt( switchInterruptHandler ); 
  ulpn.attachWatchdogInterrupt( watchdogInterruptHandler ); 

  // Give power to sensors 
  ulpn.setDevice(DEVICE_SENSORS_ON);

  SERIAL_DEBUG.begin(SERIAL_PORT_SPEED);

  // Do a I2C scan, this will look for known devices and 
  // set the accordings flags to global status
  // You can use it for debug
  if ( (tmp=ulpn.i2cScan()) > 0 ) {
    ulpn.RGBBlink(tmp, RGB_PINK, WDTO_120MS);
  }

  os_getBattLevel();

  // LMIC init
  os_init();

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

  // Enable data rate adaptation
  LMIC_setAdrMode(1);

  // Increase RX1 Windows by 1% in case of clock error on board (crystal shift)
  // This clearly increase son OTAA Join request to works first time even with SF7
  LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);

  // Join the network, sending will be
  // started after the event "Joined"
  LMIC_startJoining();

  timeToSleep = false;
}


/* ======================================================================
Function: loop
Purpose : main loop
Input   : -
Output  : - 
Comments: -
====================================================================== */
void loop() {

  static uint8_t wdt_period = APP_WATCHDOG_8S;// in 8S (set to APP_WATCHDOG_NONE for external wake only)
  static uint8_t wdt_count  = WDT_WAKE_COUNT; // number of WDT wake between transmit

  static bool led_state ;
  bool new_led_state ;
  int16_t send_packet_ms = 0; // Delay sending packet in xxx ms

  uint32_t WakeCounter = 0;
  uint32_t SwitchCounter = 0 ;
  uint32_t WatchdogCounter = 0;
  uint8_t  IrqTrigger = 0;

  // action to be done with button, default none
  btn_action_e SwitchAction = BTN_NONE;

  // Need to go sleeping ?
  if (timeToSleep) {

    // Wait n WDT wake to do things
    while (wdt_count--) {

      goSleeping( SLEEP_BOD_OFF | SLEEP_WAKE_EXT | SLEEP_WAKE_SWITCH, wdt_period );

      // IRQ are disabled, it's safe to get these values
      WakeCounter     = iWakeCounter;
      SwitchCounter   = iSwitchCounter;
      WatchdogCounter = iWatchdogCounter;
      IrqTrigger      = iIrq;

      // Only Watchdog
      if (IrqTrigger == SLEEP_WAKE_WATCHDOG && wdt_count) {
        // Ack this IRQ
        IrqTrigger &= ~SLEEP_WAKE_WATCHDOG;

      } else {
        // Break of while exit sleep mode if it's other IRQ
        wdt_count =0;
      }
    }

    // Restart out watchdog count counter
    wdt_count = WDT_WAKE_COUNT;

    // Set to true for next loop
    // will be reset in case ne need to transmit
    timeToSleep = true;
  }

  // Enable global watchdog to avoid lockups
  ulpn.setWatchdog(APP_WATCHDOG_TO);

  // Ok loop in case we've been triggered by differents IRQ
  // we need to proccess all IRQ
  // I don't think this could happen, but does not hurt to check
  while (IrqTrigger)  {
    // Reset RGB default color to none
    ulpn.RGBSetColor(RGB_OFF);

    // Waked by external Wake 
    if (IrqTrigger & SLEEP_WAKE_EXT ) {
      // Ack this IRQ
      IrqTrigger &= ~SLEEP_WAKE_EXT;

      ulpn.RGBShow(RGB_PINK);

      // Need to send a packet in 100 ms
      send_packet_ms = 100;

    // Waked by push button
    } else if (IrqTrigger & SLEEP_WAKE_SWITCH ) {

      // Get switch port state 
      uint8_t button_port = digitalRead(SWITCH_PIN);

      // Ack this IRQ
      IrqTrigger &= ~SLEEP_WAKE_SWITCH;

      // Button pressed 
      if (button_port==BTN_PRESSED) {
        btn_state_e btn_state;

        // we enter into the loop to manage
        // the function that will be done
        // depending on button press duration
        do {
          // keep watching the push button:
          btn_state = ulpn.buttonManageState(button_port);

          if (btn_state == BTN_WAIT_LONG_RELEASE)
            ulpn.setDevice(DEVICE_LED_OFF);

          // read new state button
          button_port = digitalRead(SWITCH_PIN);

          // Pat the dog, this loop can be as long 
          // as button is pressed
          wdt_reset();
        }
        // we loop until button state machine finished
        while (btn_state != BTN_WAIT_PUSH);

        // Get and save action we need to do after button analyze
        SwitchAction = ulpn.buttonAction();

      // If button still pressed
      }

    } else if (IrqTrigger & SLEEP_WAKE_WATCHDOG ) {
      // Waked by watchdog
      // Ack this IRQ
      IrqTrigger &= ~SLEEP_WAKE_WATCHDOG;

      // Need to send a packet in 100 ms
      send_packet_ms = 100;

    } else if ( IrqTrigger ) {
      // Another Wake ? weird !!!
      
      // ACK all other parasite IRQ, except the one we're dealing on 
      IrqTrigger &= ( SLEEP_WAKE_SWITCH | SLEEP_WAKE_WATCHDOG ) ;
    }

   // On button timeout we do absolutely nothing
    if ( SwitchAction != BTN_TIMEOUT) {

      // What action we want to do depending on button press ?
      if (SwitchAction != BTN_NONE ) {

        if (SwitchAction==BTN_BAD_PRESS) {
        }
        if (SwitchAction==BTN_QUICK_PRESS) {
          // Will send a packet
          send_packet_ms = 10;
        }
        // Button pressed between 1 and 2 seconds 
        if (SwitchAction==BTN_PRESSED_12) {

          // Invert ACK Mode
          if (send_packet_ack) {
            send_packet_ack = false;
            ulpn.RGBBlink(2, RGB_RED, WDTO_120MS);
          } else {
            send_packet_ack = true;
            ulpn.RGBBlink(2, RGB_GREEN, WDTO_120MS);
          }
        }
        // Button pressed between 2 and 3 seconds ?
        if ( SwitchAction==BTN_PRESSED_23) {
          // disable watchdog wake (now only external interrupts
          wdt_period = APP_WATCHDOG_NONE;
        }
        if (SwitchAction==BTN_PRESSED_34) {
          // enable watchdog wake
          wdt_period = APP_WATCHDOG_8S;
        }
        if (SwitchAction==BTN_PRESSED_45) {
        }
        if (SwitchAction==BTN_TIMEOUT) {
        }
          
      } // we had a button press

    } // if not button time out

    // Pat the dog
    wdt_reset();

  } // While IrqTrigger

  // something to send
  if (send_packet_ms) {
    timeToSleep = false;
    os_setTimedCallback(&sendjob, os_getTime()+ ms2osticks(send_packet_ms), do_send);
    send_packet_ms = 0;
  }

  // We've done all our IRQ, ACK them !!!
  cli();
  iIrq = 0;
  sei();

  // Pat the dog
  wdt_reset();

  // Don't forget LMIC STACK
  os_runloop_once();

  // All follow is Led management

  // Let join at the begining of if sequence,
  // is prior to send because joining state send data
  // Joining Quick blink 50ms on each 1/5 second
  if ( LMIC.opmode & (OP_JOINING | OP_REJOIN) )  {
    //new_led_state = ((millis() % 200) < 50) ? HIGH : LOW;
    new_led_state = ((millis() % 150) < 10) ? HIGH : LOW;

    // If sensors detected
    if (ulpn.status() & ( RF_NODE_STATE_SENSOR) ) {
      // Join deal with GREEN
      ulpn.RGBSetColor(RGB_GREEN);
    } else {
      // Join deal with RED 
      ulpn.RGBSetColor(RGB_RED);
    }

  } 

  // Small blink 100ms on each 1/2sec
  if (LMIC.opmode & (OP_TXDATA | OP_TXRXPEND)) {
    // Sending and not joining else keep join speed
    if ( !(LMIC.opmode & (OP_JOINING | OP_REJOIN)) )  {
      new_led_state = ((millis() % 500) < 10) ? HIGH : LOW;
    }

    // If sensors detected
    if (ulpn.status() & ( RF_NODE_STATE_SENSOR) ) {
      // Send deal with BLUE + GREEN
      ulpn.RGBSetColor(RGB_CYAN);
    } else {
      // Send deal with BLUE + RED 
      ulpn.RGBSetColor(RGB_PINK);
    }

  } 

  // This should not happen but blink yellow to see
  if ( LMIC.opmode & (OP_TXDATA | OP_TXRXPEND | OP_JOINING | OP_REJOIN) == 0 ) {
    new_led_state = ((millis() % 2000) < 200) ? HIGH : LOW;

    // Other all is off RED + GREEN
    ulpn.RGBSetColor(RGB_YELLOW);
  }

  // led  need to change state ?
  // avoid digitalWrite() for nothing
  if (led_state != new_led_state) {
    if (new_led_state == HIGH) {
      ulpn.RGBShow();
    } else {
      ulpn.RGBShow(RGB_OFF);
    }

    led_state = new_led_state;

  }
}



7 Likes

@Charles
I would like to thank you for sharing your works and your ideas. There is so much to learn.

I was wondering what the purpose of using the Microchip 24AA02E64T ? I don’t get it. If someone can explain, It will be kind.

@Under5hadow
Thanks for your comment. The Chip is to have a unique LoraWan ID but it’s not mandatory to have this chip, you can set your ID in your sketch code, no problem.

1 Like