Accurate millis at 20 MHz?

Running your AVR processor at 20 MHz? millis not quite right? This may help.

The changes are based on the Arduino 0022 Core. This is the original code...

// the prescaler is set so that timer0 ticks every 64 clock cycles, and the
// the overflow handler is called every 256 ticks.
#define MICROSECONDS_PER_TIMER0_OVERFLOW (clockCyclesToMicroseconds(64 * 256))

// the whole number of milliseconds per timer0 overflow
#define MILLIS_INC (MICROSECONDS_PER_TIMER0_OVERFLOW / 1000)

// the fractional number of milliseconds per timer0 overflow. we shift right
// by three to fit these numbers into a byte. (for the clock speeds we care
// about - 8 and 16 MHz - this doesn't lose precision.)
#define FRACT_INC ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3)
#define FRACT_MAX (1000 >> 3)

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

SIGNAL(TIMER0_OVF_vect)
{
	// 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;

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

	timer0_fract = f;
	timer0_millis = m;
	timer0_overflow_count++;
}

unsigned long millis()
{
	unsigned long m;
	uint8_t oldSREG = SREG;

	// disable interrupts while we read timer0_millis or we might get an
	// inconsistent value (e.g. in the middle of a write to timer0_millis)
	cli();
	m = timer0_millis;
	SREG = oldSREG;

	return m;
}

The changes are highlighted...

#if (F_CPU == 16000000L) || (F_CPU == 8000000L)

// the prescaler is set so that timer0 ticks every 64 clock cycles, and the
// the overflow handler is called every 256 ticks.
#define MICROSECONDS_PER_TIMER0_OVERFLOW (clockCyclesToMicroseconds(64 * 256))

// the whole number of milliseconds per timer0 overflow
#define MILLIS_INC (MICROSECONDS_PER_TIMER0_OVERFLOW / 1000)

// the fractional number of milliseconds per timer0 overflow. we shift right
// by three to fit these numbers into a byte. (for the clock speeds we care
// about - 8 and 16 MHz - this doesn't lose precision.)
#define FRACT_INC ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3)
#define FRACT_MAX (1000 >> 3)

typedef uint8_t timer0_fract_t;

#elif (F_CPU == 20000000L)
#define MILLIS_INC (0)
#define FRACT_INC (512)
#define FRACT_MAX (625)
typedef uint16_t timer0_fract_t;
#endif

volatile unsigned long timer0_overflow_count = 0;
volatile unsigned long timer0_millis = 0;
static timer0_fract_t timer0_fract = 0;

SIGNAL(TIMER0_OVF_vect)
{
// 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;
timer0_fract_t f = timer0_fract;

/* rmv: The code below generates considerably less code (emtpy Sketch is 440 versus 418)...

m += MILLIS_INC;
f += FRACT_INC;
if (f >= FRACT_MAX) {
f -= FRACT_MAX;
m += 1;
}
...rmv */

f += FRACT_INC;

if (f >= FRACT_MAX)
{
f -= FRACT_MAX;
m = m + MILLIS_INC + 1;
}
else
{
#if MILLIS_INC != (0)
m += MILLIS_INC;
#endif
}

timer0_fract = f;
timer0_millis = m;
timer0_overflow_count++;
}

micros, delay, delayMicroseconds will not work correctly at 20 MHz. This change only corrects millis.

At the time of this post, the code is completely untested.

Questions? Does the code work for you?

Hi,

I haven't tried your code, but I have one comment and one query:

  • It appears to me that the original code should work with any clock frequency in a reasonable range, whereas your code only works at 8, 16 and 20MHz. As a minimum, I would add "#else #error This code only works with F_CPU = 8000000, 16000000 or 20000000" before the first #endif in your code.
  • What is the reason that the original code isn't accurate enough? Could it be improved so that it is more accurate, while still retaining the ability to work with any reasonable F_CPU?

dc42:
It appears to me that the original code should work with any clock frequency in a reasonable range,

The interrupt interval has to be a multiple of 8 microseconds or the >>3 (/8) in the original code will throw away data. The /8 scaling was apparently done to fit the microsecond part of the accumulator into one byte. It works out nicely for 8 MHz and 16 MHz so it worked for Arduino but not so much at other clock rates. I think 1 MHz, 2 MHz, 4 MHz and 24 MHz will also work.

There are other places in the wiring.c and wiring.h code where you get wrong answers if you use a clock rate other than 8 MHz and 16 MHz.

dc42:
It appears to me that the original code should work with any clock frequency in a reasonable range

The reasonable range is 1, 2, 4, 8, and 16 MHz. The code produces an inaccurate millis (and micros) at all other clock frequencies.

whereas your code only works at 8, 16 and 20MHz

By design. It's meant solely as a test case for someone with a 20 MHz board.

As a minimum, I would add "#else #error This code only works with F_CPU = 8000000, 16000000 or 20000000" before the first #endif in your code.

As folks test it and provide feedback, I will fill it out (e.g. try to fix micros) and add support for more clock frequencies (and a trap for unsupported frequencies as you suggested).

Thank you for the feedback!

What is the reason that the original code isn't accurate enough?

#define FRACT_INC ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3)

The assumption is that FRACT_INC is a whole number. At 16 MHz, the actual value is exactly 3. At 20 MHz, the actual value is 102.4. The 0.4 is truncated away leaving a 0.39% error.

Could it be improved so that it is more accurate, while still retaining the ability to work with any reasonable F_CPU?

I believe making the fraction variables 16 bit would eliminate the error for 20 MHz but there is not a general purpose solution that addresses all clock frequencies.

I think 1 MHz, 2 MHz, 4 MHz and 24 MHz will also work.

Cool, lets shoot for 24Mhz, I'll just put a ice pack on the chip. :wink:

Kidding aside, I bet overclocking at 24Mhz on a 328p @ 5vdc would work OK. Heck look at how many people run at 16Mhz @ 3.3vdc and get away with it.

Lefty

I also have read several times people using their AVR's at 32Mhz with no problems at all.