The Zero Drift seconds()

During my experimenting I needed a seconds() independent from millis(). Here is a small hack which gives you seconds() function implemented around Timer0 OVF ISR, fully independent from millis(), and as precise as well you know the exact frequency of your crystal oscillator (ie. done via a measurement).

It does not use counting interrupts, but elaborates MCU cycles with a special algorithm instead. The new core uint32 seconds() function returns a precise n. of seconds since boot (like the millis() returns milliseconds) with a nice bonus - it works with any crystal oscillator frequency value. The seconds() does not drift in long run, so you may use it for measuring long periods.

The only thing you have to do is to provide your precise crystal frequency in Hz (or, better, the frequency the crystal oscillator actually runs on your board), ie. on my 16MHz board:

#define CRYSTAL_OSC_FREQ 15998457L

In wiring.c (I did it with 1.0.5 core in uecide):

..
volatile unsigned long timer0_overflow_count = 0;
volatile unsigned long timer0_millis = 0;
static unsigned char timer0_fract = 0;

// ***** PITO
#define CRYSTAL_OSC_FREQ 15998457L
volatile unsigned long timer0_seconds = 0;
volatile long timer0_ticker = CRYSTAL_OSC_FREQ;
// *****

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
	// copy these to local variables so they can be stored in registers
	// (volatile variables must be read from memory on every access)
	unsigned long m = timer0_millis;
	unsigned char f = timer0_fract;	

	// ***** PITO
	long t = timer0_ticker;
	// *****

	m += MILLIS_INC;
	f += FRACT_INC;
	if (f >= FRACT_MAX) {
		f -= FRACT_MAX;
		m += 1;
	}
	timer0_fract = f;
	timer0_millis = m;
	timer0_overflow_count++;	

	// ***** PITO - here is the seconds() zero drift algorithm
	t -= 16384L;
	if ( t < 16384L )
		{ t += CRYSTAL_OSC_FREQ;
		 timer0_seconds++; 
		}
	timer0_ticker = t;
	// *****		
}
..

// ***** PITO
unsigned long seconds()
{
	unsigned long s;
	uint8_t oldSREG = SREG;
	// disable interrupts while we read timer0_seconds or we might get an
	// inconsistent value
	cli();
	s = timer0_seconds;
	SREG = oldSREG;
	return s;
}

unsigned long ticks()
{
	unsigned long t;
	uint8_t oldSREG = SREG;
	// disable interrupts while we read timer0_overflow_count or we might get an
	// inconsistent value 
	cli();
	t = timer0_overflow_count;
	SREG = oldSREG;
	return t;
}
// *****

The sketch to test it:

// The Zero Drift Seconds() Test
void setup() {
Serial.begin(115200);
}

void loop() {
  long ms, sec, tks;
  sec = seconds();
  ms = millis();
  tks = ticks();
  Serial.print(sec);
  Serial.print(" ");
  Serial.print(ms);
  Serial.print(" ");
  Serial.print(tks);
  Serial.println();
  delay(50);
  
}

You have to add seconds() function def into Arduino.h and into keywords.txt in a similar way as with millis().

Arduino.h:

..
unsigned long millis(void);
unsigned long seconds(void);
unsigned long ticks(void);
unsigned long micros(void);
..

\lib\keywords.txt:

..
millis	KEYWORD2	Millis
ticks	KEYWORD2	Ticks
seconds	KEYWORD2	Seconds
micros	KEYWORD2	Micros
..

EDIT: included ticks(), millis() = 1.024 * ticks() :slight_smile:
For adventurous users only, provided as-is, no warranty of any kind :slight_smile:

	uint8_t oldSREG = SREG;
	// disable interrupts while we read timer0_seconds or we might get an
	// inconsistent value
	cli();

Should the SREG save be inside the atomic block, the value could change between those code lines.

Don't mind me, it can't be, but isn't this a potential fault with all similar code? Although I suppose if you did get an interrupt things are back to normal when you get to the cli instruction. Pity there isn't an atomic "save and set" instruction.


Rob