Keeping millisecond time when sleeping

I've done days and days of reading (I'm quite new to Arduino). I'm a hardware EE (previously, but now just a manager), but don't understand much software. Most easy SW stuff I understand, but interrupts and deep sleep required a lot of reading up. That intro out of the way....

I'm looking to make an accurate columb counter based off an INA226 on a custom PCB with a stripped down ATmega328P (no USB, no high quiescent power supply, no LEDs, etc). Since this is battery powered, I wanted to use LowPower.h and SLEEP_MODE_PWR_DOWN to get the current drain really low. As this is a columb counter, I wanted to keep accurate (to the millisecond) time to calculate the amp-hours (from milliamp-seconds measurements), and wanted no worse than 1% accuracy. But then I learned that millis() doesn't work in SLEEP_MODE_PWR_DOWN. Some forum posts suggested using an RTC, so I looked into that. My plan was to use the square wave clock output of the RTC at 1.024kHz to INT0, waking from sleep, and counting the (approximately) millisecond pulses. And just today I learned that the wakeup time from sleep is about 1 ms (@ 5V, 16MHz) so that means I won't sleep at all.

The reason I need millisecond accuracy is that the ADC readings from the INA226 will be about 500ms apart (but the timing is maybe 10% accurate). The INA226 will trigger an interrupt on INT1 when an ADC conversion is complete, the micro will wake up, check how much time has elapsed, get the reading, and calculate milliamp-seconds, then go back to sleep.

Summary:

  • Want to use SLEEP_MODE_PWR_DOWN for minimum power usage
  • Need millisecond accuracy time
  • millis() doesn't work while sleeping
  • micro won't sleep at all if interrupted with 1.024kHz INT0 wake-ups due to 1ms wakeup time
  • seems to be close to no savings if SLEEP_MODE_IDLE if I understand Mr. Gammon's info (still 15mA)
  • Will wake up every ~500ms to read ADC. 500ms was chosen for tradeoff between updating the user interface rate (faster) vs. maximizing power savings (slower) (UI will sleep after timeout, not on 100% of time)
  • ADC conversion elapsed time is only accurate to 10%
  • The ADC integrates/averages over this time. Need to know elapsed time so the milliamps * time calculation is accurate.

Does anyone have suggestions on how to approach this problem? I'm wondering if the solution is the not-so-good SLEEP_MODE_PWR_SAVE where Timer 2 still works and I clock that externally? My Google-foo isn't working so well on finding solutions / descriptions for this. Please point me in the right direction. Any other suggestions?

Thanks,
Chad

we9v:
The reason I need millisecond accuracy is that the ADC readings from the INA226 will be about 500ms apart (but the timing is maybe 10% accurate). The INA226 will trigger an interrupt on INT1 when an ADC conversion is complete, the micro will wake up, check how much time has elapsed, get the reading, and calculate milliamp-seconds, then go back to sleep.

Sorry, but this really doesn't explain why you need to do something every 1 millisecond. If you get samples every 500ms, you can calculate mAS using that time interval. Why not?

aarg:
Sorry, but this really doesn't explain why you need to do something every 1 millisecond. If you get samples every 500ms, you can calculate mAS using that time interval. Why not?

As stated, the conversion time is only accurate to 10%. I will read the ADC every ~500ms, but I need to know if it was 450ms, 500ms, or 550ms (or somewhere in between). In order to do that, I need some means of knowing how much time has elapsed, preferably to the millisecond accuracy. If I assume it was exactly 500ms, that would be an incorrect assumption and my calculations would be 10% wrong, which is not acceptable (21.6 hours does not equal 24 hours.) The only thing I was hoping to do every millisecond is to increment a ~millisecond counter and go immediately back to sleep until the ADC was ready.

If you need a 500 ms clock to wake the Arduino then you could set a DS3231 RTC to output a 1 Hz period square wave and then trigger the Arduino wakeup on every 500 ms rising/falling edge.

mikb55:
If you need a 500 ms clock to wake the Arduino then you could set a DS3231 RTC to output a 1 Hz period square wave and then trigger the Arduino wakeup on every 500 ms rising/falling edge.

Okay...so I wake up at exactly 500ms. The ADC integration might have completed at 450ms, or it might not be done until 550ms. But it integrates(averages) over that entire time. So I need to know if it was a 450ms integration, or a 550ms integration, so it can be properly converted to milliamp-seconds. By assuming it was exactly 500ms, I'm again off 10%. It would be much worse if I chose a 300ms ADC integration time, or 750ms. But it would be fine if I had some means of counting millis while sleeping.

I would really prefer a solution with a worst case measurement error of 1%. Maybe this isn't possible while sleeping and I need to burn the current while on battery power, which isn't attractive.

But it would be fine if I had some means of counting millis while sleeping.

Such an option is available for AVR based Arduinos, but not for standard Arduinos without hardware (board) modifications.

On a "bare bones" Arduino, you can connect a 32 kHz watch crystal to the crystal oscillator pins (with appropriate caps) and use that to drive Timer2. Timer2 will continue to count in sleep mode, and can be used to wake the MCU at regular intervals.

The MCU clock fuses must be set to use the internal 8 MHz oscillator.

The technique is described in the ATmega data sheets. I can post example code later, if this seems interesting. Otherwise, use external timing circuitry.

In theory, you could put a low-frequency crystal on it and leave timer2 running in "power-save" mode instead of power-down mode. Otherwise, since millis() is dependent on an interrupt every millisecond, it wouldn't be ideal from a power-saving PoV even if the timer it's based on stayed running.

Note that the timer2 crystal oscillator uses the same pins as the main crystal oscillator, so you'd probably want to use the internal 8MHz clock to run the CPU... OTOH, even with a 32kHz crystal, it looks like you'd get 32 interrupts/second.

Some of the newer chips (ie ATmega4809) have an additional "RTC" peripheral that might be better suited.

Don't the timers/counters continue in at least certain sleep modes? This way you could use the RTC output to connect to a counter input, to have it count the RTC pulses while sleeping.

If you want ms accurate timing, don't use millis(). Use micros(). 16 bit unsigned micros can count to 65 ms.

added: Arduino millis() skips 6 out of 256 values in the low 8 bits. When subtracting start time from end time the skipped values will throw your result off. Don't use millis() if it matters.

Arduino micros runs in interrupt service routines.

If you want to save power, run at a lower clock speed.

seems to be close to no savings if SLEEP_MODE_IDLE if I understand Mr. Gammon's info (still 15mA)

I am currently getting 2.5mA with IDLE. See my thread here. Seems we have a similar goal in some repects. My next attempt to reduce down from 2.5mA will be to drop to 1MHz during IDLE sleep and back to 8MHz when the speed is needed for short periods (once every 15mins for my application).

jremington:
On a "bare bones" Arduino, you can connect a 32 kHz watch crystal to the crystal oscillator pins (with appropriate caps) and use that to drive Timer2. Timer2 will continue to count in sleep mode, and can be used to wake the MCU at regular intervals.

The MCU clock fuses must be set to use the internal 8 MHz oscillator.

The technique is described in the ATmega data sheets. I can post example code later, if this seems interesting.

Yes, this is interesting and I'd like to understand it further. As mentioned, I'll be doing my own custom PCB, so ditching the RTC and connecting the 32.768kHz crystal directly to the 328 would be quite do-able. I skimmed section 18 of the datasheet, but it wasn't very obvious to me. Please post further details.

Otherwise, use external timing circuitry.

Such as?

wvmarle:
Don't the timers/counters continue in at least certain sleep modes? This way you could use the RTC output to connect to a counter input, to have it count the RTC pulses while sleeping.

Yes, millis() works in SLEEP_MODE_IDLE mode, but according to Mr. Gammon, this is still 15mA whereas his bare-bones board was 15.15mA not in SLEEP_MODE_IDLE. Not much of a savings at all.

But PaulRB seems to be getting down to 2.5mA, which is quite close to the 1.5mA in SLEEP_MODE_PWR_SAVE with an external 32.768kHz crystal. Neither are sub-1mA that I was shooting for, but wishes and reality don't always match up. While this project is battery powered, it's a big battery, so the difference between <1mA and 2.5mA isn't that big of a deal in the big picture. But >15mA bare-bones or >35mA developer board seems grossly excessive.

PaulRB:
I am currently getting 2.5mA with IDLE.

Very interesting Paul. Thanks for sharing. This is considerably different than the info from Mr. Gammon.

The atmega is running at 3.3V using 8MHz internal clock and I am saving power by disabling timer1, timer2, SPI, UART, TWI, and using SLEEP_MODE_IDLE.

What are the individual significant contributors to getting to 2.5mA?
What was your normal 3.3v/8Mhz current?
What was your SLEEP_MODE_IDLE with everything still enabled?

What is the required/desired timing accuracy of your project? Does the ±10% accuracy of the internal RC oscillator concern you, or are you trimming the frequency with OSCCAL?

Here is example code to make an ATmega328 RTC using a 32 kHz crystal on the OSC pins. Runs in POWER_SAVE sleep mode, waking every second to print the time.

Upon reset, it uses the 32 kHz crystal to calibrate the internal 8 MHz RC oscillator, for more accurate serial Baud rates.

My power measurements agree with Nick Gammon's, around 1.5mA average consumption at 3V operation.

Note: although the RTC has granularity 1 second, by reading TCNT2 the second can be subdivided into intervals of 1/256 second.

// low power BCD RTC using bare bones ATMega328p, no regulator, 3-5V
// update 3/1/2019
// This code creates an RTC formed by Timer2 and a 32768 xtal+2x30 pF caps on OSC pins
// The internal RC oscillator is calibrated by comparison with the 32768 standard,
// Low power operation borrowed heavily from https://www.gammon.com.au/power

// SET FUSES: 8 MHz internal RC clock, LP xtal (32768 Hz) on OSC pins
// S. James Remington 3/2013

//reminder: 8 MHz internal RC
//#define F_CPU 8000000UL

#include <avr/sleep.h>
#include <util/delay.h>

// Global variables for RTC
volatile unsigned char RTC_buf[] = {0, 0, 0, 0, 0, 0}; //initialize BCD digits coding for hh:mm:ss
volatile unsigned int dayno = 0; //days since startup

#define BUF_LEN 20
char buf[BUF_LEN];  //print message buffer

void setup() {
  Serial.begin(9600);
  Serial.println("Simple RTC");

  pinMode(2, OUTPUT); //LED on PD2
  for (int i = 1; i < 4; i++) {
    digitalWrite(2, 1);
    delay(100);
    digitalWrite(2, 0);
    delay(900);
  }
  // calibrate 8 MHz oscillator using the 32 kHz xtal
  OSCCAL_calibrate();
  Serial.print("OSCCAL (cal) ");
  Serial.println(OSCCAL);
  Serial.flush(); //finish printing before turning off USART

  //PRR Power Reduction Register (set PRADC after ADCSRA=0)
  //Bit 7 - PRTWI: Power Reduction TWI
  //Bit 6 - PRTIM2: Power Reduction Timer/Counter2
  //Bit 5 - PRTIM0: Power Reduction Timer/Counter0
  //Bit 3 - PRTIM1: Power Reduction Timer/Counter1
  //Bit 2 - PRSPI: Power Reduction Serial Peripheral Interface
  //Bit 1 - PRUSART0: Power Reduction USART0
  //Bit 0 - PRADC: Power Reduction ADC

  ADCSRA = 0; //disable ADC
  // turn off  unneeded modules. 
  PRR |= (1 << PRTWI) | (1 << PRTIM1) | (1 << PRSPI) | (1 << PRADC) | (1 << PRUSART0) ;

  // reprogram timer 2 for this application

  timer2_init();
}

void loop() {

  char t = 0;
  unsigned int batt, h, m, s;

  while (1) {

    // wake up on Timer2 overflow (1/sec)
    // output day, time and cpu voltage on serial monitor every ... (10 seconds now)

    if (t != RTC_buf[4]) { //have 10 seconds passed?

      t = RTC_buf[4];
      s = 10 * RTC_buf[4] + RTC_buf[5]; //format time
      m = 10 * RTC_buf[2] + RTC_buf[3];
      h = 10 * RTC_buf[0] + RTC_buf[1];

      PRR &= ~((1 << PRADC) | (1 << PRUSART0)); //turn on ADC and USART
      ADCSRA = (1 << ADEN); //set up ADC properly
      ADCSRA |= (1 << ADPS0) | (1 << ADPS1) | (1 << ADPS2); // ADC Prescaler=128

      delay(1); //let ADC and USART stabilize

      batt = readVcc();  //read battery voltage

      //    print day, time and battery voltage

      snprintf(buf, BUF_LEN, "%u,%02u:%02u:%02u,%u", dayno, h, m, s, batt); //max ~16 characters
      Serial.println(buf);
      Serial.flush(); //finish printing before going to sleep

      // back to sleep with modules off

      ADCSRA = 0; //ADC off
      PRR |= (1 << PRADC) | (1 << PRUSART0);  //turn off ADC and USART

    } //end if (t)

    set_sleep_mode(SLEEP_MODE_PWR_SAVE);
    sleep_enable();
    cli(); //time critical steps follow
    MCUCR = (1 << BODS) | (1 << BODSE); // turn on brown-out enable select
    MCUCR = (1 << BODS);         //Brown out off. This must be done within 4 clock cycles of above
    sei();
    sleep_cpu();

  } //end while(1)
}

//******************************************************************
//  Timer2 Interrupt Service
//  32 kKz / 256 = 1 Hz with Timer2 prescaler 128
//  provides global tick timer and BCD binary Real Time Clock
//  no check for illegal values of RTC_buffer upon startup!

ISR (TIMER2_OVF_vect) {

  // RTC function

  RTC_buf[5]++; // increment second

  if (RTC_buf[5] > 9)
  {
    RTC_buf[5] = 0; // increment ten seconds
    RTC_buf[4]++;
    if ( RTC_buf[4] > 5)
    {
      RTC_buf[4] = 0;
      RTC_buf[3]++; // increment minutes
      if (RTC_buf[3] > 9)
      {
        RTC_buf[3] = 0;
        RTC_buf[2]++; // increment ten minutes

        if (RTC_buf[2] > 5)
        {
          RTC_buf[2] = 0;
          RTC_buf[1]++; // increment hours
          char b = RTC_buf[0]; // tens of hours, handle rollover at 19 or 23
          if ( ((b < 2) && (RTC_buf[1] > 9)) || ((b == 2) && (RTC_buf[1] > 3)) )
          {
            RTC_buf[1] = 0;
            RTC_buf[0]++; // increment ten hours and day number, if midnight rollover
            if (RTC_buf[0] > 2) {
              RTC_buf[0] = 0;
              dayno++;  //count days since startup
            }
          }
        }
      }
    }
  }
}


/*
  // initialize Timer2 as asynchronous 32768 Hz timing source
*/

void timer2_init(void) {

  TCCR2B = 0;  //stop Timer 2
  TIMSK2 = 0; // disable Timer 2 interrupts
  ASSR = (1 << AS2); // select asynchronous operation of Timer2
  TCNT2 = 0; // clear Timer 2 counter
  TCCR2A = 0; //normal count up mode, no port output
  TCCR2B = (1 << CS22) | (1 << CS20); // select prescaler 128 => 1 sec between each overflow

  while (ASSR & ((1 << TCN2UB) | (1 << TCR2BUB))); // wait for TCN2UB and TCR2BUB to be cleared

  TIFR2 = (1 << TOV2); // clear interrupt-flag
  TIMSK2 = (1 << TOIE2); // enable Timer2 overflow interrupt
}

// Read 1.1V reference against AVcc
// EVERY PROCESSOR MUST BE CALIBRATED INDIVIDUALLY!

unsigned int readVcc(void) {

  unsigned int result;

  // set the reference to Vcc and the measurement to the internal 1.1V reference

  ADMUX = (1 << REFS0) | (1 << MUX3) | (1 << MUX2) | (1 << MUX1);
  delay(2); // Wait for Vref to settle

  ADCSRA |= (1 << ADSC); // Start conversion
  while (bit_is_set(ADCSRA, ADSC)); // wait until done
  result = ADC;

  // two is better than one

  ADCSRA |= (1 << ADSC); // Start conversion
  while (bit_is_set(ADCSRA, ADSC)); // wait until done
  result = ADC;

  // calibrated for Miniduino board
  result = 1195700UL / (unsigned long)result; //1126400 = 1.1*1024*1000
  return result; // Vcc in millivolts
}

//
// Calibrate the internal OSCCAL byte, using the external 32768 Hz crystal as reference.
//

void OSCCAL_calibrate(void)  //This version specific to ATmegaXX8 (tested with ATmega328)

{
  unsigned char calibrate = 0; //FALSE;
  unsigned int temp;

  TIMSK1 = 0; //disable Timer1,2 interrupts
  TIMSK2 = 0;

  ASSR = (1 << AS2);      //select asynchronous operation of timer2 (32,768kHz)
  OCR2A = 200;            // set timer2 compare value
  TCCR1A = 0;
  TCCR1B = (1 << CS11);   // start timer1 with prescaler 8
  TCCR2A = 0;
  TCCR2B = (1 << CS20);   // start timer2 with no prescaling (ATmega169 use TCCR2A!)

  while (ASSR & ((1 << TCN2UB) | (1 << TCR2BUB))); //wait for TCN2UB and TCR2BUB to be cleared

  delay(5000); //5 seconds to allow xtal osc to stabilize

  while (!calibrate)
  {
    cli(); // disable global interrupts

    TIFR1 = 0xFF;   // clear TIFR1 flags
    TIFR2 = 0xFF;   // clear TIFR2 flags

    TCNT1 = 0;      // clear timer1 counter
    TCNT2 = 0;      // clear timer2 counter

    while ( !(TIFR2 & (1 << OCF2A)) ); // wait for timer2 compareflag

    TCCR1B = 0; // stop timer1

    sei(); // enable global interrupts

    if ( (TIFR1 & (1 << TOV1)) ) temp = 0xFFFF; //overflow, load max
    else   temp = TCNT1;

    if (temp > 6150)  //expect about (1e6/32768)*201 = 6134 ticks
    {
      OSCCAL--;   //RC oscillator runs too fast, decrease OSCCAL
    }
    else if (temp < 6120)
    {
      OSCCAL++;   //RC oscillator runs too slow, increase OSCCAL
    }
    else calibrate = 1; //done

    TCCR1B = (1 << CS11); // (re)start timer1

  } //end while(!calibrate)
} //return

we9v:
What are the individual significant contributors to getting to 2.5mA?
What was your normal 3.3v/8Mhz current?
What was your SLEEP_MODE_IDLE with everything still enabled?

I updated my thread with those figures.

we9v:
What is the required/desired timing accuracy of your project? Does the ±10% accuracy of the internal RC oscillator concern you, or are you trimming the frequency with OSCCAL?

Well... my anemometer is hardly a precision instrument, its a cheap plastic one sold as a spare for home weather stations. I have not calibrated it and just used the 1rpm = 2.4Km/hr figure I found on this page. So little point my worrying too much about accuracy.

I could try using OSCCAL, but the circuit will be battery powered and outdoors in summer/winter, so I expect voltage and temp will also affect the accuracy of the internal clock.

I asked on my thread how the accuracy of the watchdog timer compares to the internal clock. I understand it's a separate circuit, and works even when the internal clock is powered off in SLEEP_MODE_POWER_DOWN.

Shameless plug.

The ESP32 has 3 cores. One of the cores is the ULP (Ultra Low Power Core) core. The ULP core can keep track of time whiles the main cores sleep.

For instance, I have a solar tracker. When dark is detected, the solar array is parked, all peripherals along with the 2 cores are powered down, and the ULP core is activated. The ULP core wakes once every 5 seconds, does a A:D and, if daylight, powers the main cores and peripherals to do their thing. The project, during night time operations, is powered by a single 2600mAh battery.

Thanks jremington and PaulRB. Thanks Idahowalker for the suggestion, but I think that's beyond my capabilities, and that part's capabilities far exceed what's needed for this project, other than the inclusion of the ULP core.

Sub 2.5mA might be acceptable, and leaving Timer0 running in IDLE sure makes it much easier. Switching to an external 8MHz crystal should be no additional current, and far more accurate than the internal RC oscillator (even with the hassle of calibrating to 1%... 10ppm crystal is 0.001%).

I may study jremington's code further. 1/256 seconds is 4ms...maybe "good enough" accuracy, as that's 4ms over a 500ms measurement period, so that's in the 1% ballpark. Tradeoff: 1% accuracy for 1mA less consumption. I did mention that this is battery powered, but I failed to say what battery (on purpose). Let's just say that 1.5mA vs. 2.5mA is in the noise, but I still didn't want the battery monitor to be a significant draw compared to other devices.

On my custom PCB, I may include pads for both an 8MHz and 32.768kHz crystal (de-poping one) so I have the option of choosing one or the other. I currently don't have a "bare bones" dev kit / breadboard, so I'm unable to see the fruits of this work yet. My custom PCB will both be the development and final product (hopefully). Not intended for production, maybe only 3 or 4 built.

Thanks for everyone's contributions to date! If there are any other ideas, please add! PCB probably won't go out to fab for a bit.

I use that basic RTC circuit as part of a remote driveway vehicle detector (with RF transmission for announcements) and it runs about 4 months on 3xAA alkaline batteries.

It must power a magnetometer four times each second for the basic measurement, and from time to time, the transmitter.

At some point I'll add a small solar panel for indefinite operation.

I'm down to 2mA with my test circuit. My sketch now drops the clock to 1MHz in code by adjusting the main clock pre-scaler. Then, to keep millis() running normally, it adjusts timer0's pre-scaler to compensate. This means I can switch between 8MHz and 1MHz when required and still use millis() for timing.

I'm out of ideas to reduce the current significantly further without using lower power sleep modes, which rely on the watchdog's inaccurate and unstable clock.

Timer2 is active in power save mode (0.75µA power consumption). According to the datasheet timer2 can count the time from the external crystal, while the ATmega328P itself runs off the internal oscillator. This means you should be able to connect a 32.768 kHz external crystal to the chip, and use that as input for timer2, go to power save mode, and count the 32.768 kHz signal.
Then set the prescaler of timer2 to 32, and you count a hair faster than milliseconds (1024 counts per second.
Next, set the timer2 overflow interrupt, and have the Arduino to wake on this interrupt, update a 1/4-second counter (increase a variable by 250), and go back to sleep.
If you're woken up by the ADC check the current value of timer2 and your counter, do the measurement, calculate the milliseconds since last measurement, and go back to sleep. You can compensate here easily for timer2 being a little too fast.

jremington:
I use that basic RTC circuit as part of a remote driveway vehicle detector (with RF transmission for announcements) and it runs about 4 months on 3xAA alkaline batteries.

It must power a magnetometer four times each second for the basic measurement, and from time to time, the transmitter.

Just wondering, would it be possible to equip that magnetometer with a permanent magnet, so that when a car passes the magnetic field is distorted, an inductor in that field produces a small voltage, a comparator output goes low triggering an interrupt waking the Arduino?