Maximize battery life of MySensors nodes

Posted by

Let’s explore a couple of strategies to maximize battery life for home automation nodes based on the MySensors framework. Applying those strategies, I have built sensor nodes that run for more than 36 months on one CR2032 coin cell.

Basics

To begin with, I applied all the usual power saving tips you can find on the internet, in particular in Nick Gammon’s excellent overview of power saving techniques for microprocessors:

  • run the processor at 3.3V rather than 5V
  • use the internal RC oscillator rather than an external crystal oscillator
  • make sure all unused GPIO pins are either configured as input with pull-up resistor, or as an output
  • if using an Arduino Pro Mini or clone, remove the voltage regulator and LEDs, and run it directly off the 3V battery

Go to sleep!

The standard wisdom is to sleep as much as possible to minimize power consumption, and only wake up when it is time to report something, or when an event has happened that needs to be reported. A window sensor would report when the window has been opened or closed.

So let’s use the sample code from the MySensors website, hook up a window contact (reed switch) to pin 3, enable the internal pull-up, and run this. The code for checking if the window is open is very simple:

bool window_open;
window_open = digitalRead( CONTACT_PIN ) == HIGH;

While the node is asleep, I measure a supply current of 6.3 µA, very nice … as long as the contact is open. When the contact is closed, current rises to 99.4 µA, way too much! Put another way: if you build a window sensor like this, you must keep the window open 99% of the time, or else your battery won’t last very long.

The reason for this is simple: when the switch is closed, current flows though the internal pull-up resistor of the AVR controller. That resistor is about 20-50 kΩ, according to the datasheet, so with a 3.3V supply voltage, we expect a current of about 66-165 µA.

Walking in your sleep

To save more power, the pin connected to the window sensor is an output most of the time, and set to low, while the µC is sleeping. No current will flow into or out of the pin. Checking if the window is open has now become

bool window_open;
pinMode( CONTACT_PIN, INPUT_PULLUP );
_delay_us(10);  // wait for the stray capacitance to charge
window_open = digitalRead( CONTACT_PIN ) == HIGH;
digitalWrite( CONTACT_PIN, LOW );
pinMode( CONTACT_PIN, OUTPUT );

I briefly re-configure the pin to be an input with a pull-up resistor, read the status of the pin, and then make the pin an output again. The supply current is now 6.3 µA independent of whether the window is open or closed.

Power problem solved — but how do we sense that the window has been opened or closed? We can’t use interrupts any more, because the pin is not an input while the processor sleeps.

When reading the MySensors documentation, I first assumed that a call like sleep( 3600000L ) would send the processor to sleep for an hour (3’600’000 milliseconds), but when I looked at the code for the function in detail, I realized that it actually sends the processor to sleep repeatedly, for 8 seconds at a time, until an hour has passed. This is because it uses the watchdog timer to wake itself up, and the longest time interval for that is 8 seconds.

So if the processor wakes up every 8 seconds anyway, then we might as well do something useful before going back to sleep … like checking if the window is open. The worst case is that the change to the window contact will be reported 8 seconds after it happened — not a problem for a home automation solution, I think.

Tick, tick, tick, …

My improved sleep library defines a tick() function like so

int8_t tick(void) __attribute__((weak));

If you implement this function in your application, then it will get called from inside the improved function called snooze() every 8 seconds or so. Your function should return 0 to say “nothing interesting happened, let’s go back to sleep”, or any value !=0 to say “sleep ends here, something worth handling in loop() has happened”, and the snooze() function will return that value.

You can’t do anything fancy in your tick() function, because the system isn’t fully awake really: no serial output, no RF messages, you can’t even read the A/D converter, because that has been turned off by the sleep function, to reduce power.

Keeping time while asleep

When you use the standard sleep() function, the Arduino millis() function will no longer give the accurate time since start, because the milliseconds counter is not advanced during sleep. My snooze() function will adjust the Arduino milliseconds counter, so you can use the millis() function to time events or decide when to execute repetitive tasks. The adjustment may be off by up to 8 seconds, but it is good enough for “do something once per hour” timing needs.

My nodes software usually contain definitions like so

#define SECONDS		* 1000uL
#define MINUTES 	* 60uL SECONDS
#define HOURS 		* 60uL MINUTES

#define BATTERY_INTERVAL     12 HOURS
#define TEMP_INTERVAL        10 MINUTES

and then the loop() function looks like this

void loop() {
  unsigned long t_now = millis();

  static unsigned long t_temp_report = 0uL;
  if ((unsigned long)(t_now-t_temp_report) > TEMP_INTERVAL) {
    t_temp_report = t_now;
    // .... report temperature
  }

  static unsigned long t_battery_report = 0uL;
  if ((unsigned long)(t_now-t_battery_report) > BATTERY_INTERVAL) {
    t_battery_report = t_now;
    // .... report battery
  }

  if (snooze( 5 MINUTES ) != MY_WAKE_UP_BY_TIMER) {
    // report some event
  }
}

Other strategies, less effective

I also tried other power saving strategies one can find on the internet. Most of them don’t work in my wireless sensor nodes, because the processor is in a sleep mode most of the time, with all clocks stopped and many peripherals disabled.

  • Disabling the A/D converter when not in use gave no additional benefit, because the MySensors framework takes care of that already.
  • Enabling the pull-up resistor on unused pins, to prevent the pins from “floating” halfway between high and low and drawing more power, gave no additional benefit — this surprised me a bit, I would have expected a small effect here.
  • Running the processor at a lower clock frequency, like 1 MHz, gave no additional benefit, because in sleep mode the clocks are stopped anyway, and a stopped 1 MHz clock consumes as much power as a stopped 8 MHz clock — none.
  • Disabling peripherals (timers, I2C interface) through the ATmega power reduction register PRR gave no additional benefit, because most peripherals are disabled during sleep anyway.
F_CPUsleep typeADCpull-up
unused pins
PRRsupply current
switch open/closed
8 MHzsleep()6.3 µA / 99.6 µA
1 MHzsleep()6.3 µA / 99.6 µA
8 MHzsleep()disabledPRADC6.3 µA / 99.6 µA
8 MHzsleep()disabledall pull-upPRADC6.3 µA / 99.6 µA
8 MHzsnooze()disabledall pull-upPRADC6.3 µA / 5.9 µA
8 MHzsnooze()disabledall pull-upPRADC, PRTWI,
PRTIM1, PRTIM2
6.3 µA / 5.9 µA

The test setup

For this demonstration, I used a minimal ATmega328 based window contact sensor based on sample code in the “Sleeping” section of the MySensors API documentation, here. The hardware consisted of a Slim Node PCB with a bare ATmega328P running at 8 MHz using the internal RC oscillator, no external crystal.

The node was powered by an FTDI USB-to-serial module, and I measured supply current with a Uni-T UT61D multimeter … not the high-precision equipment I would like to have, but good enough to see changes between different conditions, and estimate power consumption.

Leave a Reply