High precision interrupt with RTC module

Hello everyone,

i am not new to the arduino programming but new on this forum.
I am right now working on a little project, which needs a pretty precise controlling of a stepper driver. It involves a Atmega328 5V/16Mhz, and a A4988 driver board with a NEMA 17 stepper motor. Right now, i have a working code with timer1 in ctc, which sends a 1025Hz signal (2050 interrupts per second) to my driver. Thing is, i need to fine-tune this frequency later, so this value is not fixed. For what it should do, it works so far.

But because the steps i generate should come pretty precise for about 6 hours straight, i thought the bottleneck is the build in crystal of the arduino. I know 16Mhz are not 16Mhz, and sure not within diffrent temperature ranges etc. So my best guess was, why no using an RTC module (DS3231) which has a build in TCXO and use this high precision device (precise enough for my project) to generate a temperature compensated output signal for my 1025Hz.

First thought, was using the 8192Hz square-wave output from sqw of the DS3231 module as external clock for a timer. Then i realised, my ctc value will be only 3 if i want 1025Hz output. That can't be the right way.

Next idea was why not using a second timer, which mesures the signal from the 8192Hz compensated sqw output and compares both values. From this i could calculate a offset for the 16 Mhz and compensate this in my ctc value. But i have a feeling this isn't really helping accuracy either.

So can anyone give me a thought how i can use the DS3231 module to get a precise timer interrupt?

thanks in advance

On AVR processors, the 8 MHz internal oscillator has a frequency calibration register named OSCCAL, and can be tuned in steps of some size. So, your idea of using the RTC as a frequency reference could work.

Atmel had an application note on how to do that, but it might be hard to find under Microchip ownership.

Here is Arduino code that does that using a 32 kHz crystal on Timer2 as the reference and incidentally creates an RTC. Of course, you can just use the crystal as an interrupt source for your project, as well, but it has the same problem of frequency division as the 8192 Hz source.

// 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.  With Timer0 off, **can't use delay() or millis()**!
  PRR |= (1 << PRTWI) | (1 << PRTIM1) | (1 << PRSPI) | (1 << PRADC) | (1 << PRUSART0) ; //need Timer2

  // 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

i have found the OSCCAL register while researching for my problem. As far as i know this will only work with the internal oscillator, right? The board i am using is a Arduino Nano like board, which has a external 16Mhz oscillator on board.

I have thought about using an extra TCXO in 10-16 Mhz range and using this as external reference clock for the timer. But i found out, those (much too small to solder for me) SMD parts will output a too weak signal. so this must be boosted up to be recognised as reference clock. This sounds like a lot of extra hardware work just to compensate the internal clock. The RTC module would be nice little single PCB addon, if it does the job.

this will only work with the internal oscillator

Correct.

Guentec:
Hello everyone,

i am not new to the arduino programming but new on this forum.
I am right now working on a little project, which needs a pretty precise controlling of a stepper driver. It involves a Atmega328 5V/16Mhz, and a A4988 driver board with a NEMA 17 stepper motor. Right now, i have a working code with timer1 in ctc, which sends a 1025Hz signal (2050 interrupts per second) to my driver. Thing is, i need to fine-tune this frequency later, so this value is not fixed. For what it should do, it works so far.

But because the steps i generate should come pretty precise for about 6 hours straight, i thought the bottleneck is the build in crystal of the arduino. I know 16Mhz are not 16Mhz, and sure not within diffrent temperature ranges etc. So my best guess was, why no using an RTC module (DS3231) which has a build in TCXO and use this high precision device (precise enough for my project) to generate a temperature compensated output signal for my 1025Hz.

First thought, was using the 8192Hz square-wave output from sqw of the DS3231 module as external clock for a timer. Then i realised, my ctc value will be only 3 if i want 1025Hz output. That can't be the right way.

Next idea was why not using a second timer, which mesures the signal from the 8192Hz compensated sqw output and compares both values. From this i could calculate a offset for the 16 Mhz and compensate this in my ctc value. But i have a feeling this isn't really helping accuracy either.

So can anyone give me a thought how i can use the DS3231 module to get a precise timer interrupt?

thanks in advance

Finesse the issue and replace the ceramic resonator on the board with a quartz crystal?

How much accuracy do you need? DigiKey.com has some 10-parts-per-million frequency stability and 10-parts-per-million frequency tolerance 16 MHz crystals for $0.36 each.

johnwasser:
How much accuracy do you need? DigiKey.com has some 10-parts-per-million frequency stability and 10-parts-per-million frequency tolerance 16 MHz crystals for $0.36 each.

that's a good question. To answer this right i would first have to know the accuracy of the actual oscillator on the board. Guess its just a ceramic one, its a 2$ board.

I want to use this application for deep sky photography. I am building a selfmade star-tracker. You'll set it to the rotational axis of the earth and then it must rotate with the same speed as earth does (360° in 86160s). The better accuracy ill have with my rotational speed, the better i can track the stars without dots getting lines on the pictures.

Edit:

so, made a quick calculation and i think 50ppm should be max tolerance. tracking for 6 hours would be then off by 1 second.

Guentec:
But because the steps i generate should come pretty precise for about 6 hours straight,

Unless you have some very very sophisticated photography equipment and very very very dark skies, you won't get anywhere near this length of time for a single exposure.

I've dabbled in a similar project, and had a rig built that "theoretically" could run for > 3 hours, but the light pollution becomes the problem. I could only achieve about a 10 second exposure in a "dark" urban environment and maybe up to 30 seconds in a rural environment before the sky becomes washed out in a brownish tinge. For me, it was impossible to get an exposure longer than even one minute before everything became washed out. This is where stacking becomes important. Do a google search for Deep Sky Stacker (DSS).

I also realized how precisely the tracker needs to be aligned, and how tight the mechanical tolerances need to be if such a contraption is to be useful.

I hope you have success in this endeavor - it is great to combine two hobbies: arduino and photography!

Here's a sample of what I was able to achieve. This is comet Lovejoy from January 2015. It is about 100 images of 1 sec exposure, stacked together using DSS. (Canon 60D, 200mm, f/2.8, 12500ISO)

Hi John,

i am beginning small, so my first attempt is to make tracking possible in RA axis. polar alignment will be done with AZ and Alt fine adjustment in a self build equatorial wedge. With the drift align method the best polar adjustment is possible. I will and still using DSS for image stacking. But because i am at my limits with a normal wide angle lens and a static tripod, i want to build my own tracker. Plan is to track the target as long as possible while taking a whole bunch single photos for tracking. for the photos i am using a remote with timer for my canon 60D. so i can take 100 pictures in a row with 60s exposure and iso 800 without sitting beside an pushing buttons all the time. So i wont take a single 6 hours exposure, but i want to keep my target tracked for several hours.

Next step is to update my rebuild EQ3 mount with 2 stepper motors and make controlled tracking with PHD and a cmos camera possible.

That sounds like it should work. From my experience, it does seem that astrophotograpy amplifies any errors that are in the system. I made a "barn door" tracker, and even though I used a piano hinge, it still had a lot of loose play, relatively speaking. Another challenge was the screw to move the tracker - any slight bend or inconsistency in the threads was enough to induce a bit of wobble that showed in the photographs, even an incorrect or imperfect coupler between the screw and stepper motor can cause problems.
And then there was the focusing - normally done manually, but worth your time to lock it down somehow, and check it from time to time (a change in ambient temperature can affect it over time). I was going to make my system totally remote controlled, so I could adjust and monitor from indoors, but I never got that far.

I hope you have more success than I had. All the best with your project!