2

I'm trying to save power by putting my device to sleep once it has run through the code once and turn it back on every 33 milliseconds. So basically, if the code finishes executing in 20 milliseconds the device can go to sleep for the remaining 13 milliseconds and it will be turned on by the interupt which is set to go off every 33 milliseocnds.

According to the documentation an interrupt is based on a change to the pin state, but I'm sure there must be a way to base it on time. If there isn't then is there a way to set the total time that a device sleeps? The sleep function seemed to just turn the device off, but can it be given a parameter to just turn off for a certain amount of time?

  • See for example this answer for an example of setting a timer to periodically wake-up the Arduino Mega. – Edgar Bonet Jun 15 '16 at 15:16
  • I saw the sleep function, but where are they setting the actual interrupt? – Angel Lockhart Jun 15 '16 at 15:21
  • The line TIMSK1 = _BV(OCIE1A); // enable TIMER1_COMPA interrupt, as the comment says, enables the interrupt. – Edgar Bonet Jun 15 '16 at 15:23
  • Follow up question, is this code Arduino? I see a main method but no setup() or loop(). – Angel Lockhart Jun 15 '16 at 15:28
  • It's plain avr-libc, but it works under the Arduino environment. – Edgar Bonet Jun 15 '16 at 15:46
2

The easiest way to achieve your purpose seems to be to configure a timer to fire every 33 ms, and use this interrupt to wake up the MCU. For this, you first have to choose what timer to use. On the Mega, I would rather use the 16-bit timer, but the ATtiny85 only has 8-bit timers. Next you have to choose a prescaler value (clock frequency division) among those available for that timer. The prescaler should be large enough to avoid overflowing your timer, but smaller prescaler values give better time resolution.

Then you configure the timer to run in “CTC mode” (clear timer on compare match). In this mode, it counts from zero to the value that you programmed in the OCRxA register (where x is the number identifying the timer), and then goes back to zero. This way you can get an interrupt every OCRxA + 1 timer cycles.

In the example code below, you have to replace x with the appropriate timer number, and check the datasheet for the bits to set in the control registers TCCR*:

#include <avr/interrupt.h>

// Interrupt period, in prescaled clock cycles.
const uint16_t PERIOD = ...;  // or uint8_t for an 8-bit timer.

// The timer interrupt is used ONLY to wake the CPU, so there is no job
// to be done inside the interrupt service routine.
EMPTY_INTERRUPT(TIMERx_COMPA_vect);

void setup()
{
    // Disable Timer 0 interrupts.
    // Warning: this will prevent the Arduino from keeping time.
    TIMSK0 = 0;

    // Configure Timer x.
    TCCRxA = 0;            // undo the timer config done...
    TCCRxB = 0;            // ...by the Arduino core library
    TCNTx = 0;             // reset the timer
    OCRxA = PERIOD - 1;    // set the period
    TIMSKx = _BV(OCIExA);  // enable TIMERx_COMPA interrupt
    TCCRxA = ...;          // set the counting mode to CTC...
    TCCRxB = ...;          // ...and set the prescaler
}

void loop()
{
    // Sleep while waiting for the timer interrupt.
    sleep_mode();

    // Now that we have been waken up...
    do_the_job();
}

Edit: As noticed by Gerben, the Arduino core library configures Timer 0 to send periodic interrupts (one every 1024 µs), which are used for timekeeping (millis(), delay() and co.). This interrupts will wake up the MCU and prevent it from sleeping as long as you want. The simplest solution, which is implemented in the code above, is to disable the Timer 0 interrupts. The drawback is that the Arduino timekeeping functions will not work anymore.

If you need those timekeeping functions, then the proper solution is the one sketched by Gerben in his comment: set a boolean in the interrupt that can be checked in the main loop. Here is the naive implementation of this idea: EMPTY_INTERRUPT(TIMERx_COMPA_vect); is replaced by

volatile bool time_elapsed;

ISR(TIMERx_COMPA_vect)
{
    time_elapsed = true;
}

Then, in the main loop you sleep repeatedly until the ISR sets the flag:

void loop()
{
    // Warning: race condition here!
    while (!time_elapsed)
        sleep_mode();
    time_elapsed = false;

    // Now that we have been waken up...
    do_the_job();
}

As stated in the comment, this naive implementation suffers from what is known as a race condition. The problem is in the following scenario:

  1. The main loop reads the value of time_elapsed and it turns out to be false
  2. At this point the interrupt fires and the ISR sets that boolean to true
  3. Returning from the ISR, the main loop does not test the flag again (since it just did it) and goes straight to sleep_mode();.

At this point the MCU ends up sleeping although it should be performing the job which is due now.

This race condition may not a be a big deal to you: The MCU will be eventually woken up by the Timer 0 interrupt no more than 1024 µs later. In some cases it is an issue, but fortunately there is a proper solution. It is described in the page about sleeping of the avr-libc documentation. It relies on the fact that when interrupts are disabled, then enabled again with sei(), the machine instruction right after the sei() is guaranteed to be executed before any interrupt is serviced. Then the proper way to test the flag and sleep is:

void loop()
{
    while (!time_elapsed) {
        cli();
        if (!time_elapsed) {
            sleep_enable();
            sei();          // interrupts enabled
            sleep_cpu();    // this will be executed before any ISR
            sleep_disable();
        }
        sei();
    }
    time_elapsed = false;

    // Now that we have been waken up...
    do_the_job();
}

With this version, if the interrupt fires right after the test if (!time_elapsed), it will be serviced after sleep_cpu(), which will have the effect of waking the MCU right away.

  • Wow that helps a lot, but why do you subtract 1 from PERIOD? That other example did that too. Also, PERIOD is supposed to be the duration between interrupts, correct? So for my project I'd set it equal to the number of clock cycles that will equal 33 milliseconds. Right? – Angel Lockhart Jun 15 '16 at 16:09
  • The counter counts from 0 to OCRxA inclusive, so the period is OCRxA+1. The period is in prescaled clock cycles, see the prescaler setting. – Edgar Bonet Jun 15 '16 at 16:52
  • 1
    Timer0 (used for millis and delay) will also wake up the device. You probably need to set Boolean inside the interrupt, so the main loop can check if it needs to go back to sleep or not – Gerben Jul 15 '16 at 20:08
  • 1
    Thanks @Gerben for noticing. You are absolutely right. I've amended the answer to account for this issue. – Edgar Bonet Jul 16 '16 at 12:59
  • Nice solution for the race condition. – Gerben Jul 16 '16 at 19:47
0

There are many many kinds of interrupt. External interrupts (of the kind you use with attachInterrupt()) are but one.

Almost every internal peripheral in the AVR chip can generate an interrupt (or multiple interrupts in some cases). One of the most useful is the timer interrupt which is triggered by one of the timers when its timer has overflowed.

I am not familiar with the AVR's sleep mechanisms, but I would assume a timer interrupt can wake it from sleep - assuming the timer can run while sleeping.

You should check the documentation for the chip on your Arduino rather than relying on the (somewhat scant and basic, and often misleading) documentation on the Arduino website when it comes to doing anything low-level like this.

  • You are right: the timer interrupt can wake up the MCU, but only if it's sleeping in the so called “IDLE” mode... which is the default sleep mode anyway. – Edgar Bonet Jun 15 '16 at 15:13
  • Does the setting of a timer interrupt vary from microcontroller to microcontroller? I'm trying to write this code on the Arduino Mega first and then port it to an ATtiny. – Angel Lockhart Jun 15 '16 at 15:14
  • Yes. You should read the documentation for both your target microcontrollers and program accordingly - probably with copious usage of #ifdef ... #endif – Majenko Jun 15 '16 at 15:15
  • @AngelLockhart: It's often not that different. You could probably use the same code for the 16-bit timers of the ATmega2560 and the ATtiny84A. What ATtiny are you targeting? At what clock frequency? – Edgar Bonet Jun 15 '16 at 15:22
  • The principle of operation will be almost the same between them. Numbers will change though (calculations for time vs clock frequency), and register names may change (depending on number of timers available, etc). In general, though, there will be a lot of copy-and-paste of logic blocks like these when it comes to designing chips in the same broad family. – Majenko Jun 15 '16 at 15:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.