Sleep and wake intervals

I am working with a bread boarded 328P running the 8mhz internal clock boot loader and I have a 32.768khz external crystal. I am trying to put it to sleep and then wake up at predetermined times based on timer2. I have variable "seconds" and use timer2 to increment this variable. So if seconds is greater than variable A then it wake sup and takes pin A0 low and reset seconds to zero and start the cycle over. I am successfully putting it to sleep and waking it up but the cycles are not even and I cant understand why. For example with the posted code it will go to sleep and then wake up every 2 minutes and 14 seconds. It is running very reliably at that interval for the first 3-4 cycles and then the next 3-4 cycles will be all over the place. For example sometimes is it 3 minutes, sometimes 4+ minutes and each cycle is vastly different. Then it will settle down and run reliably at the 2 minutes and 14 seconds for a few cycles. Although it is set for a short duration for testing my goal is to set it for several hours delay. Any suggestions are greatly appreciated!

#include <avr/sleep.h> //Needed for sleep_mode
#include <avr/power.h> //Needed for powering down perihperals such as the ADC/TWI and Timers


volatile long seconds = 0;


int A = 28;


//The very important 32.686kHz interrupt handler
SIGNAL(TIMER2_OVF_vect){
  seconds++;
}


void setup() {                
    //To reduce power, setup all pins as inputs with no pullups
  for(int x = 1 ; x < 18 ; x++){
    pinMode(x, INPUT);
    digitalWrite(x, HIGH); //changed from low to high which was 400ua change
  }
  

 
  //Power down various bits of hardware to lower power usage  
  set_sleep_mode(SLEEP_MODE_PWR_SAVE);
  sleep_enable();


  //Shut off ADC, TWI, SPI, Timer0, Timer1

  ADCSRA &= ~(1<<ADEN); //Disable ADC
  ACSR = (1<<ACD); //Disable the analog comparator
  DIDR0 = 0x3F; //Disable digital input buffers on all ADC0-ADC5 pins
  DIDR1 = (1<<AIN1D)|(1<<AIN0D); //Disable digital input buffer on AIN1/0
  
  power_twi_disable();
  power_spi_disable();
 // power_usart0_disable();
  power_timer0_disable();
  power_timer1_disable();
  //power_timer2_disable(); //Needed for asynchronous 32kHz operation

  //Setup TIMER2
  TCCR2A = 0x00;
  TCCR2B = (1<<CS22)|(1<<CS21)|(1<<CS20); //Set CLK/1024 or overflow interrupt every 8s
  ASSR = (1<<AS2); //Enable asynchronous operation, 32kHz xtal needed
  TIMSK2 = (1<<TOIE2); //Enable the timer 2 interrupt
  
  

  //Setup external INT1 (pin3) interrupt
  EICRA = (1<<ISC11)|(1<<ISC01)|(1<<ISC10)|(1<<ISC00); //Interrupt on rising edge
  EIMSK = (1<<INT0)|(1<<INT1); //Enable INT interrupts
  //need pull downs if active high, active low can enable internal pulllups
  digitalWrite(3, LOW); 
  digitalWrite(2, LOW); 

  sei(); //Enable global interrupts
}

void loop() {
  
  sleep_mode(); //Stop everything and go to sleep. Wake up if the Timer2 buffer overflows or if you hit the button
  ADCSRA &= ~(1<<ADEN); //Disable ADC
  ACSR = (1<<ACD); //Disable the analog comparator
  DIDR0 = 0x3F; //Disable digital input buffers on all ADC0-ADC5 pins
  DIDR1 = (1<<AIN1D)|(1<<AIN0D); //Disable digital input buffer on AIN1/0
  
 //variable 1
  if(seconds > A){
    reset_5s();
 
  }

}

void reset_5s(){
  
  pinMode(A0, OUTPUT); 
  digitalWrite(A0, LOW); // needs to be LOW
  fake_msdelay(2000);
  digitalWrite(A0, HIGH); // needs to be HIGH
  pinMode(A0, INPUT); 
  
  seconds = 0;
}

//This is a not-so-accurate delay routine
//Calling fake_msdelay(100) will delay for about 100ms
//Assumes 8MHz clock
void fake_msdelay(int x){
  for( ; x > 0 ; x--)
    fake_usdelay(1000);
}

//This is a not-so-accurate delay routine
//Calling fake_usdelay(100) will delay for about 100us
//Assumes 8MHz clock
void fake_usdelay(int x){
  for( ; x > 0 ; x--) {
    __asm__("nop\n\t"); 
    __asm__("nop\n\t"); 
    __asm__("nop\n\t"); 
    __asm__("nop\n\t"); 
    __asm__("nop\n\t"); 
    __asm__("nop\n\t"); 
    __asm__("nop\n\t"); 
  }
}

If you have the timer interval set to one second it seems a bad idea to put a two seconds busy wait in the ISR.

Perhaps you could use your 'seconds' counter to determine when the two second interval is over. Sort of like the "Blink Without Delay" example.

Why the fake_msdelay() function? Why not just use millis()?

It's insufficient to declare seconds as volatile, atomic access also needs to be guaranteed. cli() and sei() can be inserted around accesses to the variable, or I would do:

#include <util/atomic.h>
...
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
	if(seconds > A){
		reset_5s();
	}
}

Not sure if that is the problem, but it is a potential problem.

Oh duh, because timer0 and hence millis() is turned off.

First, why not use the watchdog timer? Second, why not have the switch interrupt on LOW? That lets you wake it up without having to have much running.

I did a post about this sort of stuff:

If you disable brown-out detection you can get current consumption down to 6 uA, per my figures on that page.

In my code I woke up and checked a clock chip for the actual time. You could do something similar, let the watchdog get you within approximately where you want to be, and use the clock chip to get 1-second accuracy.

Seems like a perfectly valid approach, and works without the additional RTC chip.

Been playing with this, pretty interesting. Was off in the weeds a few times, and I'm afraid my code no longer looks much like yours. Found this in the datasheet, I think it's also part of the issue.

• If Timer/Counter2 is used to wake the device up from Power-save or ADC Noise Reduction
mode, precautions must be taken if the user wants to re-enter one of these modes: If reentering
sleep mode within the TOSC1 cycle, the interrupt will immediately occur and the
device wake up again. The result is multiple interrupts and wake-ups within one TOSC1 cycle
from the first interrupt. If the user is in doubt whether the time before re-entering Power-save or
ADC Noise Reduction mode is sufficient, the following algorithm can be used to ensure that
one TOSC1 cycle has elapsed:
a. Write a value to TCCR2x, TCNT2, or OCR2x.
b. Wait until the corresponding Update Busy Flag in ASSR returns to zero.
c. Enter Power-save or ADC Noise Reduction mode.

So at the bottom of loop(), I have:

    //Cannot re-enter sleep mode within one TOSC cycle. This provides the needed delay.
    OCR2A = 0;    //write to OCR2A, we're not using it, but no matter
    while (ASSR & _BV(OCR2AUB)) {}    //wait for OCR2A to be updated

There's only 1 part might worth checking. Rearrange the Timer 2 configuration steps as in the datasheet page 156.
They advise disabling the timer interrupt > set ASR mode > write new values to the prescaler > enable the interrupt.
Worth a try as they said the configuration might get corrupted if that sequence is not followed. :slight_smile:

I wonder about this part:

  //Setup external INT1 (pin3) interrupt
  EICRA = (1<<ISC11)|(1<<ISC01)|(1<<ISC10)|(1<<ISC00); //Interrupt on rising edge
  EIMSK = (1<<INT0)|(1<<INT1); //Enable INT interrupts
  //need pull downs if active high, active low can enable internal pulllups
  digitalWrite(3, LOW); 
  digitalWrite(2, LOW);

There is nothing like an internal pull down. So unless you have connected external pull downs this will cause the pins to float. Since these pins can trigger interrupts there is some chance that this is causing erratic wake ups depending on air humidity, the mood of the controller and the current phase of the moon.

Some excellent suggestions here!

@Nick, I disabled the BOD and in sleep mode I measure 1.5uA. I didn't use the watchdog timer because it only last 8 seconds and I need as much as 24 hours. Maybe this can be looped but I didn't know how to do it so I used a variable that increments from timer2. I looked over your example sketch and I did not see anything that would make the WDT timer increment more than 8 seconds. Is it possible to get hours out of it and do you think it would be a better solution than what I have now? Accuracy is a concern and I do need this to maintain a fairly precise clock over a period of years.
@Jack, I read all of the millis() articles and spent some hours last night working with it. I tried to implement it but had difficulty since timer0 gets shut off. Since I wont have any other interrupts to worry about is delay() a bad option? I have never heard of atomic.h and have no idea what it does but I will read up on that.

@Udo, I will give that a try and let you know my results. During my testing at this moment you are correct in your assumption that they are floating.

Thanks again everyone, this forum is really amazing!!

No, 8 seconds is its limit, and they say it isn't all that accurate (after all, how accurately do you keep time when you are asleep?).

However, it can be looped, as I did in the post above. So, 10 loops would give you 80 seconds. But for accuracy, I think you would need an external clock (or live with higher power consumption). The solution I did for the earlier poster was to have an onboard clock chip (itself kept alive by a lithium battery that lasts 10 years). Then Atmega sleeps for (say) a minute, powers up the clock board from a digital pin (the clock itself uses 1.5 mA) reads the time, unpowers the clock, and then goes back to sleep.

It is going to be a trade-off. To do it all onboard, accurately, you will need a sleep mode that consumes more power. The external clock solution is still pretty low current. After all, depending on your application, you might just loop the watchdog for 5 minutes, and then check the clock. The other thing you could do is calculate how much watchdog looping you think you need. For example, if you aren't planning to do anything for an hour, you could probably loop hundreds of times, without powering up the clock. Then closer to the target time, you power the clock up more frequently.

I made the change to the interrupt as suggested by Udo and the clock is now running reliably. Thank you very much!

@Nick, I am thinking I will continue to use the 32.768khz crystal which seems to be fairly accurate. I require the Arduino to wake up only a few times per day so the sleep intervals are fairly long so I think I will stick with my existing method of incrementing a variable. I have a GPS connected so I am thinking I can use the time from the NMEA data to update the seconds variable and keep everything running accurately. That is my plan anyway, we will see how it works out....

fbriggs4:
Thanks again everyone, this forum is really amazing!!

The really amazing thing is that I just happened to have a very similar circuit breadboarded, I just had to move an LED to A0 to test it out! Had quite a bit of fun with it.

Some notes:

  1. Turning off many of the individual peripherals (Timers 0 and 1, TWI, SPI, etc.) is not strictly necessary; because Power Save mode stops the associated clocks, they are effectively shut down. So not necessary to replace millis(). Of course it doesn't tick while the MCU is sleeping, but we can still use it to time intervals when not sleeping.

  2. The goToSleep() function also turns off the Brown-Out Detector. I am measuring just about 1µA while the MCU is sleeping.

  3. I took the INT0/INT1 interrupt set-up code out, so it will need to be added back in.

  4. The ATOMIC_BLOCK business is just some covenience macros for manipulating the global interrupt enable bit. From the avr-libc user manual:

The macros in this header file deal with code blocks that are guaranteed to be excuted
Atomically or Non-Atmomically. The term "Atomic" in this context refers to the unability
of the respective code to be interrupted.
These macros operate via automatic manipulation of the Global Interrupt Status (I) bit
of the SREG register. Exit paths from both block types are all managed automatically
without the need for special considerations, i. e. the interrupt status will be restored to
the same value it has been when entering the respective block.
A typical example that requires atomic access is a 16 (or more) bit variable that is
shared between the main execution path and an ISR. While declaring such a variable
as volatile ensures that the compiler will not optimize accesses to it away, it does not
guarantee atomic access to it.

So here's what I ended up with, FWIW:

#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <util/atomic.h>

volatile long ticks;                   //global variables are initialized to zero (but see below*)
long ticksCopy;                        //copy of ticks to minimize need for atomic blocks
long maxSeconds = 64;                  //can be set to any number, but because the Timer2 overflow interrupt is set
                                       //to only occur every 8 seconds, if this is not a modulo-8 number, the LED will
                                       //not blink until the next 8-second interval elapses.

void setup()
{
    //To reduce power, set all pins as inputs with pullups
    for(int x = 1 ; x < 20 ; x++) {    //was x < 18 ... jc
        pinMode(x, INPUT);
        digitalWrite(x, HIGH);
    }
 
    //Set up TIMER2
    TIMSK2 = 0;                        //stop timer2 interrupts while we set up
    ASSR = _BV(AS2);                   //Timer/Counter2 clocked from external crystal
    TCCR2A = 0;                        //override arduino settings, ensure WGM mode 0 (normal mode)
    TCCR2B = _BV(CS22) | _BV(CS21) | _BV(CS20);    //prescaler clk/1024 -- TCNT2 will overflow once every 8 seconds
    TCNT2 = 0;                         //start the timer at zero
    while (ASSR & (_BV(TCN2UB) | _BV(TCR2AUB) | _BV(TCR2BUB))) {}    //wait for the registers to be updated    
    TIFR2 = _BV(OCF2B) | _BV(OCF2A) | _BV(TOV2);                     //clear the interrupt flags
    TIMSK2 = _BV(TOIE2);               //enable interrupt on overflow
    ATOMIC_BLOCK(ATOMIC_FORCEON) {     //Arduino enables interrupts, so RESTORESTATE would work here as well
        ticks = 0;                     //*ensure we start from zero (interrupts may have occurred before we set up the timer)
    }
}

void loop()
{
    goToSleep();
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
        ticksCopy = ticks;
    }
    if(ticksCopy * 8 >= maxSeconds) {
        pinMode(A0, OUTPUT); 
        digitalWrite(A0, LOW);            //turn on the LED for a couple seconds
        delay(2000);                      //LED is wired to Vcc and connected to A0 with a current-limiting resistor
        digitalWrite(A0, HIGH);
        pinMode(A0, INPUT); 
        ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
            ticks = 0;
        }
    }
}

void goToSleep()
{
    byte adcsra, mcucr1, mcucr2;

    //Cannot re-enter sleep mode within one TOSC cycle. This provides the needed delay.
    OCR2A = 0;                        //write to OCR2A, we're not using it, but no matter
    while (ASSR & _BV(OCR2AUB)) {}    //wait for OCR2A to be updated  

    sleep_enable();
    set_sleep_mode(SLEEP_MODE_PWR_SAVE);
    adcsra = ADCSRA;                  //save the ADC Control and Status Register A
    ADCSRA = 0;                       //disable ADC
    ATOMIC_BLOCK(ATOMIC_FORCEON) {    //ATOMIC_FORCEON ensures interrupts are enabled so we can wake up again
        mcucr1 = MCUCR | _BV(BODS) | _BV(BODSE);  //turn off the brown-out detector
        mcucr2 = mcucr1 & ~_BV(BODSE);
        MCUCR = mcucr1;               //timed sequence
        MCUCR = mcucr2;               //BODS stays active for 3 cycles, sleep instruction must be executed while it's active
    }
    sleep_cpu();                      //go to sleep
                                      //wake up here
    sleep_disable();
    ADCSRA = adcsra;                  //restore ADCSRA
}

//The Timer/Counter2 Overflow Interrupt Handler
ISR(TIMER2_OVF_vect)
{
    ticks++;
}

Jack- I appreciate you sharing your sketch. I read up on the atomic library and as I add functionality to my sketch I think you are right that I will need that.