Realtime Clock (RTC) with 32kHz Crystal and Sleep

Submitted by Ed_B on Mon, 02/07/2011 - 12:11

Printer-friendly versionPrinter-friendly version

I'm still working on the unnamed project. It involves low power operation. The story is moving forward on different fronts simultaneously. Previously I looked at power consumption issues on a running ATMega168/ ATMega328 configuered as Arduino Lilypads. Significant issues turned out to be floating pins and the Power Reduction Register (PRR). To adjust the processor speed and clock source, I had to get comfortable with changing the fuse settings in the AVR. The next piece of the puzzle is setting up sleep mode. Sleep involves going to sleep and waking up, and also knowing what time it is when the MCU wakes up. The ATMega provides a higly accurate (moreso than the normal 16 MHz crystal) super low power clock using a special feature of TIMER2. TIMER2 can generate its own clock, separate from the normal system clock. It uses external watch crystal crystal connected to the TOSC1 and TOSC2 pins. Because this clock is accessable only to TIMER2, it can run while the rest of the AVR is asleep. If TIMER2 is configued to generate a periodic interrupt (there's more than one configuration that will work) that interrupt will wake up the AVR. Whenever the work is done, a few magic lines of code in the program can be used to put the AVR back to sleep. The TIMER2 interrupt with the watch crystal can be used to generate accurate interrupts for any purpose, not just sleep.

 CLICK READ MORE TO SEE THE REST OF THE POST

The Details

The crystal must be the 32.768kHz type. It is connected to the pins TOSC1 and TOSC2, pins 8 and 9 on the DIP '168 and '328. To use these pins, you must remove the 16MHZ crystal or ceranic resonator, and the two capacitors on pins 8 and 9. This leaves the AVR's CPU with no clock. To provide a clock, change the fuse settings to use the 8MHz internal clock. This is the only option for a system clock at this point. The only Arduino out-of-the-box fuse settings that run this configuration are for the Lilypad on the ATMega168.

Watch crystals come in several varieties. There is even an Atmel application note on choosing and properly connecting a watch crystal. The capcitance of the crystsal seems to matter a lot. The 6pF value requires no external caps, but the 12pF requires two caps to be added -- and they're not the same value as the ones for a normal crystal. For the watch crystal, so far I'm havng good luck with a Citizen "CFS206-32.768DZB-UB" from digikey. Their part number is 300-8301-ND. I'm using the Evil Mad Scientist "ATMegaxx8 target board", and I slodered my crystal right next to the chip pins. The only parts on the board are the ATMega168, a 10K pullup resistor on pin 1, and the watch crystal.

The TIMER2 oscillator runs all the time. In the normal watch crystal mode, an interrupt is generated every 1 second. Any interrupt handler code should be short and fast. For making a real time clock, the usual practice is to increment a seconds counter variable and do a little math and formatting to convert the raw seconds count to hh:mm:ss format. That's fast. To get other code to run at the interupt trigger, it's best to use  one or more flag variables which are set in the interrupt handler. The flags are constantly checked (polled) in the main loop to see what functon should be called. TIMER2 runs all the time and is always generating interupts. It's important that an interrupt can finish and begin waiting for the next trigger from TIMER2. The flag allows the interrupt handler to communicate wth the rest of the program without being stuck making function calls and doing normal processing types of work, and you don't run the risk of missing a new interrupt. Flags are a great way to cleanly seperate the interrupt handler from the main program.

In this program, the main loop puts the processor to sleep after its current task is finished. When TIMER2 generates an interrupt, two things hsppen: the code in the interrupt handler runs, and the main loop of the main program wakes up and picks up execution where it left off at sleep time. Do remember that TIMER2's interrupt handler fires off every 1 second (in this program) and does its work regardless of whether the main loop is awake or asleep.

Here's my test program that gets the bare minimum working as the basis for some experiments.

// LEDtimer1.pde -- steps toward an RTC based low power timer controller.
// by Ed Bennett, 2011
// Everything works. Now need to make a way to set the clock.
// Sleeps at 450uA at 5V
// Uses fuse settings for mega168 Lilypad. This includes the 8MHz internal
clock.
// RTC time is thru timer2 being run by a 32.768kHz xtal on pins TOSC1&2
// Timer2 wakes mcu from sleep each 1 second.

#include <avr/sleep.h>

#define TASK1         1    // id number
#define TASK1_BEGIN   10   // start time
#define TASK1_END     22   // end time

#define TASK2         2
#define TASK2_BEGIN   33
#define TASK2_END     44

#define TASK3         3
#define TASK3_BEGIN   50
#define TASK3_END     59

#define RUN           1
#define FINISHED      0

int whichTask = 0;

unsigned long seconds;

int task1_flag;
int task2_flag;
int task3_flag;

int led1val;
int LED1 = 7;
int led2val;
int LED2 = 6;

void rtc_init(void);

void setup(){
  pinMode(LED1, OUTPUT);  
  pinMode(LED2, OUTPUT);
  Serial.begin(9600);

  rtc_init();  //initialise the timer
  sei();

  set_sleep_mode(SLEEP_MODE_PWR_SAVE);
}

void task1(void){ // task1 has some print statements to show beginning and
ending times

  Serial.println("");
  Serial.print("T1 begin ");
  Serial.print(seconds);
  delay(1000);
  
  while(task1_flag == RUN){
    led2val ^= 1;
    digitalWrite(LED2, led2val);
    delay(25);
  }

  Serial.println("");
  Serial.print("T1 end ");
  Serial.print(seconds);
  delay(1000);

}

void task2(void){
  
  while(task2_flag == RUN){
    led2val ^= 1;
    digitalWrite(LED2, led2val);
    delay(2000);
  }
}

void task3(void){

  while(task3_flag == RUN){
    led2val ^= 1;
    digitalWrite(LED2, led2val);
    delay(25);
  }
}

void loop(){

  switch (whichTask){

  case TASK1:
    task1();
    break;

  case TASK2:
    task2();
    break;

  case TASK3:
    task3();
    break;
  }

  gnight();
}

void rtc_init(void)
{  
  TCCR2A = 0x00;  //overflow
  TCCR2B = 0x05;  //5 gives 1 sec. prescale
  TIMSK2 = 0x01;  //enable timer2A overflow interrupt
  ASSR  = 0x20;   //enable asynchronous mode
}

void eval(void){  // THIS IS ACTUALLY PART OF THE ISR
  led1val ^= 1; // blinky for testing
  digitalWrite(LED1, led1val);

  switch(seconds){
  case TASK1_BEGIN:     // a timer setpoint match value
    whichTask = TASK1;  // for loop() to start the task
    task1_flag = RUN;   // for the task to know when to finish
    break;

  case TASK2_BEGIN:
    whichTask = TASK2;
    task2_flag = RUN;
    break;

  case TASK3_BEGIN:
    whichTask = TASK3;
    task3_flag = RUN;
    break;

  case TASK1_END:
    task1_flag = FINISHED; // for the task to know when to finish
    break;

  case TASK2_END:
    task2_flag = FINISHED;
    break;

  case TASK3_END:
    task3_flag = FINISHED;
    break;

  default:  
    whichTask = FINISHED;

  }
}

void gnight(void){

  digitalWrite(LED2, 0); // turn off White LED
  whichTask = FINISHED;
  sleep_enable();
  sleep_mode();
  sleep_disable();
}

ISR(TIMER2_OVF_vect)
{
  if(++seconds == 60)seconds=0;
  eval();
}