Computing stuff tied to the physical world

Developing a low-power sketch

In AVR, Software on Dec 13, 2011 at 00:01

As you’ll know if you’ve been reading this weblog for more than a few nanoseconds, I put a lot of time and effort into making the ATmega-based JeeNode use as little power as possible – microwatts, usually.

In the world of ultra-low power, the weakest link in the chain will determine whether your sketch runs days, weeks, months, or years… low power can be a surprisingly elusive goal. The last microoulombs are the hardest!

But it’s actually quite easy to get some real savings with only a little effort. Here are some things to avoid:

  • Don’t optimize in the wrong place – it’s tempting to start coding in a way which seems like a good idea in terms of power consumption, although more often than not the actual gains will be disappointing.

  • Don’t leave the lights on – the ATmega is amazingly easy to power down, which instantly reduces its consumption by several orders of magnitude. Make sure you do the same for every major power consumer.

  • Don’t just sit there, waiting – the worst thing you can do in terms of power consumption is wait. Unfortunately, that’s precisely what the Arduino runtime’s delay() and delayMicroseconds() calls do.

Ok, with this out of the way, I’ll describe a very simple way to get power consumption down – and hence battery lifetimes up (waaay up in fact, usually).

The trick is to use a convenience function from JeeLib (a.k.a. the Ports library). It’s in the “Sleepy” class, and it’s called loseSomeTime(). So if you have this in your code:

    delay(100);

… then you should replace it with this:

    Sleepy::loseSomeTime(100);

You also need to include the following code at the top of your sketch to avoid compilation and run-time errors:

    #include <JeeLib.h>

    ISR(WDT_vect) { Sleepy::watchdogEvent(); }

As the name indicates, the timing is not exact. That’s because the ATmega is put into a power down mode, and then later gets woken up by the watchdog timer (this is hardware, part of the ATmega). This timer can be several percent off, and although the milliseconds timer will automatically be adjusted by loseSomeTime(), it won’t be as accurate as when updated by the crystal or the ceramic resonator often used as system clock.

The second issue with the watchdog is that it can only delay in multiples of ≈ 16 ms. Any call to loseSomeTime() with an argument less than 16 will cause it to return immediately.

Furthermore, loseSomeTime() can only work with argument values up to 60,000 (60 seconds). If you need longer delays, you can simply create a loop, i.e. to wait 120 minutes in ultra-low power mode, use this:

    for (byte i = 0; i < 120; ++i)
      Sleepy::loseSomeTime(60000);

One last aspect of loseSomeTime() to be aware of, is that it will abort if an interrupt occurs. This doesn’t normally happen, since the ATmega is shut down, and with it most interrupt sources. But not all – so if loseSomeTime() returns prematurely, it will return 0. Normally, it returns 1.

The trade-off of loseSomeTime() is power consumption (system clock and timers are shut down) versus accuracy.

But the gains can be huge. Even this simple LED blink demo will use about 10 mA less (the ATmega’s power consumption while running at 16 MHz) than a version based on delay() calls:

    void loop () {
      digitalWrite(4, 1);
      Sleepy::loseSomeTime(250);
      digitalWrite(4, 0);
      Sleepy::loseSomeTime(250);
    }

To reduce this even further, you could shorten the blink ON time as follows:

    void loop () {
      digitalWrite(4, 1);
      Sleepy::loseSomeTime(50);
      digitalWrite(4, 0);
      Sleepy::loseSomeTime(450);
    }

The LED may be somewhat dimmer, but the battery will last 10x longer vs. the original delay() version.

Battery-powered operation isn’t hard, you just have to think a bit more about where the energy is going!

  1. I helped a friend build an Arduino-based timer (for timelapse photography) and I was wondering if I should try and make it low power (short answer: not worth the trouble). But it made me wonder about how accurate the clock of the Arduino (i.e., the value returned by millis()) remains if you start using loseSomeTime() instead of delay().

    Lets assume that loseSomeTime(60000) actually sleeps for only 59 seconds. When the ATMega wakes up, will millis() show that the device slept for 60 seconds or only the actual 59 seconds? In other words, in a time-sensitive application, will loseSomeTime() screw up the timing completely? (after re-re-reading your text, it seems it will screw it up, but I’d like to have confirmation). And as a a bonus question, is it possible, maybe using additional hardware, to save a lot of energy while keeping accurate time?

    • Matthieu, I was thinking about the same question (some time ago) and my answer is:

      During the low power down states, there is no ‘exact’ (crystal based) oscillator running, only the watchdog timer (which uses a voltage and temperature dependant RC oscillator). So there is absolutely no chance that the timing is ‘exact’. And, yes, you can use timer 2 as RTC with an additional external clock crystal (32768 Hz) and use this for exact timing with ~1uA supply current during sleep. There are ANs for this on the Atmel website.

    • Yes it can be done. JC did it a few months ago… The trick is to run the ATmega from it’s built in internal resonator (8Mhz) and then connect an accurate timing crystal across the crystal pins. The accurate crystal will then generate accurate interrupt pulses which can wake the ATmega up, increment a counter, then let it sleep again.

      http://jeelabs.org/2011/06/28/jeenode-with-a-32-khz-crystal/

  2. This is useful – thanks.

    Do you only use loseSomeTime() if you are going to be running on batteries and can trade accuracy, or do you use it all the time unless you absolutely require accurate timing?

    With regards to an interrupt waking up the ATMega – is there an easy way to tell how long it was asleep if loseSomeTime() returns 0 so you can put it back to sleep for the remainder?

    Thanks,

    Ian

    • I tend to do more and more on batteries (and even on AC it’s all going low-power as the last months of weblog posts show). So yeah, for all but the simplest code – I do at least try to keep low-power mode possible.

      Speaking of which – someone emailed me with power-down/-up calls for EtherCard, so I’ve added those to GitHub.

      As for your second Q: that’s the trouble with an interrupt waking up the ATmega – it’s almost impossible to find out when that happens (after all, all clocks were stopped!). Only thing one could theoretically do, is to leave the watchdog enabled, and hope it fires later on – at which point there is a reference to when it was initiated. This is probably not a practical option though. If tracking time is important, then the best you can do is repeat constant 16 ms watchdog requests – that way the inaccuracy is bounded if one of ‘em gets interrupted.

  3. Thanks. I guess the answer is to use loseSomeTime() in all cases where accuracy is not required. If accuracy is required then you use another method at the expense of power efficiency…

  4. ALSO – either GET RID OF or GET THE RIGHT voltage regulator.

    I have been using the LM2936. Its ok – but I have seem some boost/buck dc-dc converters that use only 3 microamps to run.

    Another alternitive (not as safe – but more often than not works) take 6 volts of AA batteries. Take a 1n4001 diode (1v voltage drop). Now you have 5 volts.

    In many cases – I have found that the voltage regulator takes many times more power to run than the ATmega chip (with brownout off, ADC off, Analog comparitor off, using watchdog timer wake in sleep_mode_power_down)

    The basic theroy I have found is HURRY UP and GET SLEEPING.

    Do what you need to do with the clock, ADC, and the like running. Then get into a low power mode.

    Turning down the clock is also another interesting thing that can be done to decrease power useage. Kinda. You will run the sketch longer, but at a lower power state. so really you dont save power – but if your voltage regulator has a 50ma max – you can get under this pretty easy by slowing things down. I used this to reduce delays to delay(1). It makes things cleaner. I was also trying to do something else – but it didnt work too well.

    It should be noted however that you cant slow the watchdog timer in this way. It has its own independant clock – that only goes to 8 seconds. You have to wake once every 8 seconds. You cant slow the arduino programaticly by 1/2 and get that to 16 seconds. I have not however tried different clock crystals, or using the internal 8 mhz clock.

    Have fun and get that power down. I have a remote control I built that will last for 5-7 years on 4 AA batteries EASY running all the time. It can be done.

    • I’d be very interested to hear what switching regulators can run on 3 µA, that would be fantastic.

      FWIW, the JeeNode uses an MCP1702, which draws around 2 µA when quiescent.

    • AS1323. Take a look at it. I may have another one for you later today – but the datasheer (and hence part number) is on my laptop at my parents house. Going there for christmas today – so I’ll try and get it up.

    • Ooh – interesting!

  5. Hi, Just trying the indoor.pde from Ports.zip from http://jeelabs.net/projects/11/wiki/Ports (as I’m still on 0022). It fails to compile with the following

    indoor.cpp:7:20: error: ST7565.h: No such file or directory
    indoor.cpp:11:55: error: RF12.h: No such file or directory
    indoor:12: error: ‘ST7565’ does not name a type
    indoor.cpp: In function ‘void setup()’:
    indoor:28: error: ‘RF12_868MHZ’ was not declared in this scope
    indoor:28: error: ‘rf12_initialize’ was not declared in this scope
    indoor:29: error: ‘RF12_SLEEP’ was not declared in this scope
    indoor:29: error: ‘rf12_sleep’ was not declared in this scope
    indoor:33: error: ‘glcd’ was not declared in this scope
    indoor:34: error: ‘CMD_DISPLAY_ON’ was not declared in this scope
    indoor:35: error: ‘CMD_SET_ALLPTS_NORMAL’ was not declared in this scope
    indoor.cpp: In function ‘void loop()’:
    indoor:40: error: ‘glcd’ was not declared in this scope

    Is there a simple Sleepy example that does compile ?

  6. Thanks – am just on that steep part of the learning curve you’re talking about… when even simple things are hard.

Comments are closed.