Now the trick is to count the 96µs increments in a way that doesn't exceed the byte limit (255). A 1ms adjustment is required when 96µs counts sum up to >= 1000 (1ms), i.e. after 10-11 overflows (96µs*11=1.056ms). The remainders of 56µs and 96µs will not lose precision when shifted right by 3 bits, because 56>>3 is exactly 7 and 96>>3 is exactly 12, even 1000>>3 == 125. So this cannot be a problem, provided my calculations are right - please check.
If I understand your argument correctly, it means that the millis() function will not be offset significantly am I right? I took a look deeper into the wiring.c file and came to the following:
The Atmel ATmega168/328 based Arduino has 3 timers, of which the millis function uses the microcontroller’s Timer #0. When looking on the default preset for an Atmega328, the Timer #0 prescale factor is 64.
// set timer 0 prescale factor to 64
#elif defined(TCCR0B) && defined(CS01) && defined(CS00)
// this combination is for the standard 168/328/1280/2560
sbi(TCCR0B, CS01);
sbi(TCCR0B, CS00);
So if I understand it correctly, the timer is ticking 64*(1/4,000,000) times, which is every 1.6e-5 s. Given that Timer #0 has an 8-bit register, the timer will roll-over back to 0 when it tries to increment to 256. Which will trigger an timer interrupt each (1/4,000,000)64256*1000 = 4.096 milliseconds.
When digging through different Arduino hardware files, I came across the following defines which seemed of interest:
#define clockCyclesPerMicrosecond() ( F_CPU / 1000000L )
#define clockCyclesToMicroseconds (a) ( ((a) * 1000L) / (F_CPU / 1000L) )
#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)
If I understood correctly the F_CPU is provided during sketch compilation, is this correct? If this is the case with an F_CPU of 4,000,000 I get the following:
clockCyclesPerMicrosecond = 4
MICROSECONDS_PER_TIMER0_OVERFLOW = 4096
MILLIS_INC = 4.096 ~ 4 (I suppose it rounds down to an integer?)
FRACT_INC = 96 >> 3 = 12
FRACT_MAX = 125
When I now look at the millis function, which is provided as:
#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;
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;
}
I assume plain and simple this just gives me a long of the ISR function, which simplified should give me something like:
ISR(TIMER0_OVF_vect)
{
timer0_millis += 4;
timer0_fract += 12;
if (timer0_fract >= 125) {
timer0_fract -= 125;
timer0_millis += 1;
}
timer0_overflow_count++;
}
So if I understand it correctly this would mean that Timer #0 increments timer0_millis by 4 every 4.096 milliseconds and adds another 1 every time it exceeds 125 (to catch up with counting?). So does this mean without any additional alteration of the code the millis() function itself will account for the missing 0.096 ms from every overflow during 125/12 = 10.4167 ms intervals? If this is correct it would mean that timer0_millis accumulates a 0.096 error every time it executes, until the error approaches 1 ms where it will correct with a jump by 5 instead of 4 to correct itself?
If this is the case it would be great, given that there will be no explicit need for me to adjust any code regarding the millis function. The only downside with regards to a 16MHz clock is that I accumulate errors over a 4ms interval instead of a 1ms interval.
I also found out that the delayMicroseconds() function does not have a compatibility with 4MHz. However, I suppose since I have no interest in using delays() or microsecond delays accurately it should not impose huge issues, am I right?
See init() for the implemented settings of the prescalers. Keep in mind that T0 can also be used for PWM, so that the prescaler rate should be chosen for common PWM frequencies (480 or 960Hz), even when clocked by 4MHz.
Given that the current pre-defined pre-scaler also works with millis(), having a MHz clock with the same pre-scaler to perform PWM should also be fine am I right? If I understand it correctly I am having a slightly more inaccurate timing since I accumulate errors every 4ms instead of 1ms, but it shouldn't impose any significant issues regarding PWM. Or am I forgetting something?
Thanks for your help! It already made me understand the underlying concepts a lot more.